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