import { html, render } from "/js/lib/lit-html.js"; import { Component } from "/js/components/component.js"; import { scrollLocks } from "/js/scrollLocks.js"; import { displayRelativeTime } from "/js/utils.js"; import { closeWithAnimation } from "/js/dialogHelpers.js"; import { enableDragToDismiss } from "/js/dragHelpers.js"; import { Signal, ReactiveStore, effect, untrack } from "/js/signals.js"; import { confirmModal } from "/js/modals/confirm.modal.js"; import { showToast } from "/js/toasts.js"; import "/js/components/app-icon.js"; import { getLocalRefsFromDraft, getImagesFromDraftPost, } from "/js/dataHelpers.js"; import { getDraftDeviceId } from "/js/drafts.js"; import { parseGifFromUrl } from "/js/embedHelpers.js"; import "/js/components/infinite-scroll-container.js"; function draftTagTemplate(name, label, { warning = false } = {}) { return html`${label}`; } function draftMediaTemplate({ images, gif, video }) { const thumbs = []; for (const image of images) { if (image.previewUrl) { thumbs.push( html`${image.alt`, ); } } if (gif) { thumbs.push( html`${gif.alt}`, ); } if (video?.exists) { thumbs.push( html`
`, ); } if (thumbs.length === 0) { return ""; } return html`
${thumbs}
`; } function draftItemTemplate({ draftView, onSelect, onDelete }) { const posts = draftView.posts; const firstPost = posts[0] ?? {}; const images = getImagesFromDraftPost(firstPost); const video = firstPost.embedVideos?.[0] ?? null; const gif = parseGifFromUrl(firstPost.embedExternals?.[0]?.uri ?? ""); const isOriginatingDevice = draftView.draft.deviceId === getDraftDeviceId(); const hasMissingMedia = posts.some( (draftPost) => getImagesFromDraftPost(draftPost).some((image) => !image.exists) || draftPost.embedVideos?.[0]?.exists === false, ); const hasQuotes = posts.some( (draftPost) => (draftPost.embedRecords?.length ?? 0) > 0, ); const replyCount = posts.length - 1; const showForeignMediaTag = !isOriginatingDevice && hasMissingMedia; const showMissingMediaTag = isOriginatingDevice && hasMissingMedia; return html`
onSelect(draftView)} @keydown=${(e) => { if (e.key === "Enter") { onSelect(draftView); } }} >
${displayRelativeTime(draftView.updatedAt)}
${firstPost.text ? html`
${firstPost.text}
` : ""} ${isOriginatingDevice ? draftMediaTemplate({ images, gif, video }) : ""}
`; } class DraftsDialog extends Component { connectedCallback() { if (this.initialized) { return; } this.setAttribute("data-dialog-wrapper", ""); this.scrollLock = null; this.state = new ReactiveStore("drafts-dialog"); this.state.$loadError = new Signal.State(false); this.state.$isLoadingMore = new Signal.State(false); this.innerHTML = ""; this._disposeEffect = effect(() => { this.render(); }); this._loadInitial(); this.initialized = true; } disconnectedCallback() { this._disposeEffect?.(); this._disposeEffect = null; } async _loadInitial() { if (untrack(() => this.dataLayer.derived.$hydratedDrafts.get()) !== null) { return; } try { await this.dataLayer.requests.loadDrafts({ reload: true }); } catch (error) { console.error("Failed to load drafts", error); this.state.$loadError.set(true); } } async _loadMore(resume) { if (untrack(() => this.state.$isLoadingMore.get())) { resume(); return; } this.state.$isLoadingMore.set(true); try { await this.dataLayer.requests.loadDrafts(); } catch (error) { console.error("Failed to load more drafts", error); } finally { this.state.$isLoadingMore.set(false); resume(); } } _onSelect(draftView) { // Start the close animation, but restore immediately without waiting for it this.close(); this.dispatchEvent( new CustomEvent("draft-selected", { detail: { draftView } }), ); } async _onDelete(draftView) { const confirmed = await confirmModal( "This draft will be permanently deleted.", { title: "Discard draft?", confirmButtonStyle: "danger", confirmButtonText: "Delete", }, ); if (!confirmed) return; try { await this.dataLayer.mutations.deleteDraft({ draftId: draftView.id, localRefs: getLocalRefsFromDraft(draftView.draft), }); this.dispatchEvent( new CustomEvent("draft-deleted", { detail: { draftId: draftView.id }, }), ); } catch (error) { console.error("Failed to delete draft", error); showToast("Failed to delete draft", { style: "error" }); } } render() { const data = this.dataLayer?.derived.$hydratedDrafts.get() ?? null; const draftViews = data?.drafts ?? null; const cursor = data?.cursor ?? null; const loadError = this.state.$loadError.get(); const isLoadingMore = this.state.$isLoadingMore.get(); render( html` { if (event.target === event.currentTarget) { this.close(); } }} @cancel=${(event) => { event.preventDefault(); this.close(); }} @close=${() => { this.scrollLock?.release(); this.scrollLock = null; this.dispatchEvent(new CustomEvent("dialog-closed")); }} >

Drafts

${loadError ? html`
Failed to load drafts
` : draftViews === null ? html`
` : draftViews.length === 0 ? html`
No drafts yet
` : html` this._loadMore(e.detail.resume)} > ${draftViews.map((draftView) => draftItemTemplate({ draftView, onSelect: (selected) => this._onSelect(selected), onDelete: (selected) => this._onDelete(selected), }), )} ${isLoadingMore ? html`
` : ""}
`}
`, this, ); } open() { this.scrollLock ??= scrollLocks.acquire({ target: this }); const dialog = this.querySelector("dialog"); if (dialog?.open) return; dialog.showModal(); enableDragToDismiss(dialog, { onDismiss: () => this.close(), scrollContainer: this.querySelector(".drafts-dialog-list"), dragHandle: this.querySelector(".drafts-dialog-header"), ignoreTouchTarget: (element) => element.closest("button") !== null, disableWhenKeyboardOpen: true, }); } close() { return closeWithAnimation(this.querySelector("dialog")); } } DraftsDialog.register();