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;