import type { EntityID } from '@wovin/core/applog' import type { Component } from 'solid-js' import type { phosphorIcons } from '../../unocss.safelist' import type { QuickSharePhase } from '../ipfs/quick-share' import type { DivProps } from '../ui/utils-ui' import { useLocation, useNavigate } from '@solidjs/router' import { Logger } from 'besonders-logger' import classNames from 'classnames' import { createMemo, createSignal, For, onMount, Show, splitProps } from 'solid-js' import { useAgent } from '../data/agent/AgentState' import { insertApplogs } from '../data/ApplogDB' import { useBlockContext } from '../data/block-ui-helpers' import { copyToClipboard } from '../data/block-ui-helpers-paste' import { addBlock, getAddBlockRelationLogs, getRecursiveKidsJSON, getRecursiveKidsOPML, indentBlk, outdentBlk, removeBlockRelAndMaybeDelete, } from '../data/block-utils' import { useBlk } from '../data/VMs/BlockVM' import { REL, useRel } from '../data/VMs/RelationVM' import { quickShareBlock } from '../ipfs/quick-share' import { useAppSettings } from '../ui/app-settings' import { useCurrentThread, useRawThread, useReadOnlyState, useRelation } from '../ui/reactive' import { focusViewOnBlock, isTouchDevice, makeNote3Url, notifyToast, onClickOrLongPress, useLocationNavigate } from '../ui/utils-ui' import { FlexBetween, Iconify, Spinner } from './mini-components' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.DEBUG) // eslint-disable-line unused-imports/no-unused-vars const [openSignal, setShareChooserOpen] = createSignal(false) const [shareChooserBlockID, setShareChooserBlockID] = createSignal(null) const [onShareSelection, setOnShareSelect] = createSignal(choice => DEBUG('share choice', choice)) export const ShareChooser: Component = () => { const onShow = evt => DEBUG('show share chooser', evt) const { shares } = useAgent() const rawThread = useRawThread() const onSel = createMemo(() => evt => onShareSelection()(evt)) // Quick Share state const [quickSharePhase, setQuickSharePhase] = createSignal('idle') const [quickShareUrl, setQuickShareUrl] = createSignal(null) const [quickShareError, setQuickShareError] = createSignal(null) const [withoutHistory, setWithoutHistory] = createSignal(true) const blockID = createMemo(() => shareChooserBlockID()) const block = createMemo(() => { const id = blockID() return id ? useBlk(id) : null }) const resetQuickShare = () => { setQuickSharePhase('idle') setQuickShareUrl(null) setQuickShareError(null) } const handleQuickShare = async () => { const id = blockID() if (!id) return resetQuickShare() try { const result = await quickShareBlock(rawThread, id, { withoutHistory: withoutHistory(), onPhaseChange: setQuickSharePhase, }) setQuickShareUrl(result.url) copyToClipboard(result.url) } catch (err) { ERROR('[ShareChooser] Quick share failed:', err) setQuickSharePhase('error') setQuickShareError((err as Error).message || 'Unknown error') } } const handleClose = () => { // Show toast if we have a successful quick share URL const blk = block() const id = blockID() if (quickSharePhase() === 'done' && quickShareUrl() && blk) { const preview = blk.contentPlaintext?.slice(0, 30) || id notifyToast(`Quick share link for '${preview}${blk.contentPlaintext?.length > 30 ? '...' : ''}' copied`, 'success') } setShareChooserOpen(false) // Reset state after close setTimeout(resetQuickShare, 300) } return ( Via Publication Quick Share No publications configured. Create one in Settings.} > {({ id, name }) => ( {name} - {id.slice(-7)} )}

Create a shareable link for this block and its children.

