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;