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 (
)
}