import type { CarReader } from '@ipld/car' import type { Applog, PromiseType } from '@wovin/core' import type { Component, JSX, ParentComponent, Resource } from 'solid-js' import type { otherIcons } from '../../unocss.safelist' import type { retrievePubDataWithExtras } from '../ipfs/store-sync' import type { DivProps } from '../ui/utils-ui' import { createTimer } from '@solid-primitives/timer' import { A } from '@solidjs/router' import { decodePubFromCar, isEncryptedApplog, queryAndMap, querySingleAndMap, ThreadInMemory } from '@wovin/core' import { mapArrayToObj } from '@wovin/utils/types' import { Logger } from 'besonders-logger' import classNames from 'classnames' import { partition, size, uniqueId } from 'lodash-es' import { createEffect, createMemo, createResource, createSignal, For, mergeProps, onMount, Show, splitProps, Suspense } from 'solid-js' import { Dynamic } from 'solid-js/web' import { phosphorIcons } from '../../unocss.safelist' import { initialized, setStorageError } from '../appInit' import { useAgent } from '../data/agent/AgentState' import { BlockVM } from '../data/VMs/BlockVM' import { retrievePubToThread } from '../ipfs/retrievePubToThread' import { getPubExtraInfo } from '../ipfs/store-sync' import { DefaultCenter, DefaultFull, DefaultSmall } from '../types/unocss-utils' import { getStorageIssues, isOnline, isStorageReachable } from '../ui/online-second' import { useRawThread } from '../ui/reactive' import { createAsyncButtonHandler, createAsyncMemo, explorerUrl, isColorDark, makeGlobalHover, mergeStyles, stopPropagation, stringToColor, svgElementToDataURL, useLocationNavigate, } from '../ui/utils-ui' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) export const ShortID: Component<{ id: string, href?: string }> = (props) => { return ( {/* max-w-5 truncate */} {props.id.slice(-7)} ) } export const CopyableID: Component<{ id: string, label?: string, buttonSize?: string }> = (_props) => { const props = mergeProps({ buttonSize: '5' }, _props) return ( {props.id.slice(-7)} {' '} {/* t-[2px] */} ) } export const Spinner: ParentComponent<{ size?: string, color?: string }> = (props) => { return (
{props.children}
) } export const DynamicColored: ParentComponent< { text: string } & JSX.HTMLAttributes > = (props) => { const [ourProps, otherProps] = splitProps(props, ['text', 'children']) const color = createMemo(() => stringToColor(ourProps.text)) return ( {ourProps.children || ourProps.text} ) } export const [/* _idHover */, useIdHover] = makeGlobalHover((active: boolean, myID: boolean) => ({ 'data-test': 123, 'class': classNames( 'transition-duration-300 transition-property-[opacity]', (active || myID) ? 'opacity-100' : 'opacity-40', myID && 'font-bold underline', ), })) // TODO: useChangeHighlight (?) export const HighlightSameOnHover: ParentComponent< { id: string } & JSX.HTMLAttributes > = (props) => { const [ourProps, otherProps] = splitProps(props, ['id', 'children']) return ( {ourProps.children} ) } export const IconifyNames = mapArrayToObj(phosphorIcons) type PhosphorNames = typeof phosphorIcons[number] type OtherNames = typeof otherIcons[number] export type IconName = PhosphorNames | OtherNames | null // https://icon-sets.iconify.design/ph/ export const Iconify: Component<{ name: IconName size?: number | string scale?: number | string class?: string style?: JSX.CSSProperties customSVG?: () => SVGElement // URLencoded svg [x: string]: any }> = (fullProps) => { const [props, otherProps] = splitProps(fullProps, ['class', 'size', 'scale', 'name', 'style', 'customSVG']) const cursorClass = (typeof otherProps.onclick === 'function') ? 'cursor-pointer' : '' const sizes = createMemo(() => props.size ? ( (typeof props.size === 'string' && props.size.includes(' ')) ? props.size.split(' ') : [props.size, props.size] ) : [4, 4], ) const customSVGdataURLstring = () => { if (props.customSVG && props.customSVG()) { DEBUG('customSVG', { propsstyle: props.style }, svgElementToDataURL(props.customSVG())) return `url(${svgElementToDataURL(props.customSVG())})` } } const iconUUID = `iconify-${uniqueId()}` return (
) } export const ResourceSpinner: ParentComponent<{ resource: Resource, spinner?: JSX.Element }> = (props) => { createEffect(() => props.resource.error && ERROR(`ResourceSpinner error:`, props.resource.error)) // otherwise, we just render but don't log return ( } >
{props.resource.error.message} )} > {props.children} ) } export const KidCount: Component<{ block: string }> = (props) => { const rawDS = useRawThread() const [kidCount] = createDeferredResource(() => props.block, async (block) => { // if (Math.random() > 0.5) throw new Error(`TEST err`) // for (let index = 0; index < 1000000; index++) { // Math.random() // // DEBUG(`T`) // } return BlockVM.get(block, rawDS).recursiveKidCount }) return ( {kidCount()} ) } export function* getPersistenceStory() { // https://storage.spec.whatwg.org/#persistence // new situation apparently if (navigator.storageBuckets) { // chrome only ? const inboxBucket = yield navigator.storageBuckets.open('note3keys', { durability: 'strict', // Or `'relaxed'`. persisted: true, // Or `false`. }) } const wasPersisted = yield navigator.storage.persisted() const persistGranted = yield navigator.storage.persist() const estimate = yield navigator.storage.estimate() WARN(`Persisted storage status:`, { persistGranted, wasPersisted, estimate }) return { persistGranted, wasPersisted } } export async function askForStoragePerms() { const isChromium = !!(window as any).chrome // https://stackoverflow.com/a/62797156/ if (isChromium) { VERBOSE.force('try grant notification') const notificationGrantHack = await Notification.requestPermission() // https://stackoverflow.com/a/53573849/2919380 DEBUG.force('notificationGrantHack result', notificationGrantHack) // will be string 'granted' upon request } const persistStillNotGranted = !(await navigator.storage.persist()) if (persistStillNotGranted) { getPersistenceStory() window.location.reload() } else { DEBUG('Storage permissions acquired :)') setStorageError(null) } } export const StoragePermsErr: Component<{ bold: boolean [x: string]: any // TODO keyof typeof Iconify props somehow }> = (fullProps) => { const isChromium = !!(window as any).chrome // https://stackoverflow.com/a/62797156/ return (
Persistent storage permission is not granted, your data might not persist.
In chromium, there is no way to ask for this permission. 🙄
Awkwardly enough , Notification Permissions are the best way to (maybe) get persistent storage, so:
You need to grant the permission in the site settings in your browser.
Click to {isChromium ? 'request Notification' : 'check for'} permissions
) } type CloudIcon = 'cloud-check' | 'cloud-slash' | 'cloud-check-bold' | 'cloud-slash-bold' | 'cloud-warning' | 'cloud-warning-bold' export const Online: Component<{ bold: boolean [x: string]: any // TODO keyof typeof Iconify props somehow }> = (fullProps) => { const [props, otherProps] = splitProps(fullProps, ['bold']) const iconName = createMemo(() => { const bold = props.bold ? '-bold' : '' const warningOrCheck = !isStorageReachable() ? '-warning' : '-check' DEBUG({ bold, warning: warningOrCheck }) return (isOnline() ? `cloud${warningOrCheck}${bold}` : `cloud-slash${bold}`) as CloudIcon }) const tt = createAsyncMemo(async () => { if (isOnline()) { if (isStorageReachable()) { return 'All good' } else { if (!initialized()) return online, but storage not reachable and app not initialized const storageSituation = await getStorageIssues() LOG({ storageSituation }) return (
online, but storage not reachable {([eachURL, eachRes]) => ( {eachURL} )}
) // TODO more specific feedback about failures } } else { return 'Offline: Check your device connection' } }) return ( LOG(await getStorageIssues())} {...otherProps} name={iconName()} />
{tt()}
) } export function createDeferredResource( source: S | false | null | (() => S | false | null), fetcher: (arg: S) => Promise, { deferOnUpdate }: { deferOnUpdate?: boolean } = {}, ) { let loadedOnce = false // ? this was a preemptive measure, not sure if needed return createResource(source, async (source) => { // await new Promise(resolve => queueMicrotask(resolve)) // (i) https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide#timeout_and_microtask_example DEBUG(`[createDeferredResource] loadedOnce=${loadedOnce}, deferOnUpdate=${deferOnUpdate}`) if (!loadedOnce || deferOnUpdate) { await new Promise(resolve => setTimeout(resolve)) // i.e. defer } loadedOnce = true // await new Promise(resolve => setTimeout(resolve, 1000)) // for testing return fetcher(source) // const result = await fetcher(source) // DEBUG(`[createDeferredResource] result:`, result) // return result }) } // export function createDeferredMemo(func: () => R) { // const [val, setVal] = createSignal(null) // setTimeout(() => { // // await new Promise(resolve => queueMicrotask(resolve)) // (i) https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide#timeout_and_microtask_example // await new Promise(resolve => setTimeout(resolve)) // i.e. defer // // await new Promise(resolve => setTimeout(resolve, 1000)) // for testing // return func() // }) // return val // } export const DeleteWithConfirm: Component< DivProps & { handler: () => Promise | void label?: string confirmText?: string confirmButtonLabel?: string buttonVariant?: string buttonIcon?: string certaintySeconds?: number } > = (allProps) => { const [props, restProps] = splitProps(allProps, [ 'label', 'handler', 'confirmText', 'certaintySeconds', 'buttonVariant', 'buttonIcon', 'confirmButtonLabel', ]) const [opened, setOpened] = createSignal(false) const [openedSecs, setOpenedSecs] = createSignal(0) createTimer(() => setOpenedSecs(s => s + 1), () => opened() && 1000, setInterval) createEffect(() => !opened() && setOpenedSecs(0)) const deleteButtonProps = createAsyncButtonHandler(props.handler) return ( setOpened(true)} onsl-hide={() => setOpened(false)} > {props.label}
{props.confirmText ?? 'You sure bout\' dat?'}
{props.certaintySeconds && openedSecs() < props.certaintySeconds ? `Think ${(props.certaintySeconds - openedSecs())}s` : (props.confirmButtonLabel || 'Yes, Delete')}
) } export const DeleteWithConfirmDialog: Component< DivProps & { handler: () => Promise | void label?: string dialogText?: string | Element dialogLabel?: string certaintySeconds?: number } > = (allProps) => { const [props, restProps] = splitProps(allProps, ['label', 'handler', 'dialogText', 'certaintySeconds', 'dialogLabel']) const [opened, setOpened] = createSignal(false) const [openedSecs, setOpenedSecs] = createSignal(0) createTimer(() => setOpenedSecs(s => s + 1), () => opened() && 1000, setInterval) createEffect(() => !opened() && setOpenedSecs(0)) const deleteButtonProps = createAsyncButtonHandler(props.handler) let dialogRef return ( <> dialogRef?.show()}>
{props.label} setOpened(true)} onsl-hide={() => setOpened(false)} >
{props.dialogText ?? 'You sure bout\' dat?'}
{props.certaintySeconds && openedSecs() < props.certaintySeconds ? `Think ${(props.certaintySeconds - openedSecs())}s` : 'Yes, Delete'}
) } export const PubInfoBox: Component<{ pubID?: string | null, pinCID?: string, label?: string, carFile?: CarReader }> = (props) => { const appThread = useRawThread() const [fetchedPub] = createDeferredResource( () => ({ pubID: props.pubID, pinCID: props.pinCID, carFile: props.carFile }), async ({ pubID, pinCID, carFile }) => /* : Promise>> */ { DEBUG(`PubInfoBox.fetch`, { pubID, pinCID }) if (!pubID && !pinCID && !carFile) return null // if (matchingSubOrPub()) return null // throw new Error(`TEST pubinfo err`) if (carFile) { const { applogs, info } = await decodePubFromCar(carFile) // TODO: display INFO const [encryptedLogs, plainLogs] = partition(applogs, isEncryptedApplog) const thread = ThreadInMemory.fromArray((plainLogs as any) as Applog[], `preview-carfile`) return { info, thread, encryptedCount: encryptedLogs.length, ...getPubExtraInfo(appThread, thread), } } else { const thread = ThreadInMemory.empty(`pubInfo-${pinCID || pubID}`) const { cid, info } = await retrievePubToThread(thread, pubID, { pinCID }, appThread) return { info, thread, cid, // encryptedCount, ...getPubExtraInfo(appThread, thread), } } }, ) return (
) } export const PubInfos: Component<{ pub: PromiseType> }> = (props) => { const agent = useAgent() const info = createMemo(() => props.pub.info) // const encryptedApplogCount = info().maybeEncryptedApplogs.filter(log => !log?.ag).length // encrypted logs are byte arrays, so they have no .ag prop // HACKish // const encryptedFor= filterAndMap(lastWriteWins(info().thread), { en: publicationNameString, at: 'pub/encryptedFor' }, 'vl') // no need for a filter just doing a find below instead const infoLogs = createMemo(() => info().logs) const encryptedFor = queryAndMap(infoLogs(), { at: 'pub/sharedKey' }, 'en') const pubName = querySingleAndMap(infoLogs(), { at: 'pub/name' }, 'vl') // TODO: editable :) const pubBy = querySingleAndMap(infoLogs(), { at: 'pub/name' }, 'en') // TODO: editable :) const sharedKeyLog: Applog = agent.getSharedKeyApplogForCurrentAgent(infoLogs()) DEBUG({ sharedKeyLog, encryptedFor }) const isEncryptedForMe = createMemo(() => encryptedFor.includes(agent.ag)) const encryptedForWithoutMe = createMemo(() => encryptedFor.filter(eachEn => eachEn !== agent.ag) .map(eachOtherEn => [eachOtherEn, `${agent.getKnownAgents().get()?.get(eachOtherEn)?.agString || 'unknown'}`]), ) // if (sharedKeyLog) encryptedFor = sharedKeyLog.en // TODO check agents for if we know their agent string. have any atoms from them, and have a public derivation key from them return ( {pubName.get()} {encryptedFor.toString()} {props.pub.agents.join(', ') /* TODO sharedKeyLogs */} ➜ {props.pub.cid.toString()} {`${props.pub.thread.size} Applogs `} ({props.pub.newLogs ? `of which ${props.pub.newLogs} are not in the local data` : `all exist in your local data`})
encrypted logs for: {isEncryptedForMe() ? ( {` you & ${encryptedForWithoutMe().length} others`}
{(agentInfo, _index) =>
{agentInfo[0]}:{agentInfo[1]}
}
) : ' NOT you: '}
Related to {' '} {props.pub.entityOverlapCount.get()} entities {' '} in your local data
) } export const NameValueBadge: ParentComponent<{ name: string, variant?: string }> = (props) => { return ( {props.name}: {' '} {props.children} ) } export const KeyValueDiv: ParentComponent = (allProps) => { const [props, restProps] = splitProps(allProps, ['label', 'children']) return (
{props.label}: {props.children}
) } export const ButtonGroup: Component< DivProps & { size?: typeof DefaultSmall buttonData: any } > = (allProps) => { const [props, topProps] = splitProps(allProps, ['size', 'buttonData']) // const { size = DefaultSmall } = props // ! only include non reactive props return ( {({ restProps, icon, click }) => ( )} ) } export const AgentButton: Component< DivProps & { size?: typeof DefaultSmall buttonData: any } > = (allProps) => { const [props, topProps] = splitProps(allProps, ['size', 'buttonData']) // const { size = DefaultSmall } = props // ! only include non reactive props const { restProps, icon, click, text, title } = props.buttonData return ( {text} ) } export const FlexBetween: ParentComponent< JSX.HTMLAttributes & { w?: typeof DefaultFull items?: typeof DefaultCenter } > = (allProps) => { // const { children, items = DefaultCenter, w = DefaultFull, class: classes, ...restProps } = props const [props, restProps] = splitProps(allProps, ['w', 'children', 'items', 'class']) return (
{props.children}
) } export const FileSelect: ParentComponent<{ onFile: (file: FileList) => any }> = (props) => { let fileInput: HTMLInputElement function onFileChange(this: HTMLInputElement, event) { event.preventDefault() DEBUG(`[FileSelect] onFileChange`, { self: this, event }) props.onFile(this.files) } onMount(() => { fileInput.addEventListener('change', onFileChange, false) return () => fileInput?.removeEventListener('change', onFileChange, false) }) return (
{/* {props.children}:  */} fileInput.click()}> {props.children}
) } export const RedirectWorkaround: ParentComponent< DivProps & { url: string replace?: boolean } > = (props) => { const [ourProps, otherProps] = splitProps(props, ['url']) const { locnav, location, navigate } = useLocationNavigate() // createEffect(()=> { if (isPreviewOrSearch()) { queueMicrotask(() => navigate(props.url, { replace: props.replace ?? true })) // }}) return (
Redirecting to {' '} {ourProps.url} {props.children}
) }