import type { SlSelect } from '@shoelace-style/shoelace' import type { useNavigate } from '@solidjs/router' import type { IShare, WriteableThread } from '@wovin/core' import type { Applog } from '@wovin/core/applog' import type { Accessor, Component } from 'solid-js' import { entityCount, filterAndMap, lastWriteWins, prepareSnapshotForPush, withTimeout } from '@wovin/core' import { isoDateStrCompare } from '@wovin/core/applog' import { computed, toJS } from '@wovin/core/mobx' import { catchReturn } from '@wovin/utils' import { Logger } from 'besonders-logger' import { groupBy, last } from 'lodash-es' import sortBy from 'lodash-es/sortBy' import { Collapse } from 'solid-collapse' import { createEffect, createMemo, createSignal, For, Match, Show, Suspense, Switch, untrack } from 'solid-js' import { boundInput, getAgentString, useAgent } from '../../data/agent/AgentState' import { DefaultAgentBanner, StorageMissingBanner } from '../../data/agent/utils-agent' import { appLogIDB } from '../../data/datalog/local-applog-idb' import { getShareThread, makeBlocksSelector, makeTagSelector, RE_SELECTOR_BLOCKS, RE_SELECTOR_TAGS, RE_SELECTOR_WITHOUT_HISTORY, toggleLastWriteWinsSelector, } from '../../data/getShareThread' import { useBlk } from '../../data/VMs/BlockVM' import { addShare, updateShareEncryption } from '../../ipfs/share-sync' import { doSync } from '../../ipfs/sync-service' import { useAllTags, useRawThread, useRoots } from '../../ui/reactive' import { createAsyncButtonHandler, createAsyncMemo, makeNote3Url, promptBlobDownload, singleBlockOfShare, useLocationNavigate, } from '../../ui/utils-ui' import { BackLink } from '../BackLink' import { CopyableID, DeleteWithConfirm, Iconify, KeyValueDiv, NameValueBadge, Spinner } from '../mini-components' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars export const SharesSettings: Component<{ navigate: ReturnType // HACK: context is broken by w3 keyring provider activeSection?: Accessor params?: Accessor }> = ( props, ) => { const focussedShare = createMemo(() => props.params()?.[0]) const agent = useAgent() const isAllSetUp = computed(() => agent.hasStorageSetup && agent.isAgentStringSetup) untrack(() => DEBUG(`Creating `, { agent, hasStorageSetup: agent.hasStorageSetup, isAllSetUp: isAllSetUp.get(), props, }), ) return ( //

//
) } export const SharesList: Component<{ focussedShare?: Accessor }> = (props) => { DEBUG('create ') const agent = useAgent() // const {navigate }=useLocationNavigate() const list = createMemo(() => { const list = agent.shares.slice() // .slice is also to un-mobx proxy list.sort((a, b) => isoDateStrCompare(a.createdAt, b.createdAt, 'desc')) // newest first DEBUG(`list:`, { list }) return list }) const focusedOrList = createMemo(() => { let newList = list() if (props.focussedShare()) { newList = newList.filter(p => p.id === props.focussedShare()) } DEBUG(`focusedOrList: FOCUSSSED???`, props.focussedShare(), { list, newList }) return newList }) createEffect(() => DEBUG(`focussed:`, props.focussedShare())) createEffect(() => DEBUG(`shares:`, toJS(agent.shares))) return (
Share not found:
{props.focussedShare()}
No shares (yet) )} > {(share: IShare, _nodeIdx) => { const isFocussed = createMemo(() => props.focussedShare() === share.id) return }}
) } export const AddShareButton: Component<{}> = (props) => { const { navigate } = useLocationNavigate() const addButtonProps = createAsyncButtonHandler(async () => { const share = await addShare() navigate(`/settings/shares/${share.id}`) }) return (
Create share { /* Publish all {/* db.count() * /}applogs {encryptFor() ? `for: ${encryptFor()}` : 'unencrypted'}
to:
{/* * /}
to IPNS (hosted in your Web3.storage): {/* {currentPub() ? `${currentPub().id}` : 'No w3name set up'} * /}
Push All Applogs {/* loading={uploading()} disabled={!getW3NamePublic()} * /}
Push
*/ }
) } export async function purgeIrrelevantShareLogs(ds: WriteableThread, share: IShare, forcePurge = false, timeSpanMinutes: number = 5) { // TODO avoid inconsistencies, notes from call: // check if the purged logs are already published elsewhere // how to prepare for real-time // do we need to change the pv pointer ? is that dangerously mutable? // what part of this belongs in wovin? // interim applog/tuple that points from newest purged cid (that has a pv pointing to it) to the newest preserved applog in the chain // how to prevent eventually purgable applogs from being written in the first place const shareThread = getShareThread(ds, share) const thresholdMS = timeSpanMinutes * 60000 type LogsByEntity = Record>> const entitiesWithContentLogs = groupBy( sortBy([...filterAndMap(shareThread, { at: 'block/content' }, ({ en, cid, ts, vl }) => ({ en, cid, ts, vl }))], 'ts') .reverse(), 'en', ) as unknown as LogsByEntity LOG({ entitiesWithContentLogs }) const logsToPurge = [] const entityMapPurged: LogsByEntity = {} for (const [eachEN, eachLogArray] of Object.entries(entitiesWithContentLogs)) { for (const [_eachEN, eachLog] of Object.entries(eachLogArray)) { entityMapPurged[eachEN] = entityMapPurged[eachEN] || [eachLog] const lastLogForThisEN = last(entityMapPurged[eachEN]) const lastTS = new Date(lastLogForThisEN.ts).getTime() const thisTS = new Date(eachLog.ts).getTime() const diffTS = lastTS - thisTS const isPurgable = diffTS > 0 && diffTS < thresholdMS if (isPurgable) { logsToPurge.push(eachLog.cid) } else { entityMapPurged[eachEN].push(eachLog) } } } DEBUG({ entityMapPurged, logsToPurge }) if (forcePurge || share.purgeBeforePush) { await appLogIDB.applogs.bulkDelete(logsToPurge) const purgeCount = ds.purge(logsToPurge) LOG({ purgeCount, entityMapPurged, logsToPurge }) } // const mapped = entitiesWithContentLogs.reduce(()) } export const SingleSharePanel: Component<{ share: IShare isFocussed?: Accessor | undefined }> = ( props, ) => { const { navigate } = useLocationNavigate() const rawDS = useRawThread() // const [uploadState, uploader] = useW3Uploader() const agent = useAgent() const [isExpanded, setIsExpanded] = createSignal(props.isFocussed?.()) const [expandedOnce, setExpandedOnce] = createSignal(isExpanded()) createEffect(() => { if (props.isFocussed?.()) { setIsExpanded(true) setExpandedOnce(true) } // if it was in the list, but not focussed - it will not be recreated }) const encryptedFor = createMemo(() => props.share.sharedAgents?.join(',') ?? '') async function deleteShare(id) { await agent.deleteShare(id) if (props.isFocussed?.()) navigate(`/settings/shares`) } const pushButtonProps = createAsyncButtonHandler(async () => { DEBUG(`Pushing '${props.share.name}'`, { share: props.share, rawDS }) // purgeIrrelevantShareLogs(rawDS, share) - //TODO consider thoroughly await doSync({ share: props.share.id }) }) async function downloadCar() { LOG(`Download as CAR: '${props.share.name}'`, { share: props.share, rawDS }) const shareData = getShareThread(rawDS, props.share) const { cid, blob } = await prepareSnapshotForPush(agent, rawDS, shareData, props.share, null) // TODO: download incremental? promptBlobDownload(blob, `${props.share.name ?? props.share.id} - ${new Date().toISOString()}.car`) // download `car` as file in javascript } async function resetPrev() { LOG(`Resetting prev to nothing: '${props.share.name}'`, { share: props.share }) await agent.updateShare(props.share.id, { lastCID: null, // ? lastPush: null, }) } const selectedBlockIDs = createMemo(() => { const selectors = props.share.selectors?.filter(s => s.match(RE_SELECTOR_BLOCKS)) if (!selectors?.length) return [] if (selectors.length > 1) WARN(`Multi selector not supported yet:`, selectors) // TODO: multi rootBlockIDs return selectors[0].split(/[()]/, 3)[1].split(',') }) function onBlockIDsChanged(e: CustomEvent) { const newRoots = (e.target as SlSelect).value as string[] // HACK: event doesn't seem to have that info DEBUG(`RootIDs changed:`, e, newRoots) const prevSelectors = props.share.selectors ?? [] const prevSelector = prevSelectors.find(s => s.match(RE_SELECTOR_BLOCKS)) const selectorIdx = prevSelector && prevSelectors.indexOf(prevSelector) const newSelector = newRoots.length ? (prevSelector ? prevSelector.replace(RE_SELECTOR_BLOCKS, makeBlocksSelector(newRoots)) : makeBlocksSelector(newRoots)) : null const newSelectors = [...prevSelectors] if (prevSelector) newSelectors.splice(selectorIdx) if (newSelector) newSelectors.push(newSelector) DEBUG(`[share] updating selectors`, props.share.id, prevSelectors, newSelectors, { prevSelector, newSelector, newRoots }) agent.updateShare(props.share.id, { selectors: newSelectors }) } const tags = createMemo(() => { const selectors = props.share.selectors?.filter(s => s.match(RE_SELECTOR_TAGS)) if (!selectors?.length) return [] if (selectors.length > 1) WARN(`Multi selector not supported yet:`, selectors) return selectors[0].split(/[()]/, 3)[1].split(',') }) function onTagsChanged(e: CustomEvent) { const newTags = (e.target as SlSelect).value as string[] // HACK: event doesn't seem to have that info DEBUG(`Tags changed:`, e, newTags) const prevSelectors = props.share.selectors ?? [] const prevSelector = prevSelectors.find(s => !!s.match(RE_SELECTOR_TAGS)) const selectorIdx = prevSelector && prevSelectors.indexOf(prevSelector) const newSelector = newTags.length ? (prevSelector ? prevSelector.replace(RE_SELECTOR_TAGS, makeTagSelector(newTags)) : makeTagSelector(newTags)) : null const newSelectors = [...prevSelectors] if (prevSelector) newSelectors.splice(selectorIdx) if (newSelector) newSelectors.push(newSelector) DEBUG(`[share] updating selectors`, props.share.id, prevSelectors, newSelectors, { prevSelector, newSelector, newRoots: newTags }) agent.updateShare(props.share.id, { selectors: newSelectors }) } const isWithoutHistory = createMemo(() => { return !!props.share.selectors?.find(s => s.match(RE_SELECTOR_WITHOUT_HISTORY)) }) const onToggleWithHistory = (e) => { const lastWriteWins = e.target.checked const prevSelectors = props.share.selectors ?? [] let newSelectors: string[] if (!prevSelectors.length) { newSelectors = lastWriteWins ? ['lastWriteWins'] : [] } else { newSelectors = prevSelectors.map(s => toggleLastWriteWinsSelector(s, lastWriteWins)).filter(s => !!s) } DEBUG(`[share] Setting new selectors`, props.share.id, newSelectors, { prevSelectors }) return agent.updateShare(props.share.id, { selectors: newSelectors }) } const onToggleAutopush = e => agent.updateShare(props.share.id, { autopush: e.target.checked }) const isAutopush = createMemo(() => { return props.share.autopush }) const shortDerivationPublicKey = createAsyncMemo(async () => { await agent.getPublicDerivationECDH() return agent.getPublicDerivationECDHshortSync() }) const allTags = useAllTags() const resultThreadOrError = computed(() => catchReturn(() => { return withTimeout(700, () => getShareThread(rawDS, props.share)) }), ) // const pubRoots = computed(() => pubData.get() && withDS(pubData.get(), () => useRoots())) const knownAgentMap = agent.getKnownAgents(rawDS) const setEncryptionTargets = (evt: CustomEvent) => { const { value: selectedAgentIDs } = evt.target as SlSelect const shareUpdate: Parameters[0] = { id: props.share.id } if (selectedAgentIDs?.length) { shareUpdate.sharedAgents = selectedAgentIDs as string[] shareUpdate.encryptedFor = null // TODO fully deprecate // shareUpdate.encryptedWith = null DEBUG('updating', { share: props.share, shareUpdate }) updateShareEncryption(shareUpdate) } else { shareUpdate.sharedAgents = null shareUpdate.encryptedFor = null // shareUpdate.encryptedWith = null updateShareEncryption(shareUpdate) } LOG('setEncryptionTargets', { evt, selectedAgentIDs, shareUpdate }) } const [loadAllRoots, setLoadAllRoots] = createSignal(false) const allRootIDs = createMemo(() => !loadAllRoots() ? selectedBlockIDs() : useRoots()) const encryptionSelectValue = () => props.share?.sharedAgents ?? '' const pushAndPurge = async () => { purgeIrrelevantShareLogs(rawDS, props.share, true) await doSync({ share: props.share.id }) } let panelRef: HTMLDivElement let caretRef: HTMLDivElement let buttonRow: HTMLDivElement return (
{ DEBUG(`.onClick`, e, { panelRef }) if (e.target !== panelRef && e.target !== caretRef && e.target !== buttonRow) return e.stopPropagation() setIsExpanded(!isExpanded()) setExpandedOnce(true) }} >
{boundInput('sharesMap', [], { shareID: props.share.id, padding: 'py-1 px-2' })} {props.share.lastPush ? : 'never pushed'} {/* */} {/* , {pub.autopush ? 'auto' : 'manual'} */}
{ /* ({pub.encryptedFor ? `for ${pub.encryptedFor}` : 'unencrypted'}, last push:{' '} */ }
{encryptedFor() ? `for ${encryptedFor()}` : 'unencrypted'}
deleteShare(props.share.id)} confirmText={'Sure you want to delete this share?\n(local only, already published data will not change)'} /> last push: {' '} never
autopush : {props.share.autopush ? 'on' : 'off'} )} >
{/* PANEL CONTENT */} collapsed content
}> }>
never ⇒ {' '}
Push
Download as file Autopush {' '}
{({ ag: eachAg, jwkd: eachJwkString }, _logIdx) => { const agString = getAgentString(eachAg) return ( {`[${eachAg} : ${agString}] : ...${(eachJwkString as string).slice(-8)}`} ) }}
resetPrev()} label='Reset share history' confirmText={`Ignore previously published data and start over.\nYou will loose history that is not on this device.`} confirmButtonLabel='Forget history' />
Selectors
Without history 
setLoadAllRoots(true)} >
{(rootID) => { const block = useBlk(rootID) return {block.contentPlaintext} }} b - a)}> {([tag, count]) => { return ( {tag} {' '} ({count}) ) }} { /*
*/ } Selectors: {' '}
{JSON.stringify(props.share.selectors, undefined, 4).replaceAll(/\n\s*/g, ' ')}
Publication contains no data
}>
Result { /* Push & Purge */ }
(after you pushed):
Error generating: {resultThreadOrError.get().message}
} > {entityCount(lastWriteWins(resultThreadOrError.get()))} {resultThreadOrError.get().size} { /* {(rootID) => { // const content = useBlockAt(rootID, 'content') const block = useBlk(rootID) return (
- {block.contentPlaintext} ? ( kids)
) }}
*/ } Share is legacy - no derivation info
}>
Derivation Info
This share is derived from your local agent's key: {' '}
agent: {' '} {agent.ag} {' '} [ {agent.agentString} ] {' '}
ECDH publickey: {' '} {shortDerivationPublicKey()} {' '}
) }