<commit_before>import { Textcomplete } from "@textcomplete/core"
import { TextareaEditor } from "@textcomplete/textarea"
import { component, html, useCallback, useEffect, useRef, useState } from "haunted"
import { nothing } from "lit-html"
import { repeat } from "lit-html/directives/repeat.js"
import { authStore, useStore } from "../ctx.js"
import { ref } from "../directives/ref.js"
import { request, subscribe } from "../http.js"
import "./intersectable-comp.js"
import "./post-item.js"
import "./toast-item.js"

const pageSize = 10

export default function () {
    return html`<home-page></home-page>`
}

function HomePage() {
    const [timeline, setTimeline] = useState([])
    const [timelineEndCursor, setTimelineEndCursor] = useState(null)
    const [fetchingTimeline, setFetchingTimeline] = useState(timeline.length === 0)
    const [err, setErr] = useState(null)
    const [loadingMore, setLoadingMore] = useState(false)
    const [noMoreTimelineItems, setNoMoreTimelineItems] = useState(false)
    const [endReached, setEndReached] = useState(false)
    const [queue, setQueue] = useState([])
    const [toast, setToast] = useState(null)

    const onTimelineItemCreated = useCallback(ev => {
        const payload = ev.detail
        setTimeline(tt => [payload, ...queue, ...tt])
        setQueue([])
    }, [queue])

    const onNewTimelineItemArrive = useCallback(ti => {
        setQueue(tt => [ti, ...tt])
    }, [])

    const onRemovedFromTimeline = useCallback(ev => {
        const payload = ev.detail
        setTimeline(tt => tt.filter(ti => ti.id !== payload.timelineItemID))
    }, [])

    const onPostDeleted = useCallback(ev => {
        const payload = ev.detail
        setTimeline(tt => tt.filter(ti => ti.post.id !== payload.id))
    }, [])

    const onQueueBtnClick = useCallback(() => {
        setTimeline(tt => [...queue, ...tt])
        setQueue([])
    }, [queue])

    const loadMore = useCallback(() => {
        if (loadingMore || noMoreTimelineItems) {
            return
        }

        setLoadingMore(true)
        fetchTimeline(timelineEndCursor).then(({ items: timeline, endCursor }) => {
            setTimeline(tt => [...tt, ...timeline])
            setTimelineEndCursor(endCursor)

            if (timeline.length < pageSize) {
                setNoMoreTimelineItems(true)
                setEndReached(true)
            }
        }, err => {
            const msg = "could not fetch more timeline items: " + err.message
            console.error(msg)
            setToast({ type: "error", content: msg })
        }).finally(() => {
            setLoadingMore(false)
        })
    }, [loadingMore, noMoreTimelineItems, timelineEndCursor])

    useEffect(() => {
        setFetchingTimeline(true)
        fetchTimeline().then(({ items: timeline, endCursor }) => {
            setTimeline(timeline)
            setTimelineEndCursor(endCursor)

            if (timeline.length < pageSize) {
                setNoMoreTimelineItems(true)
            }
        }, err => {
            console.error("could not fetch timeline:", err)
            setErr(err)
        }).finally(() => {
            setFetchingTimeline(false)
        })
    }, [])

    useEffect(() => subscribeToTimeline(onNewTimelineItemArrive), [])

    return html`
        <main class="container home-page">
            <h1>Timeline</h1>
            <post-form @timeline-item-created=${onTimelineItemCreated}></post-form>
            ${err !== null ? html`
            <p class="error" role="alert">Could not fetch timeline: ${err.message}</p>
            ` : fetchingTimeline ? html`
            <p class="loader" aria-busy="true" aria-live="polite">Loading timeline... please wait.<p>
                    ` : html`
                    ${queue.length !== 0 ? html`
                    <button class="queue-btn" @click=${onQueueBtnClick}>${queue.length} new timeline items</button>
                    ` : nothing}
                    ${timeline.length === 0 ? html`
                    <p>0 timeline items</p>
                    ` : html`
                    <div class="posts" role="feed">
                        ${repeat(timeline, ti => ti.id, ti => html`<post-item .post=${ti.post} .type=${"timeline_item"}
                            .timelineItemID=${ti.id} @removed-from-timeline=${onRemovedFromTimeline}
                            @resource-deleted=${onPostDeleted}></post-item>`)}
                    </div>
                    ${!noMoreTimelineItems ? html`
                    <intersectable-comp @is-intersecting=${loadMore}></intersectable-comp>
                    <p class="loader" aria-busy="true" aria-live="polite">Loading timeline... please wait.<p>
                            ` : endReached ? html`
                            <p>End reached.</p>
                            ` : nothing}
                            `}
                            `}
        </main>
        ${toast !== null ? html`<toast-item .toast=${toast}></toast-item>` : nothing}
    `
}

