Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 57 additions & 25 deletions src/stores/clipboard.store.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import {solNative} from 'lib/SolNative'
import {autorun, makeAutoObservable, runInAction, toJS} from 'mobx'
import {EmitterSubscription} from 'react-native'
import {IRootStore} from 'store'
import {Widget} from './ui.store'
import { solNative } from 'lib/SolNative'
import { autorun, makeAutoObservable, runInAction, toJS } from 'mobx'
import { EmitterSubscription } from 'react-native'
import { IRootStore } from 'store'
import { Widget } from './ui.store'
import MiniSearch from 'minisearch'
import {storage} from './storage'
import {captureException} from '@sentry/react-native'
import { storage } from './storage'
import { captureException } from '@sentry/react-native'

const MAX_ITEMS = 1000

Expand All @@ -20,27 +20,31 @@ export type PasteItem = {
text: string
url?: string | null
bundle?: string | null
datetime: number // Unix timestamp when copied
}

let minisearch = new MiniSearch({
fields: ['text', 'bundle'],
storeFields: ['text', 'url', 'bundle'],
storeFields: ['id', 'text', 'url', 'bundle', 'datetime'],
tokenize: (text: string, fieldName?: string) => text.split(/[\s\.]+/),
searchOptions: {
boost: {text: 2},
fuzzy: true,
prefix: true,
},
})

