diff --git a/build/xcode/Save to Raindrop.io/Save to Raindrop.io.xcodeproj/project.xcworkspace/xcuserdata/exentrich.xcuserdatad/UserInterfaceState.xcuserstate b/build/xcode/Save to Raindrop.io/Save to Raindrop.io.xcodeproj/project.xcworkspace/xcuserdata/exentrich.xcuserdatad/UserInterfaceState.xcuserstate
index 8501f119..586849a3 100644
Binary files a/build/xcode/Save to Raindrop.io/Save to Raindrop.io.xcodeproj/project.xcworkspace/xcuserdata/exentrich.xcuserdatad/UserInterfaceState.xcuserstate and b/build/xcode/Save to Raindrop.io/Save to Raindrop.io.xcodeproj/project.xcworkspace/xcuserdata/exentrich.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/package.json b/package.json
index 6987a026..7ed6df6d 100755
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "app",
- "version": "5.6.73",
+ "version": "5.6.74",
"description": "All-in-one bookmark manager",
"author": "Rustem Mussabekov",
"license": "MIT",
diff --git a/src/co/bookmarks/add/fallback/link.js b/src/co/bookmarks/add/fallback/link.js
index 3d4b6964..b08e9f4e 100644
--- a/src/co/bookmarks/add/fallback/link.js
+++ b/src/co/bookmarks/add/fallback/link.js
@@ -1,127 +1,70 @@
import s from './link.module.css'
-import React, { useCallback, useState, useRef, useEffect, useMemo } from 'react'
-import { useDispatch, useSelector } from 'react-redux'
+import React, { useCallback, useState, useRef, useEffect } from 'react'
+import { useDispatch } from 'react-redux'
import t from '~t'
-import useDebounce from '~modules/format/callback/use-debounce'
-import isURL from 'validator/es/lib/isURL'
-import { oneCreate } from '~data/actions/bookmarks'
-import { suggestFields } from '~data/actions/bookmarks'
-import { makeSuggestedFields } from '~data/selectors/bookmarks'
-import { makeCollectionPath } from '~data/selectors/collections'
-import { isPro } from '~data/selectors/user'
+import { manyCreate, manyReparseInplace } from '~data/actions/bookmarks'
+import { extractURLs } from '~modules/format/url'
import { Error } from '~co/overlay/dialog'
import Popover from '~co/overlay/popover'
-import { Layout } from '~co/common/form'
+import { Layout, Text, Label, Buttons } from '~co/common/form'
import Button from '~co/common/button'
import Icon from '~co/common/icon'
-import ItemLink from '~co/bookmarks/edit/form/link'
-
-function Suggestion({ id, primary, disabled, onClick }) {
- const getCollectionPath = useMemo(()=>makeCollectionPath(), [])
- const path = useSelector(state=>getCollectionPath(state, id, { self: true }))
- const shortPath = useMemo(()=>path.map((p)=>p.title).slice(-2).join(' / '), [path])
- const fullPath = useMemo(()=>path.map((p)=>p.title).join(' / '), [path])
- const collection = useMemo(()=>path?.[path.length-1], [path])
- const onSelfClick = useCallback(e=>onClick(parseInt(e.currentTarget.getAttribute('data-id'))), [onClick])
-
- if (!collection?.title)
- return null
-
- return (
-
- )
-}
-
-function Suggestions({ item, loading, onClick }) {
- const dispatch = useDispatch()
-
- //load suggestions
- const debounced = useDebounce(item, 300)
- useEffect(()=>dispatch(suggestFields(debounced)), [debounced.link])
-
- //get suggestions
- const enabled = useSelector(state=>state.config.ai_suggestions)
- const pro = useSelector(state=>isPro(state))
- const getSuggestedFields = useMemo(()=>makeSuggestedFields(), [])
- const { collections=[] } = useSelector(state=>getSuggestedFields(state, item))
-
- return (
-
-
-
- {enabled && pro && collections.length ? (<>
- {t.s('or')} {t.s('to')}
-
- {collections.map(id=>(
-
- ))}
- >) : null}
-
- )
-}
function AddForm({ spaceId, onEdit, pin, onClose }) {
const dispatch = useDispatch()
- const [item, setItem] = useState(()=>({ collectionId: parseInt(spaceId)||-1 }))
- const [loading, setLoading] = useState(true)
+ const input = useRef(null)
+ const [text, setText] = useState('')
+ const [loading, setLoading] = useState(false)
- const onChangeField = useCallback((changed)=>
- setItem(item=>({...item, ...changed})),
- [setItem]
- )
+ //grab link from clipboard
+ useEffect(()=>{
+ (async()=>{
+ //in safari readText works bad, so this line prevents run on safari
+ await navigator.permissions.query({name: 'clipboard-read'})
+ return extractURLs(await navigator.clipboard.readText())
+ })()
+ .then(urls=>{
+ setText(urls.join('\n'))
+ input.current?.select?.()
+ })
+ .catch(e=>console.error(e))
+ }, [setText, input])
- const onAddTo = useCallback((collectionId)=>{
- setLoading(true)
+ const onChangeInput = useCallback(e=>{
+ e.currentTarget?.setCustomValidity('')
+ setText(e.currentTarget.value)
+ }, [setText])
- dispatch(oneCreate({ ...item, collectionId }, (items)=>{
- onEdit && onEdit(items)
- onClose()
- }, e=>{
- Error(e)
- setLoading(false)
- }))
- }, [item, onEdit, setLoading, onClose, dispatch])
-
const onSubmitForm = useCallback(e=>{
e.preventDefault()
e.stopPropagation()
- onAddTo(item.collectionId)
- }, [onAddTo, item.collectionId])
- //grab link from clipboard
- useEffect(()=>{
- async function getLink() {
- //in safari readText works bad, so this line prevents run on safari
- await navigator.permissions.query({name: 'clipboard-read'})
- const link = await navigator.clipboard.readText()
- if (isURL(link, { require_protocol: true })) return link
+ const urls = extractURLs(text)
+ input.current?.setCustomValidity?.(urls.length ? '' : 'Please enter a valid URL(s)')
+ input.current?.reportValidity?.()
+
+ if (urls.length) {
+ setLoading(true)
+
+ dispatch(
+ manyCreate(
+ urls.map(link=>({
+ link,
+ collectionId: parseInt(spaceId)||-1
+ })),
+ (items)=>{
+ dispatch(manyReparseInplace(items))
+ onClose()
+ }, e=>{
+ Error(e)
+ setLoading(false)
+ }
+ )
+ )
}
-
- getLink()
- .then(link=>onChangeField({ link }))
- .catch(e=>console.error(e))
- .finally(()=>setLoading(false))
- }, [setLoading, onChangeField])
+ }, [text, input, spaceId, dispatch, setLoading])
return (
diff --git a/src/co/bookmarks/add/fallback/link.module.css b/src/co/bookmarks/add/fallback/link.module.css
index 74d022dd..6a2bccd3 100644
--- a/src/co/bookmarks/add/fallback/link.module.css
+++ b/src/co/bookmarks/add/fallback/link.module.css
@@ -1,15 +1,3 @@
.modal {
width: 11rem
-}
-
-.suggestions {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: var(--padding-small);
- padding-bottom: var(--padding-small)
-}
-
-.or {
- color: var(--secondary-text-color)
}
\ No newline at end of file
diff --git a/src/data/actions/bookmarks/single.js b/src/data/actions/bookmarks/single.js
index 9e04451f..5f69ac0e 100644
--- a/src/data/actions/bookmarks/single.js
+++ b/src/data/actions/bookmarks/single.js
@@ -3,7 +3,8 @@ import {
BOOKMARK_LOAD_REQ, BOOKMARK_CREATE_REQ, BOOKMARKS_CREATE_REQ, BOOKMARK_UPDATE_REQ, BOOKMARK_REMOVE_REQ, BOOKMARK_UPLOAD_REQ,
BOOKMARK_RECOVER, BOOKMARK_IMPORTANT, BOOKMARK_SCREENSHOT, BOOKMARK_REPARSE, BOOKMARK_MOVE,
BOOKMARK_REORDER,
- BOOKMARK_SUGGEST_FIELDS
+ BOOKMARK_SUGGEST_FIELDS,
+ BOOKMARKS_REPARSE_INPLACE
} from '../../constants/bookmarks'
//High level API
@@ -95,4 +96,9 @@ export const manyCreate = (items=[], onSuccess, onFail)=>({
items,
onSuccess: wrapFunc(onSuccess),
onFail: wrapFunc(onFail)
+})
+
+export const manyReparseInplace = (items=[])=>({
+ type: BOOKMARKS_REPARSE_INPLACE,
+ items
})
\ No newline at end of file
diff --git a/src/data/constants/bookmarks.js b/src/data/constants/bookmarks.js
index d512db0e..9bc0c49a 100644
--- a/src/data/constants/bookmarks.js
+++ b/src/data/constants/bookmarks.js
@@ -36,6 +36,7 @@ export const
BOOKMARK_CREATE_ERROR = 'BOOKMARK_CREATE_ERROR',
BOOKMARKS_CREATE_REQ = 'BOOKMARKS_CREATE_REQ',
+ BOOKMARKS_REPARSE_INPLACE = 'BOOKMARKS_REPARSE_INPLACE',
BOOKMARK_UPLOAD_REQ = 'BOOKMARK_UPLOAD_REQ',
BOOKMARK_UPLOAD_PROGRESS = 'BOOKMARK_UPLOAD_PROGRESS',
diff --git a/src/data/sagas/bookmarks/single.js b/src/data/sagas/bookmarks/single.js
index 07e16e6d..331b9357 100755
--- a/src/data/sagas/bookmarks/single.js
+++ b/src/data/sagas/bookmarks/single.js
@@ -12,7 +12,8 @@ import {
BOOKMARK_REORDER,
BOOKMARK_SUGGEST_FIELDS, BOOKMARK_SUGGESTED_FIELDS,
- BOOKMARK_RECOVER, BOOKMARK_IMPORTANT, BOOKMARK_SCREENSHOT, BOOKMARK_REPARSE, BOOKMARK_MOVE
+ BOOKMARK_RECOVER, BOOKMARK_IMPORTANT, BOOKMARK_SCREENSHOT, BOOKMARK_REPARSE, BOOKMARK_MOVE,
+ BOOKMARKS_REPARSE_INPLACE
} from '../../constants/bookmarks'
import {
@@ -43,6 +44,7 @@ export default function* () {
//many
yield takeEvery(BOOKMARKS_CREATE_REQ, createBookmarks)
+ yield takeEvery(BOOKMARKS_REPARSE_INPLACE, reparseInplace)
}
function* loadBookmark({ ignore=false, _id }) {
@@ -393,4 +395,25 @@ function* suggestFields({ obj, ignore }) {
} catch (error) {
console.error(error)
}
+}
+
+function* reparseInplace({ items, ignore }) {
+ if (ignore) return
+
+ try{
+ for(const { _id, link } of items) {
+ const parsed = yield call(Api.get, 'import/url/parse?url='+encodeURIComponent(link))
+
+ yield put({
+ type: BOOKMARK_UPDATE_REQ,
+ _id: _id,
+ set: {
+ ...parsed.item,
+ pleaseParse: null
+ }
+ })
+ }
+ } catch (error) {
+ console.error(error)
+ }
}
\ No newline at end of file
diff --git a/src/modules/format/url/extractURLs.js b/src/modules/format/url/extractURLs.js
new file mode 100644
index 00000000..27c98746
--- /dev/null
+++ b/src/modules/format/url/extractURLs.js
@@ -0,0 +1,14 @@
+import _ from 'lodash-es'
+import isURL from 'validator/es/lib/isURL'
+
+const urlPattern = /https?:\/\/[^\s]+/gi;
+
+export function extractURLs(text) {
+ return _.uniq(
+ (String(text)||'').match(urlPattern) || []
+ )
+ .filter(url=>
+ (url.match(/https?/g) || []).length == 1 &&
+ isURL(url, { require_protocol: true, require_tld: true })
+ )
+}
\ No newline at end of file
diff --git a/src/modules/format/url/index.js b/src/modules/format/url/index.js
index 702eaa0a..dec8100b 100644
--- a/src/modules/format/url/index.js
+++ b/src/modules/format/url/index.js
@@ -1,3 +1,4 @@
export * from './getDomain'
export * from './normalizeURL'
-export * from './isSPA'
\ No newline at end of file
+export * from './isSPA'
+export * from './extractURLs'
\ No newline at end of file