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=""
|
PRIVATE_INSTANCES_API_TOKEN=""
|
||||||
|
|
||||||
PUBLIC_DEPLOYED_VERSION="1.0.0"
|
PUBLIC_DEPLOYED_VERSION="1.1.0"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
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 { 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);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
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 { 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);
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user