setWithoutHistory(e.target.checked)} > Without history Share Now
Preparing snapshot...
Uploading to storage...
Uploaded successfully!
{quickShareError()} Try Again
) } export const BulletMenu: Component< DivProps & { blockID: EntityID onlyShowOnHover?: boolean iconProps: { scale: number, name: typeof phosphorIcons[number], icon: any } showBlockSettings: () => void } > = (allprops) => { const [props, restProps] = splitProps(allprops, ['blockID', 'iconProps', 'showBlockSettings']) const { blockID, showBlockSettings } = props // ! non-reactive if (!blockID) return null const readOnly = useReadOnlyState() const agent = useAgent() const { locnav, location, navigate } = useLocationNavigate() // const { onCopyLink, onCopyContent, onCopyID, onHistory, onFocus, onDelete, onIndent, onOutent, onReply } = handlers // const navigate = useNavigate() const block = useBlk(blockID) const hasKids = createMemo(() => block.kidRelations.length) const appSettings = useAppSettings() let triggerRef let bulletIconRef let menuRef const pubChooserRefHolder = { ref: null } const handlers = selectionHandlersForBulletMenu(setShareChooserOpen, setOnShareSelect, showBlockSettings) const onSelection = (evt) => { const itemValue = evt.detail.item.value const itemHandlerParams = evt.detail.item.attributes?.['data-handler-param']?.nodeValue if (handlers[itemValue]) { handlers[itemValue](evt, itemHandlerParams) } else { WARN('unknown bullet menu item value') } } let preventClickHack = false // HACK - https://chatgpt.com/share/67349aed-1d98-800b-81ca-91cbcb368b9f const onClick = (evt: MouseEvent) => { if (preventClickHack) evt.stopPropagation() } const onClickOrLong = (evt: MouseEvent, long: boolean) => { DEBUG(`BULLET ${long ? 'LONG' : 'CLICK'}`, evt) if (readOnly()) return if (evt.button === 0 && block.isTodo && !long) { evt.stopPropagation() evt.stopImmediatePropagation() evt.preventDefault() block.setTodoDone(!block.isTodoDone) preventClickHack = true setTimeout(() => { preventClickHack = false }, 0) } } onMount(() => onClickOrLongPress(bulletIconRef, () => onClickOrLong)) // HACK: https://github.com/solidjs/solid/discussions/722 const IconID = `BulletMenu_${blockID}` return ( {/* */} { if (e.button === 1) { e.preventDefault() focusViewOnBlock({ id: blockID }, locnav) } }} onContextMenu={(e) => { if (isTouchDevice()) return e.preventDefault() e.stopPropagation() e.target.parentElement.show() }} /> {/* */} {/* //TODO get highligting on roll over better */} Outdent Indent Zoom To
Middle click
Delete {' '} {/* TODO: "Remove" when the block has other placements */} {/* Copy note3:// link */} Copy ID Copy Link Copy Share Link Copy Content Copy html ul tag Copy md (for pasting into logseq, zettlr etc) Copy OPML (for pasting into WF, note3, etc) Create smartlist Reply Add To Home Block Settings History
) } function selectionHandlersForBulletMenu(setShareChooserOpen, setOnShareSelect, showBlockSettings) { const rawThread = useRawThread() const currentThread = useCurrentThread() const blockContext = useBlockContext() const relation = blockContext.relationToParent && useRelation(blockContext.relationToParent) const { parentContext } = blockContext const grandParentRelID = parentContext?.relationToParent ?? null // const grandParentVM = grandParentID ? BlockVM.get(grandParentID, rawDS) : null const grandParentRelVM = useRel(grandParentRelID, currentThread) const grandParentID = grandParentRelVM?.childOf const blockID = blockContext.id const location = useLocation() const navigator = useNavigate() const appSettings = useAppSettings() const onCopyLink = (evt: MouseEvent) => { DEBUG('copyLink', evt) const pub = useAgent().w3NamePublic // TODO: smart-select pub & UX for other cases // const n3url = `note3://publication/${pub}/?focus=${blockID}` const n3url = makeNote3Url({ pub, block: blockID }) DEBUG('Note3 url', n3url) copyToClipboard(n3url) } const onReply = (evt: MouseEvent) => { const blkVM = useBlk(blockID, currentThread) const quotedContent = blkVM.selectionText ? `>${blkVM.selectionText}` : '' const added = addBlock({ thread: useCurrentThread(), asChildOf: blockID, content: quotedContent }) const newBlkVM = useBlk(added, currentThread) VERBOSE('new reply block', added, { blkVM, newBlkVM, selection: blkVM.selectionText }) const currentRelation = newBlkVM.currentRelation const replyApplog = { en: currentRelation.en, at: REL.isReply, vl: true, } DEBUG('new block as reply', replyApplog, added, { blkVM, newBlkVM, currentRelation }) insertApplogs(currentThread, [replyApplog]) } const onCopyShareLink = (evt: MouseEvent, mode: 'opml' | 'md' | 'html' = 'opml') => { DEBUG('opening share chooser for', blockID) const onSelect = (selEvt) => { DEBUG(blockID, selEvt) const share = selEvt.detail.item.value DEBUG(blockID, 'selected', share) const n3url = makeNote3Url({ pub: share, block: blockID, previewPub: true }) // HACK:use preview until ?pub= UX is better DEBUG('Note3 url', n3url) copyToClipboard(n3url) setShareChooserOpen(false) } setOnShareSelect(() => onSelect) setShareChooserBlockID(blockID) setShareChooserOpen(true) } const onCopyContent = (evt: MouseEvent, mode: 'opml' | 'md' | 'html' = 'opml') => { const formattedContent = mode === 'opml' ? getRecursiveKidsOPML(blockID) : mode === 'md' ? getRecursiveKidsJSON(blockID, 'md') : getRecursiveKidsJSON(blockID, 'html') DEBUG('Note3 content', blockID, { evt, formattedContent }) copyToClipboard(formattedContent) } function createSmartlist() { const block = useBlk(blockID, rawThread) block.toggleSmartlist() } const settings = () => { DEBUG('show settings', blockID) showBlockSettings() } const addToHome = () => { if (!appSettings.homeBlock) return WARN('no homeBlock set') const addRelationLogs = getAddBlockRelationLogs({ thread: currentThread, asChildOf: appSettings.homeBlock, blockID, focus: true, bottom: true, }) DEBUG({ addRelationLogs }) insertApplogs(currentThread, addRelationLogs) } const returnHandlers = { copyID: (_evt: MouseEvent) => copyToClipboard(blockID), copyLink: onCopyLink, copyShareLink: onCopyShareLink, copyContent: onCopyContent, reply: onReply, history: () => blockContext.setPanel({ type: 'history' }), focus: () => focusViewOnBlock({ id: blockID }, { location, navigate: navigator }), delete: () => removeBlockRelAndMaybeDelete(currentThread, blockID, relation?.get().en), indent: () => indentBlk(currentThread, relation?.get()), outdent: () => outdentBlk(currentThread, relation?.get(), grandParentID), addToHome, settings, createSmartlist, } as const return returnHandlers }