Merge pull request #8 from Org013/dev

Bump create threads feature into main.
This commit is contained in:
Guz
2023-05-30 10:28:09 -03:00
committed by GitHub
11 changed files with 254 additions and 90 deletions

View File

@@ -3,4 +3,4 @@ PRIVATE_TWITTER_OAUTH2_API_SECRET=""
PRIVATE_INSTANCES_API_TOKEN="" PRIVATE_INSTANCES_API_TOKEN=""
PUBLIC_DEPLOYED_VERSION="1.0.0" PUBLIC_DEPLOYED_VERSION="1.1.0"

View File

@@ -1,6 +1,6 @@
{ {
"name": "twooth", "name": "twooth",
"version": "0.0.1", "version": "1.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",

View File

@@ -18,6 +18,7 @@ export const version = persistent<string>('twooth.version', '1.0.0');
export function updateAppState(): void { export function updateAppState(): void {
if (get(version) !== PUBLIC_DEPLOYED_VERSION) { if (get(version) !== PUBLIC_DEPLOYED_VERSION) {
console.log('Updated!'); console.log('Updated!');
version.set(PUBLIC_DEPLOYED_VERSION);
} }
return; return;
} }

View File

@@ -39,7 +39,9 @@ export interface UserProfile {
darkMode: boolean darkMode: boolean
} }
state: { state: {
posts: {
text: string text: string
}[]
} }
} }
@@ -55,7 +57,7 @@ export const usersMap = persistent<UserMap>(
twitter: { enabled: true }, twitter: { enabled: true },
mastodon: { enabled: true }, mastodon: { enabled: true },
config: { color: getRandomColor(), darkMode: true }, config: { color: getRandomColor(), darkMode: true },
state: { text: '' }, state: { posts: [{ text: '' }] },
}), }),
(map) => JSON.stringify( (map) => JSON.stringify(
@@ -113,7 +115,7 @@ export function createNewUser(): UserProfile {
twitter: { enabled: true }, twitter: { enabled: true },
mastodon: { enabled: true }, mastodon: { enabled: true },
config: { color: getUniqueColor(), darkMode: true }, config: { color: getUniqueColor(), darkMode: true },
state: { text: '' }, state: { posts: [{ text: '' }] },
}); });
return map; return map;
}); });

View File

@@ -1,61 +1,50 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition'; import PostArea from "./PostArea.svelte";
import RadialProgress from '$lib/components/RadialProgress.svelte'; import { posts, currentPostId } from "./data";
import Textarea from '$lib/components/Textarea.svelte'; import { currentUser } from "$lib/state/users";
import { currentUser } from '$lib/state/users'; import { writable } from "svelte/store";
import PlatformSelection from './PlatformSelection.svelte'; import ButtonIcon from "$lib/components/ButtonIcon.svelte";
import Thread from "./Thread.svelte";
import SubmitButton from './SubmitButton.svelte'; posts.subscribe((posts) => {
import { text } from './data';
let overLimit = false;
let characters = 0;
let characterLimit = 240;
text.subscribe((text) => {
currentUser.update((user) => { currentUser.update((user) => {
user.state.text = text; user.state.posts = posts;
return user; return user;
}); });
}); });
function addThread() {
posts.update(($posts) => {
$posts.push({ text: "" });
return $posts;
});
}
</script> </script>
<div class="w-full"> <span un-gap="1" un-flex="~ col">
<Textarea {#each $posts as post, id}
placeholder="Make a twooth!" {#if id < $currentPostId}
name="twooth" <Thread {id} {post} />
{characterLimit} {:else if id === $currentPostId}
bind:value={$text} <PostArea postId={$currentPostId} />
bind:overLimit {:else if id > $currentPostId}
bind:characters <Thread {id} {post} />
required
/>
<menu class="h-10 flex items-center justify-between p-0 py-3">
<section>
<p class="m-0 mb-1 text-sm text-gray7">Post on:</p>
<div>
<PlatformSelection />
</div>
</section>
<section class="inline-flex items-center justify-between gap-3">
{#if characters > characterLimit / 3}
<span transition:fade={{ delay: 100, duration: 200 }}>
<RadialProgress
width="w-8"
height="h-8"
color={overLimit ? 'hue-red text-hue10' : 'text-hue7'}
class="inline-block"
label={`${characters} / ${characterLimit}`}
value={characters}
max={characterLimit}
/>
</span>
{/if} {/if}
{#if characters > 0} {/each}
<span transition:fade={{ delay: 100, duration: 200 }}>
<SubmitButton disabled={overLimit} /> <button
class="h-10 w-full cursor-pointer appearance-none gap-2 b-0 rounded-xl font-bold"
un-outline="hover:(hue8 2 solid)"
un-p="x-3 y-2 hover:(x-3 y-2)"
un-text="gray12 hover:(gray12)"
un-flex="~ items-center"
un-bg="gray2 hover:hue8"
un-m="t-2"
on:click={() => addThread()}
>
<span
class={`my-auto text-xl inline-block i-solar:add-circle-line-duotone`}
/>
<span class="my-auto">Add thread</span>
</button>
</span> </span>
{/if}
</section>
</menu>
</div>

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { fade } from "svelte/transition";
import RadialProgress from "$lib/components/RadialProgress.svelte";
import Textarea from "$lib/components/Textarea.svelte";
import PlatformSelection from "./PlatformSelection.svelte";
import { posts } from "./data";
import SubmitButton from './SubmitButton.svelte';
import { writable } from "svelte/store";
export let postId: number = 0;
let overLimit = false;
let characters = 0;
let characterLimit = 240;
const text = writable<string>($posts[postId].text ?? '');
$: posts.update(($posts) => {
$posts[postId].text = $text;
return $posts;
});
</script>
<div class="w-full">
<Textarea
placeholder="Make a twooth!"
name="twooth"
{characterLimit}
bind:value={$text}
bind:overLimit
bind:characters
required
/>
<menu class="h-10 flex items-center justify-between p-0 py-3">
<section>
<p class="m-0 mb-1 text-sm text-gray7">Post on:</p>
<div>
<PlatformSelection />
</div>
</section>
<section class="inline-flex items-center justify-between gap-3">
{#if characters > characterLimit / 3}
<span transition:fade={{ delay: 100, duration: 200 }}>
<RadialProgress
width="w-8"
height="h-8"
color={overLimit ? "hue-red text-hue10" : "text-hue7"}
class="inline-block"
label={`${characters} / ${characterLimit}`}
value={characters}
max={characterLimit}
/>
</span>
{/if}
{#if characters > 0}
<span transition:fade={{ delay: 100, duration: 200 }}>
<SubmitButton disabled={overLimit} />
</span>
{/if}
</section>
</menu>
</div>

View File

@@ -1,5 +1,5 @@
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { text } from './data'; import { currentPostId, posts } from './data';
import { currentUser, lastMastodonPost, lastTwitterPost } from '$lib/state/users'; import { currentUser, lastMastodonPost, lastTwitterPost } from '$lib/state/users';
import qFetch from '$lib/qFetch'; import qFetch from '$lib/qFetch';
@@ -12,7 +12,7 @@ export async function submitTwooth(): Promise<void> {
const { id } = await qFetch<{ id: number }>( const { id } = await qFetch<{ id: number }>(
'/api/mastodon/post', '/api/mastodon/post',
{ {
query: { instance: user.mastodon.instance?.url, status: get(text) }, query: { instance: user.mastodon.instance?.url, statuses: JSON.stringify(get(posts)) },
options: { options: {
headers: { Authorization: `Bearer ${user.mastodon.token}` }, headers: { Authorization: `Bearer ${user.mastodon.token}` },
method: 'POST', method: 'POST',
@@ -28,7 +28,7 @@ export async function submitTwooth(): Promise<void> {
const { id } = await qFetch<{ id: number }>( const { id } = await qFetch<{ id: number }>(
'/api/twitter/post', '/api/twitter/post',
{ {
query: { status: get(text) }, query: { statuses: JSON.stringify(get(posts)) },
options: { options: {
headers: { Authorization: `Bearer ${user.twitter.token}` }, headers: { Authorization: `Bearer ${user.twitter.token}` },
method: 'POST', method: 'POST',
@@ -41,6 +41,9 @@ export async function submitTwooth(): Promise<void> {
lastTwitterPost.set(undefined); lastTwitterPost.set(undefined);
} }
if (success[0] || success[1]) text.set(''); if (success[0] || success[1]) {
posts.set([{ text: '' }]);
currentPostId.set(0);
};
} }

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import { posts, currentPostId } from "./data";
export let id: number;
export let post: { text: string; file?: string };
function removeThread(id: number) {
posts.update(($posts) => {
$posts.splice(id, 1);
return $posts;
});
}
</script>
<span un-flex un-justify="between">
<button
class="h-10 w-[90%] cursor-pointer appearance-none gap-2 b-0 rounded-xl font-bold"
un-outline="hover:(hue8 2 solid)"
un-p="x-3 y-2 hover:(x-3 y-2)"
un-text="gray12 hover:(gray12)"
un-flex="~ items-center"
un-bg="gray2 hover:gray2"
on:click={() => ($currentPostId = id)}
>
<span
class={`my-auto text-xl inline-block i-solar:document-text-line-duotone`}
/>
<span class="my-auto text-truncate max-w-[80%]"
>{post.text || "Thread"}</span
>
</button>
<button
class="h-10 cursor-pointer appearance-none gap-2 b-0 rounded-xl font-bold"
un-outline="hover:(tomato8 2 solid)"
un-p="x-3 y-2 hover:(x-3 y-2)"
un-text="gray12 hover:(gray12)"
un-flex="~ items-center"
un-bg="gray2 hover:gray2"
on:click={() => removeThread(id)}
>
<span
class={`my-auto text-xl inline-block i-solar:trash-bin-trash-line-duotone`}
/>
</button>
</span>

View File

@@ -1,5 +1,6 @@
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
import { currentUser } from '$lib/state/users'; import { currentUser } from '$lib/state/users';
export const text = writable<string>(get(currentUser).state.text); export const posts = writable<{ text: string }[]>(get(currentUser).state.posts);
export const posted = writable<boolean>(false); export const posted = writable<boolean>(false);
export const currentPostId = writable(0);

View File

@@ -6,20 +6,26 @@ export const POST = (async ({ request, url }): Promise<Response> => {
const token = request.headers.get('Authorization'); const token = request.headers.get('Authorization');
const instanceUrl = url.searchParams.get('instance'); const instanceUrl = url.searchParams.get('instance');
const status = url.searchParams.get('status'); const statuses: { text: string }[] = JSON.parse(url.searchParams.get('statuses') ?? 'undefined');
if (!token || !instanceUrl || !status) throw error(400); if (!token || !instanceUrl || !statuses) throw error(400);
const postReq = await qFetch<{ id: number }>( const postReq = await postStatus(statuses[0].text, instanceUrl, token);
`${instanceUrl}/api/v1/statuses`,
{ if (statuses.length > 1) {
query: { status }, let pastStatusId: number = postReq.id;
options: { for (const status of statuses) {
headers: { Authorization: token }, if (
method: 'POST', statuses.indexOf(status) === 0 ||
}, status.text === ''
}, ) continue;
);
const replyReq = await replyStatus(status.text, pastStatusId, instanceUrl, token);
if (!replyReq) new Response('', { status: 500 });
pastStatusId = replyReq.id;
}
}
if (postReq) return new Response( if (postReq) return new Response(
JSON.stringify({ id: postReq.id }), { JSON.stringify({ id: postReq.id }), {
@@ -31,3 +37,28 @@ export const POST = (async ({ request, url }): Promise<Response> => {
}) satisfies RequestHandler; }) satisfies RequestHandler;
async function postStatus(status: string, instanceUrl: string, token: string): Promise<{ id: number }> {
return await qFetch<{ id: number }>(
`${instanceUrl}/api/v1/statuses`,
{
query: { status },
options: {
headers: { Authorization: token },
method: 'POST',
},
},
);
}
async function replyStatus(reply: string, replyToId: number, instanceUrl: string, token: string): Promise<{ id: number }> {
return await qFetch<{ id: number }>(
`${instanceUrl}/api/v1/statuses`,
{
query: { status: reply, in_reply_to_id: replyToId },
options: {
headers: { Authorization: token },
method: 'POST',
},
},
);
}

View File

@@ -5,20 +5,26 @@ import type { RequestHandler } from './$types';
export const POST = (async ({ request, url }): Promise<Response> => { export const POST = (async ({ request, url }): Promise<Response> => {
const token = request.headers.get('Authorization'); const token = request.headers.get('Authorization');
const status = url.searchParams.get('status'); const statuses: { text: string }[] = JSON.parse(url.searchParams.get('statuses') ?? 'undefined');
if (!token || !status) throw error(400); if (!token || !statuses) throw error(400);
const postReq = await qFetch<{ data: { id: number } }>( const postReq = await postStatus(statuses[0].text, token);
'https://api.twitter.com/2/tweets',
{ if (statuses.length > 1) {
options: { let pastStatusId: number = postReq.data.id;
headers: { Authorization: token }, for (const status of statuses) {
method: 'POST', if (
body: { text: status }, statuses.indexOf(status) === 0 ||
}, status.text === ''
}, ) continue;
);
const replyReq = await replyStatus(status.text, pastStatusId, token);
if (!replyReq) new Response('', { status: 500 });
pastStatusId = replyReq.data.id;
}
}
if (postReq) return new Response( if (postReq) return new Response(
JSON.stringify({ id: postReq.data.id }), { JSON.stringify({ id: postReq.data.id }), {
@@ -30,3 +36,28 @@ export const POST = (async ({ request, url }): Promise<Response> => {
}) satisfies RequestHandler; }) satisfies RequestHandler;
async function postStatus(status: string, token: string): Promise<{ data: { id: number } }> {
return await qFetch<{ data: { id: number } }>(
'https://api.twitter.com/2/tweets',
{
options: {
headers: { Authorization: token },
method: 'POST',
body: { text: status },
},
},
);
}
async function replyStatus(reply: string, replyToId: number, token: string): Promise<{ data: { id: number } }> {
return await qFetch<{ data: { id: number } }>(
'https://api.twitter.com/2/tweets',
{
options: {
headers: { Authorization: token },
method: 'POST',
body: { text: reply, reply: { in_reply_to_tweet_id: replyToId } },
},
},
);
}