export const createClipboardStore = (root: IRootStore) => {
const store = makeAutoObservable({
deleteItem: (index: number) => {
if (index >= 0 && index < store.items.length) {
minisearch.remove(store.items[index])
store.items.splice(index, 1)
}
},
deleteAllItems: () => {
store.items = []
minisearch.removeAll()
},
items: [] as PasteItem[],
saveHistory: false,
onFileCopied: (obj: {text: string; url: string; bundle: string | null}) => {
let newItem = {id: +Date.now(), ...obj}

console.warn(`Received copied file! ${JSON.stringify(newItem)}`)
onFileCopied: (obj: { text: string; url: string; bundle: string | null }) => {
let newItem: PasteItem = { id: +Date.now(), datetime: Date.now(), ...obj }

// If save history move file to more permanent storage
if (store.saveHistory) {
Expand All @@ -65,12 +69,12 @@ export const createClipboardStore = (root: IRootStore) => {
// Remove last item from minisearch
store.removeLastItemIfNeeded()
},
onTextCopied: (obj: {text: string; bundle: string | null}) => {
onTextCopied: (obj: { text: string; bundle: string | null }) => {
if (!obj.text) {
return
}

let newItem = {id: Date.now().valueOf(), ...obj}
let newItem: PasteItem = { id: Date.now().valueOf(), datetime: Date.now(), ...obj }

const index = store.items.findIndex(t => t.text === newItem.text)
// Item already exists, move to top
Expand All @@ -91,11 +95,27 @@ export const createClipboardStore = (root: IRootStore) => {
store.removeLastItemIfNeeded()
},
get clipboardItems(): PasteItem[] {
let items = store.items;

if (!root.ui.query || root.ui.focusedWidget !== Widget.CLIPBOARD) {
return root.clipboard.items
return items
}

return minisearch.search(root.ui.query) as any
// Boost recent items in search results
const now = Date.now();
return minisearch.search(root.ui.query, {
boostDocument: (documentId, term, storedFields) => {
const dt = typeof storedFields?.datetime === 'number' ? storedFields.datetime : Number(storedFields?.datetime);
if (!dt || isNaN(dt)) return 1;
// Boost items copied in the last 24h, scale down for older
const hoursAgo = (now - dt) / (1000 * 60 * 60);
if (hoursAgo < 1) return 2; // very recent
if (hoursAgo < 24) return 1.5; // recent
return 1;
},
boost: { text: 2 },
fuzzy: true,
}) as any
},
removeLastItemIfNeeded: () => {
if (store.items.length > MAX_ITEMS) {
Expand Down Expand Up @@ -160,7 +180,13 @@ export const createClipboardStore = (root: IRootStore) => {

if (entry) {
let items = JSON.parse(entry)

// Ensure all items have datetime
items = items.map((item: any) => ({
...item,
datetime: typeof item.datetime === 'number' && !isNaN(item.datetime)
? item.datetime
: (item.id || Date.now()), // fallback: use id or now
}))
runInAction(() => {
store.items = items
minisearch.addAll(store.items)
Expand All @@ -171,18 +197,24 @@ export const createClipboardStore = (root: IRootStore) => {

const persist = async () => {
if (store.saveHistory) {
const history = toJS(store)
// Ensure all items have datetime before persisting
const itemsToPersist = store.items.map(item => ({
...item,
datetime: typeof item.datetime === 'number' && !isNaN(item.datetime)
? item.datetime
: (item.id || Date.now()),
}))
try {
await solNative.securelyStore(
'@sol.clipboard_history_v2',
JSON.stringify(history.items),
JSON.stringify(itemsToPersist),
)
} catch (e) {
console.warn('Could not persist data', e)
}
}

let storeWithoutItems = {...store}
let storeWithoutItems = { ...store }
storeWithoutItems.items = []

try {
Expand Down
11 changes: 11 additions & 0 deletions src/stores/keystroke.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ export const createKeystrokeStore = (root: IRootStore) => {
shift: boolean
}) => {
switch (keyCode) {
case 51: {
if (root.ui.focusedWidget === Widget.CLIPBOARD) {
if (shift) {
root.clipboard.deleteAllItems()
} else {
root.clipboard.deleteItem(root.ui.selectedIndex)
}
return
}
break
}
// "j" key
case 38: {
// simulate a down key press
Expand Down
42 changes: 27 additions & 15 deletions src/widgets/clipboard.widget.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import {LegendList, LegendListRef} from '@legendapp/list'
import { LegendList, LegendListRef } from '@legendapp/list'
import clsx from 'clsx'
import {FileIcon} from 'components/FileIcon'
import {LoadingBar} from 'components/LoadingBar'
import {MainInput} from 'components/MainInput'
import {observer} from 'mobx-react-lite'
import {FC, useEffect, useRef} from 'react'
import {StyleSheet, Text, TouchableOpacity, View, ViewStyle} from 'react-native'
import {useStore} from 'store'
import {PasteItem} from 'stores/clipboard.store'
import { FileIcon } from 'components/FileIcon'
import { Key } from 'components/Key'
import { LoadingBar } from 'components/LoadingBar'
import { MainInput } from 'components/MainInput'
import { observer } from 'mobx-react-lite'
import { FC, useEffect, useRef } from 'react'
import { StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native'
import { useStore } from 'store'
import { PasteItem } from 'stores/clipboard.store'

interface Props {
style?: ViewStyle
className?: string
}

const RenderItem = observer(
({item, index}: {item: PasteItem; index: number}) => {
({ item, index }: { item: PasteItem; index: number }) => {
const store = useStore()
const selectedIndex = store.ui.selectedIndex
const isActive = index === selectedIndex
Expand All @@ -32,8 +33,8 @@ const RenderItem = observer(
<FileIcon
url={decodeURIComponent(
item.bundle?.replace('file://', '') ??
item.url?.replace('file://', '') ??
'',
item.url?.replace('file://', '') ??
'',
)}
className="h-6 w-6"
/>
Expand Down Expand Up @@ -62,7 +63,7 @@ function isPngOrJpg(url: string | null | undefined) {
)
}

export const ClipboardWidget: FC<Props> = observer(({style}) => {
export const ClipboardWidget: FC<Props> = observer(() => {
const store = useStore()
const data = store.clipboard.clipboardItems
const selectedIndex = store.ui.selectedIndex
Expand All @@ -80,13 +81,15 @@ export const ClipboardWidget: FC<Props> = observer(({style}) => {
return (
<View className="flex-1">
<View className="flex-row px-3">
<MainInput placeholder="Search pasteboard history..." showBackButton />
<MainInput placeholder="Search Pasteboard..." showBackButton />
</View>
<LoadingBar />
<View className="flex-1 flex-row">
<View className="w-64">
<View className="w-64 h-full">
<LegendList
key={`${data.length}`}
data={data}
className='flex-1'
contentContainerStyle={STYLES.contentContainer}
ref={listRef}
recycleItems
Expand Down Expand Up @@ -130,6 +133,15 @@ export const ClipboardWidget: FC<Props> = observer(({style}) => {
)}
</View>
</View>
{/* Shortcut bar at the bottom */}
<View className="py-2 px-4 flex-row items-center justify-end gap-1 subBg">
<Text className="text-xs darker-text mr-1">Delete All</Text>
<Key symbol={'⇧'} />
<Key symbol={'⏎'} />
<View className="mx-2" />
<Text className="text-xs darker-text mr-1">Delete</Text>
<Key symbol={'⌫'} />
</View>
</View>
)
})
Expand Down