Skip to content

Commit ef9a6f1

Browse files
committed
feat: ✨ Add multi-account support, more options for posts, UI improvements
1 parent 48954ba commit ef9a6f1

36 files changed

+643
-338
lines changed

app.vue

+34-4
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
<script setup lang="ts">
1414
import { convert } from "html-to-text";
1515
import "iconify-icon";
16+
import { nanoid } from "nanoid";
1617
// Use SSR-safe IDs for Headless UI
1718
provideHeadlessUseId(() => useId());
1819
1920
const code = useRequestURL().searchParams.get("code");
2021
const appData = useAppData();
21-
const tokenData = useTokenData();
22-
const client = useClient(tokenData);
22+
const identity = useCurrentIdentity();
23+
const identities = useIdentities();
24+
const client = useClient();
2325
const instance = useInstance();
2426
const description = useExtendedDescription(client);
2527
@@ -56,19 +58,47 @@ if (code) {
5658
code,
5759
new URL("/", useRequestURL().origin).toString(),
5860
)
59-
.then((res) => {
60-
tokenData.value = res.data;
61+
.then(async (res) => {
62+
const tempClient = useClient(res.data).value;
63+
64+
const [accountOutput, instanceOutput] = await Promise.all([
65+
tempClient.verifyAccountCredentials(),
66+
tempClient.getInstance(),
67+
]);
68+
69+
// Get account data
70+
if (
71+
!identities.value.find(
72+
(i) => i.account.id === accountOutput.data.id,
73+
)
74+
)
75+
identity.value = {
76+
id: nanoid(),
77+
tokens: res.data,
78+
account: accountOutput.data,
79+
instance: instanceOutput.data,
80+
permissions: [],
81+
emojis: [],
82+
};
6183
6284
// Remove code from URL
6385
window.history.replaceState(
6486
{},
6587
document.title,
6688
window.location.pathname,
6789
);
90+
91+
// Redirect to home
92+
window.location.pathname = "/";
6893
});
6994
}
7095
}
7196
97+
useListen("identity:change", (newIdentity) => {
98+
identity.value = newIdentity;
99+
window.location.pathname = "/";
100+
});
101+
72102
useCacheRefresh(client);
73103
</script>
74104

bun.lockb

-448 Bytes
Binary file not shown.

components/buttons/DropdownElement.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<ButtonsBase class="hover:bg-white/20 !rounded-sm !text-left flex flex-row gap-x-3 !ring-0 !p-4 sm:!p-2">
2+
<ButtonsBase class="enabled:hover:bg-white/20 !rounded-sm !text-left flex flex-row gap-x-3 !ring-0 !p-4 sm:!p-2">
33
<iconify-icon :icon="icon" width="none" class="text-gray-200 size-5" aria-hidden="true" />
44
<slot />
55
</ButtonsBase>

components/composer/composer.vue

