mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge branch 'main' of github.com:omnivore-app/omnivore into feat/integrations-page
This commit is contained in:
commit
a496e0ac58
64 changed files with 9824 additions and 1356 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
android/Omnivore/app/src/main/res/drawable-v24/minus.xml
Normal file
1
android/Omnivore/app/src/main/res/drawable-v24/minus.xml
Normal 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>
|
||||
1
android/Omnivore/app/src/main/res/drawable-v24/plus.xml
Normal file
1
android/Omnivore/app/src/main/res/drawable-v24/plus.xml
Normal 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>
|
||||
|
|
@ -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 = "";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export async function createMobileEmailSignUpResponse(
|
|||
json: {},
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
return signUpFailedPayload
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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="{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/b116ac39-34b8-4868-a2b0-b20c79f610c3_256x419.gif","fullscreen":null,"imageSize":null,"height":419,"width":256,"resizeWidth":null,"bytes":275502,"alt":"Saving with the Omnivore iOS Share Extension","title":null,"type":"image/gif","href":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="{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/4255167b-bc82-4187-a043-3150cbbdc17d_320x152.png","fullscreen":null,"imageSize":null,"height":152,"width":320,"resizeWidth":null,"bytes":15668,"alt":null,"title":null,"type":null,"href":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 Omnivore’s 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 </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 </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 browser’s 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 </h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p>
|
||||
<span>Install the</span> <a href="https://omnivore.app/install/mac" rel="">Mac App</a><span>. </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. </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 </h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p>
|
||||
<em><strong>Theme:</strong></em> <span>Tap your photo, initial, or avatar in the top right corner to access the profile menu. Select the white or black thumbnail to choose the Light or Dark theme.</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: </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. </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. </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="{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/f382aca6-3fbf-4a0c-a69e-8270c1554e24_320x693.png","fullscreen":null,"imageSize":null,"height":693,"width":320,"resizeWidth":null,"bytes":43062,"alt":null,"title":null,"type":null,"href":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="{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/658efff4-341a-4720-8cf6-9b2bdbedfaa7_800x668.gif","fullscreen":null,"imageSize":null,"height":668,"width":800,"resizeWidth":null,"bytes":148745,"alt":"Saving with the Omnivore Browser Extension","title":null,"type":"image/gif","href":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="{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/598e774d-1c20-43e3-bccf-be875f7b6616_1400x955.png","fullscreen":null,"imageSize":null,"height":955,"width":1400,"resizeWidth":null,"bytes":297887,"alt":"Sharing a PDF with Omnivore","title":null,"type":"image/png","href":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
|
|
@ -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>, it’s 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). </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="{"videoId":"k6RkIqepAig","startTime":null,"endTime":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 don’t already have an Omnivore account, you can sign up for free from the login screen. </p>
|
||||
<p>
|
||||
<em>Note:</em> <span>If you haven’t 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 (we’ve 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. </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> </span>
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>
|
||||
<span>While viewing the page you’d 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>. </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>
|
||||
1456
packages/api/src/services/popular_reads/omnivore_ios-original.html
Normal file
1456
packages/api/src/services/popular_reads/omnivore_ios-original.html
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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([])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
32
packages/content-handler/src/newsletters/ghost-handler.ts
Normal file
32
packages/content-handler/src/newsletters/ghost-handler.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
1292
packages/content-handler/test/data/convertkit-newsletter.html
Normal file
1292
packages/content-handler/test/data/convertkit-newsletter.html
Normal file
File diff suppressed because it is too large
Load diff
361
packages/content-handler/test/data/ghost-newsletter.html
Normal file
361
packages/content-handler/test/data/ghost-newsletter.html
Normal file
File diff suppressed because one or more lines are too long
288
packages/content-handler/test/data/node-weekly-newsletter.html
Normal file
288
packages/content-handler/test/data/node-weekly-newsletter.html
Normal 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 you’re 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 you’re a Powershell user and you’re finding that some arguments aren’t 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 what’s 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 you’re 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> — We’d be surprised if you’d get away with this for long given there’s an official API, but it’s always interesting to see how it’s done.</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 & 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. There’s <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> — Doppler’s 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>
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 Omnivore’s basic functions and advanced features, divided into four main activities:</p>
|
||||
<p> This guide will show you how to use Omnivore’s 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 </p>
|
||||
<p> Saving from a Browser </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 </a></p>
|
||||
<p>
|
||||
<a href="https://omnivore.app/install/chrome" rel="">Chrome </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 browser’s 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 browser’s 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 </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 </h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p><span>Install the </span><a href="https://omnivore.app/install/mac" rel="">Mac App</a><span>. </span></p>
|
||||
<p>
|
||||
<span>Install the</span> <a href="https://omnivore.app/install/mac" rel="">Mac App</a><span>. </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. </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. </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 </h3>
|
||||
<h3> Change Formatting </h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p><em><strong>Theme:</strong></em><span> Tap your photo, initial, or avatar in the top right corner to access the profile menu. Select the white or black thumbnail to choose the Light or Dark theme.</span></p>
|
||||
<p>
|
||||
<em><strong>Theme:</strong></em> <span>Tap your photo, initial, or avatar in the top right corner to access the profile menu. Select the white or black thumbnail to choose the Light or Dark theme.</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: </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: </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. </p>
|
||||
<p> To search through all your saved links, enter a keyword or phrase in the search bar. </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. </span></p>
|
||||
<p>
|
||||
<span>Select</span> <strong>Highlights</strong> <span>to view the text selections you have highlighted in all your saved pages. </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
|
|
@ -1 +1 @@
|
|||
https://blog.omnivore.app/p/d0d30ea5-49aa-4c04-8fae-c004be9c51b9
|
||||
https://blog.omnivore.app/p/getting-started-with-omnivore-382
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"title": "Elon Musk’s 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
|
||||
}
|
||||
118
packages/readabilityjs/test/test-pages/slowboring/expected.html
Normal file
118
packages/readabilityjs/test/test-pages/slowboring/expected.html
Normal 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 there’s nothing wrong with that per se, it means Musk has to watch what he says regarding the PRC, not just in his personal capacity as a business executive but potentially in his institutional role as well. And he’s not alone; Apple TV+, for example, has a rule that</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> That’s an unfortunate but straightforward consequence of Apple TV+ being so small compared to Apple’s core business of making and selling smartphones: they compromise the content business for the sake of the manufacturing business. The good news for the world is that Apple TV+ is a very small share of western cultural output. They’re doing well with niche content (I love “For All Mankind”), and they won an Oscar for “Coda.” But it’s a small service in the scheme of things. </p>
|
||||
<p>
|
||||
<span>The problem for the world is that Twitter would be the Apple TV+ of Elon Musk’s enterprises, much smaller and less important than Tesla, so its interests will always be sacrificed to advance Tesla’s interests. And Tesla, like Apple’s hardware business, is deeply enmeshed in China. But Twitter is</span> <em>much</em> <span>more important to global politics and culture than Apple TV+. That’s the whole reason the Musk/Twitter saga has been such a subject of fascination. Twitter is one of a handful of other influential media properties — The New York Times, the three cable networks, AM talk radio stations — that exert a cultural and political influence that far exceeds their modest financial footprints. Apple executives are much less polarizing and controversial than Musk. But pretty much everyone on both the left and right knows they’re a bit squirrelly about China for business reasons. And if they bought the New York Times, that would have dire implications for the integrity of their China coverage.</span>
|
||||
</p>
|
||||
<p> Musk is mercurial and I won’t pretend to be able to predict what he will do. But I think his business relationships with China and tendency to take pro-PRC positions in his public statements raise some disturbing questions about the future of Twitter that deserve much more scrutiny relative to the concern that he won’t be strict enough in policing hate speech. </p>
|
||||
<h4> China leans very effectively on western businesses </h4>
|
||||
<p> We don’t know exactly why Apple adopted its “no China” content policy, but we can come up with a pretty clear guess based on a handful of high-profile blowups. Back in 2018, for example, Mercedes Benz posted a Dalai Lama quote in English that had nothing to do with China. </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="{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/cf961dc1-2440-4e54-976a-bb7fa8115e99_730x1000.png","fullscreen":null,"imageSize":null,"height":1000,"width":730,"resizeWidth":null,"bytes":658567,"alt":null,"title":null,"type":"image/png","href":null,"belowTheFold":false,"internalRedirect":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 didn’t 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 didn’t 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 league’s 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 haven’t 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="{"url":"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","full_text":"John Cena Begging China For Forgiveness Proves That Money Beats Morality Every Time <a class=\"tweet-url\" href=\"http://dlvr.it/S0NzBW\">dlvr.it/S0NzBW</a> ","username":"realDailyWire","name":"Daily Wire","date":"Tue May 25 16:29:11 +0000 2021","photos":[{"img_url":"https://pbs.substack.com/media/E2PzsqWUYAQFpdA.jpg","link_url":"https://t.co/smDNnC4kfb","alt_text":null}],"quoted_tweet":{},"retweet_count":280,"like_count":1862,"expanded_url":{},"video_url":null,"belowTheFold":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 company’s stock price, they’d 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. I’m sure that if you asked Intel executives whether it’s good that China is perpetrating a genocide and using forced labor in Xinjiang, the vast majority of them would say, in private, that it’s actually bad. But in practice, Intel has</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="{"src":"https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/83868ad9-201b-45c3-81bd-e32d187a3249_1220x398.png","fullscreen":null,"imageSize":null,"height":398,"width":1220,"resizeWidth":null,"bytes":83077,"alt":null,"title":null,"type":"image/png","href":null,"belowTheFold":true,"internalRedirect":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 company’s largest factory in the world</a><span>.</span>
|
||||
</p>
|
||||
<p> There’s nothing particularly untoward or suspect about this. Many more people live in East Asia than live in North America, so it’s natural that over the long term any manufacturer of large objects is going to want to have their biggest production facilities in Asia. </p>
|
||||
<p>
|
||||
<span>But Tesla has received unusually generous treatment from the Chinese government. Part of China’s strategy for industrializing has been to say to foreign companies operating in key sectors that if they want to be in the China market, they need to operate as a joint venture with a Chinese-owned firm. It’s become pretty clear that this is a mechanism for Chinese companies to learn foreign technology and business methods, and ultimately pull Chinese-owned companies up the value chain. Foreign companies don’t like it, but in most cases the lure of the China market is just too good to resist. Telsa, though, managed to become the first automaker to receive an</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>That’s a huge coup for Musk and a testament both to the quality of Tesla’s products and also to Musk’s shrewdness and skill as a businessman. But without being a hater, I have to say that “shrewdness and skill at making Xi Jinping like you and want to give you favorable treatment” is a mixed ethical bag. The</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 I’m 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 Musk’s 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 doesn’t 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 Musk’s business grew, he became more China-friendly in his public statements. Initially this took the relatively sensible form adopted by many who need to come up with something nice to say about the PRC: praise for their extensive infrastructure buildout. This is something I’ve done myself — it’s incredibly impressive that China built mid-sized metro systems in a couple of dozen cities over the course of time that it took the United States to build three miles of subway on the Upper East Side. </p>
|
||||
<div data-tweet-id="968608879914270721" class="tweet-placeholder"></div>
|
||||
<p> But in more recent years, Musk’s thinking on China has evolved in less defensible directions. We know, for example, that part of his rightward political evolution over the past several years was driven by the restrictiveness of California’s Covid-19 protocols. Yet apart from a token March 2020 gesture of praise for Xi’s success in controlling the virus, he’s had absolutely nothing to say about China’s Covid Zero policies that have been much more draconian than anything offered in the United States. </p>
|
||||
<div data-tweet-id="1240753430001356801" class="tweet-placeholder"></div>
|
||||
<p> The same is true, though obviously in a much broader sense, of Musk’s emergence as a champion of free speech who is skeptical of heavy-handed content moderation policies by big tech platforms. Reasonable people can disagree as to exactly where Twitter and Facebook should draw these lines, and I tend to agree with Musk about some of the specifics here. But obviously the speech restrictions on Twitter and Facebook are much less onerous than the ones the PRC imposes on their domestic internet. That’s part of the reason Twitter and Facebook are banned in China! But Musk has never said or done anything to champion free speech in China. That’s not surprising — nobody with business interests in China criticizes the PRC’s speech repression. But it is a reminder that Musk, like many business people, prioritizes being in the good graces of the dictators who run China over maintaining his commitment to free speech. </p>
|
||||
<p>
|
||||
<span>And, again, Musk’s silence on free speech in China isn’t because he doesn’t 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 he’s 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 don’t know what Musk’s business ties to China mean for Twitter. And I wish that more of the reporting and commentary on this takeover proposal focused on this. After all, here are some things that we know: </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 he’s 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 don’t know, in part because Musk is mercurial and hard to predict. But also in part because it’s not clear exactly how Chinese censorship exporting works. I assume nobody had to explicitly tell Musk that criticizing Covid Zero would be bad for business — it’s just something he knows, so he doesn’t do it. By the same token, I doubt there was ever an explicit conversation between Tim Cook and someone from the Chinese government about Apple TV+ programming. Apple just knows the score. </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 what’s good for business. So what does Twitter do once it’s owned by the CEO of a major industrial brand? I don’t know, but I wish the reporters on the Musk beat would ask. And frankly, I wish Congress and the FTC would, too. </p>
|
||||
</div>
|
||||
</article>
|
||||
</DIV>
|
||||
1830
packages/readabilityjs/test/test-pages/slowboring/source.html
Normal file
1830
packages/readabilityjs/test/test-pages/slowboring/source.html
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
https://www.slowboring.com/p/elon-musks-business-ties-deserve
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
54
yarn.lock
54
yarn.lock
|
|
@ -11547,6 +11547,19 @@ copy-to-clipboard@^3.3.1:
|
|||
dependencies:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
copyfiles@^2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.4.1.tgz#d2dcff60aaad1015f09d0b66e7f0f1c5cd3c5da5"
|
||||
integrity sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==
|
||||
dependencies:
|
||||
glob "^7.0.5"
|
||||
minimatch "^3.0.3"
|
||||
mkdirp "^1.0.4"
|
||||
noms "0.0.0"
|
||||
through2 "^2.0.1"
|
||||
untildify "^4.0.0"
|
||||
yargs "^16.1.0"
|
||||
|
||||
core-js-compat@^3.20.2, core-js-compat@^3.21.0:
|
||||
version "3.21.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.1.tgz#cac369f67c8d134ff8f9bd1623e3bc2c42068c82"
|
||||
|
|
@ -14586,6 +14599,18 @@ glob@7.2.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glo
|
|||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.0.5:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.1.1"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
global-dirs@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686"
|
||||
|
|
@ -18720,7 +18745,7 @@ minimatch@5.0.1:
|
|||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^3.0.2, minimatch@^3.0.4:
|
||||
minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.1.1:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
|
|
@ -19454,6 +19479,14 @@ nodemon@^2.0.15:
|
|||
undefsafe "^2.0.5"
|
||||
update-notifier "^5.1.0"
|
||||
|
||||
noms@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859"
|
||||
integrity sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==
|
||||
dependencies:
|
||||
inherits "^2.0.1"
|
||||
readable-stream "~1.0.31"
|
||||
|
||||
nopt@^4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
|
||||
|
|
@ -21876,6 +21909,16 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre
|
|||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readable-stream@~1.0.31:
|
||||
version "1.0.34"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
|
||||
integrity sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readdir-scoped-modules@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309"
|
||||
|
|
@ -23549,6 +23592,11 @@ string_decoder@^1.0.0, string_decoder@^1.1.1:
|
|||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
string_decoder@~0.10.x:
|
||||
version "0.10.31"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
||||
integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
|
|
@ -24083,7 +24131,7 @@ throttleit@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
||||
integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=
|
||||
|
||||
through2@^2.0.0:
|
||||
through2@^2.0.0, through2@^2.0.1:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||
integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
|
||||
|
|
@ -25977,7 +26025,7 @@ yargs-unparser@2.0.0:
|
|||
flat "^5.0.2"
|
||||
is-plain-obj "^2.1.0"
|
||||
|
||||
yargs@16.2.0, yargs@^16.0.0, yargs@^16.1.1, yargs@^16.2.0:
|
||||
yargs@16.2.0, yargs@^16.0.0, yargs@^16.1.0, yargs@^16.1.1, yargs@^16.2.0:
|
||||
version "16.2.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
||||
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
|
||||
|
|
|
|||
Loading…
Reference in a new issue