creates video voice memos from audio clips; with bluesky integration. trill.ptr.pet
1import { Component, createSignal } from "solid-js"; 2 3import { SendIcon, XIcon } from "lucide-solid"; 4import { Stack } from "styled-system/jsx"; 5import { IconButton } from "~/components/ui/icon-button"; 6import { Spinner } from "~/components/ui/spinner"; 7import { Text } from "~/components/ui/text"; 8import { Link } from "~/components/ui/link"; 9 10import { parseCanonicalResourceUri } from "@atcute/lexicons/syntax"; 11import { css } from "styled-system/css"; 12import { sendPost } from "~/lib/at"; 13import { toaster } from "~/components/Toaster"; 14import { Dialog } from "~/components/ui/dialog"; 15import { Textarea } from "~/components/ui/textarea"; 16import { Account } from "~/lib/accounts"; 17 18const PostDialog = (props: { 19 trigger: Component; 20 result: Blob; 21 account: Account | undefined; 22}) => { 23 const [postContent, setPostContent] = createSignal<string>(""); 24 const [posting, setPosting] = createSignal(false); 25 const [open, setOpen] = createSignal(false); 26 27 return ( 28 <Dialog.Root open={open()} onOpenChange={(e) => setOpen(e.open)}> 29 <Dialog.Trigger 30 asChild={(triggerProps) => <props.trigger {...triggerProps()} />} 31 /> 32 <Dialog.Backdrop /> 33 <Dialog.Positioner> 34 <Dialog.Content> 35 <Stack> 36 <Stack gap="0"> 37 <video 38 class={css({ maxW: "sm", roundedTop: "xs" })} 39 controls 40 src={URL.createObjectURL(props.result)} 41 ></video> 42 <Textarea 43 placeholder="enter text content..." 44 id="post-content" 45 value={postContent()} 46 onChange={(e) => setPostContent(e.target.value)} 47 rows={2} 48 resize="none" 49 border="none" 50 borderTop="1px solid var(--colors-border-muted)" 51 boxShadow={{ base: "none", _focus: "none" }} 52 /> 53 </Stack> 54 <Stack 55 borderTop="1px solid var(--colors-border-muted)" 56 gap="2" 57 p="3" 58 direction="row" 59 align="center" 60 > 61 <Stack direction="row" align="center"> 62 <Dialog.Title> 63 post to {props.account?.handle ?? props.account?.did} 64 </Dialog.Title> 65 </Stack> 66 <div class={css({ flexGrow: 1 })} /> 67 {posting() ? ( 68 <Spinner 69 borderLeftColor="bg.emphasized" 70 borderBottomColor="bg.emphasized" 71 borderWidth="4px" 72 size="sm" 73 /> 74 ) : ( 75 <Dialog.CloseTrigger 76 asChild={(closeTriggerProps) => ( 77 <IconButton 78 {...closeTriggerProps()} 79 aria-label="Close Dialog" 80 variant="ghost" 81 size="sm" 82 > 83 <XIcon /> 84 </IconButton> 85 )} 86 /> 87 )} 88 <IconButton 89 disabled={posting()} 90 onClick={() => { 91 setPosting(true); 92 sendPost(props.account?.did!, props.result, postContent()) 93 .then((result) => { 94 const parsedUri = parseCanonicalResourceUri(result.uri); 95 if (!parsedUri.ok) throw "failed to parse atproto uri"; 96 const { repo, rkey } = parsedUri.value; 97 toaster.create({ 98 title: "post sent", 99 description: ( 100 <> 101 <Text>view post </Text> 102 <Link 103 href={`https://bsky.app/profile/${repo}/post/${rkey}`} 104 color={{ 105 base: "colorPalette.text", 106 _hover: "colorPalette.emphasized", 107 }} 108 textDecoration={{ _hover: "underline" }} 109 > 110 here 111 </Link> 112 </> 113 ), 114 type: "success", 115 }); 116 setOpen(false); 117 }) 118 .catch((error) => { 119 toaster.create({ 120 title: "send post failed", 121 description: error, 122 type: "error", 123 }); 124 }) 125 .finally(() => { 126 setPosting(false); 127 }); 128 }} 129 variant="ghost" 130 size="sm" 131 > 132 <SendIcon /> 133 </IconButton> 134 </Stack> 135 </Stack> 136 </Dialog.Content> 137 </Dialog.Positioner> 138 </Dialog.Root> 139 ); 140}; 141 142export default PostDialog;