customElements.define("home-page", component(HomePage, { useShadowDOM: false }))

const reMention = /\B@([\-+\w]*)$/

function PostForm() {
    const [auth] = useStore(authStore)
    const [content, setContent] = useState("")
    const [fetching, setFetching] = useState(false)
    const [nsfw, setNSFW] = useState(false)
    const [isSpoiler, setIsSpoiler] = useState(false)
    const [spoilerOf, setSpoilerOf] = useState("")
    const spoilerOfDialogRef = useRef(null)
    const [initialTextAreaHeight, setInitialTextAreaHeight] = useState(0)
    const textAreaRef = useRef(null)
    const textcompleteRef = useRef(null)
    const [toast, setToast] = useState(null)

    const dispatchTimelineItemCreated = post => {
        this.dispatchEvent(new CustomEvent("timeline-item-created", { bubbles: true, detail: post }))
    }

    const onSubmit = useCallback(ev => {
        ev.preventDefault()

        setFetching(true)
        createTimelineItem({
            content,
            spoilerOf: spoilerOf.trim() === "" ? null : spoilerOf.trim(),
            nsfw,
        }).then(ti => {
            ti.post.user = auth.user
            setContent("")
            setNSFW(false)
            setIsSpoiler(false)
            setSpoilerOf("")
            textcompleteRef.current.hide()
            textAreaRef.current.style.height = initialTextAreaHeight + "px"

            dispatchTimelineItemCreated(ti)
        }, err => {
            const msg = "could not create post: " + err.message
            console.error(msg)
            setToast({ type: "error", content: msg })
        }).finally(() => {
            setFetching(false)
        })
    }, [content, nsfw, spoilerOf, auth, textAreaRef, initialTextAreaHeight])

    const onTextAreaInput = useCallback(() => {
        setContent(textAreaRef.current.value)
        updateTextAreaHeight()
    }, [content, textAreaRef])

    const updateTextAreaHeight = useCallback(() => {
        textAreaRef.current.style.height = initialTextAreaHeight + "px"
        if (textAreaRef.current.value !== "") {
            textAreaRef.current.style.height = Math.max(textAreaRef.current.scrollHeight, initialTextAreaHeight) + "px"
        }
    }, [initialTextAreaHeight, textAreaRef])

    const onNSFWInputChange = useCallback(ev => {
        const checked = ev.currentTarget.checked
        setNSFW(checked)
    }, [])

    const onIsSpoilerInputChange = useCallback(ev => {
        const checked = ev.currentTarget.checked
        setIsSpoiler(checked)
        if (!checked) {
            setSpoilerOf("")
        } else {
            spoilerOfDialogRef.current.showModal()
        }
    }, [spoilerOfDialogRef])

    const onSpoilerOfInput = useCallback(ev => {
        setSpoilerOf(ev.currentTarget.value)
    }, [])

    const onSpoilerOfDialogClose = useCallback(() => {
        if (spoilerOf.trim() === "") {
            setSpoilerOf("")
            setIsSpoiler(false)
        }
    }, [spoilerOf])

    const onSpoilerOfFormSubmit = useCallback(ev => {
        ev.preventDefault()
        spoilerOfDialogRef.current.close()
    }, [])

    const onSpoilerOfCancelBtnClick = useCallback(() => {
        setSpoilerOf("")
        setIsSpoiler(false)
        spoilerOfDialogRef.current.close()
    }, [spoilerOfDialogRef])

    useEffect(() => {
        if (spoilerOfDialogRef.current !== null && !(window.HTMLDialogElement || spoilerOfDialogRef.current.showModal)) {
            import("dialog-polyfill").then(m => m.default).then(dialogPolyfill => {
                dialogPolyfill.registerDialog(spoilerOfDialogRef.current)
            }).catch(err => {
                console.error("could not import dialog polyfill:", err)
            })
        }
    }, [spoilerOfDialogRef])

    useEffect(() => {
        if (textAreaRef.current === null) {
            return
        }

        const editor = new TextareaEditor(textAreaRef.current)
        textcompleteRef.current = new Textcomplete(editor, [{
            match: reMention,
            search: async (term, cb) => {
                cb(await fetchUsernames(term).then(page => page.items, err => {
                    console.error("could not fetch mentions usernames:", err)
                    return []
                }))
            },
            replace: username => `@${username} `,
        }])

        setInitialTextAreaHeight(textAreaRef.current.scrollHeight)

        return () => {
            textcompleteRef.current.destroy()
        }
    }, [textAreaRef, textcompleteRef])

    // Share Target.
    useEffect(() => {
        const params = new URLSearchParams(window.location.search.slice(1))
        const preContent = []
        let cleanup = false
        if (params.has("text")) {
            cleanup = true
            const text = decodeURIComponent(params.get("text")).trim()
            if (text !== "") {
                preContent.push(text)
            }
        }
        if (params.has("url")) {
            cleanup = true
            const url = decodeURIComponent(params.get("url"))
            if (url !== "") {
                preContent.push(url)
            }
        }

        if (preContent.length !== 0) {
            setContent(preContent.join(" "))
            updateTextAreaHeight()
        }

        if (cleanup) {
            history.replaceState(history.state, document.title, "/")
        }
    }, [])

    return html`
        <form class="post-form${content !== "" ? " has-content" : ""}" name="post-form" @submit=${onSubmit}><commit_msg>web/app: update post form textarea height on set content<commit_after>import { Textcomplete } from "@textcomplete/core"
import { TextareaEditor } from "@textcomplete/textarea"
import { component, html, useCallback, useEffect, useRef, useState } from "haunted"
import { nothing } from "lit-html"
import { repeat } from "lit-html/directives/repeat.js"
import { authStore, useStore } from "../ctx.js"
import { ref } from "../directives/ref.js"
import { request, subscribe } from "../http.js"
import "./intersectable-comp.js"
import "./post-item.js"
import "./toast-item.js"

const pageSize = 10

export default function () {
    return html`<home-page></home-page>`
}

function HomePage() {
    const [timeline, setTimeline] = useState([])
    const [timelineEndCursor, setTimelineEndCursor] = useState(null)
    const [fetchingTimeline, setFetchingTimeline] = useState(timeline.length === 0)
    const [err, setErr] = useState(null)
    const [loadingMore, setLoadingMore] = useState(false)
    const [noMoreTimelineItems, setNoMoreTimelineItems] = useState(false)
    const [endReached, setEndReached] = useState(false)
    const [queue, setQueue] = useState([])
    const [toast, setToast] = useState(null)

    const onTimelineItemCreated = useCallback(ev => {
        const payload = ev.detail
        setTimeline(tt => [payload, ...queue, ...tt])
        setQueue([])
    }, [queue])

    const onNewTimelineItemArrive = useCallback(ti => {
        setQueue(tt => [ti, ...tt])
    }, [])

    const onRemovedFromTimeline = useCallback(ev => {
        const payload = ev.detail
        setTimeline(tt => tt.filter(ti => ti.id !== payload.timelineItemID))
    }, [])

    const onPostDeleted = useCallback(ev => {
        const payload = ev.detail
        setTimeline(tt => tt.filter(ti => ti.post.id !== payload.id))
    }, [])

    const onQueueBtnClick = useCallback(() => {
        setTimeline(tt => [...queue, ...tt])
        setQueue([])
    }, [queue])

    const loadMore = useCallback(() => {
        if (loadingMore || noMoreTimelineItems) {
            return
        }

        setLoadingMore(true)
        fetchTimeline(timelineEndCursor).then(({ items: timeline, endCursor }) => {
            setTimeline(tt => [...tt, ...timeline])
            setTimelineEndCursor(endCursor)

            if (timeline.length < pageSize) {
                setNoMoreTimelineItems(true)
                setEndReached(true)
            }
        }, err => {
            const msg = "could not fetch more timeline items: " + err.message
            console.error(msg)
            setToast({ type: "error", content: msg })
        }).finally(() => {
            setLoadingMore(false)
        })
    }, [loadingMore, noMoreTimelineItems, timelineEndCursor])

    useEffect(() => {
        setFetchingTimeline(true)
        fetchTimeline().then(({ items: timeline, endCursor }) => {
            setTimeline(timeline)
            setTimelineEndCursor(endCursor)

            if (timeline.length < pageSize) {
                setNoMoreTimelineItems(true)
            }
        }, err => {
            console.error("could not fetch timeline:", err)
            setErr(err)
        }).finally(() => {
            setFetchingTimeline(false)
        })
    }, [])

    useEffect(() => subscribeToTimeline(onNewTimelineItemArrive), [])

    return html`
        <main class="container home-page">
            <h1>Timeline</h1>
            <post-form @timeline-item-created=${onTimelineItemCreated}></post-form>
            ${err !== null ? html`
            <p class="error" role="alert">Could not fetch timeline: ${err.message}</p>
            ` : fetchingTimeline ? html`
            <p class="loader" aria-busy="true" aria-live="polite">Loading timeline... please wait.<p>
                    ` : html`
                    ${queue.length !== 0 ? html`
                    <button class="queue-btn" @click=${onQueueBtnClick}>${queue.length} new timeline items</button>
                    ` : nothing}
                    ${timeline.length === 0 ? html`
                    <p>0 timeline items</p>
                    ` : html`
                    <div class="posts" role="feed">
                        ${repeat(timeline, ti => ti.id, ti => html`<post-item .post=${ti.post} .type=${"timeline_item"}
                            .timelineItemID=${ti.id} @removed-from-timeline=${onRemovedFromTimeline}
                            @resource-deleted=${onPostDeleted}></post-item>`)}
                    </div>
                    ${!noMoreTimelineItems ? html`
                    <intersectable-comp @is-intersecting=${loadMore}></intersectable-comp>
                    <p class="loader" aria-busy="true" aria-live="polite">Loading timeline... please wait.<p>
                            ` : endReached ? html`
                            <p>End reached.</p>
                            ` : nothing}
                            `}
                            `}
        </main>
        ${toast !== null ? html`<toast-item .toast=${toast}></toast-item>` : nothing}
    `
}

customElements.define("home-page", component(HomePage, { useShadowDOM: false }))

const reMention = /\B@([\-+\w]*)$/

function PostForm() {
    const [auth] = useStore(authStore)
    const [content, setContent] = useState("")
    const [fetching, setFetching] = useState(false)
    const [nsfw, setNSFW] = useState(false)
    const [isSpoiler, setIsSpoiler] = useState(false)
    const [spoilerOf, setSpoilerOf] = useState("")
    const spoilerOfDialogRef = useRef(null)
    const [initialTextAreaHeight, setInitialTextAreaHeight] = useState(0)
    const textAreaRef = useRef(null)
    const textcompleteRef = useRef(null)
    const [toast, setToast] = useState(null)

    const dispatchTimelineItemCreated = post => {
        this.dispatchEvent(new CustomEvent("timeline-item-created", { bubbles: true, detail: post }))
    }

    const onSubmit = useCallback(ev => {
        ev.preventDefault()

        setFetching(true)
        createTimelineItem({
            content,
            spoilerOf: spoilerOf.trim() === "" ? null : spoilerOf.trim(),
            nsfw,
        }).then(ti => {
            ti.post.user = auth.user
            setContent("")
            setNSFW(false)
            setIsSpoiler(false)
            setSpoilerOf("")
            textcompleteRef.current.hide()

            dispatchTimelineItemCreated(ti)
        }, err => {
            const msg = "could not create post: " + err.message
            console.error(msg)
            setToast({ type: "error", content: msg })
        }).finally(() => {
            setFetching(false)
        })
    }, [content, nsfw, spoilerOf, auth, textAreaRef, initialTextAreaHeight])

    const onTextAreaInput = useCallback(() => {
        setContent(textAreaRef.current.value)
    }, [content, textAreaRef])

    const onNSFWInputChange = useCallback(ev => {
        const checked = ev.currentTarget.checked
        setNSFW(checked)
    }, [])

    const onIsSpoilerInputChange = useCallback(ev => {
        const checked = ev.currentTarget.checked
        setIsSpoiler(checked)
        if (!checked) {
            setSpoilerOf("")
        } else {
            spoilerOfDialogRef.current.showModal()
        }
    }, [spoilerOfDialogRef])

    const onSpoilerOfInput = useCallback(ev => {
        setSpoilerOf(ev.currentTarget.value)
    }, [])

    const onSpoilerOfDialogClose = useCallback(() => {
        if (spoilerOf.trim() === "") {
            setSpoilerOf("")
            setIsSpoiler(false)
        }
    }, [spoilerOf])

    const onSpoilerOfFormSubmit = useCallback(ev => {
        ev.preventDefault()
        spoilerOfDialogRef.current.close()
    }, [])

    const onSpoilerOfCancelBtnClick = useCallback(() => {
        setSpoilerOf("")
        setIsSpoiler(false)
        spoilerOfDialogRef.current.close()
    }, [spoilerOfDialogRef])

    useEffect(() => {
        if (spoilerOfDialogRef.current !== null && !(window.HTMLDialogElement || spoilerOfDialogRef.current.showModal)) {
            import("dialog-polyfill").then(m => m.default).then(dialogPolyfill => {
                dialogPolyfill.registerDialog(spoilerOfDialogRef.current)
            }).catch(err => {
                console.error("could not import dialog polyfill:", err)
            })
        }
    }, [spoilerOfDialogRef])

    useEffect(() => {
        if (textAreaRef.current === null) {
            return
        }

        const editor = new TextareaEditor(textAreaRef.current)
        textcompleteRef.current = new Textcomplete(editor, [{
            match: reMention,
            search: async (term, cb) => {
                cb(await fetchUsernames(term).then(page => page.items, err => {
                    console.error("could not fetch mentions usernames:", err)
                    return []
                }))
            },
            replace: username => `@${username} `,
        }])

        setInitialTextAreaHeight(textAreaRef.current.scrollHeight)

        return () => {
            textcompleteRef.current.destroy()
        }
    }, [textAreaRef, textcompleteRef])

    // Share Target.
    useEffect(() => {
        const params = new URLSearchParams(window.location.search.slice(1))
        const preContent = []
        let cleanup = false
        if (params.has("text")) {
            cleanup = true
            const text = decodeURIComponent(params.get("text")).trim()
            if (text !== "") {
                preContent.push(text)
            }
        }
        if (params.has("url")) {
            cleanup = true
            const url = decodeURIComponent(params.get("url"))
            if (url !== "") {
                preContent.push(url)
            }
        }

        if (preContent.length !== 0) {
            setContent(preContent.join(" "))
        }

        if (cleanup) {
            history.replaceState(history.state, document.title, "/")
        }
    }, [])

    useEffect(() => {
        textAreaRef.current.style.height = initialTextAreaHeight + "px"
        if (textAreaRef.current.value !== "") {
            textAreaRef.current.style.height = Math.max(textAreaRef.current.scrollHeight, initialTextAreaHeight) + "px"
        }
    }, [content, initialTextAreaHeight, textAreaRef])

    return html`
        <form class="post-form${content !== "" ? " has-content" : ""}" name="post-form" @submit=${onSubmit}>
{'fraction_non_alphanumeric': 0.13857354983953632, 'fraction_numerical': 0.0010537912535325956, 'mean_word_length': 2.104073743681237, 'pattern_counts': {'":': 0, '<': 65, '<?xml version=': 0, '>': 157, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3722962', 'n_tokens_mistral': 6033, 'n_tokens_neox': 5954, 'n_words': 1111}