+6-7
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const { input: content } = useTextareaAutosize({
7171
const { Control_Enter, Command_Enter, Control_Alt } = useMagicKeys();
7272
const respondingTo = ref<Status | null>(null);
7373
const respondingType = ref<"reply" | "quote" | "edit" | null>(null);
74-
const me = useMe();
74+
const identity = useCurrentIdentity();
7575
const cw = ref(false);
7676
const cwContent = ref("");
7777
const markdown = ref(true);
@@ -151,15 +151,15 @@ onMounted(() => {
151151
useListen("composer:reply", (note: Status) => {
152152
respondingTo.value = note;
153153
respondingType.value = "reply";
154-
if (note.account.id !== me.value?.id)
154+
if (note.account.id !== identity.value?.account.id)
155155
content.value = `@${note.account.acct} `;
156156
textarea.value?.focus();
157157
});
158158
159159
useListen("composer:quote", (note: Status) => {
160160
respondingTo.value = note;
161161
respondingType.value = "quote";
162-
if (note.account.id !== me.value?.id)
162+
if (note.account.id !== identity.value?.account.id)
163163
content.value = `@${note.account.acct} `;
164164
textarea.value?.focus();
165165
});
@@ -175,7 +175,7 @@ onMounted(() => {
175175
}));
176176
177177
// Fetch source
178-
const source = await client.value?.getStatusSource(note.id);
178+
const source = await client.value.getStatusSource(note.id);
179179
180180
if (source?.data) {
181181
respondingTo.value = note;
@@ -205,12 +205,11 @@ const canSubmit = computed(
205205
(content.value?.trim().length > 0 || files.value.length > 0) &&
206206
content.value?.trim().length <= characterLimit.value,
207207
);
208-
const tokenData = useTokenData();
209-
const client = useClient(tokenData);
208+
const client = useClient();
210209
211210
const send = async () => {
212211
loading.value = true;
213-
if (!tokenData.value || !client.value) {
212+
if (!identity.value || !client.value) {
214213
throw new Error("Not authenticated");
215214
}
216215

components/composer/emoji-suggestbox.vue

+7-4
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
</template>
1111

1212
<script lang="ts" setup>
13+
import type { LysandClient } from "@lysand-org/client";
1314
import { distance } from "fastest-levenshtein";
14-
import type { UnwrapRef } from "vue";
15+
import type { CustomEmoji } from "~/composables/Identities";
1516
const props = defineProps<{
1617
currentlyTypingEmoji: string | null;
1718
}>();
1819
1920
const emojiRefs = ref<Element[]>([]);
20-
const customEmojis = useCustomEmojis();
2121
const { Tab, ArrowRight, ArrowLeft, Enter } = useMagicKeys({
2222
passive: false,
2323
onEventFired(e) {
@@ -28,12 +28,15 @@ const { Tab, ArrowRight, ArrowLeft, Enter } = useMagicKeys({
2828
e.preventDefault();
2929
},
3030
});
31-
const topEmojis = ref<UnwrapRef<typeof customEmojis> | null>(null);
31+
const identity = useCurrentIdentity();
32+
const topEmojis = ref<CustomEmoji[] | null>(null);
3233
const selectedEmojiIndex = ref<number | null>(null);
3334
3435
watchEffect(() => {
36+
if (!identity.value) return;
37+
3538
if (props.currentlyTypingEmoji !== null)
36-
topEmojis.value = customEmojis.value
39+
topEmojis.value = identity.value.emojis
3740
.map((emoji) => ({
3841
...emoji,
3942
distance: distance(

components/composer/file-uploader.vue

+3-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div>
33
<input type="file" ref="fileInput" @change="handleFileInput" style="display: none" multiple />
44
<div class="flex flex-row gap-2 overflow-x-auto *:shrink-0 p-1 mb-4" v-if="files.length > 0">
5-
<div v-for="(data) in files.toReversed()" :key="data.id" role="button" tabindex="0"
5+
<div v-for="(data) in files" :key="data.id" role="button" tabindex="0"
66
:class="['size-28 bg-dark-800 rounded flex items-center relative justify-center ring-1 ring-white/20 overflow-hidden', data.progress !== 1.0 && 'animate-pulse']"
77
@keydown.enter="removeFile(data.id)">
88
<template v-if="data.file.type.startsWith('image/')">
@@ -73,8 +73,7 @@ const files = defineModel<
7373
required: true,
7474
});
7575
76-
const tokenData = useTokenData();
77-
const client = useClient(tokenData);
76+
const client = useClient();
7877
const fileInput = ref<HTMLInputElement | null>(null);
7978
8079
const openFilePicker = () => {
@@ -165,7 +164,7 @@ const uploadFile = async (file: File) => {
165164
return data;
166165
});
167166
168-
client.value?.uploadMedia(file).then((response) => {
167+
client.value.uploadMedia(file).then((response) => {
169168
const attachment = response.data;
170169
171170
files.value = files.value.map((data) => {

components/composer/modal.client.vue

+4-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<Dialog.Content class="overflow-y-auto w-full max-h-full md:py-16">
2020
<div
2121
class="relative overflow-hidden max-w-xl mx-auto rounded-lg bg-dark-700 ring-1 ring-dark-800 text-left shadow-xl transition-all w-full">
22-
<Composer v-if="instance" :instance="instance" />
22+
<Composer v-if="instance" :instance="instance as any" />
2323
</div>
2424
</Dialog.Content>
2525
</HeadlessTransitionChild>
@@ -32,6 +32,8 @@
3232
<script lang="ts" setup>
3333
import { Dialog } from "@ark-ui/vue";
3434
const open = ref(false);
35+
36+
const identity = useCurrentIdentity();
3537
useListen("note:reply", async (note) => {
3638
open.value = true;
3739
await nextTick();
@@ -48,11 +50,10 @@ useListen("note:edit", async (note) => {
4850
useEvent("composer:edit", note);
4951
});
5052
useListen("composer:open", () => {
51-
if (tokenData.value) open.value = true;
53+
if (identity.value) open.value = true;
5254
});
5355
useListen("composer:close", () => {
5456
open.value = false;
5557
});
56-
const tokenData = useTokenData();
5758
const instance = useInstance();
5859
</script>

components/dropdowns/AdaptiveDropdown.vue

+11-13
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,18 @@ const id = useId();
3939
4040
// HACK: Fix the menu children not reacting to touch events as click for some reason
4141
const registerClickHandlers = () => {
42-
const targetElement = document.querySelector(`.${id}`);
43-
if (targetElement) {
44-
for (const el of targetElement.children) {
45-
el.addEventListener("touchstart", (e) => {
46-
e.stopPropagation();
47-
e.preventDefault();
48-
// Click all element children
49-
for (const elChild of Array.from(el.children)) {
50-
if (elChild instanceof HTMLElement) {
51-
elChild.click();
52-
}
42+
const targetElements = document.querySelectorAll(`.${id} [data-part=item]`);
43+
for (const el of targetElements) {
44+
el.addEventListener("touchstart", (e) => {
45+
e.stopPropagation();
46+
e.preventDefault();
47+
// Click all element children
48+
for (const elChild of Array.from(el.children)) {
49+
if (elChild instanceof HTMLElement) {
50+
elChild.click();
5351
}
54-
});
55-
}
52+
}
53+
});
5654
}
5755
};
5856

components/headers/greeting.vue

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
<template>
22
<ClientOnly>
3-
<div v-if="me" class="bg-dark-800 p-6 my-5 rounded ring-1 ring-white/5">
4-
<div class="sm:flex sm:items-center sm:justify-between">
5-
<div class="sm:flex sm:space-x-5">
6-
<AvatarsCentered :src="me.avatar"
3+
<div v-if="identity" class="bg-dark-800 z-0 p-6 my-5 relative overflow-hidden rounded ring-1 ring-white/5">
4+
<div class="sm:flex sm:items-center sm:justify-between gap-3">
5+
<div class="sm:flex sm:space-x-5 grow">
6+
<AvatarsCentered :src="identity.account.avatar"
77
class="mx-auto shrink-0 size-20 rounded overflow-hidden ring-1 ring-white/10" />
8-
<div class="mt-4 text-center sm:mt-0 sm:pt-1 sm:text-left">
8+
<div
9+
class="mt-4 text-center flex flex-col justify-center sm:mt-0 sm:text-left bg-dark-800 py-2 px-4 rounded grow ring-1 ring-white/10">
910
<p class="text-sm font-medium text-gray-300">Welcome back,</p>
1011
<p class="text-xl font-bold text-gray-50 sm:text-2xl line-clamp-1"
11-
v-html="useParsedContent(me.display_name, []).value"></p>
12-
<p class="text-sm font-medium text-gray-500">@{{ me.acct }}</p>
12+
v-html="useParsedContent(identity.account.display_name, []).value"></p>
1313
</div>
1414
</div>
15-
<div class="mt-5 flex justify-center sm:mt-0">
15+
<!-- <div class="mt-5 flex justify-center sm:mt-0">
1616
<ButtonsSecondary @click="useEvent('composer:open')">
1717
Compose
1818
</ButtonsSecondary>
19-
</div>
19+
</div> -->
2020
</div>
2121
</div>
2222
</ClientOnly>
2323
</template>
2424

2525
<script lang="ts" setup>
26-
const me = useMe();
26+
const identity = useCurrentIdentity();
2727
</script>

components/notifications/Renderer.vue

+38-35
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,46 @@
11
<template>
2-
<div aria-live="assertive" class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6">
3-
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
4-
<!-- Notification panel, dynamically insert this into the live region when it needs to be displayed -->
5-
<TransitionGroup enter-active-class="transform ease-out duration-300 transition"
6-
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
7-
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
8-
leave-active-class="transition transform ease-in duration-100"
9-
leave-from-class="translate-y-0 opacity-100 sm:translate-x-0"
10-
leave-to-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2">
11-
<div v-for="notification in notifications" :key="notification.id"
12-
class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-dark-500 shadow-lg ring-1 ring-white/10">
13-
<div class="p-4">
14-
<div class="flex items-start">
15-
<div class="shrink-0 h-6 w-6">
16-
<iconify-icon v-if="notification.type === 'success'" icon="tabler:check" height="none"
17-
class="h-6 w-6 text-green-400" aria-hidden="true" />
18-
<iconify-icon v-else-if="notification.type === 'error'" icon="tabler:alert-triangle"
19-
height="none" class="h-6 w-6 text-red-400" aria-hidden="true" />
20-
<iconify-icon v-else-if="notification.type === 'progress'" icon="tabler:loader"
21-
height="none" class="h-6 w-6 text-pink-500 animate-spin" aria-hidden="true" />
22-
</div>
23-
<div class="ml-3 w-0 flex-1 pt-0.5">
24-
<p class="text-sm font-semibold text-gray-50">{{ notification.title }}</p>
25-
<p class="mt-1 text-sm text-gray-400" v-if="notification.message">{{
26-
notification.message }}</p>
27-
</div>
28-
<div class="ml-4 flex flex-shrink-0">
29-
<button type="button" title="Close this notification"
30-
@click="notifications.splice(notifications.indexOf(notification), 1); notification.onDismiss?.()"
31-
class="inline-flex rounded-md text-gray-400 hover:text-gray-300 duration-200">
32-
<iconify-icon icon="tabler:x" class="h-5 w-5" aria-hidden="true" />
33-
</button>
2+
<Teleport to="body">
3+
<div aria-live="assertive"
4+
class="pointer-events-none fixed inset-0 flex items-end px-4 pt-6 pb-24 sm:pb-6 sm:items-start sm:p-6 z-50">
5+
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
6+
<!-- Notification panel, dynamically insert this into the live region when it needs to be displayed -->
7+
<TransitionGroup enter-active-class="transform ease-out duration-300 transition"
8+
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
9+
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
10+
leave-active-class="transition transform ease-in duration-100"
11+
leave-from-class="translate-y-0 opacity-100 sm:translate-x-0"
12+
leave-to-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2">
13+
<div v-for="notification in notifications" :key="notification.id"
14+
class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-dark-500 shadow-lg ring-1 ring-white/10">
15+
<div class="p-4">
16+
<div class="flex items-start">
17+
<div class="shrink-0 h-6 w-6">
18+
<iconify-icon v-if="notification.type === 'success'" icon="tabler:check"
19+
height="none" class="h-6 w-6 text-green-400" aria-hidden="true" />
20+
<iconify-icon v-else-if="notification.type === 'error'" icon="tabler:alert-triangle"
21+
height="none" class="h-6 w-6 text-red-400" aria-hidden="true" />
22+
<iconify-icon v-else-if="notification.type === 'progress'" icon="tabler:loader"
23+
height="none" class="h-6 w-6 text-pink-500 animate-spin" aria-hidden="true" />
24+
</div>
25+
<div class="ml-3 w-0 flex-1 pt-0.5">
26+
<p class="text-sm font-semibold text-gray-50">{{ notification.title }}</p>
27+
<p class="mt-1 text-sm text-gray-400" v-if="notification.message">{{
28+
notification.message }}</p>
29+
</div>
30+
<div class="ml-4 flex flex-shrink-0">
31+
<button type="button" title="Close this notification"
32+
@click="notifications.splice(notifications.indexOf(notification), 1); notification.onDismiss?.()"
33+
class="inline-flex rounded-md text-gray-400 hover:text-gray-300 duration-200">
34+
<iconify-icon icon="tabler:x" class="h-5 w-5" aria-hidden="true" />
35+
</button>
36+
</div>
3437
</div>
3538
</div>
3639
</div>
37-
</div>
38-
</TransitionGroup>
40+
</TransitionGroup>
41+
</div>
3942
</div>
40-
</div>
43+
</Teleport>
4144
</template>
4245

4346
<script lang="ts" setup>

0 commit comments

Comments
 (0)