slightly better status, internationalization
This commit is contained in:
		
							parent
							
								
									7c5acad535
								
							
						
					
					
						commit
						a06dd22da4
					
				
					 22 changed files with 2416 additions and 1009 deletions
				
			
		| 
						 | 
				
			
			@ -1,7 +1,36 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { useI18n } from "vue-i18n";
 | 
			
		||||
import type Activity from "@/lib/api/entities/activity";
 | 
			
		||||
import { IconWorld, IconHome, IconLock, IconMail } from "@tabler/icons-vue";
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
defineProps<{ status: Activity }>();
 | 
			
		||||
 | 
			
		||||
const statusScopeKey = ({ visibility }: Activity) => {
 | 
			
		||||
	switch (visibility) {
 | 
			
		||||
		case "direct":
 | 
			
		||||
			return "status.visibility.directMessage";
 | 
			
		||||
		case "private":
 | 
			
		||||
			return "status.visibility.followersOnly";
 | 
			
		||||
		case "unlisted":
 | 
			
		||||
			return "status.visibility.unlisted";
 | 
			
		||||
		case "public":
 | 
			
		||||
			return "status.visibility.public";
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const statusScopeIcon = ({ visibility }: Activity) => {
 | 
			
		||||
	switch (visibility) {
 | 
			
		||||
		case "direct":
 | 
			
		||||
			return IconMail
 | 
			
		||||
		case "private":
 | 
			
		||||
			return IconLock
 | 
			
		||||
		case "unlisted":
 | 
			
		||||
			return IconHome
 | 
			
		||||
		case "public":
 | 
			
		||||
			return IconWorld
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
| 
						 | 
				
			
			@ -12,8 +41,8 @@ defineProps<{ status: Activity }>();
 | 
			
		|||
					class="rounded-full m-2"
 | 
			
		||||
					:src="status.account.avatar"
 | 
			
		||||
					width="64"
 | 
			
		||||
					alt="Avatar for {{ status.account.acct }}"
 | 
			
		||||
					title="Avatar for {{ status.account.acct }}"
 | 
			
		||||
					:alt="t('status.avatar', { name: status.account.acct })"
 | 
			
		||||
					:title="t('status.avatar', { name: status.account.acct })"
 | 
			
		||||
				/>
 | 
			
		||||
			</RouterLink>
 | 
			
		||||
			<div class="flex flex-col my-1">
 | 
			
		||||
| 
						 | 
				
			
			@ -29,11 +58,10 @@ defineProps<{ status: Activity }>();
 | 
			
		|||
		</div>
 | 
			
		||||
		<div class="my-1 flex flex-row">
 | 
			
		||||
			<span>
 | 
			
		||||
				<span title="{t(statusScopeKey(status))}" aria-hidden="true">
 | 
			
		||||
					<!-- <StatusScopeIcon status="{status}" /> -->
 | 
			
		||||
					Public
 | 
			
		||||
				<span :title="t(statusScopeKey(status))" aria-hidden="true">
 | 
			
		||||
					<component :is="statusScopeIcon(status)" />
 | 
			
		||||
				</span>
 | 
			
		||||
				<span class="sr-only">{t(statusScopeKey(status))}</span>
 | 
			
		||||
				<span class="sr-only">{{ t(statusScopeKey(status)) }}</span>
 | 
			
		||||
			</span>
 | 
			
		||||
			<RouterLink :to="`/@${status.account.acct}/statuses/${status.id}`">
 | 
			
		||||
				{humanizeDuration(time)}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										35
									
								
								src/i18n.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/i18n.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
import { nextTick } from "vue";
 | 
			
		||||
import { createI18n } from "vue-i18n";
 | 
			
		||||
import en from "./locale/en.yaml";
 | 
			
		||||
 | 
			
		||||
export const supportedLocales = ["en", "en-PR"];
 | 
			
		||||
export const i18n = createI18n({
 | 
			
		||||
	legacy: false,
 | 
			
		||||
	locale: "en",
 | 
			
		||||
	messages: {
 | 
			
		||||
		en: en,
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function setLanguage(locale: string) {
 | 
			
		||||
	// @ts-ignore
 | 
			
		||||
	i18n.global.locale.value = locale;
 | 
			
		||||
	document.querySelector("html")!.setAttribute("lang", locale);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function loadLocale(locale: string) {
 | 
			
		||||
	// Check if the locale is already loaded. If not, return
 | 
			
		||||
	// @ts-ignore
 | 
			
		||||
	if (i18n.global.availableLocales.indexOf(locale) !== -1) return;
 | 
			
		||||
 | 
			
		||||
	if (supportedLocales.indexOf(locale) === -1) throw `${locale} is not a valid locale`;
 | 
			
		||||
 | 
			
		||||
	const messages = await locales[locale as keyof typeof locales]();
 | 
			
		||||
	i18n.global.setLocaleMessage(locale, messages.default);
 | 
			
		||||
 | 
			
		||||
	return nextTick();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const locales = {
 | 
			
		||||
	"en-PR": () => import("./locale/en.pirate.yaml"),
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,48 +1,48 @@
 | 
			
		|||
import type { CustomEmoji } from "./custom_emoji";
 | 
			
		||||
 | 
			
		||||
	export interface Account {
 | 
			
		||||
	  id: string;
 | 
			
		||||
	  /** Username, not including domain */
 | 
			
		||||
	  username: string;
 | 
			
		||||
	  /** Webfinger account URI */
 | 
			
		||||
	  acct: string;
 | 
			
		||||
	  /** The user's profile page */
 | 
			
		||||
	  url: string;
 | 
			
		||||
	  /** The user's nickname/display name */
 | 
			
		||||
	  display_name: string;
 | 
			
		||||
	  /** The user's bio */
 | 
			
		||||
	  note: string;
 | 
			
		||||
	  /** The user's avatar URL */
 | 
			
		||||
	  avatar: string;
 | 
			
		||||
	  /** The user's avatar URL as a static image. Same as `avatar` if the avatar is not animated */
 | 
			
		||||
	  avatar_static: string;
 | 
			
		||||
	  /** The user's header URL */
 | 
			
		||||
	  header: string;
 | 
			
		||||
	  /** The user's header URL as a static image. Same as `header` if the header is not animated */
 | 
			
		||||
	  header_static: string;
 | 
			
		||||
	  /** Whether the account manually approves follow requests */
 | 
			
		||||
	  locked: boolean;
 | 
			
		||||
	  /** Additional metadata attached to a profile as name-value pairs */
 | 
			
		||||
	  fields: Field[];
 | 
			
		||||
	  /** Custom emoji entities to be used when rendering the profile */
 | 
			
		||||
	  emojis: CustomEmoji[];
 | 
			
		||||
	  /** Indicates that the account may perform automated actions, may not be monitored, or identifies as a robot */
 | 
			
		||||
	  bot: boolean;
 | 
			
		||||
	  /** When the account was created */
 | 
			
		||||
	  created_at: string;
 | 
			
		||||
	  /** When the most recent status was posted */
 | 
			
		||||
	  last_status_at: string | null;
 | 
			
		||||
	  /** How many statuses are attached to this account */
 | 
			
		||||
	  statuses_count: number;
 | 
			
		||||
	  /** The reported followers of this profile */
 | 
			
		||||
	  followers_count: number;
 | 
			
		||||
	  /** The reported follows of this profile */
 | 
			
		||||
	  following_count: number;
 | 
			
		||||
	}
 | 
			
		||||
export interface Account {
 | 
			
		||||
	id: string;
 | 
			
		||||
	/** Username, not including domain */
 | 
			
		||||
	username: string;
 | 
			
		||||
	/** Webfinger account URI */
 | 
			
		||||
	acct: string;
 | 
			
		||||
	/** The user's profile page */
 | 
			
		||||
	url: string;
 | 
			
		||||
	/** The user's nickname/display name */
 | 
			
		||||
	display_name: string;
 | 
			
		||||
	/** The user's bio */
 | 
			
		||||
	note: string;
 | 
			
		||||
	/** The user's avatar URL */
 | 
			
		||||
	avatar: string;
 | 
			
		||||
	/** The user's avatar URL as a static image. Same as `avatar` if the avatar is not animated */
 | 
			
		||||
	avatar_static: string;
 | 
			
		||||
	/** The user's header URL */
 | 
			
		||||
	header: string;
 | 
			
		||||
	/** The user's header URL as a static image. Same as `header` if the header is not animated */
 | 
			
		||||
	header_static: string;
 | 
			
		||||
	/** Whether the account manually approves follow requests */
 | 
			
		||||
	locked: boolean;
 | 
			
		||||
	/** Additional metadata attached to a profile as name-value pairs */
 | 
			
		||||
	fields: Field[];
 | 
			
		||||
	/** Custom emoji entities to be used when rendering the profile */
 | 
			
		||||
	emojis: CustomEmoji[];
 | 
			
		||||
	/** Indicates that the account may perform automated actions, may not be monitored, or identifies as a robot */
 | 
			
		||||
	bot: boolean;
 | 
			
		||||
	/** When the account was created */
 | 
			
		||||
	created_at: string;
 | 
			
		||||
	/** When the most recent status was posted */
 | 
			
		||||
	last_status_at: string | null;
 | 
			
		||||
	/** How many statuses are attached to this account */
 | 
			
		||||
	statuses_count: number;
 | 
			
		||||
	/** The reported followers of this profile */
 | 
			
		||||
	followers_count: number;
 | 
			
		||||
	/** The reported follows of this profile */
 | 
			
		||||
	following_count: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	export interface Field {
 | 
			
		||||
  /** The key of a given field’s key-value pair */
 | 
			
		||||
	  name: string;
 | 
			
		||||
	  /** The value associated with the name key */
 | 
			
		||||
	  value: string;
 | 
			
		||||
	}
 | 
			
		||||
export interface Field {
 | 
			
		||||
	/** The key of a given field’s key-value pair */
 | 
			
		||||
	name: string;
 | 
			
		||||
	/** The value associated with the name key */
 | 
			
		||||
	value: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										32
									
								
								src/locale/en.pirate.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/locale/en.pirate.yaml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
status:
 | 
			
		||||
  avatar: "{name}'s mugshot"
 | 
			
		||||
  characterCount: "{count} character | {count} characters"
 | 
			
		||||
  showContent: "Show treasure ({@:status.characterCount})"
 | 
			
		||||
  hideContent: "Hide treasure ({@:status.characterCount})"
 | 
			
		||||
  visibility:
 | 
			
		||||
    directMessage: Direct bottle
 | 
			
		||||
    followersOnly: Only shown to their mateys
 | 
			
		||||
    unlisted: Quietly thrown in the sea
 | 
			
		||||
    public: Free as the wind
 | 
			
		||||
navbar:
 | 
			
		||||
  homeTimeine: Yar friends' bottles
 | 
			
		||||
  localTimeline: Ship's bottles
 | 
			
		||||
  bubbleTimeline: Fleet's bottles
 | 
			
		||||
  globalTimeline: The seven seas
 | 
			
		||||
  settings: Yar pref'rences
 | 
			
		||||
  login: Report fer duty
 | 
			
		||||
  logout: Go to sleep
 | 
			
		||||
settings:
 | 
			
		||||
  darkTheme: Like the moon's out
 | 
			
		||||
  lightTheme: Like the sun's out
 | 
			
		||||
sidebar:
 | 
			
		||||
  postbox:
 | 
			
		||||
    placeholder: What be happening on deck?
 | 
			
		||||
    postButton: Roll up an' send
 | 
			
		||||
    directMessage: Direct bottle
 | 
			
		||||
    followersOnly: Only send to yer mateys
 | 
			
		||||
    unlisted: Quietly throw in the sea
 | 
			
		||||
    public: Post free as the wind
 | 
			
		||||
    emojiButton: Draw lil' faces
 | 
			
		||||
    pollButton: Start a vote
 | 
			
		||||
    fileButton: Draw images
 | 
			
		||||
							
								
								
									
										32
									
								
								src/locale/en.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/locale/en.yaml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
status:
 | 
			
		||||
  avatar: "{name}'s avatar"
 | 
			
		||||
  characterCount: "{count} character | {count} characters"
 | 
			
		||||
  showContent: "Show content ({@:status.characterCount})"
 | 
			
		||||
  hideContent: "Hide content ({@:status.characterCount})"
 | 
			
		||||
  visibility:
 | 
			
		||||
    directMessage: Direct message
 | 
			
		||||
    followersOnly: Followers only
 | 
			
		||||
    unlisted: Unlisted
 | 
			
		||||
    public: Public
 | 
			
		||||
navbar:
 | 
			
		||||
  homeTimeine: Home timeline
 | 
			
		||||
  localTimeline: Local timeline
 | 
			
		||||
  bubbleTimeline: Bubble timeline
 | 
			
		||||
  globalTimeline: Global timeline
 | 
			
		||||
  settings: Settings
 | 
			
		||||
  login: Log in
 | 
			
		||||
  logout: Log out
 | 
			
		||||
settings:
 | 
			
		||||
  darkTheme: Dark theme
 | 
			
		||||
  lightTheme: Light theme
 | 
			
		||||
sidebar:
 | 
			
		||||
  postbox:
 | 
			
		||||
    placeholder: What's going on?
 | 
			
		||||
    postButton: Post
 | 
			
		||||
    directMessage: Direct message
 | 
			
		||||
    followersOnly: Followers only
 | 
			
		||||
    unlisted: Unlisted
 | 
			
		||||
    public: Public
 | 
			
		||||
    emojiButton: Emoji
 | 
			
		||||
    pollButton: Create poll
 | 
			
		||||
    fileButton: Attach a file
 | 
			
		||||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ import "./assets/style.css";
 | 
			
		|||
 | 
			
		||||
import { createApp } from "vue";
 | 
			
		||||
import { createPinia } from "pinia";
 | 
			
		||||
import { i18n } from "./i18n";
 | 
			
		||||
 | 
			
		||||
import App from "./App.vue";
 | 
			
		||||
import router from "./router";
 | 
			
		||||
| 
						 | 
				
			
			@ -10,5 +11,6 @@ const app = createApp(App);
 | 
			
		|||
 | 
			
		||||
app.use(createPinia());
 | 
			
		||||
app.use(router);
 | 
			
		||||
app.use(i18n);
 | 
			
		||||
 | 
			
		||||
app.mount("#app");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,24 +6,24 @@ import { ref } from "vue";
 | 
			
		|||
const localStorageKey = "vulpine-oauth-app";
 | 
			
		||||
 | 
			
		||||
export const useAppStore = defineStore("oauth-app", () => {
 | 
			
		||||
    const json = localStorage.getItem(localStorageKey)
 | 
			
		||||
    const app = ref<Application>(json ? JSON.parse(json) : undefined);
 | 
			
		||||
	const json = localStorage.getItem(localStorageKey);
 | 
			
		||||
	const app = ref<Application>(json ? JSON.parse(json) : undefined);
 | 
			
		||||
 | 
			
		||||
    async function getApp() {
 | 
			
		||||
        if (app.value) return app.value;
 | 
			
		||||
	async function getApp() {
 | 
			
		||||
		if (app.value) return app.value;
 | 
			
		||||
 | 
			
		||||
        const resp = await apiFetch<Application>("/api/v1/apps", {
 | 
			
		||||
            method: "POST",
 | 
			
		||||
            body: {
 | 
			
		||||
                client_name: "vulpine-fe",
 | 
			
		||||
                redirect_uris: `${window.location.origin}/auth/callback`,
 | 
			
		||||
                scopes: "read write follow push",
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        app.value = resp;
 | 
			
		||||
        localStorage.setItem(localStorageKey, JSON.stringify(resp));
 | 
			
		||||
        return app.value;
 | 
			
		||||
    }
 | 
			
		||||
		const resp = await apiFetch<Application>("/api/v1/apps", {
 | 
			
		||||
			method: "POST",
 | 
			
		||||
			body: {
 | 
			
		||||
				client_name: "vulpine-fe",
 | 
			
		||||
				redirect_uris: `${window.location.origin}/auth/callback`,
 | 
			
		||||
				scopes: "read write follow push",
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
		app.value = resp;
 | 
			
		||||
		localStorage.setItem(localStorageKey, JSON.stringify(resp));
 | 
			
		||||
		return app.value;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
    return { app, getApp }
 | 
			
		||||
})
 | 
			
		||||
	return { app, getApp };
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,7 @@ watch(
 | 
			
		|||
	},
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<div v-if="error">Failed to load: {{ error }}</div>
 | 
			
		||||
	<FwbSpinner v-else-if="!data" size="10" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue