mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 03:29:16 +01:00
refactor: ♻️ Rewrite state system to use Pinia for composer and auth
This commit is contained in:
parent
a6db9e059d
commit
b510782a30
80 changed files with 999 additions and 1011 deletions
|
|
@ -32,7 +32,7 @@
|
|||
<TriangleAlert class="!size-5" />
|
||||
</Toggle>
|
||||
</ComposerButton>
|
||||
<CharacterCounter class="ml-auto" :max="(identity as Identity).instance.configuration.statuses.max_characters" :current="rawContent.length" />
|
||||
<CharacterCounter class="ml-auto" :max="authStore.instance?.configuration.statuses.max_characters ?? 0" :current="rawContent.length" />
|
||||
<Button type="submit" size="lg" :disabled="sending || !canSend" @click="emit('submit')">
|
||||
<Loader v-if="sending" class="!size-5 animate-spin" />
|
||||
{{
|
||||
|
|
@ -57,7 +57,7 @@ import { Button } from "../ui/button";
|
|||
import { Toggle } from "../ui/toggle";
|
||||
import ComposerButton from "./button.vue";
|
||||
import CharacterCounter from "./character-counter.vue";
|
||||
import { type ComposerState, visibilities } from "./composer";
|
||||
import { visibilities } from "./visibilities";
|
||||
import VisibilityPicker from "./visibility-picker.vue";
|
||||
|
||||
const { relation, sending, canSend, rawContent } = defineProps<{
|
||||
|
|
@ -66,6 +66,7 @@ const { relation, sending, canSend, rawContent } = defineProps<{
|
|||
canSend: boolean;
|
||||
rawContent: string;
|
||||
}>();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const contentType = defineModel<ComposerState["contentType"]>("contentType", {
|
||||
required: true,
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
import type { ResponseError } from "@versia/client";
|
||||
import type { Attachment, Status, StatusSource } from "@versia/client/schemas";
|
||||
import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
|
||||
import type { FunctionalComponent } from "vue";
|
||||
import { toast } from "vue-sonner";
|
||||
import type { z } from "zod";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
|
||||
export interface ComposerState {
|
||||
relation?: {
|
||||
type: "reply" | "quote" | "edit";
|
||||
note: z.infer<typeof Status>;
|
||||
source?: z.infer<typeof StatusSource>;
|
||||
};
|
||||
content: string;
|
||||
rawContent: string;
|
||||
sensitive: boolean;
|
||||
contentWarning: string;
|
||||
contentType: "text/html" | "text/plain";
|
||||
visibility: z.infer<typeof Status.shape.visibility>;
|
||||
files: {
|
||||
apiId?: string;
|
||||
file: File;
|
||||
alt?: string;
|
||||
uploading: boolean;
|
||||
updating: boolean;
|
||||
}[];
|
||||
sending: boolean;
|
||||
canSend: boolean;
|
||||
}
|
||||
|
||||
const { play } = useAudio();
|
||||
export const state = reactive<ComposerState>({
|
||||
relation: undefined,
|
||||
content: "",
|
||||
rawContent: "",
|
||||
sensitive: false,
|
||||
contentWarning: "",
|
||||
contentType: "text/html",
|
||||
visibility: preferences.default_visibility.value,
|
||||
files: [],
|
||||
sending: false,
|
||||
canSend: false,
|
||||
});
|
||||
|
||||
watch(
|
||||
state,
|
||||
(newState) => {
|
||||
const characterLimit =
|
||||
identity.value?.instance.configuration.statuses.max_characters ?? 0;
|
||||
const characterCount = newState.rawContent.length;
|
||||
|
||||
state.canSend =
|
||||
characterCount > 0
|
||||
? characterCount <= characterLimit
|
||||
: newState.files.length > 0;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
export const visibilities: Record<
|
||||
z.infer<typeof Status.shape.visibility>,
|
||||
{
|
||||
icon: FunctionalComponent;
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
> = {
|
||||
public: {
|
||||
icon: Globe,
|
||||
name: m.lost_trick_dog_grace(),
|
||||
text: m.last_mean_peacock_zip(),
|
||||
},
|
||||
unlisted: {
|
||||
icon: LockOpen,
|
||||
name: m.funny_slow_jannes_walk(),
|
||||
text: m.grand_strong_gibbon_race(),
|
||||
},
|
||||
private: {
|
||||
icon: Lock,
|
||||
name: m.grassy_empty_raven_startle(),
|
||||
text: m.white_teal_ostrich_yell(),
|
||||
},
|
||||
direct: {
|
||||
icon: AtSign,
|
||||
name: m.pretty_bold_baboon_wave(),
|
||||
text: m.lucky_mean_robin_link(),
|
||||
},
|
||||
};
|
||||
|
||||
export const getRandomSplash = (): string => {
|
||||
const splashes = useConfig().COMPOSER_SPLASHES;
|
||||
|
||||
return splashes[Math.floor(Math.random() * splashes.length)] as string;
|
||||
};
|
||||
|
||||
export const calculateMentionsFromReply = (
|
||||
note: z.infer<typeof Status>,
|
||||
): string => {
|
||||
const peopleToMention = note.mentions
|
||||
.concat(note.account)
|
||||
// Deduplicate mentions
|
||||
.filter((men, i, a) => a.indexOf(men) === i)
|
||||
// Remove self
|
||||
.filter((men) => men.id !== identity.value?.account.id);
|
||||
|
||||
if (peopleToMention.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const mentions = peopleToMention.map((me) => `@${me.acct}`).join(" ");
|
||||
|
||||
return `${mentions} `;
|
||||
};
|
||||
|
||||
const fileFromUrl = (url: URL | string): Promise<File> => {
|
||||
return fetch(url).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch file");
|
||||
}
|
||||
|
||||
return response.blob().then((blob) => {
|
||||
const file = new File([blob], "file", { type: blob.type });
|
||||
return file;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const stateFromRelation = async (
|
||||
relationType: "reply" | "quote" | "edit",
|
||||
note: z.infer<typeof Status>,
|
||||
source?: z.infer<typeof StatusSource>,
|
||||
): Promise<void> => {
|
||||
state.relation = {
|
||||
type: relationType,
|
||||
note,
|
||||
source,
|
||||
};
|
||||
state.content = note.content || calculateMentionsFromReply(note);
|
||||
state.rawContent = source?.text || "";
|
||||
|
||||
if (relationType === "edit") {
|
||||
state.sensitive = note.sensitive;
|
||||
state.contentWarning = source?.spoiler_text || note.spoiler_text;
|
||||
state.visibility = note.visibility;
|
||||
state.files = await Promise.all(
|
||||
note.media_attachments.map(async (file) => ({
|
||||
apiId: file.id,
|
||||
alt: file.description ?? undefined,
|
||||
file: await fileFromUrl(file.url),
|
||||
uploading: false,
|
||||
updating: false,
|
||||
})),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadFile = (file: File): Promise<void> => {
|
||||
const index =
|
||||
state.files.push({
|
||||
file,
|
||||
uploading: true,
|
||||
updating: false,
|
||||
}) - 1;
|
||||
|
||||
return client.value
|
||||
.uploadMedia(file)
|
||||
.then((media) => {
|
||||
if (!state.files[index]) {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
|
||||
state.files[index].uploading = false;
|
||||
state.files[index].apiId = (
|
||||
media.data as z.infer<typeof Attachment>
|
||||
).id;
|
||||
})
|
||||
.catch(() => {
|
||||
state.files.splice(index, 1);
|
||||
});
|
||||
};
|
||||
|
||||
export const send = async (): Promise<void> => {
|
||||
if (state.sending) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.sending = true;
|
||||
|
||||
try {
|
||||
if (state.relation?.type === "edit") {
|
||||
const { data } = await client.value.editStatus(
|
||||
state.relation.note.id,
|
||||
{
|
||||
status: state.content,
|
||||
content_type: state.contentType,
|
||||
sensitive: state.sensitive,
|
||||
spoiler_text: state.sensitive
|
||||
? state.contentWarning
|
||||
: undefined,
|
||||
media_ids: state.files
|
||||
.map((f) => f.apiId)
|
||||
.filter((f) => f !== undefined),
|
||||
},
|
||||
);
|
||||
|
||||
useEvent("composer:send-edit", data);
|
||||
play("publish");
|
||||
useEvent("composer:close");
|
||||
} else {
|
||||
const { data } = await client.value.postStatus(state.content, {
|
||||
content_type: state.contentType,
|
||||
sensitive: state.sensitive,
|
||||
spoiler_text: state.sensitive
|
||||
? state.contentWarning
|
||||
: undefined,
|
||||
media_ids: state.files
|
||||
.map((f) => f.apiId)
|
||||
.filter((f) => f !== undefined),
|
||||
quote_id:
|
||||
state.relation?.type === "quote"
|
||||
? state.relation.note.id
|
||||
: undefined,
|
||||
in_reply_to_id:
|
||||
state.relation?.type === "reply"
|
||||
? state.relation.note.id
|
||||
: undefined,
|
||||
visibility: state.visibility,
|
||||
});
|
||||
|
||||
useEvent("composer:send", data as z.infer<typeof Status>);
|
||||
play("publish");
|
||||
useEvent("composer:close");
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error((e as ResponseError).message);
|
||||
} finally {
|
||||
state.sending = false;
|
||||
}
|
||||
};
|
||||
|
|
@ -3,19 +3,19 @@
|
|||
<Note :note="relation.note" :hide-actions="true" :small-layout="true" />
|
||||
</div>
|
||||
|
||||
<ContentWarning v-if="state.sensitive" v-model="state.contentWarning" />
|
||||
<ContentWarning v-if="store.sensitive" v-model="store.contentWarning" />
|
||||
|
||||
<EditorContent @paste-files="uploadFiles" v-model:content="state.content" v-model:raw-content="state.rawContent" :placeholder="getRandomSplash()"
|
||||
<EditorContent @paste-files="uploadFiles" v-model:content="store.content" v-model:raw-content="store.rawContent" :placeholder="getRandomSplash()"
|
||||
class="[&>.tiptap]:!border-none [&>.tiptap]:!ring-0 [&>.tiptap]:!outline-none [&>.tiptap]:rounded-none p-0 [&>.tiptap]:max-h-[50dvh] [&>.tiptap]:overflow-y-auto [&>.tiptap]:min-h-48 [&>.tiptap]:!ring-offset-0 [&>.tiptap]:h-full"
|
||||
:disabled="state.sending" :mode="state.contentType === 'text/html' ? 'rich' : 'plain'" />
|
||||
:disabled="store.sending" :mode="store.contentType === 'text/html' ? 'rich' : 'plain'" />
|
||||
|
||||
<div class="w-full flex flex-row gap-2 overflow-x-auto *:shrink-0 pb-2">
|
||||
<input type="file" ref="fileInput" @change="uploadFileFromEvent" class="hidden" multiple />
|
||||
<Files v-model:files="state.files" />
|
||||
<Files v-model:files="store.files" :composer-key="composerKey" />
|
||||
</div>
|
||||
|
||||
<DialogFooter class="items-center flex-row overflow-x-auto">
|
||||
<ComposerButtons @submit="send" @pick-file="fileInput?.click()" v-model:content-type="state.contentType" v-model:sensitive="state.sensitive" v-model:visibility="state.visibility" :relation="state.relation" :sending="state.sending" :can-send="state.canSend" :raw-content="state.rawContent" />
|
||||
<ComposerButtons @submit="send" @pick-file="fileInput?.click()" v-model:content-type="store.contentType" v-model:sensitive="store.sensitive" v-model:visibility="store.visibility" :relation="store.relation" :sending="store.sending" :can-send="store.canSend" :raw-content="store.rawContent" />
|
||||
</DialogFooter>
|
||||
</template>
|
||||
|
||||
|
|
@ -24,37 +24,53 @@ import Note from "~/components/notes/note.vue";
|
|||
import EditorContent from "../editor/content.vue";
|
||||
import { DialogFooter } from "../ui/dialog";
|
||||
import ComposerButtons from "./buttons.vue";
|
||||
import {
|
||||
type ComposerState,
|
||||
getRandomSplash,
|
||||
send,
|
||||
state,
|
||||
stateFromRelation,
|
||||
uploadFile,
|
||||
} from "./composer";
|
||||
import ContentWarning from "./content-warning.vue";
|
||||
import Files from "./files.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
relation?: ComposerState["relation"];
|
||||
}>();
|
||||
|
||||
const { Control_Enter, Command_Enter } = useMagicKeys();
|
||||
const { play } = useAudio();
|
||||
const fileInput = useTemplateRef<HTMLInputElement>("fileInput");
|
||||
const composerKey = props.relation
|
||||
? (`${props.relation.type}-${props.relation.note.id}` as const)
|
||||
: "blank";
|
||||
const store = useComposerStore(composerKey)();
|
||||
|
||||
watch([Control_Enter, Command_Enter], () => {
|
||||
if (state.sending || !preferences.ctrl_enter_send.value) {
|
||||
if (store.sending || !preferences.ctrl_enter_send.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
send();
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
relation?: ComposerState["relation"];
|
||||
}>();
|
||||
const getRandomSplash = (): string => {
|
||||
const splashes = useConfig().COMPOSER_SPLASHES;
|
||||
|
||||
return splashes[Math.floor(Math.random() * splashes.length)] as string;
|
||||
};
|
||||
|
||||
const send = async () => {
|
||||
const result =
|
||||
store.relation?.type === "edit"
|
||||
? await store.sendEdit()
|
||||
: await store.send();
|
||||
|
||||
if (result) {
|
||||
play("publish");
|
||||
store.$reset();
|
||||
useEvent("composer:close");
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
props,
|
||||
async (props) => {
|
||||
if (props.relation) {
|
||||
await stateFromRelation(
|
||||
store.stateFromRelation(
|
||||
props.relation.type,
|
||||
props.relation.note,
|
||||
props.relation.source,
|
||||
|
|
@ -69,7 +85,7 @@ const uploadFileFromEvent = (e: Event) => {
|
|||
const files = Array.from(target.files ?? []);
|
||||
|
||||
for (const file of files) {
|
||||
uploadFile(file);
|
||||
store.uploadFile(file);
|
||||
}
|
||||
|
||||
target.value = "";
|
||||
|
|
@ -77,7 +93,7 @@ const uploadFileFromEvent = (e: Event) => {
|
|||
|
||||
const uploadFiles = (files: File[]) => {
|
||||
for (const file of files) {
|
||||
uploadFile(file);
|
||||
store.uploadFile(file);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import {
|
|||
import * as m from "~~/paraglide/messages.js";
|
||||
import Composer from "./composer.vue";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
useListen("composer:open", () => {
|
||||
if (identity.value) {
|
||||
if (authStore.isSignedIn) {
|
||||
open.value = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -21,7 +23,7 @@ useListen("composer:edit", async (note) => {
|
|||
const id = toast.loading(m.wise_late_fireant_walk(), {
|
||||
duration: 0,
|
||||
});
|
||||
const { data: source } = await client.value.getStatusSource(note.id);
|
||||
const { data: source } = await authStore.client.getStatusSource(note.id);
|
||||
relation.value = {
|
||||
type: "edit",
|
||||
note,
|
||||
|
|
|
|||
|
|
@ -5,22 +5,19 @@
|
|||
:disabled="file.uploading || file.updating"
|
||||
class="block bg-card text-card-foreground shadow-sm h-28 overflow-hidden rounded relative min-w-28 *:disabled:opacity-50"
|
||||
>
|
||||
<img :src="createObjectURL(file.file)" class="object-contain h-28 w-full" :alt="file.alt" />
|
||||
<img v-if="file.file?.type.startsWith('image/')" :src="createObjectURL(file.file)" class="object-contain h-28 w-full" :alt="file.alt" />
|
||||
<FileIcon v-else class="size-6 m-auto text-muted-foreground" />
|
||||
<Badge
|
||||
v-if="!(file.uploading || file.updating)"
|
||||
v-if="file.file && !(file.uploading || file.updating)"
|
||||
class="absolute bottom-1 right-1"
|
||||
variant="default"
|
||||
>{{ formatBytes(file.file.size) }}</Badge
|
||||
>
|
||||
<Spinner v-else class="absolute bottom-1 right-1 size-8 p-1.5" />
|
||||
<Spinner v-else-if="file.file" class="absolute bottom-1 right-1 size-8 p-1.5" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="min-w-48">
|
||||
<DropdownMenuLabel>{{ file.file.name }}</DropdownMenuLabel>
|
||||
<DropdownMenuLabel v-if="file.file">{{ file.file.name }}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem @click="editName">
|
||||
<TextCursorInput />
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="editCaption">
|
||||
<Captions />
|
||||
Add caption
|
||||
|
|
@ -35,7 +32,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Captions, Delete, TextCursorInput } from "lucide-vue-next";
|
||||
import { Captions, Delete, FileIcon } from "lucide-vue-next";
|
||||
import Spinner from "~/components/graphics/spinner.vue";
|
||||
import { confirmModalService } from "~/components/modals/composable.ts";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
|
|
@ -47,45 +44,22 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import type { ComposerState } from "./composer";
|
||||
import type { ComposerStateKey } from "~/stores/composer";
|
||||
|
||||
const file = defineModel<ComposerState["files"][number]>("file", {
|
||||
const { composerKey } = defineProps<{
|
||||
composerKey: ComposerStateKey;
|
||||
}>();
|
||||
|
||||
const file = defineModel<ComposerFile>("file", {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const composerStore = useComposerStore(composerKey)();
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [];
|
||||
}>();
|
||||
|
||||
const editName = async () => {
|
||||
const result = await confirmModalService.confirm({
|
||||
title: "Enter a new name",
|
||||
defaultValue: file.value.file.name,
|
||||
confirmText: "Edit",
|
||||
inputType: "text",
|
||||
});
|
||||
|
||||
if (result.confirmed) {
|
||||
file.value.updating = true;
|
||||
file.value.file = new File(
|
||||
[file.value.file],
|
||||
result.value ?? file.value.file.name,
|
||||
{
|
||||
type: file.value.file.type,
|
||||
lastModified: file.value.file.lastModified,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await client.value.updateMedia(file.value.apiId ?? "", {
|
||||
file: file.value.file,
|
||||
});
|
||||
} finally {
|
||||
file.value.updating = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const editCaption = async () => {
|
||||
const result = await confirmModalService.confirm({
|
||||
title: "Enter a caption",
|
||||
|
|
@ -97,16 +71,10 @@ const editCaption = async () => {
|
|||
});
|
||||
|
||||
if (result.confirmed) {
|
||||
file.value.updating = true;
|
||||
file.value.alt = result.value;
|
||||
|
||||
try {
|
||||
await client.value.updateMedia(file.value.apiId ?? "", {
|
||||
description: file.value.alt,
|
||||
});
|
||||
} finally {
|
||||
file.value.updating = false;
|
||||
}
|
||||
await composerStore.updateFileDescription(
|
||||
file.value.id,
|
||||
result.value ?? "",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
<template>
|
||||
<FilePreview v-for="(file, index) in files" :key="file.apiId" :file="file" @update:file="files[index] = $event"
|
||||
<FilePreview v-for="(file, index) in files" :key="file.apiId" :file="file" @update:file="files[index] = $event" :composer-key="composerKey"
|
||||
@remove="files.splice(index, 1)" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ComposerState } from "./composer";
|
||||
import type { ComposerStateKey } from "~/stores/composer";
|
||||
import FilePreview from "./file-preview.vue";
|
||||
|
||||
const files = defineModel<ComposerState["files"]>("files", {
|
||||
const { composerKey } = defineProps<{
|
||||
composerKey: ComposerStateKey;
|
||||
}>();
|
||||
|
||||
const files = defineModel<ComposerFile[]>("files", {
|
||||
required: true,
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
35
app/components/composer/visibilities.ts
Normal file
35
app/components/composer/visibilities.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import type { Status } from "@versia/client/schemas";
|
||||
import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
|
||||
import type { FunctionalComponent } from "vue";
|
||||
import type { z } from "zod";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
|
||||
export const visibilities: Record<
|
||||
z.infer<typeof Status.shape.visibility>,
|
||||
{
|
||||
icon: FunctionalComponent;
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
> = {
|
||||
public: {
|
||||
icon: Globe,
|
||||
name: m.lost_trick_dog_grace(),
|
||||
text: m.last_mean_peacock_zip(),
|
||||
},
|
||||
unlisted: {
|
||||
icon: LockOpen,
|
||||
name: m.funny_slow_jannes_walk(),
|
||||
text: m.grand_strong_gibbon_race(),
|
||||
},
|
||||
private: {
|
||||
icon: Lock,
|
||||
name: m.grassy_empty_raven_startle(),
|
||||
text: m.white_teal_ostrich_yell(),
|
||||
},
|
||||
direct: {
|
||||
icon: AtSign,
|
||||
name: m.pretty_bold_baboon_wave(),
|
||||
text: m.lucky_mean_robin_link(),
|
||||
},
|
||||
};
|
||||
|
|
@ -18,14 +18,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "~/components/ui/select";
|
||||
import { type ComposerState, visibilities } from "./composer";
|
||||
import { visibilities } from "./visibilities";
|
||||
|
||||
const visibility = defineModel<ComposerState["visibility"]>("visibility", {
|
||||
required: true,
|
||||
|
|
|
|||
|
|
@ -68,9 +68,10 @@ watch(active, (value) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<BubbleMenu :editor="editor" class="bg-popover rounded-md">
|
||||
<BubbleMenu :editor="editor">
|
||||
<ToggleGroup type="multiple"
|
||||
v-model="active"
|
||||
class="bg-popover rounded-md"
|
||||
>
|
||||
<ToggleGroupItem value="bold">
|
||||
<BoldIcon />
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const {
|
|||
mode?: "rich" | "plain";
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const emit = defineEmits<{
|
||||
pasteFiles: [files: File[]];
|
||||
|
|
@ -53,13 +54,13 @@ const editor = new Editor({
|
|||
}),
|
||||
Emoji.configure({
|
||||
emojis: emojis.concat(
|
||||
identity.value?.emojis.map((emoji) => ({
|
||||
authStore.emojis.map((emoji) => ({
|
||||
name: emoji.shortcode,
|
||||
shortcodes: [emoji.shortcode],
|
||||
group: emoji.category ?? undefined,
|
||||
tags: [],
|
||||
fallbackImage: emoji.url,
|
||||
})) || [],
|
||||
})),
|
||||
),
|
||||
HTMLAttributes: {
|
||||
class: "emoji not-prose",
|
||||
|
|
|
|||
|
|
@ -26,14 +26,13 @@ const { items, command } = defineProps<{
|
|||
items: string[];
|
||||
command: (value: { name: string }) => void;
|
||||
}>();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const selectedIndex = ref(0);
|
||||
const emojis = computed(() => {
|
||||
return items
|
||||
.map((item) => {
|
||||
return identity.value?.emojis.find(
|
||||
(emoji) => emoji.shortcode === item,
|
||||
);
|
||||
return authStore.emojis.find((emoji) => emoji.shortcode === item);
|
||||
})
|
||||
.filter((emoji) => emoji !== undefined);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export type UserData = {
|
|||
value: z.infer<typeof Account>;
|
||||
};
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const updatePosition = (editor: Editor, element: HTMLElement): void => {
|
||||
const virtualElement = {
|
||||
getBoundingClientRect: () =>
|
||||
|
|
@ -42,7 +44,9 @@ export const mentionSuggestion = {
|
|||
return [];
|
||||
}
|
||||
|
||||
const users = await client.value.searchAccount(query, { limit: 20 });
|
||||
const users = await authStore.client.searchAccount(query, {
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
return go(
|
||||
query,
|
||||
|
|
@ -122,7 +126,7 @@ export const emojiSuggestion = {
|
|||
return [];
|
||||
}
|
||||
|
||||
const emojis = (identity.value as Identity).emojis;
|
||||
const emojis = authStore.emojis;
|
||||
|
||||
return go(
|
||||
query,
|
||||
|
|
|
|||
|
|
@ -17,12 +17,23 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { LogIn } from "lucide-vue-next";
|
||||
import { toast } from "vue-sonner";
|
||||
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
|
||||
const appData = useAppData();
|
||||
const signInAction = async () => signIn(appData, await askForInstance());
|
||||
const authStore = useAuthStore();
|
||||
const signInAction = async () => {
|
||||
const instance = await askForInstance();
|
||||
|
||||
const id = toast.loading(m.level_due_ox_greet());
|
||||
|
||||
try {
|
||||
await authStore.startSignIn(instance);
|
||||
} catch (e) {
|
||||
toast.dismiss(id);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<Tabs v-model:model-value="current">
|
||||
<TabsList>
|
||||
<TabsTrigger v-for="timeline in timelines.filter(
|
||||
i => i.requiresLogin ? !!identity : true,
|
||||
i => i.requiresLogin ? authStore.isSignedIn : true,
|
||||
)" :key="timeline.value" :value="timeline.value" :as="NuxtLink" :href="timeline.url">
|
||||
{{ timeline.name }}
|
||||
</TabsTrigger>
|
||||
|
|
@ -47,12 +47,12 @@ const timelines = [
|
|||
},
|
||||
];
|
||||
|
||||
const { beforeEach } = useRouter();
|
||||
const { path } = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const current = computed(() => {
|
||||
if (path === "/") {
|
||||
return identity.value ? "home" : "public";
|
||||
return authStore.isSignedIn ? "home" : "public";
|
||||
}
|
||||
|
||||
const timeline = timelines.find((i) => i.url === path);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<div class="flex flex-row w-full max-w-sm items-stretch justify-between">
|
||||
<ActionButton :icon="Reply" @click="emit('reply')" :title="m.drab_tense_turtle_comfort()" :disabled="!identity">
|
||||
<ActionButton :icon="Reply" @click="emit('reply')" :title="m.drab_tense_turtle_comfort()" :disabled="!authStore.isSignedIn">
|
||||
{{ numberFormat(replyCount) }}
|
||||
</ActionButton>
|
||||
<ActionButton :icon="Heart" @click="liked ? unlike() : like()" :title="liked ? m.vexed_fluffy_clownfish_dance() : m.royal_close_samuel_scold()" :disabled="!identity" :class="liked && '*:fill-red-600 *:text-red-600'">
|
||||
<ActionButton :icon="Heart" @click="liked ? unlike() : like()" :title="liked ? m.vexed_fluffy_clownfish_dance() : m.royal_close_samuel_scold()" :disabled="!authStore.isSignedIn" :class="liked && '*:fill-red-600 *:text-red-600'">
|
||||
{{ numberFormat(likeCount) }}
|
||||
</ActionButton>
|
||||
<ActionButton :icon="Repeat" @click="reblogged ? unreblog() : reblog()" :title="reblogged ? m.lime_neat_ox_stab() : m.aware_helpful_marlin_drop()" :disabled="!identity" :class="reblogged && '*:text-green-600'">
|
||||
<ActionButton :icon="Repeat" @click="reblogged ? unreblog() : reblog()" :title="reblogged ? m.lime_neat_ox_stab() : m.aware_helpful_marlin_drop()" :disabled="!authStore.isSignedIn" :class="reblogged && '*:text-green-600'">
|
||||
{{ numberFormat(reblogCount) }}
|
||||
</ActionButton>
|
||||
<ActionButton :icon="Quote" @click="emit('quote')" :title="m.true_shy_jackal_drip()" :disabled="!identity" />
|
||||
<ActionButton :icon="Quote" @click="emit('quote')" :title="m.true_shy_jackal_drip()" :disabled="!authStore.isSignedIn" />
|
||||
<Picker @pick="react">
|
||||
<ActionButton :icon="Smile" :title="m.bald_cool_kangaroo_jump()" :disabled="!identity" />
|
||||
<ActionButton :icon="Smile" :title="m.bald_cool_kangaroo_jump()" :disabled="!authStore.isSignedIn" />
|
||||
</Picker>
|
||||
<Menu :api-note-string="apiNoteString" :url="url" :remote-url="remoteUrl" :is-remote="isRemote" :author-id="authorId" @edit="emit('edit')" :note-id="noteId" @delete="emit('delete')">
|
||||
<ActionButton :icon="Ellipsis" :title="m.busy_merry_cowfish_absorb()" />
|
||||
|
|
@ -54,6 +54,7 @@ const emit = defineEmits<{
|
|||
react: [];
|
||||
}>();
|
||||
const { play } = useAudio();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const like = async () => {
|
||||
if (preferences.confirm_actions.value.includes("like")) {
|
||||
|
|
@ -71,7 +72,7 @@ const like = async () => {
|
|||
|
||||
play("like");
|
||||
const id = toast.loading(m.slimy_candid_tiger_read());
|
||||
const { data } = await client.value.favouriteStatus(noteId);
|
||||
const { data } = await authStore.client.favouriteStatus(noteId);
|
||||
toast.dismiss(id);
|
||||
toast.success(m.mealy_slow_buzzard_commend());
|
||||
useEvent("note:edit", data);
|
||||
|
|
@ -92,7 +93,7 @@ const unlike = async () => {
|
|||
}
|
||||
|
||||
const id = toast.loading(m.busy_active_leopard_strive());
|
||||
const { data } = await client.value.unfavouriteStatus(noteId);
|
||||
const { data } = await authStore.client.unfavouriteStatus(noteId);
|
||||
toast.dismiss(id);
|
||||
toast.success(m.fresh_direct_bear_affirm());
|
||||
useEvent("note:edit", data);
|
||||
|
|
@ -113,7 +114,7 @@ const reblog = async () => {
|
|||
}
|
||||
|
||||
const id = toast.loading(m.late_sunny_cobra_scold());
|
||||
const { data } = await client.value.reblogStatus(noteId);
|
||||
const { data } = await authStore.client.reblogStatus(noteId);
|
||||
toast.dismiss(id);
|
||||
toast.success(m.weird_moving_hawk_lift());
|
||||
useEvent(
|
||||
|
|
@ -137,7 +138,7 @@ const unreblog = async () => {
|
|||
}
|
||||
|
||||
const id = toast.loading(m.white_sharp_gorilla_embrace());
|
||||
const { data } = await client.value.unreblogStatus(noteId);
|
||||
const { data } = await authStore.client.unreblogStatus(noteId);
|
||||
toast.dismiss(id);
|
||||
toast.success(m.royal_polite_moose_catch());
|
||||
useEvent("note:edit", data);
|
||||
|
|
@ -149,7 +150,7 @@ const react = async (emoji: z.infer<typeof CustomEmoji> | UnicodeEmoji) => {
|
|||
? (emoji as UnicodeEmoji).unicode
|
||||
: `:${(emoji as z.infer<typeof CustomEmoji>).shortcode}:`;
|
||||
|
||||
const { data } = await client.value.createEmojiReaction(noteId, text);
|
||||
const { data } = await authStore.client.createEmojiReaction(noteId, text);
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success(m.main_least_turtle_fall());
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const { copy } = useClipboard();
|
||||
const loggedIn = !!identity.value;
|
||||
const authorIsMe = loggedIn && authorId === identity.value?.account.id;
|
||||
const authStore = useAuthStore();
|
||||
const authorIsMe = authStore.isSignedIn && authorId === authStore.account?.id;
|
||||
|
||||
const copyText = (text: string) => {
|
||||
copy(text);
|
||||
|
|
@ -47,7 +47,7 @@ const copyText = (text: string) => {
|
|||
|
||||
const blockUser = async (userId: string) => {
|
||||
const id = toast.loading(m.top_cute_bison_nudge());
|
||||
await client.value.blockAccount(userId);
|
||||
await authStore.client.blockAccount(userId);
|
||||
toast.dismiss(id);
|
||||
|
||||
toast.success(m.main_weary_racoon_peek());
|
||||
|
|
@ -68,7 +68,7 @@ const _delete = async () => {
|
|||
}
|
||||
|
||||
const id = toast.loading(m.new_funny_fox_boil());
|
||||
await client.value.deleteStatus(noteId);
|
||||
await authStore.client.deleteStatus(noteId);
|
||||
toast.dismiss(id);
|
||||
|
||||
toast.success(m.green_tasty_bumblebee_beam());
|
||||
|
|
@ -122,8 +122,8 @@ const _delete = async () => {
|
|||
{{ m.tense_quick_cod_favor() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator v-if="loggedIn && !authorIsMe" />
|
||||
<DropdownMenuGroup v-if="loggedIn && !authorIsMe">
|
||||
<DropdownMenuSeparator v-if="authStore.isSignedIn && !authorIsMe" />
|
||||
<DropdownMenuGroup v-if="authStore.isSignedIn && !authorIsMe">
|
||||
<DropdownMenuItem as="button" :disabled="true">
|
||||
<Flag />
|
||||
{{ m.great_few_jaguar_rise() }}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ const emit = defineEmits<{
|
|||
pick: [emoji: z.infer<typeof CustomEmoji> | UnicodeEmoji];
|
||||
}>();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const open = ref(false);
|
||||
const selectedEmoji = ref<z.infer<typeof CustomEmoji> | UnicodeEmoji | null>(
|
||||
null,
|
||||
|
|
@ -59,12 +60,10 @@ const selectedEmoji = ref<z.infer<typeof CustomEmoji> | UnicodeEmoji | null>(
|
|||
const emojiContainer = useTemplateRef<HTMLDivElement>("emojiContainer");
|
||||
const filter = ref("");
|
||||
|
||||
const customEmojis = computed(() => identity.value?.emojis ?? []);
|
||||
|
||||
const customEmojiCategories = computed(() => {
|
||||
const categories: Record<string, z.infer<typeof CustomEmoji>[]> = {};
|
||||
|
||||
for (const emoji of customEmojis.value) {
|
||||
for (const emoji of authStore.emojis) {
|
||||
const categoryName = emoji.category || "Uncategorized";
|
||||
|
||||
if (!categories[categoryName]) {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ const { reaction, emoji, statusId } = defineProps<{
|
|||
emoji?: z.infer<typeof CustomEmoji>;
|
||||
}>();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const formatNumber = (number: number) =>
|
||||
new Intl.NumberFormat(getLocale(), {
|
||||
notation: "compact",
|
||||
|
|
@ -63,12 +65,13 @@ const formatNumber = (number: number) =>
|
|||
const accounts = ref<z.infer<typeof Account>[] | null>(null);
|
||||
|
||||
const refreshReactions = async () => {
|
||||
const { data } = await client.value.getStatusReactions(statusId);
|
||||
const { data } = await authStore.client.getStatusReactions(statusId);
|
||||
const accountIds =
|
||||
data.find((r) => r.name === reaction.name)?.account_ids.slice(0, 10) ??
|
||||
[];
|
||||
|
||||
const { data: accountsData } = await client.value.getAccounts(accountIds);
|
||||
const { data: accountsData } =
|
||||
await authStore.client.getAccounts(accountIds);
|
||||
|
||||
accounts.value = accountsData;
|
||||
};
|
||||
|
|
@ -76,7 +79,7 @@ const refreshReactions = async () => {
|
|||
const react = async () => {
|
||||
const id = toast.loading(m.gray_stale_antelope_roam());
|
||||
|
||||
const { data } = await client.value.createEmojiReaction(
|
||||
const { data } = await authStore.client.createEmojiReaction(
|
||||
statusId,
|
||||
reaction.name,
|
||||
);
|
||||
|
|
@ -89,7 +92,7 @@ const react = async () => {
|
|||
const unreact = async () => {
|
||||
const id = toast.loading(m.many_weary_bat_intend());
|
||||
|
||||
const { data } = await client.value.deleteEmojiReaction(
|
||||
const { data } = await authStore.client.deleteEmojiReaction(
|
||||
statusId,
|
||||
reaction.name,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,5 +25,5 @@ const { note } = defineProps<{
|
|||
note: z.infer<typeof Status>;
|
||||
}>();
|
||||
|
||||
const parent = useNote(client, note.in_reply_to_id);
|
||||
const parent = useNote(note.in_reply_to_id);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ const { follower } = defineProps<{
|
|||
const loading = ref(true);
|
||||
const followerUrl = `/@${follower.acct}`;
|
||||
const [username, domain] = follower.acct.split("@");
|
||||
const { relationship } = useRelationship(client, follower.id);
|
||||
const { relationship } = useRelationship(follower.id);
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// TODO: Add "followed" notification
|
||||
watch(relationship, () => {
|
||||
|
|
@ -58,7 +59,7 @@ const accept = async () => {
|
|||
const id = toast.loading(m.cool_slimy_coyote_affirm());
|
||||
loading.value = true;
|
||||
|
||||
const { data } = await client.value.acceptFollowRequest(follower.id);
|
||||
const { data } = await authStore.client.acceptFollowRequest(follower.id);
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success(m.busy_awful_mouse_jump());
|
||||
|
|
@ -70,7 +71,7 @@ const reject = async () => {
|
|||
const id = toast.loading(m.front_sunny_penguin_flip());
|
||||
loading.value = true;
|
||||
|
||||
const { data } = await client.value.rejectFollowRequest(follower.id);
|
||||
const { data } = await authStore.client.rejectFollowRequest(follower.id);
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success(m.green_flat_mayfly_trust());
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ for (const name of [
|
|||
}
|
||||
|
||||
const issuerRedirectUrl = (issuerId: string) => {
|
||||
const url = new URL("/oauth/sso", client.value.url);
|
||||
const url = new URL("/oauth/sso", useRequestURL().origin);
|
||||
|
||||
for (const name of [
|
||||
"redirect_uri",
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ const copy = (data: string) => {
|
|||
toast.success("Copied to clipboard");
|
||||
};
|
||||
|
||||
const appData = useAppData();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const data: [string, string | VNode][] = [
|
||||
["User ID", identity.value?.account.id ?? ""],
|
||||
["Instance domain", identity.value?.instance.domain ?? ""],
|
||||
["Instance version", identity.value?.instance.versia_version ?? ""],
|
||||
["Client ID", appData.value?.client_id ?? ""],
|
||||
["User ID", authStore.account?.id ?? ""],
|
||||
["Instance domain", authStore.instance?.domain ?? ""],
|
||||
["Instance version", authStore.instance?.versia_version ?? ""],
|
||||
["Client ID", authStore.application?.client_id ?? ""],
|
||||
[
|
||||
"Client secret",
|
||||
<Button
|
||||
|
|
@ -39,7 +39,7 @@ const data: [string, string | VNode][] = [
|
|||
class="font-sans"
|
||||
size="sm"
|
||||
// @ts-expect-error missing onClick types
|
||||
onClick={() => copy(appData.value?.client_secret ?? "")}
|
||||
onClick={() => copy(authStore.application?.client_secret ?? "")}
|
||||
>
|
||||
Click to copy
|
||||
</Button>,
|
||||
|
|
@ -51,7 +51,7 @@ const data: [string, string | VNode][] = [
|
|||
class="font-sans"
|
||||
size="sm"
|
||||
// @ts-expect-error missing onClick types
|
||||
onClick={() => copy(identity.value?.tokens.access_token ?? "")}
|
||||
onClick={() => copy(authStore.token?.access_token ?? "")}
|
||||
>
|
||||
Click to copy
|
||||
</Button>,
|
||||
|
|
|
|||
|
|
@ -62,22 +62,11 @@ const categories = Object.fromEntries(
|
|||
}),
|
||||
);
|
||||
|
||||
const { account: author1 } = useAccountFromAcct(
|
||||
client,
|
||||
"jessew@vs.cpluspatch.com",
|
||||
);
|
||||
|
||||
const { account: author2 } = useAccountFromAcct(
|
||||
client,
|
||||
"aprl@social.lysand.org",
|
||||
);
|
||||
|
||||
const { account: author3 } = useAccountFromAcct(
|
||||
client,
|
||||
"lina@social.lysand.org",
|
||||
);
|
||||
|
||||
const { account: author4 } = useAccountFromAcct(client, "nyx@v.everypizza.im");
|
||||
const { account: author1 } = useAccountFromAcct("jessew@vs.cpluspatch.com");
|
||||
const { account: author2 } = useAccountFromAcct("aprl@social.lysand.org");
|
||||
const { account: author3 } = useAccountFromAcct("lina@social.lysand.org");
|
||||
const { account: author4 } = useAccountFromAcct("nyx@v.everypizza.im");
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const open = ref(false);
|
||||
|
||||
|
|
@ -87,14 +76,14 @@ useListen("preferences:open", () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open" v-if="identity">
|
||||
<Dialog v-model:open="open" v-if="authStore.isSignedIn">
|
||||
<DialogContent class="md:max-w-5xl w-full h-full p-0 md:max-h-[70dvh] overflow-hidden">
|
||||
<Tabs class="md:grid-cols-[auto_minmax(0,1fr)] !grid gap-2 *:p-4 overflow-hidden *:overflow-y-auto *:h-full" orientation="vertical"
|
||||
:default-value="pages[0]">
|
||||
<DialogHeader class="gap-6 grid grid-rows-[auto_minmax(0,1fr)] border-b md:border-b-0 md:border-r min-w-60 text-left">
|
||||
<div class="grid gap-3 items-center grid-cols-[auto_minmax(0,1fr)]">
|
||||
<Avatar :name="identity.account.display_name || identity.account.username"
|
||||
:src="identity.account.avatar" />
|
||||
<Avatar :name="authStore.account!.display_name || authStore.account!.username"
|
||||
:src="authStore.account!.avatar" />
|
||||
<DialogTitle>Preferences</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription class="sr-only">
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ const { emojis } = defineProps<{
|
|||
emojis: z.infer<typeof CustomEmoji>[];
|
||||
}>();
|
||||
|
||||
const permissions = usePermissions();
|
||||
const authStore = useAuthStore();
|
||||
const canEdit =
|
||||
(!emojis.some((e) => e.global) &&
|
||||
permissions.value.includes(RolePermission.ManageOwnEmojis)) ||
|
||||
permissions.value.includes(RolePermission.ManageEmojis);
|
||||
authStore.permissions.includes(RolePermission.ManageOwnEmojis)) ||
|
||||
authStore.permissions.includes(RolePermission.ManageEmojis);
|
||||
|
||||
const deleteAll = async () => {
|
||||
if (!identity.value) {
|
||||
if (!authStore.isSignedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -57,14 +57,16 @@ const deleteAll = async () => {
|
|||
);
|
||||
try {
|
||||
await Promise.all(
|
||||
emojis.map((emoji) => client.value.deleteEmoji(emoji.id)),
|
||||
emojis.map((emoji) => authStore.client.deleteEmoji(emoji.id)),
|
||||
);
|
||||
toast.dismiss(id);
|
||||
toast.success("Emojis deleted");
|
||||
|
||||
identity.value.emojis = identity.value.emojis.filter(
|
||||
(e) => !emojis.some((emoji) => e.id === emoji.id),
|
||||
);
|
||||
authStore.updateActiveIdentity({
|
||||
emojis: authStore.emojis.filter(
|
||||
(e) => !emojis.some((emoji) => e.id === emoji.id),
|
||||
),
|
||||
});
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,14 +42,10 @@ const { emoji } = defineProps<{
|
|||
emoji: z.infer<typeof CustomEmoji>;
|
||||
}>();
|
||||
|
||||
const permissions = usePermissions();
|
||||
const canEdit =
|
||||
(!emoji.global &&
|
||||
permissions.value.includes(RolePermission.ManageOwnEmojis)) ||
|
||||
permissions.value.includes(RolePermission.ManageEmojis);
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const editName = async () => {
|
||||
if (!identity.value) {
|
||||
if (!authStore.isSignedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -63,16 +59,18 @@ const editName = async () => {
|
|||
if (result.confirmed) {
|
||||
const id = toast.loading(m.teary_tame_gull_bless());
|
||||
try {
|
||||
const { data } = await client.value.updateEmoji(emoji.id, {
|
||||
const { data } = await authStore.client.updateEmoji(emoji.id, {
|
||||
shortcode: result.value,
|
||||
});
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success(m.gaudy_lime_bison_adore());
|
||||
|
||||
identity.value.emojis = identity.value.emojis.map((e) =>
|
||||
e.id === emoji.id ? data : e,
|
||||
);
|
||||
authStore.updateActiveIdentity({
|
||||
emojis: authStore.emojis.map((e) =>
|
||||
e.id === emoji.id ? data : e,
|
||||
),
|
||||
});
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
}
|
||||
|
|
@ -80,7 +78,7 @@ const editName = async () => {
|
|||
};
|
||||
|
||||
const _delete = async () => {
|
||||
if (!identity.value) {
|
||||
if (!authStore.isSignedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -93,13 +91,13 @@ const _delete = async () => {
|
|||
if (confirmed) {
|
||||
const id = toast.loading(m.weary_away_liger_zip());
|
||||
try {
|
||||
await client.value.deleteEmoji(emoji.id);
|
||||
await authStore.client.deleteEmoji(emoji.id);
|
||||
toast.dismiss(id);
|
||||
toast.success(m.crisp_whole_canary_tear());
|
||||
|
||||
identity.value.emojis = identity.value.emojis.filter(
|
||||
(e) => e.id !== emoji.id,
|
||||
);
|
||||
authStore.updateActiveIdentity({
|
||||
emojis: authStore.emojis.filter((e) => e.id !== emoji.id),
|
||||
});
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div v-if="emojis.length > 0" class="grow">
|
||||
<Table :emojis="emojis" :can-upload="canUpload" />
|
||||
<div v-if="authStore.emojis.length > 0" class="grow">
|
||||
<Table :emojis="authStore.emojis" :can-upload="canUpload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -8,12 +8,10 @@
|
|||
import { RolePermission } from "@versia/client/schemas";
|
||||
import Table from "./table.vue";
|
||||
|
||||
const permissions = usePermissions();
|
||||
const authStore = useAuthStore();
|
||||
const canUpload = computed(
|
||||
() =>
|
||||
permissions.value.includes(RolePermission.ManageOwnEmojis) ||
|
||||
permissions.value.includes(RolePermission.ManageEmojis),
|
||||
authStore.permissions.includes(RolePermission.ManageOwnEmojis) ||
|
||||
authStore.permissions.includes(RolePermission.ManageEmojis),
|
||||
);
|
||||
|
||||
const emojis = computed(() => identity.value?.emojis ?? []);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -189,8 +189,10 @@ import { Textarea } from "~/components/ui/textarea";
|
|||
import * as m from "~~/paraglide/messages.js";
|
||||
|
||||
const open = ref(false);
|
||||
const permissions = usePermissions();
|
||||
const hasEmojiAdmin = permissions.value.includes(RolePermission.ManageEmojis);
|
||||
const authStore = useAuthStore();
|
||||
const hasEmojiAdmin = authStore.permissions.includes(
|
||||
RolePermission.ManageEmojis,
|
||||
);
|
||||
const createObjectURL = URL.createObjectURL;
|
||||
|
||||
const formSchema = toTypedSchema(
|
||||
|
|
@ -202,11 +204,11 @@ const formSchema = toTypedSchema(
|
|||
.refine(
|
||||
(v) =>
|
||||
v.size <=
|
||||
(identity.value?.instance.configuration.emojis
|
||||
(authStore.instance?.configuration.emojis
|
||||
.emoji_size_limit ?? Number.POSITIVE_INFINITY),
|
||||
m.orange_weird_parakeet_hug({
|
||||
count:
|
||||
identity.value?.instance.configuration.emojis
|
||||
authStore.instance?.configuration.emojis
|
||||
.emoji_size_limit ?? Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
),
|
||||
|
|
@ -214,11 +216,11 @@ const formSchema = toTypedSchema(
|
|||
.string()
|
||||
.min(1)
|
||||
.max(
|
||||
identity.value?.instance.configuration.emojis
|
||||
authStore.instance?.configuration.emojis
|
||||
.max_shortcode_characters ?? Number.POSITIVE_INFINITY,
|
||||
m.solid_inclusive_owl_hug({
|
||||
count:
|
||||
identity.value?.instance.configuration.emojis
|
||||
authStore.instance?.configuration.emojis
|
||||
.max_shortcode_characters ??
|
||||
Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
|
|
@ -237,11 +239,11 @@ const formSchema = toTypedSchema(
|
|||
alt: z
|
||||
.string()
|
||||
.max(
|
||||
identity.value?.instance.configuration.emojis
|
||||
authStore.instance?.configuration.emojis
|
||||
.max_description_characters ?? Number.POSITIVE_INFINITY,
|
||||
m.key_ago_hound_emerge({
|
||||
count:
|
||||
identity.value?.instance.configuration.emojis
|
||||
authStore.instance?.configuration.emojis
|
||||
.max_description_characters ??
|
||||
Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
|
|
@ -254,14 +256,14 @@ const { isSubmitting, handleSubmit, values, setFieldValue } = useForm({
|
|||
});
|
||||
|
||||
const submit = handleSubmit(async (values) => {
|
||||
if (!identity.value) {
|
||||
if (!authStore.isSignedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = toast.loading(m.factual_gray_mouse_believe());
|
||||
|
||||
try {
|
||||
const { data } = await client.value.uploadEmoji(
|
||||
const { data } = await authStore.client.uploadEmoji(
|
||||
values.shortcode,
|
||||
values.image,
|
||||
{
|
||||
|
|
@ -274,7 +276,10 @@ const submit = handleSubmit(async (values) => {
|
|||
toast.dismiss(id);
|
||||
toast.success(m.cool_trite_gull_quiz());
|
||||
|
||||
identity.value.emojis = [...identity.value.emojis, data];
|
||||
authStore.updateActiveIdentity({
|
||||
emojis: [...authStore.emojis, data],
|
||||
});
|
||||
|
||||
open.value = false;
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { toTypedSchema } from "@vee-validate/zod";
|
||||
import type { Instance } from "@versia/client/schemas";
|
||||
import { z } from "zod";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
|
||||
const characterRegex = new RegExp(/^[a-z0-9_-]+$/);
|
||||
|
||||
export const formSchema = (identity: Identity) =>
|
||||
export const formSchema = (instance: z.infer<typeof Instance>) =>
|
||||
toTypedSchema(
|
||||
z.strictObject({
|
||||
banner: z
|
||||
|
|
@ -12,11 +13,10 @@ export const formSchema = (identity: Identity) =>
|
|||
.refine(
|
||||
(v) =>
|
||||
v.size <=
|
||||
(identity.instance.configuration.accounts
|
||||
.header_limit ?? Number.POSITIVE_INFINITY),
|
||||
(instance.configuration.accounts.header_limit ??
|
||||
Number.POSITIVE_INFINITY),
|
||||
m.civil_icy_ant_mend({
|
||||
size: identity.instance.configuration.accounts
|
||||
.header_limit,
|
||||
size: instance.configuration.accounts.header_limit,
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
|
|
@ -25,11 +25,10 @@ export const formSchema = (identity: Identity) =>
|
|||
.refine(
|
||||
(v) =>
|
||||
v.size <=
|
||||
(identity.instance.configuration.accounts
|
||||
.avatar_limit ?? Number.POSITIVE_INFINITY),
|
||||
(instance.configuration.accounts.avatar_limit ??
|
||||
Number.POSITIVE_INFINITY),
|
||||
m.zippy_caring_raven_edit({
|
||||
size: identity.instance.configuration.accounts
|
||||
.avatar_limit,
|
||||
size: instance.configuration.accounts.avatar_limit,
|
||||
}),
|
||||
)
|
||||
.or(z.string().url())
|
||||
|
|
@ -37,22 +36,15 @@ export const formSchema = (identity: Identity) =>
|
|||
name: z
|
||||
.string()
|
||||
.max(
|
||||
identity.instance.configuration.accounts
|
||||
.max_displayname_characters,
|
||||
instance.configuration.accounts.max_displayname_characters,
|
||||
),
|
||||
username: z
|
||||
.string()
|
||||
.regex(characterRegex, m.still_upper_otter_dine())
|
||||
.max(
|
||||
identity.instance.configuration.accounts
|
||||
.max_username_characters,
|
||||
),
|
||||
.max(instance.configuration.accounts.max_username_characters),
|
||||
bio: z
|
||||
.string()
|
||||
.max(
|
||||
identity.instance.configuration.accounts
|
||||
.max_note_characters,
|
||||
),
|
||||
.max(instance.configuration.accounts.max_note_characters),
|
||||
bot: z.boolean().default(false),
|
||||
locked: z.boolean().default(false),
|
||||
discoverable: z.boolean().default(true),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<form v-if="identity" class="grid gap-6" @submit="save">
|
||||
<form class="grid gap-6" @submit="save">
|
||||
<Transition name="slide-up">
|
||||
<Alert v-if="dirty" layout="button" class="absolute bottom-2 z-10 inset-x-2 w-[calc(100%-1rem)]">
|
||||
<SaveOff class="size-4" />
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
<FormField v-slot="{ setValue }" name="avatar">
|
||||
<TextInput :title="m.safe_icy_bulldog_quell()">
|
||||
<ImageUploader v-model:image="identity.account.avatar" @submit-file="(file) => setValue(file)"
|
||||
<ImageUploader v-model:image="authStore.account!.avatar" @submit-file="(file) => setValue(file)"
|
||||
@submit-url="(url) => setValue(url)" />
|
||||
</TextInput>
|
||||
</FormField>
|
||||
|
|
@ -85,25 +85,25 @@ import ImageUploader from "./profile/image-uploader.vue";
|
|||
|
||||
const dirty = computed(() => form.meta.value.dirty);
|
||||
const submitting = ref(false);
|
||||
const authStore = useAuthStore();
|
||||
|
||||
if (!identity.value) {
|
||||
throw new Error("Identity not found.");
|
||||
if (!(authStore.instance && authStore.account)) {
|
||||
throw new Error("Not signed in.");
|
||||
}
|
||||
|
||||
const account = computed(() => identity.value?.account as Identity["account"]);
|
||||
const schema = formSchema(identity.value);
|
||||
const schema = formSchema(authStore.instance);
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: schema,
|
||||
initialValues: {
|
||||
bio: account.value.source?.note ?? "",
|
||||
bot: account.value.bot ?? false,
|
||||
locked: account.value.locked ?? false,
|
||||
discoverable: account.value.discoverable ?? true,
|
||||
username: account.value.username,
|
||||
name: account.value.display_name,
|
||||
bio: authStore.account.source?.note ?? "",
|
||||
bot: authStore.account.bot ?? false,
|
||||
locked: authStore.account.locked ?? false,
|
||||
discoverable: authStore.account.discoverable ?? true,
|
||||
username: authStore.account.username,
|
||||
name: authStore.account.display_name,
|
||||
fields:
|
||||
account.value.source?.fields.map((f) => ({
|
||||
authStore.account.source?.fields.map((f) => ({
|
||||
name: f.name,
|
||||
value: f.value,
|
||||
})) ?? [],
|
||||
|
|
@ -111,7 +111,7 @@ const form = useForm({
|
|||
});
|
||||
|
||||
const save = form.handleSubmit(async (values) => {
|
||||
if (submitting.value) {
|
||||
if (submitting.value || !authStore.account) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -120,25 +120,29 @@ const save = form.handleSubmit(async (values) => {
|
|||
|
||||
const changedData = {
|
||||
display_name:
|
||||
values.name === account.value.display_name
|
||||
values.name === authStore.account.display_name
|
||||
? undefined
|
||||
: values.name,
|
||||
username:
|
||||
values.username === account.value.username
|
||||
values.username === authStore.account.username
|
||||
? undefined
|
||||
: values.username,
|
||||
note:
|
||||
values.bio === account.value.source?.note ? undefined : values.bio,
|
||||
bot: values.bot === account.value.bot ? undefined : values.bot,
|
||||
values.bio === authStore.account.source?.note
|
||||
? undefined
|
||||
: values.bio,
|
||||
bot: values.bot === authStore.account.bot ? undefined : values.bot,
|
||||
locked:
|
||||
values.locked === account.value.locked ? undefined : values.locked,
|
||||
values.locked === authStore.account.locked
|
||||
? undefined
|
||||
: values.locked,
|
||||
discoverable:
|
||||
values.discoverable === account.value.discoverable
|
||||
values.discoverable === authStore.account.discoverable
|
||||
? undefined
|
||||
: values.discoverable,
|
||||
// Can't compare two arrays directly in JS, so we need to check if all fields are the same
|
||||
fields_attributes: values.fields.every((field) =>
|
||||
account.value.source?.fields?.some(
|
||||
authStore.account?.source?.fields?.some(
|
||||
(f) => f.name === field.name && f.value === field.value,
|
||||
),
|
||||
)
|
||||
|
|
@ -157,7 +161,7 @@ const save = form.handleSubmit(async (values) => {
|
|||
}
|
||||
|
||||
try {
|
||||
const { data } = await client.value.updateCredentials(
|
||||
const { data } = await authStore.client.updateCredentials(
|
||||
Object.fromEntries(
|
||||
Object.entries(changedData).filter(([, v]) => v !== undefined),
|
||||
),
|
||||
|
|
@ -166,9 +170,9 @@ const save = form.handleSubmit(async (values) => {
|
|||
toast.dismiss(id);
|
||||
toast.success(m.spry_honest_kestrel_arrive());
|
||||
|
||||
if (identity.value) {
|
||||
identity.value.account = data;
|
||||
}
|
||||
authStore.updateActiveIdentity({
|
||||
account: data,
|
||||
});
|
||||
|
||||
form.resetForm({
|
||||
values: {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@
|
|||
{{ m.active_trite_lark_inspire() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator v-if="isLoggedIn && !isMe" />
|
||||
<DropdownMenuGroup v-if="isLoggedIn && !isMe">
|
||||
<DropdownMenuSeparator v-if="authStore.isSignedIn && !isMe" />
|
||||
<DropdownMenuGroup v-if="authStore.isSignedIn && !isMe">
|
||||
<DropdownMenuItem as="button" @click="muteUser(account.id)">
|
||||
<VolumeX />
|
||||
{{ m.spare_wild_mole_intend() }}
|
||||
|
|
@ -51,8 +51,8 @@
|
|||
{{ m.slow_chunky_chipmunk_hush() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator v-if="isLoggedIn && !isMe" />
|
||||
<DropdownMenuGroup v-if="isLoggedIn && !isMe">
|
||||
<DropdownMenuSeparator v-if="authStore.isSignedIn && !isMe" />
|
||||
<DropdownMenuGroup v-if="authStore.isSignedIn && !isMe">
|
||||
<DropdownMenuItem as="button" :disabled="true">
|
||||
<Flag />
|
||||
{{ m.great_few_jaguar_rise() }}
|
||||
|
|
@ -91,8 +91,8 @@ const { account } = defineProps<{
|
|||
account: z.infer<typeof Account>;
|
||||
}>();
|
||||
|
||||
const isMe = identity.value?.account.id === account.id;
|
||||
const isLoggedIn = !!identity.value;
|
||||
const authStore = useAuthStore();
|
||||
const isMe = authStore.account?.id === account.id;
|
||||
|
||||
const { copy } = useClipboard();
|
||||
const copyText = (text: string) => {
|
||||
|
|
@ -105,7 +105,7 @@ const isRemote = account.acct.includes("@");
|
|||
|
||||
const muteUser = async (userId: string) => {
|
||||
const id = toast.loading(m.ornate_tidy_coyote_grow());
|
||||
await client.value.muteAccount(userId);
|
||||
await authStore.client.muteAccount(userId);
|
||||
toast.dismiss(id);
|
||||
|
||||
toast.success("User muted");
|
||||
|
|
@ -113,7 +113,7 @@ const muteUser = async (userId: string) => {
|
|||
|
||||
const blockUser = async (userId: string) => {
|
||||
const id = toast.loading(m.empty_smug_raven_bloom());
|
||||
await client.value.blockAccount(userId);
|
||||
await authStore.client.blockAccount(userId);
|
||||
toast.dismiss(id);
|
||||
|
||||
toast.success("User blocked");
|
||||
|
|
@ -121,7 +121,7 @@ const blockUser = async (userId: string) => {
|
|||
|
||||
const refresh = async () => {
|
||||
const id = toast.loading(m.real_every_macaw_wish());
|
||||
await client.value.refetchAccount(account.id);
|
||||
await authStore.client.refetchAccount(account.id);
|
||||
toast.dismiss(id);
|
||||
|
||||
toast.success(m.many_cool_fox_love());
|
||||
|
|
|
|||
|
|
@ -33,15 +33,14 @@ import ProfileBadge from "./profile-badge.vue";
|
|||
const { account } = defineProps<{
|
||||
account: z.infer<typeof Account>;
|
||||
}>();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const config = useConfig();
|
||||
const roles = account.roles.filter((r) => r.visible);
|
||||
// Get user handle in username@instance format
|
||||
const handle = account.acct.includes("@")
|
||||
? account.acct
|
||||
: `${account.acct}@${
|
||||
identity.value?.instance.domain ?? window.location.host
|
||||
}`;
|
||||
: `${account.acct}@${authStore.instance?.domain ?? window.location.host}`;
|
||||
const isDeveloper = config.DEVELOPER_HANDLES.includes(handle);
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<Button variant="secondary" :disabled="isLoading || relationship?.requested" v-if="!isMe && identity"
|
||||
<Button variant="secondary" :disabled="isLoading || relationship?.requested" v-if="!isMe && authStore.isSignedIn"
|
||||
@click="relationship?.following ? unfollow() : follow()">
|
||||
<Loader v-if="isLoading" class="animate-spin" />
|
||||
<span v-else>
|
||||
|
|
@ -27,8 +27,9 @@ const { account } = defineProps<{
|
|||
account: z.infer<typeof Account>;
|
||||
}>();
|
||||
|
||||
const { relationship, isLoading } = useRelationship(client, account.id);
|
||||
const isMe = identity.value?.account.id === account.id;
|
||||
const { relationship, isLoading } = useRelationship(account.id);
|
||||
const authStore = useAuthStore();
|
||||
const isMe = authStore.account?.id === account.id;
|
||||
|
||||
const follow = async () => {
|
||||
if (preferences.confirm_actions.value.includes("follow")) {
|
||||
|
|
@ -47,7 +48,7 @@ const follow = async () => {
|
|||
}
|
||||
|
||||
const id = toast.loading(m.quick_basic_peacock_bubble());
|
||||
const { data } = await client.value.followAccount(account.id);
|
||||
const { data } = await authStore.client.followAccount(account.id);
|
||||
toast.dismiss(id);
|
||||
|
||||
relationship.value = data;
|
||||
|
|
@ -71,7 +72,7 @@ const unfollow = async () => {
|
|||
}
|
||||
|
||||
const id = toast.loading(m.big_safe_guppy_mix());
|
||||
const { data } = await client.value.unfollowAccount(account.id);
|
||||
const { data } = await authStore.client.unfollowAccount(account.id);
|
||||
toast.dismiss(id);
|
||||
|
||||
relationship.value = data;
|
||||
|
|
|
|||
|
|
@ -10,15 +10,15 @@
|
|||
Manage your accounts and settings.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div v-if="identities.length > 0" class="grid gap-4 py-2">
|
||||
<div v-for="identity of identities" :key="identity.account.id"
|
||||
<div v-if="authStore.identities.length > 0" class="grid gap-4 py-2">
|
||||
<div v-for="identity of authStore.identities" :key="identity.account.id"
|
||||
class="grid grid-cols-[1fr_auto] has-[>[data-switch]]:grid-cols-[1fr_auto_auto] gap-2">
|
||||
<TinyCard :account="identity.account" :domain="identity.instance.domain" naked />
|
||||
<Button data-switch v-if="currentIdentity?.id !== identity.id"
|
||||
@click="switchAccount(identity.account.id)" variant="outline">
|
||||
<Button data-switch v-if="authStore.identity?.id !== identity.id"
|
||||
@click="authStore.setActiveIdentity(identity.id)" variant="outline">
|
||||
Switch
|
||||
</Button>
|
||||
<Button @click="signOut(appData, identity)" variant="outline" size="icon"
|
||||
<Button @click="signOutAction(identity.id)" variant="outline" size="icon"
|
||||
:title="m.sharp_big_mallard_reap()">
|
||||
<LogOut />
|
||||
</Button>
|
||||
|
|
@ -47,7 +47,6 @@
|
|||
import { LogIn, LogOut, UserPlus } from "lucide-vue-next";
|
||||
import { toast } from "vue-sonner";
|
||||
import { NuxtLink } from "#components";
|
||||
import { identity as currentIdentity } from "#imports";
|
||||
import TinyCard from "~/components/profiles/tiny-card.vue";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
|
|
@ -61,31 +60,25 @@ import {
|
|||
} from "~/components/ui/dialog";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
|
||||
const appData = useAppData();
|
||||
const authStore = useAuthStore();
|
||||
const signInAction = async () => {
|
||||
const instance = await askForInstance();
|
||||
|
||||
const signInAction = async () => signIn(appData, await askForInstance());
|
||||
const id = toast.loading(m.level_due_ox_greet());
|
||||
|
||||
const switchAccount = async (userId: string) => {
|
||||
if (userId === currentIdentity.value?.account.id) {
|
||||
return await navigateTo(`/@${currentIdentity.value.account.username}`);
|
||||
}
|
||||
|
||||
const id = toast.loading("Switching account...");
|
||||
|
||||
const identityToSwitch = identities.value.find(
|
||||
(i) => i.account.id === userId,
|
||||
);
|
||||
|
||||
if (!identityToSwitch) {
|
||||
try {
|
||||
await authStore.startSignIn(instance);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.dismiss(id);
|
||||
toast.error("No identity to switch to");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
currentIdentity.value = identityToSwitch;
|
||||
const signOutAction = async (identityId: string) => {
|
||||
const id = toast.loading("Signing out...");
|
||||
|
||||
await authStore.signOut(identityId);
|
||||
toast.dismiss(id);
|
||||
toast.success("Switched account");
|
||||
|
||||
window.location.href = "/";
|
||||
toast.success("Signed out");
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import * as m from "~~/paraglide/messages.js";
|
|||
import AccountManager from "../account/account-manager.vue";
|
||||
|
||||
const { $pwa } = useNuxtApp();
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -25,8 +26,8 @@ const { $pwa } = useNuxtApp();
|
|||
<SidebarMenu class="gap-3">
|
||||
<SidebarMenuItem>
|
||||
<AccountManager>
|
||||
<SidebarMenuButton v-if="identity" size="lg">
|
||||
<TinyCard :account="identity.account" :domain="identity.instance.domain" naked />
|
||||
<SidebarMenuButton v-if="authStore.account && authStore.instance" size="lg">
|
||||
<TinyCard :account="authStore.account" :domain="authStore.instance.domain" naked />
|
||||
<ChevronsUpDown class="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButton v-else>
|
||||
|
|
@ -37,14 +38,14 @@ const { $pwa } = useNuxtApp();
|
|||
</AccountManager>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem class="flex flex-col gap-2">
|
||||
<Button v-if="identity" variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
|
||||
<Button v-if="authStore.isSignedIn" variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
|
||||
@click="useEvent('composer:open')">
|
||||
<Pen />
|
||||
<span class="group-data-[collapsible=icon]:hidden">
|
||||
{{ m.salty_aloof_turkey_nudge() }}
|
||||
</span>
|
||||
</Button>
|
||||
<Button v-if="identity" size="lg" variant="secondary" @click="useEvent('preferences:open')">
|
||||
<Button v-if="authStore.isSignedIn" size="lg" variant="secondary" @click="useEvent('preferences:open')">
|
||||
<Cog />
|
||||
Preferences
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
SidebarMenuItem,
|
||||
} from "~/components/ui/sidebar";
|
||||
|
||||
const instance = useInstance();
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -14,7 +14,7 @@ const instance = useInstance();
|
|||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<NuxtLink href="/">
|
||||
<InstanceSmallCard v-if="instance" :instance="instance" />
|
||||
<InstanceSmallCard v-if="authStore.instance" :instance="authStore.instance" />
|
||||
</NuxtLink>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<NavItems
|
||||
:items="
|
||||
sidebarConfig.other.filter((i) =>
|
||||
i.requiresLogin ? !!identity : true
|
||||
i.requiresLogin ? authStore.isSignedIn : true
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
|
@ -33,4 +33,6 @@ import * as m from "~~/paraglide/messages.js";
|
|||
import FooterActions from "./footer/footer-actions.vue";
|
||||
import InstanceHeader from "./instance/instance-header.vue";
|
||||
import NavItems from "./navigation/nav-items.vue";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const showTimelines = computed(
|
|||
["/", "/home", "/local", "/public", "/global"].includes(route.path) &&
|
||||
isMd.value,
|
||||
);
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -23,5 +24,5 @@ const showTimelines = computed(
|
|||
</header>
|
||||
<slot />
|
||||
</main>
|
||||
<RightSidebar v-if="identity" v-show="preferences.display_notifications_sidebar" />
|
||||
<RightSidebar v-if="authStore.isSignedIn" v-show="preferences.display_notifications_sidebar" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const {
|
|||
loadPrev,
|
||||
removeItem,
|
||||
updateItem,
|
||||
} = useAccountTimeline(client.value, props.id);
|
||||
} = useAccountTimeline(props.id);
|
||||
|
||||
useListen("note:delete", ({ id }) => {
|
||||
removeItem(id);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const {
|
|||
loadPrev,
|
||||
removeItem,
|
||||
updateItem,
|
||||
} = useGlobalTimeline(client.value);
|
||||
} = useGlobalTimeline();
|
||||
|
||||
useListen("note:delete", ({ id }) => {
|
||||
removeItem(id);
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
:update-item="updateItem" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>import { useHomeTimeline } from "~/composables/HomeTimeline";
|
||||
<script lang="ts" setup>
|
||||
import { useHomeTimeline } from "~/composables/HomeTimeline";
|
||||
import Timeline from "./timeline.vue";
|
||||
import type { z } from "zod";
|
||||
|
||||
const {
|
||||
error,
|
||||
|
|
@ -17,7 +17,7 @@ const {
|
|||
loadPrev,
|
||||
removeItem,
|
||||
updateItem,
|
||||
} = useHomeTimeline(client.value);
|
||||
} = useHomeTimeline();
|
||||
|
||||
useListen("note:delete", ({ id }) => {
|
||||
removeItem(id);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const {
|
|||
loadPrev,
|
||||
removeItem,
|
||||
updateItem,
|
||||
} = useLocalTimeline(client.value);
|
||||
} = useLocalTimeline();
|
||||
|
||||
useListen("note:delete", ({ id }) => {
|
||||
removeItem(id);
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@ const {
|
|||
loadPrev,
|
||||
removeItem,
|
||||
updateItem,
|
||||
} = useNotificationTimeline(client.value);
|
||||
} = useNotificationTimeline();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const {
|
|||
loadPrev,
|
||||
removeItem,
|
||||
updateItem,
|
||||
} = usePublicTimeline(client.value);
|
||||
} = usePublicTimeline();
|
||||
|
||||
useListen("note:delete", ({ id }) => {
|
||||
removeItem(id);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue