Merge pull request #8 from Org013/dev
Bump create threads feature into main.
This commit is contained in:
@@ -3,4 +3,4 @@ PRIVATE_TWITTER_OAUTH2_API_SECRET=""
|
||||
|
||||
PRIVATE_INSTANCES_API_TOKEN=""
|
||||
|
||||
PUBLIC_DEPLOYED_VERSION="1.0.0"
|
||||
PUBLIC_DEPLOYED_VERSION="1.1.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twooth",
|
||||
"version": "0.0.1",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -18,6 +18,7 @@ export const version = persistent<string>('twooth.version', '1.0.0');
|
||||
export function updateAppState(): void {
|
||||
if (get(version) !== PUBLIC_DEPLOYED_VERSION) {
|
||||
console.log('Updated!');
|
||||
version.set(PUBLIC_DEPLOYED_VERSION);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,9 @@ export interface UserProfile {
|
||||
darkMode: boolean
|
||||
}
|
||||
state: {
|
||||
text: string
|
||||
posts: {
|
||||
text: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ export const usersMap = persistent<UserMap>(
|
||||
twitter: { enabled: true },
|
||||
mastodon: { enabled: true },
|
||||
config: { color: getRandomColor(), darkMode: true },
|
||||
state: { text: '' },
|
||||
state: { posts: [{ text: '' }] },
|
||||
}),
|
||||
|
||||
(map) => JSON.stringify(
|
||||
@@ -113,7 +115,7 @@ export function createNewUser(): UserProfile {
|
||||
twitter: { enabled: true },
|
||||
mastodon: { enabled: true },
|
||||
config: { color: getUniqueColor(), darkMode: true },
|
||||
state: { text: '' },
|
||||
state: { posts: [{ text: '' }] },
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
@@ -1,61 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import RadialProgress from '$lib/components/RadialProgress.svelte';
|
||||
import Textarea from '$lib/components/Textarea.svelte';
|
||||
import { currentUser } from '$lib/state/users';
|
||||
import PlatformSelection from './PlatformSelection.svelte';
|
||||
import PostArea from "./PostArea.svelte";
|
||||
import { posts, currentPostId } from "./data";
|
||||
import { currentUser } from "$lib/state/users";
|
||||
import { writable } from "svelte/store";
|
||||
import ButtonIcon from "$lib/components/ButtonIcon.svelte";
|
||||
import Thread from "./Thread.svelte";
|
||||
|
||||
import SubmitButton from './SubmitButton.svelte';
|
||||
import { text } from './data';
|
||||
posts.subscribe((posts) => {
|
||||
currentUser.update((user) => {
|
||||
user.state.posts = posts;
|
||||
return user;
|
||||
});
|
||||
});
|
||||
|
||||
let overLimit = false;
|
||||
let characters = 0;
|
||||
let characterLimit = 240;
|
||||
|
||||
text.subscribe((text) => {
|
||||
currentUser.update((user) => {
|
||||
user.state.text = text;
|
||||
return user;
|
||||
});
|
||||
});
|
||||
function addThread() {
|
||||
posts.update(($posts) => {
|
||||
$posts.push({ 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>
|
||||
<span un-gap="1" un-flex="~ col">
|
||||
{#each $posts as post, id}
|
||||
{#if id < $currentPostId}
|
||||
<Thread {id} {post} />
|
||||
{:else if id === $currentPostId}
|
||||
<PostArea postId={$currentPostId} />
|
||||
{:else if id > $currentPostId}
|
||||
<Thread {id} {post} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<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>
|
||||
|
||||
61
src/routes/TwoothInput/PostArea.svelte
Normal file
61
src/routes/TwoothInput/PostArea.svelte
Normal 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>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { text } from './data';
|
||||
import { currentPostId, posts } from './data';
|
||||
import { currentUser, lastMastodonPost, lastTwitterPost } from '$lib/state/users';
|
||||
import qFetch from '$lib/qFetch';
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function submitTwooth(): Promise<void> {
|
||||
const { id } = await qFetch<{ id: number }>(
|
||||
'/api/mastodon/post',
|
||||
{
|
||||
query: { instance: user.mastodon.instance?.url, status: get(text) },
|
||||
query: { instance: user.mastodon.instance?.url, statuses: JSON.stringify(get(posts)) },
|
||||
options: {
|
||||
headers: { Authorization: `Bearer ${user.mastodon.token}` },
|
||||
method: 'POST',
|
||||
@@ -28,7 +28,7 @@ export async function submitTwooth(): Promise<void> {
|
||||
const { id } = await qFetch<{ id: number }>(
|
||||
'/api/twitter/post',
|
||||
{
|
||||
query: { status: get(text) },
|
||||
query: { statuses: JSON.stringify(get(posts)) },
|
||||
options: {
|
||||
headers: { Authorization: `Bearer ${user.twitter.token}` },
|
||||
method: 'POST',
|
||||
@@ -41,6 +41,9 @@ export async function submitTwooth(): Promise<void> {
|
||||
lastTwitterPost.set(undefined);
|
||||
}
|
||||
|
||||
if (success[0] || success[1]) text.set('');
|
||||
if (success[0] || success[1]) {
|
||||
posts.set([{ text: '' }]);
|
||||
currentPostId.set(0);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
45
src/routes/TwoothInput/Thread.svelte
Normal file
45
src/routes/TwoothInput/Thread.svelte
Normal 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>
|
||||
@@ -1,5 +1,6 @@
|
||||
import { get, writable } from 'svelte/store';
|
||||
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 currentPostId = writable(0);
|
||||
|
||||
@@ -6,20 +6,26 @@ export const POST = (async ({ request, url }): Promise<Response> => {
|
||||
|
||||
const token = request.headers.get('Authorization');
|
||||
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 }>(
|
||||
`${instanceUrl}/api/v1/statuses`,
|
||||
{
|
||||
query: { status },
|
||||
options: {
|
||||
headers: { Authorization: token },
|
||||
method: 'POST',
|
||||
},
|
||||
},
|
||||
);
|
||||
const postReq = await postStatus(statuses[0].text, instanceUrl, token);
|
||||
|
||||
if (statuses.length > 1) {
|
||||
let pastStatusId: number = postReq.id;
|
||||
for (const status of statuses) {
|
||||
if (
|
||||
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(
|
||||
JSON.stringify({ id: postReq.id }), {
|
||||
@@ -31,3 +37,28 @@ export const POST = (async ({ request, url }): Promise<Response> => {
|
||||
|
||||
}) 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',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,20 +5,26 @@ import type { RequestHandler } from './$types';
|
||||
export const POST = (async ({ request, url }): Promise<Response> => {
|
||||
|
||||
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 } }>(
|
||||
'https://api.twitter.com/2/tweets',
|
||||
{
|
||||
options: {
|
||||
headers: { Authorization: token },
|
||||
method: 'POST',
|
||||
body: { text: status },
|
||||
},
|
||||
},
|
||||
);
|
||||
const postReq = await postStatus(statuses[0].text, token);
|
||||
|
||||
if (statuses.length > 1) {
|
||||
let pastStatusId: number = postReq.data.id;
|
||||
for (const status of statuses) {
|
||||
if (
|
||||
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(
|
||||
JSON.stringify({ id: postReq.data.id }), {
|
||||
@@ -30,3 +36,28 @@ export const POST = (async ({ request, url }): Promise<Response> => {
|
||||
|
||||
}) 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 } },
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user