Merge branch 'main' into userquin/feat-add-pinch-to-zoom-setting

# Conflicts:
#	styles/global.css
This commit is contained in:
userquin 2023-01-30 14:35:15 +01:00
commit d8960ff691
28 changed files with 124 additions and 429 deletions

View file

@ -30,7 +30,7 @@ const emit = defineEmits<{
</p> </p>
{{ $t('help.desc_para3') }} {{ $t('help.desc_para3') }}
<p flex="~ gap-2 wrap" mxa> <p flex="~ gap-2 wrap" mxa>
<template v-for="team of teams" :key="team.github"> <template v-for="team of elkTeamMembers" :key="team.github">
<NuxtLink :href="`https://github.com/sponsors/${team.github}`" target="_blank" external rounded-full transition duration-300 border="~ transparent" hover="scale-105 border-primary"> <NuxtLink :href="`https://github.com/sponsors/${team.github}`" target="_blank" external rounded-full transition duration-300 border="~ transparent" hover="scale-105 border-primary">
<img :src="`/avatars/${team.github}-100x100.png`" :alt="team.display" rounded-full w-15 h-15 height="60" width="60"> <img :src="`/avatars/${team.github}-100x100.png`" :alt="team.display" rounded-full w-15 h-15 height="60" width="60">
</NuxtLink> </NuxtLink>

View file

@ -34,7 +34,7 @@ const toggleApply = () => {
text-white px2 py2 rounded-full cursor-pointer text-white px2 py2 rounded-full cursor-pointer
@click="$emit('remove')" @click="$emit('remove')"
> >
<div i-ri:close-line text-3 :class="[isHydrated && isSmallScreen ? 'text-6' : 'text-3']" /> <div i-ri:close-line text-3 text-6 md:text-3 />
</div> </div>
</div> </div>
<div absolute right-2 bottom-2> <div absolute right-2 bottom-2>

View file

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import ISO6391 from 'iso-639-1'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
let { modelValue } = $defineModel<{ let { modelValue } = $defineModel<{
@ -10,17 +9,7 @@ const { t } = useI18n()
const languageKeyword = $ref('') const languageKeyword = $ref('')
const languageList: { const fuse = new Fuse(languagesNameList, {
code: string
nativeName: string
name: string
}[] = ISO6391.getAllCodes().map(code => ({
code,
nativeName: ISO6391.getNativeName(code),
name: ISO6391.getName(code),
}))
const fuse = new Fuse(languageList, {
keys: ['code', 'nativeName', 'name'], keys: ['code', 'nativeName', 'name'],
shouldSort: true, shouldSort: true,
}) })
@ -28,7 +17,7 @@ const fuse = new Fuse(languageList, {
const languages = $computed(() => const languages = $computed(() =>
languageKeyword.trim() languageKeyword.trim()
? fuse.search(languageKeyword).map(r => r.item) ? fuse.search(languageKeyword).map(r => r.item)
: [...languageList].sort(({ code: a }, { code: b }) => { : [...languagesNameList].sort(({ code: a }, { code: b }) => {
return a === modelValue ? -1 : b === modelValue ? 1 : a.localeCompare(b) return a === modelValue ? -1 : b === modelValue ? 1 : a.localeCompare(b)
}), }),
) )
@ -39,13 +28,15 @@ function chooseLanguage(language: string) {
</script> </script>
<template> <template>
<div> <div relative of-x-hidden>
<input <div p2>
v-model="languageKeyword" <input
:placeholder="t('language.search')" v-model="languageKeyword"
p2 mb2 border-rounded w-full bg-transparent :placeholder="t('language.search')"
outline-none border="~ base" p2 border-rounded w-full bg-transparent
> outline-none border="~ base"
>
</div>
<div max-h-40vh overflow-auto> <div max-h-40vh overflow-auto>
<CommonDropdownItem <CommonDropdownItem
v-for="{ code, nativeName, name } in languages" v-for="{ code, nativeName, name } in languages"

View file

@ -6,7 +6,7 @@ import type { Draft } from '~/types'
const { const {
draftKey, draftKey,
initial = getDefaultDraft() as never /* Bug of vue-core */, initial = getDefaultDraft,
expanded = false, expanded = false,
placeholder, placeholder,
dialogLabelledBy, dialogLabelledBy,
@ -35,7 +35,7 @@ const {
dropZoneRef, dropZoneRef,
} = $(useUploadMediaAttachment($$(draft))) } = $(useUploadMediaAttachment($$(draft)))
let { shouldExpanded, isExpanded, isSending, isPublishDisabled, publishDraft, failedMessages } = $(usePublish( let { shouldExpanded, isExpanded, isSending, isPublishDisabled, publishDraft, failedMessages, preferredLanguage } = $(usePublish(
{ {
draftState, draftState,
...$$({ expanded, isUploading, initialDraft: initial }), ...$$({ expanded, isUploading, initialDraft: initial }),
@ -62,6 +62,7 @@ const { editor } = useTiptap({
}, },
onPaste: handlePaste, onPaste: handlePaste,
}) })
const characterCount = $computed(() => { const characterCount = $computed(() => {
let length = stringLength(htmlToText(editor.value?.getHTML() || '')) let length = stringLength(htmlToText(editor.value?.getHTML() || ''))
@ -76,6 +77,8 @@ const characterCount = $computed(() => {
return length return length
}) })
const postLanguageDisplay = $computed(() => languagesNameList.find(i => i.code === (draft.params.language || preferredLanguage))?.nativeName)
async function handlePaste(evt: ClipboardEvent) { async function handlePaste(evt: ClipboardEvent) {
const files = evt.clipboardData?.files const files = evt.clipboardData?.files
if (!files || files.length === 0) if (!files || files.length === 0)
@ -147,7 +150,7 @@ defineExpose({
> >
<ContentMentionGroup v-if="draft.mentions?.length && shouldExpanded" replying> <ContentMentionGroup v-if="draft.mentions?.length && shouldExpanded" replying>
<button v-for="m, i of draft.mentions" :key="m" text-primary hover:color-red @click="draft.mentions?.splice(i, 1)"> <button v-for="m, i of draft.mentions" :key="m" text-primary hover:color-red @click="draft.mentions?.splice(i, 1)">
{{ acctToShortHandle(m) }} {{ accountToShortHandle(m) }}
</button> </button>
</ContentMentionGroup> </ContentMentionGroup>
@ -278,6 +281,20 @@ defineExpose({
{{ characterCount ?? 0 }}<span text-secondary-light>/</span><span text-secondary-light>{{ characterLimit }}</span> {{ characterCount ?? 0 }}<span text-secondary-light>/</span><span text-secondary-light>{{ characterLimit }}</span>
</div> </div>
<CommonTooltip placement="top" :content="$t('tooltip.change_language')">
<CommonDropdown placement="bottom" auto-boundary-max-size>
<button btn-action-icon :aria-label="$t('tooltip.change_language')" w-max mr1>
<span v-if="postLanguageDisplay" text-secondary text-sm ml1>{{ postLanguageDisplay }}</span>
<div v-else i-ri:translate-2 />
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
</button>
<template #popper>
<PublishLanguagePicker v-model="draft.params.language" min-w-80 />
</template>
</CommonDropdown>
</CommonTooltip>
<CommonTooltip placement="top" :content="$t('tooltip.add_content_warning')"> <CommonTooltip placement="top" :content="$t('tooltip.add_content_warning')">
<button btn-action-icon :aria-label="$t('tooltip.add_content_warning')" @click="toggleSensitive"> <button btn-action-icon :aria-label="$t('tooltip.add_content_warning')" @click="toggleSensitive">
<div v-if="draft.params.sensitive" i-ri:alarm-warning-fill text-orange /> <div v-if="draft.params.sensitive" i-ri:alarm-warning-fill text-orange />
@ -285,19 +302,6 @@ defineExpose({
</button> </button>
</CommonTooltip> </CommonTooltip>
<CommonTooltip placement="top" :content="$t('tooltip.change_language')">
<CommonDropdown placement="bottom" auto-boundary-max-size>
<button btn-action-icon :aria-label="$t('tooltip.change_language')" w-12 mr--1>
<div i-ri:translate-2 />
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
</button>
<template #popper>
<PublishLanguagePicker v-model="draft.params.language" min-w-80 p3 />
</template>
</CommonDropdown>
</CommonTooltip>
<PublishVisibilityPicker v-model="draft.params.visibility" :editing="!!draft.editingStatus"> <PublishVisibilityPicker v-model="draft.params.visibility" :editing="!!draft.editingStatus">
<template #default="{ visibility }"> <template #default="{ visibility }">
<button :disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon :class="{ 'w-12': !draft.editingStatus }"> <button :disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon :class="{ 'w-12': !draft.editingStatus }">

View file

@ -5,7 +5,7 @@ const all = useUsers()
const router = useRouter() const router = useRouter()
const clickUser = (user: UserLogin) => { const clickUser = (user: UserLogin) => {
if (user.account.id === currentUser.value?.account.id) if (user.account.acct === currentUser.value?.account.acct)
router.push(getAccountRoute(user.account)) router.push(getAccountRoute(user.account))
else else
switchUser(user) switchUser(user)
@ -21,7 +21,7 @@ const clickUser = (user: UserLogin) => {
flex rounded flex rounded
cursor-pointer cursor-pointer
aria-label="Switch user" aria-label="Switch user"
:class="user.account.id === currentUser?.account.id ? '' : 'op25 grayscale'" :class="user.account.acct === currentUser?.account.acct ? '' : 'op25 grayscale'"
hover="filter-none op100" hover="filter-none op100"
@click="clickUser(user)" @click="clickUser(user)"
> >

View file

@ -51,7 +51,7 @@ const clickUser = (user: UserLogin) => {
:text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])" :text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])"
icon="i-ri:logout-box-line rtl-flip" icon="i-ri:logout-box-line rtl-flip"
w-full w-full
@click="signout" @click="signOut"
/> />
</div> </div>
</div> </div>

View file

@ -7,7 +7,7 @@ export interface Team {
mastodon: string mastodon: string
} }
export const teams: Team[] = [ export const elkTeamMembers: Team[] = [
{ {
github: 'antfu', github: 'antfu',
display: 'Anthony Fu', display: 'Anthony Fu',

View file

@ -349,7 +349,7 @@ export const provideGlobalCommands = () => {
icon: 'i-ri:logout-box-line', icon: 'i-ri:logout-box-line',
onActivate() { onActivate() {
signout() signOut()
}, },
}) })
} }

View file

@ -21,7 +21,7 @@ export function contentToVNode(
return h(Fragment, (tree.children as Node[] || []).map(n => treeToVNode(n))) return h(Fragment, (tree.children as Node[] || []).map(n => treeToVNode(n)))
} }
export function nodeToVNode(node: Node): VNode | string | null { function nodeToVNode(node: Node): VNode | string | null {
if (node.type === TEXT_NODE) if (node.type === TEXT_NODE)
return node.value return node.value

11
composables/langugage.ts Normal file
View file

@ -0,0 +1,11 @@
import ISO6391 from 'iso-639-1'
export const languagesNameList: {
code: string
nativeName: string
name: string
}[] = ISO6391.getAllCodes().map(code => ({
code,
nativeName: ISO6391.getNativeName(code),
name: ISO6391.getName(code),
}))

View file

@ -7,14 +7,14 @@ export function getDisplayName(account: mastodon.v1.Account, options?: { rich?:
return displayName.replace(/:([\w-]+?):/g, '') return displayName.replace(/:([\w-]+?):/g, '')
} }
export function acctToShortHandle(acct: string) { export function accountToShortHandle(acct: string) {
return `@${acct.includes('@') ? acct.split('@')[0] : acct}` return `@${acct.includes('@') ? acct.split('@')[0] : acct}`
} }
export function getShortHandle({ acct }: mastodon.v1.Account) { export function getShortHandle({ acct }: mastodon.v1.Account) {
if (!acct) if (!acct)
return '' return ''
return acctToShortHandle(acct) return accountToShortHandle(acct)
} }
export function getServerName(account: mastodon.v1.Account) { export function getServerName(account: mastodon.v1.Account) {

View file

@ -47,7 +47,7 @@ export function mastoLogin(masto: ElkMasto, user: Pick<UserLogin, 'server' | 'to
setParams({ setParams({
streamingApiUrl: newInstance.urls.streamingApi, streamingApiUrl: newInstance.urls.streamingApi,
}) })
instances.value[server] = newInstance instanceStorage.value[server] = newInstance
}) })
return instance return instance

View file

@ -4,15 +4,18 @@ import type { mastodon } from 'masto'
import type { UseDraft } from './statusDrafts' import type { UseDraft } from './statusDrafts'
import type { Draft } from '~~/types' import type { Draft } from '~~/types'
export const usePublish = (options: { export function usePublish(options: {
draftState: UseDraft draftState: UseDraft
expanded: Ref<boolean> expanded: Ref<boolean>
isUploading: Ref<boolean> isUploading: Ref<boolean>
initialDraft: Ref<() => Draft> initialDraft: Ref<() => Draft>
}) => { }) {
const { expanded, isUploading, initialDraft } = $(options) const { expanded, isUploading, initialDraft } = $(options)
let { draft, isEmpty } = $(options.draftState) let { draft, isEmpty } = $(options.draftState)
const { client } = $(useMasto()) const { client } = $(useMasto())
const settings = useUserSettings()
const preferredLanguage = $computed(() => (settings.value?.language || 'en').split('-')[0])
let isSending = $ref(false) let isSending = $ref(false)
const isExpanded = $ref(false) const isExpanded = $ref(false)
@ -31,6 +34,7 @@ export const usePublish = (options: {
async function publishDraft() { async function publishDraft() {
if (isPublishDisabled) if (isPublishDisabled)
return return
let content = htmlToText(draft.params.status || '') let content = htmlToText(draft.params.status || '')
if (draft.mentions?.length) if (draft.mentions?.length)
content = `${draft.mentions.map(i => `@${i}`).join(' ')} ${content}` content = `${draft.mentions.map(i => `@${i}`).join(' ')} ${content}`
@ -39,11 +43,12 @@ export const usePublish = (options: {
...draft.params, ...draft.params,
status: content, status: content,
mediaIds: draft.attachments.map(a => a.id), mediaIds: draft.attachments.map(a => a.id),
language: draft.params.language || preferredLanguage,
...(isGlitchEdition.value ? { 'content-type': 'text/markdown' } : {}), ...(isGlitchEdition.value ? { 'content-type': 'text/markdown' } : {}),
} as mastodon.v1.CreateStatusParams } as mastodon.v1.CreateStatusParams
if (process.dev) { if (process.dev) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.info({ console.info({
raw: draft.params.status, raw: draft.params.status,
...payload, ...payload,
@ -60,6 +65,7 @@ export const usePublish = (options: {
let status: mastodon.v1.Status let status: mastodon.v1.Status
if (!draft.editingStatus) if (!draft.editingStatus)
status = await client.v1.statuses.create(payload) status = await client.v1.statuses.create(payload)
else else
status = await client.v1.statuses.update(draft.editingStatus.id, payload) status = await client.v1.statuses.update(draft.editingStatus.id, payload)
if (draft.params.inReplyToId) if (draft.params.inReplyToId)
@ -84,14 +90,14 @@ export const usePublish = (options: {
shouldExpanded, shouldExpanded,
isPublishDisabled, isPublishDisabled,
failedMessages, failedMessages,
preferredLanguage,
publishDraft, publishDraft,
}) })
} }
export type MediaAttachmentUploadError = [filename: string, message: string] export type MediaAttachmentUploadError = [filename: string, message: string]
export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => { export function useUploadMediaAttachment(draftRef: Ref<Draft>) {
const draft = $(draftRef) const draft = $(draftRef)
const { client } = $(useMasto()) const { client } = $(useMasto())
const { t } = useI18n() const { t } = useI18n()
@ -117,7 +123,7 @@ export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
draft.attachments.push(attachment) draft.attachments.push(attachment)
} }
catch (e) { catch (e) {
// TODO: add some human-readable error message, problem is that masto api will not return response code // TODO: add some human-readable error message, problem is that masto api will not return response code
console.error(e) console.error(e)
failedAttachments = [...failedAttachments, [file.name, (e as Error).message]] failedAttachments = [...failedAttachments, [file.name, (e as Error).message]]
} }
@ -159,9 +165,10 @@ export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
return $$({ return $$({
isUploading, isUploading,
isExceedingAttachmentLimit, isExceedingAttachmentLimit,
isOverDropZone,
failedAttachments, failedAttachments,
dropZoneRef, dropZoneRef,
isOverDropZone,
uploadAttachments, uploadAttachments,
pickAttachments, pickAttachments,

View file

@ -33,7 +33,7 @@ export function getDefaultDraft(options: Partial<Mutable<mastodon.v1.CreateStatu
visibility: visibility || 'public', visibility: visibility || 'public',
sensitive: sensitive ?? false, sensitive: sensitive ?? false,
spoilerText: spoilerText || '', spoilerText: spoilerText || '',
language: language || getDefaultLanguage(), language: language || '', // auto inferred from current language on posting
}, },
mentions, mentions,
lastUpdated: Date.now(), lastUpdated: Date.now(),
@ -52,16 +52,6 @@ export async function getDraftFromStatus(status: mastodon.v1.Status): Promise<Dr
}) })
} }
function getDefaultLanguage() {
const userSettings = useUserSettings()
const defaultLanguage = userSettings.value.language
if (defaultLanguage)
return defaultLanguage.split('-')[0]
return 'en'
}
function getAccountsToMention(status: mastodon.v1.Status) { function getAccountsToMention(status: mastodon.v1.Status) {
const userId = currentUser.value?.account.id const userId = currentUser.value?.account.id
const accountsToMention = new Set<string>() const accountsToMention = new Set<string>()

View file

@ -2,6 +2,4 @@ import { breakpointsTailwind } from '@vueuse/core'
export const breakpoints = useBreakpoints(breakpointsTailwind) export const breakpoints = useBreakpoints(breakpointsTailwind)
export const isSmallScreen = breakpoints.smallerOrEqual('md')
export const isMediumScreen = breakpoints.smallerOrEqual('lg')
export const isMediumOrLargeScreen = breakpoints.between('sm', 'xl') export const isMediumOrLargeScreen = breakpoints.between('sm', 'xl')

View file

@ -7,7 +7,6 @@ import type { UserLogin } from '~/types'
import type { Overwrite } from '~/types/utils' import type { Overwrite } from '~/types/utils'
import { import {
DEFAULT_POST_CHARS_LIMIT, DEFAULT_POST_CHARS_LIMIT,
STORAGE_KEY_CURRENT_USER,
STORAGE_KEY_CURRENT_USER_HANDLE, STORAGE_KEY_CURRENT_USER_HANDLE,
STORAGE_KEY_NODES, STORAGE_KEY_NODES,
STORAGE_KEY_NOTIFICATION, STORAGE_KEY_NOTIFICATION,
@ -44,20 +43,20 @@ const initializeUsers = async (): Promise<Ref<UserLogin[]> | RemovableRef<UserLo
} }
const users = await initializeUsers() const users = await initializeUsers()
export const instances = useLocalStorage<Record<string, mastodon.v1.Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true }) const nodes = useLocalStorage<Record<string, any>>(STORAGE_KEY_NODES, {}, { deep: true })
export const nodes = useLocalStorage<Record<string, any>>(STORAGE_KEY_NODES, {}, { deep: true }) const currentUserHandle = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER_HANDLE, mock ? mock.user.account.id : '')
const currentUserId = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER, mock ? mock.user.account.id : '') export const instanceStorage = useLocalStorage<Record<string, mastodon.v1.Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true })
export type ElkInstance = Partial<mastodon.v1.Instance> & { export type ElkInstance = Partial<mastodon.v1.Instance> & {
uri: string uri: string
/** support GoToSocial */ /** support GoToSocial */
accountDomain?: string | null accountDomain?: string | null
} }
export const getInstanceCache = (server: string): mastodon.v1.Instance | undefined => instances.value[server] export const getInstanceCache = (server: string): mastodon.v1.Instance | undefined => instanceStorage.value[server]
export const currentUser = computed<UserLogin | undefined>(() => { export const currentUser = computed<UserLogin | undefined>(() => {
if (currentUserId.value) { if (currentUserHandle.value) {
const user = users.value.find(user => user.account?.id === currentUserId.value) const user = users.value.find(user => user.account?.acct === currentUserHandle.value)
if (user) if (user)
return user return user
} }
@ -66,7 +65,7 @@ export const currentUser = computed<UserLogin | undefined>(() => {
}) })
const publicInstance = ref<ElkInstance | null>(null) const publicInstance = ref<ElkInstance | null>(null)
export const currentInstance = computed<null | ElkInstance>(() => currentUser.value ? instances.value[currentUser.value.server] ?? null : publicInstance.value) export const currentInstance = computed<null | ElkInstance>(() => currentUser.value ? instanceStorage.value[currentUser.value.server] ?? null : publicInstance.value)
export function getInstanceDomain(instance: ElkInstance) { export function getInstanceDomain(instance: ElkInstance) {
return instance.accountDomain || withoutProtocol(instance.uri) return instance.accountDomain || withoutProtocol(instance.uri)
@ -84,12 +83,12 @@ if (process.client) {
const windowReload = () => { const windowReload = () => {
document.visibilityState === 'visible' && window.location.reload() document.visibilityState === 'visible' && window.location.reload()
} }
watch(currentUserId, async (id, oldId) => { watch(currentUserHandle, async (handle, oldHandle) => {
// when sign in or switch account // when sign in or switch account
if (id) { if (handle) {
if (id === currentUser.value?.account?.id) { if (handle === currentUser.value?.account?.acct) {
// when sign in, the other tab will not have the user, idb is not reactive // when sign in, the other tab will not have the user, idb is not reactive
const newUser = users.value.find(user => user.account?.id === id) const newUser = users.value.find(user => user.account?.acct === handle)
// if the user is there, then we are switching account // if the user is there, then we are switching account
if (newUser) { if (newUser) {
// check if the change is on current tab: if so, don't reload // check if the change is on current tab: if so, don't reload
@ -101,19 +100,13 @@ if (process.client) {
window.addEventListener('visibilitychange', windowReload, { capture: true }) window.addEventListener('visibilitychange', windowReload, { capture: true })
} }
// when sign out // when sign out
else if (oldId) { else if (oldHandle) {
const oldUser = users.value.find(user => user.account?.id === oldId) const oldUser = users.value.find(user => user.account?.acct === oldHandle)
// when sign out, the other tab will not have the user, idb is not reactive // when sign out, the other tab will not have the user, idb is not reactive
if (oldUser) if (oldUser)
window.addEventListener('visibilitychange', windowReload, { capture: true }) window.addEventListener('visibilitychange', windowReload, { capture: true })
} }
}, { immediate: true, flush: 'post' }) }, { immediate: true, flush: 'post' })
// for injected script to read
const currentUserHandle = computed(() => currentUser.value?.account.acct || '')
watchEffect(() => {
localStorage.setItem(STORAGE_KEY_CURRENT_USER_HANDLE, currentUserHandle.value)
})
} }
export const useUsers = () => users export const useUsers = () => users
@ -144,7 +137,7 @@ export async function loginTo(masto: ElkMasto, user: Overwrite<UserLogin, { acco
const account = getUser()?.account const account = getUser()?.account
if (account) if (account)
currentUserId.value = account.id currentUserHandle.value = account.acct
const [me, pushSubscription] = await Promise.all([ const [me, pushSubscription] = await Promise.all([
fetchAccountInfo(client, user.server), fetchAccountInfo(client, user.server),
@ -168,7 +161,7 @@ export async function loginTo(masto: ElkMasto, user: Overwrite<UserLogin, { acco
}) })
} }
currentUserId.value = me.id currentUserHandle.value = me.acct
} }
export async function fetchAccountInfo(client: mastodon.Client, server: string) { export async function fetchAccountInfo(client: mastodon.Client, server: string) {
@ -238,7 +231,7 @@ export async function switchUser(user: UserLogin) {
} }
} }
export async function signout() { export async function signOut() {
// TODO: confirm // TODO: confirm
if (!currentUser.value) if (!currentUser.value)
return return
@ -253,21 +246,21 @@ export async function signout() {
// Clear stale data // Clear stale data
clearUserLocalStorage() clearUserLocalStorage()
if (!users.value.some((u, i) => u.server === currentUser.value!.server && i !== index)) if (!users.value.some((u, i) => u.server === currentUser.value!.server && i !== index))
delete instances.value[currentUser.value.server] delete instanceStorage.value[currentUser.value.server]
await removePushNotifications(currentUser.value) await removePushNotifications(currentUser.value)
await removePushNotificationData(currentUser.value) await removePushNotificationData(currentUser.value)
currentUserId.value = '' currentUserHandle.value = ''
// Remove the current user from the users // Remove the current user from the users
users.value.splice(index, 1) users.value.splice(index, 1)
} }
// Set currentUserId to next user if available // Set currentUserId to next user if available
currentUserId.value = users.value[0]?.account?.id currentUserHandle.value = users.value[0]?.account?.acct
if (!currentUserId.value) if (!currentUserHandle.value)
await useRouter().push('/') await useRouter().push('/')
loginTo(masto, currentUser.value) loginTo(masto, currentUser.value)

View file

@ -7,7 +7,6 @@ export const STORAGE_KEY_DRAFTS = 'elk-drafts'
export const STORAGE_KEY_USERS = 'elk-users' export const STORAGE_KEY_USERS = 'elk-users'
export const STORAGE_KEY_SERVERS = 'elk-servers' export const STORAGE_KEY_SERVERS = 'elk-servers'
export const STORAGE_KEY_NODES = 'elk-nodes' export const STORAGE_KEY_NODES = 'elk-nodes'
export const STORAGE_KEY_CURRENT_USER = 'elk-current-user'
export const STORAGE_KEY_CURRENT_USER_HANDLE = 'elk-current-user-handle' export const STORAGE_KEY_CURRENT_USER_HANDLE = 'elk-current-user-handle'
export const STORAGE_KEY_NOTIFY_TAB = 'elk-notify-tab' export const STORAGE_KEY_NOTIFY_TAB = 'elk-notify-tab'
export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit' export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit'

View file

@ -22,6 +22,7 @@ export default defineNuxtModule({
...nuxt.options.alias, ...nuxt.options.alias,
'unstorage/drivers/fs': 'unenv/runtime/mock/proxy', 'unstorage/drivers/fs': 'unenv/runtime/mock/proxy',
'unstorage/drivers/cloudflare-kv-http': 'unenv/runtime/mock/proxy', 'unstorage/drivers/cloudflare-kv-http': 'unenv/runtime/mock/proxy',
'#storage-config': resolve('./runtime/storage-config'),
'node:events': 'unenv/runtime/node/events/index', 'node:events': 'unenv/runtime/node/events/index',
'#build-info': resolve('./runtime/build-info'), '#build-info': resolve('./runtime/build-info'),
} }

View file

@ -0,0 +1,2 @@
export const driver = undefined
export const fsBase = ''

View file

@ -1,4 +1,4 @@
import { createResolver } from '@nuxt/kit' import { createResolver, useNuxt } from '@nuxt/kit'
import Inspect from 'vite-plugin-inspect' import Inspect from 'vite-plugin-inspect'
import { isCI, isDevelopment, isWindows } from 'std-env' import { isCI, isDevelopment, isWindows } from 'std-env'
import { isPreview } from './config/env' import { isPreview } from './config/env'
@ -86,6 +86,11 @@ export default defineNuxtConfig({
'postcss-nested': {}, 'postcss-nested': {},
}, },
}, },
appConfig: {
storage: {
driver: process.env.NUXT_STORAGE_DRIVER ?? (isCI ? 'cloudflare' : 'fs'),
},
},
runtimeConfig: { runtimeConfig: {
adminKey: '', adminKey: '',
cloudflare: { cloudflare: {
@ -102,8 +107,7 @@ export default defineNuxtConfig({
defaultServer: 'm.webtoo.ls', defaultServer: 'm.webtoo.ls',
}, },
storage: { storage: {
driver: isCI ? 'cloudflare' : 'fs', fsBase: 'node_modules/.cache/app',
fsBase: 'node_modules/.cache/servers',
}, },
}, },
routeRules: { routeRules: {
@ -126,6 +130,13 @@ export default defineNuxtConfig({
ignore: ['/settings'], ignore: ['/settings'],
}, },
}, },
hooks: {
'nitro:config': function (config) {
const nuxt = useNuxt()
config.virtual = config.virtual || {}
config.virtual['#storage-config'] = `export const driver = ${JSON.stringify(nuxt.options.appConfig.storage.driver)}`
},
},
app: { app: {
keepalive: true, keepalive: true,
head: { head: {

View file

@ -118,7 +118,7 @@
"unplugin-vue-inspector": "^0.0.2", "unplugin-vue-inspector": "^0.0.2",
"vite-plugin-inspect": "^0.7.14", "vite-plugin-inspect": "^0.7.14",
"vite-plugin-pwa": "^0.14.1", "vite-plugin-pwa": "^0.14.1",
"vitest": "^0.28.1", "vitest": "^0.28.3",
"vue-tsc": "^1.0.24", "vue-tsc": "^1.0.24",
"workbox-build": "^6.5.4", "workbox-build": "^6.5.4",
"workbox-window": "^6.5.4" "workbox-window": "^6.5.4"

View file

@ -115,7 +115,7 @@ const handleShowCommit = () => {
</p> </p>
<SettingsItem <SettingsItem
v-for="team in teams" :key="team.github" v-for="team in elkTeamMembers" :key="team.github"
:text="team.display" :text="team.display"
:to="`https://github.com/sponsors/${team.github}`" :to="`https://github.com/sponsors/${team.github}`"
external target="_blank" external target="_blank"

View file

@ -119,7 +119,7 @@ importers:
unplugin-vue-inspector: ^0.0.2 unplugin-vue-inspector: ^0.0.2
vite-plugin-inspect: ^0.7.14 vite-plugin-inspect: ^0.7.14
vite-plugin-pwa: ^0.14.1 vite-plugin-pwa: ^0.14.1
vitest: ^0.28.1 vitest: ^0.28.3
vue-advanced-cropper: ^2.8.8 vue-advanced-cropper: ^2.8.8
vue-tsc: ^1.0.24 vue-tsc: ^1.0.24
vue-virtual-scroller: 2.0.0-beta.7 vue-virtual-scroller: 2.0.0-beta.7
@ -216,7 +216,7 @@ importers:
unplugin-auto-import: 0.13.0_@vueuse+core@9.11.1 unplugin-auto-import: 0.13.0_@vueuse+core@9.11.1
unplugin-vue-inspector: 0.0.2 unplugin-vue-inspector: 0.0.2
vite-plugin-inspect: 0.7.14 vite-plugin-inspect: 0.7.14
vite-plugin-pwa: 0.14.1_tz3vz2xt4jvid2diblkpydcyn4 vite-plugin-pwa: 0.14.1
vitest: 0.28.3_jsdom@21.1.0 vitest: 0.28.3_jsdom@21.1.0
vue-tsc: 1.0.24_typescript@4.9.4 vue-tsc: 1.0.24_typescript@4.9.4
workbox-build: 6.5.4 workbox-build: 6.5.4
@ -12949,12 +12949,10 @@ packages:
- supports-color - supports-color
dev: true dev: true
/vite-plugin-pwa/0.14.1_tz3vz2xt4jvid2diblkpydcyn4: /vite-plugin-pwa/0.14.1:
resolution: {integrity: sha512-5zx7yhQ8RTLwV71+GA9YsQQ63ALKG8XXIMqRJDdZkR8ZYftFcRgnzM7wOWmQZ/DATspyhPih5wCdcZnAIsM+mA==} resolution: {integrity: sha512-5zx7yhQ8RTLwV71+GA9YsQQ63ALKG8XXIMqRJDdZkR8ZYftFcRgnzM7wOWmQZ/DATspyhPih5wCdcZnAIsM+mA==}
peerDependencies: peerDependencies:
vite: ^3.1.0 || ^4.0.0 vite: ^3.1.0 || ^4.0.0
workbox-build: ^6.5.4
workbox-window: ^6.5.4
dependencies: dependencies:
'@rollup/plugin-replace': 5.0.2_rollup@3.10.1 '@rollup/plugin-replace': 5.0.2_rollup@3.10.1
debug: 4.3.4 debug: 4.3.4
@ -12964,6 +12962,7 @@ packages:
workbox-build: 6.5.4 workbox-build: 6.5.4
workbox-window: 6.5.4 workbox-window: 6.5.4
transitivePeerDependencies: transitivePeerDependencies:
- '@types/babel__core'
- supports-color - supports-color
dev: true dev: true

View file

@ -1,7 +1,7 @@
import { join, resolve } from 'pathe' import { join, resolve } from 'pathe'
import fs from 'fs-extra' import fs from 'fs-extra'
import { $fetch } from 'ohmyfetch' import { $fetch } from 'ohmyfetch'
import { teams } from '../composables/about' import { elkTeamMembers } from '../composables/about'
const avatarsDir = resolve('./public/avatars/') const avatarsDir = resolve('./public/avatars/')
@ -24,7 +24,7 @@ async function download(url: string, fileName: string) {
async function fetchAvatars() { async function fetchAvatars() {
await fs.ensureDir(avatarsDir) await fs.ensureDir(avatarsDir)
await Promise.all(teams.reduce((acc, { github }) => { await Promise.all(elkTeamMembers.reduce((acc, { github }) => {
acc.push(...sizes.map(s => download(`https://github.com/${github}.png?size=${s}`, join(avatarsDir, `${github}-${s}x${s}.png`)))) acc.push(...sizes.map(s => download(`https://github.com/${github}.png?size=${s}`, join(avatarsDir, `${github}-${s}x${s}.png`))))
return acc return acc
}, [] as Promise<void>[])) }, [] as Promise<void>[]))

View file

@ -14,29 +14,31 @@ import cached from './cache-driver'
// @ts-expect-error virtual import // @ts-expect-error virtual import
import { env } from '#build-info' import { env } from '#build-info'
// @ts-expect-error virtual import
import { driver } from '#storage-config'
import type { AppInfo } from '~/types' import type { AppInfo } from '~/types'
import { APP_NAME } from '~/constants' import { APP_NAME } from '~/constants'
const config = useRuntimeConfig()
const fs = _fs as typeof import('unstorage/dist/drivers/fs')['default'] const fs = _fs as typeof import('unstorage/dist/drivers/fs')['default']
const kv = _kv as typeof import('unstorage/dist/drivers/cloudflare-kv-http')['default'] const kv = _kv as typeof import('unstorage/dist/drivers/cloudflare-kv-http')['default']
const memory = _memory as typeof import('unstorage/dist/drivers/memory')['default'] const memory = _memory as typeof import('unstorage/dist/drivers/memory')['default']
const storage = useStorage() as Storage const storage = useStorage() as Storage
if (config.storage.driver === 'fs') { if (driver === 'fs') {
const config = useRuntimeConfig()
storage.mount('servers', fs({ base: config.storage.fsBase })) storage.mount('servers', fs({ base: config.storage.fsBase }))
} }
else if (config.storage.driver === 'cloudflare') { else if (driver === 'cloudflare') {
const config = useRuntimeConfig()
storage.mount('servers', cached(kv({ storage.mount('servers', cached(kv({
accountId: config.cloudflare.accountId, accountId: config.cloudflare.accountId,
namespaceId: config.cloudflare.namespaceId, namespaceId: config.cloudflare.namespaceId,
apiToken: config.cloudflare.apiToken, apiToken: config.cloudflare.apiToken,
}))) })))
} }
else if (config.storage.driver === 'memory') { else if (driver === 'memory') {
storage.mount('servers', memory()) storage.mount('servers', memory())
} }

View file

@ -1,164 +0,0 @@
// Vitest Snapshot v1
exports[`content-rich > block with backticks 1`] = `"<p><pre class=\\"code-block\\">[(\`number string) (\`tag string)]</pre></p>"`;
exports[`content-rich > block with injected html, with a known language 1`] = `
"<pre>
<code class=\\"language-js\\">
&lt;a href=&quot;javascript:alert(1)&quot;&gt;click me&lt;/a&gt;
</code>
</pre>
"
`;
exports[`content-rich > block with injected html, with an unknown language 1`] = `
"<pre>
<code class=\\"language-xyzzy\\">
&lt;a href=&quot;javascript:alert(1)&quot;&gt;click me&lt;/a&gt;
</code>
</pre>
"
`;
exports[`content-rich > block with injected html, without language 1`] = `
"<pre>
<code>
&lt;a href=&quot;javascript:alert(1)&quot;&gt;click me&lt;/a&gt;
</code>
</pre>
"
`;
exports[`content-rich > code frame 1`] = `
"<p>Testing code block</p><p></p><p><pre class=\\"code-block\\">import { useMouse, usePreferredDark } from &apos;@vueuse/core&apos;
// tracks mouse position
const { x, y } = useMouse()
// is the user prefers dark theme
const isDark = usePreferredDark()</pre></p>"
`;
exports[`content-rich > code frame 2 1`] = `
"<p>
<span class=\\"h-card\\"
><a
class=\\"u-url mention\\"
rel=\\"nofollow noopener noreferrer\\"
to=\\"/webtoo.ls/@antfu\\"
></a
></span>
Testing<br />
<pre class=\\"code-block\\">const a = hello</pre>
</p>
"
`;
exports[`content-rich > code frame empty 1`] = `"<p><pre class=\\"code-block\\"></pre><br></p>"`;
exports[`content-rich > code frame no lang 1`] = `"<p><pre class=\\"code-block\\">hello world</pre><br>no lang</p>"`;
exports[`content-rich > custom emoji 1`] = `
"Daniel Roe
<picture alt=\\":nuxt:\\" class=\\"custom-emoji\\" data-emoji-id=\\"nuxt\\"
><source
srcset=\\"
https://media.webtoo.ls/custom_emojis/images/000/000/366/original/73330dfc9dda4078.png
\\"
media=\\"(prefers-reduced-motion: reduce)\\" />
<img
src=\\"https://media.webtoo.ls/custom_emojis/images/000/000/366/original/73330dfc9dda4078.png\\"
alt=\\":nuxt:\\"
/></picture>
"
`;
exports[`content-rich > empty 1`] = `""`;
exports[`content-rich > group mention > html 1`] = `
"<p>
<span class=\\"h-card\\"
><a
class=\\"u-url mention\\"
rel=\\"nofollow noopener noreferrer\\"
to=\\"//@pilipinas@lemmy.ml\\"
></a
></span>
</p>
"
`;
exports[`content-rich > handles formatting from servers 1`] = `
"<h1>Fedi HTML Support Survey</h1>
<p>Does the following formatting come through accurately for you?</p>
<p></p>
<ul>
<li>This is an indented bulleted list (not just asterisks).</li>
<li><strong>This line is bold.</strong></li>
<li><em>This line is italic.</em></li>
</ul>
<ol>
<li>This list...</li>
<li>...is numbered and indented</li>
</ol>
<h1>This line is larger.</h1>
"
`;
exports[`content-rich > handles html within code blocks 1`] = `
"<p>
HTML block code:<br />
<pre class=\\"code-block\\">
&lt;span class=&quot;icon--noto icon--noto--1st-place-medal&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;icon--noto icon--noto--2nd-place-medal-medal&quot;&gt;&lt;/span&gt;</pre
>
</p>
"
`;
exports[`content-rich > inline code with link 1`] = `
"<p>
Inline code with link:
<code
>https://api.iconify.design/noto.css?icons=1st-place-medal,2nd-place-medal</code
>
</p>
"
`;
exports[`content-rich > link + mention 1`] = `
"<p>
Happy
<img
src=\\"/emojis/twemoji/1f917.svg\\"
alt=\\"🤗\\"
class=\\"iconify-emoji iconify-emoji--twemoji\\"
/>
were now using
<span class=\\"h-card\\"
><a
class=\\"u-url mention\\"
rel=\\"nofollow noopener noreferrer\\"
to=\\"/webtoo.ls/@vitest\\"
></a
></span>
(migrated from chai+mocha)
<a
href=\\"https://github.com/ayoayco/astro-reactive-library/pull/203\\"
rel=\\"nofollow noopener noreferrer\\"
target=\\"_blank\\"
><span class=\\"invisible\\">https://</span
><span class=\\"ellipsis\\">github.com/ayoayco/astro-react</span
><span class=\\"invisible\\">ive-library/pull/203</span></a
>
</p>
"
`;
exports[`content-rich > plain text 1`] = `
"hello there
"
`;
exports[`editor > transform mentions 1`] = `
"
@elk Hello"
`;

View file

@ -1,144 +0,0 @@
// Vitest Snapshot v1
exports[`html-parse > code frame > html 1`] = `
"<p>Testing code block</p><p></p><p><pre><code class=\\"language-ts\\">import { useMouse, usePreferredDark } from '@vueuse/core'
// tracks mouse position
const { x, y } = useMouse()
// is the user prefers dark theme
const isDark = usePreferredDark()</code></pre></p>"
`;
exports[`html-parse > code frame > text 1`] = `
"Testing code block
\`\`\`ts
import { useMouse, usePreferredDark } from '@vueuse/core'
// tracks mouse position
const { x, y } = useMouse()
// is the user prefers dark theme
const isDark = usePreferredDark()
\`\`\`"
`;
exports[`html-parse > code frame 2 > html 1`] = `
"<p>
<span class=\\"h-card\\"
><a
href=\\"https://webtoo.ls/@antfu\\"
class=\\"u-url mention\\"
rel=\\"nofollow noopener noreferrer\\"
target=\\"_blank\\"
>@<span>antfu</span></a
></span
>
Testing<br />
<pre><code class=\\"language-ts\\">const a = hello</code></pre>
</p>
"
`;
exports[`html-parse > code frame 2 > text 1`] = `
"@antfu Testing
\`\`\`ts
const a = hello
\`\`\`"
`;
exports[`html-parse > custom emoji > html 1`] = `
"Daniel Roe
<picture alt=\\":nuxt:\\" class=\\"custom-emoji\\" data-emoji-id=\\"nuxt\\"
><source
srcset=\\"
https://media.webtoo.ls/custom_emojis/images/000/000/366/original/73330dfc9dda4078.png
\\"
media=\\"(prefers-reduced-motion: reduce)\\" />
<img
src=\\"https://media.webtoo.ls/custom_emojis/images/000/000/366/original/73330dfc9dda4078.png\\"
alt=\\":nuxt:\\"
/></picture>
"
`;
exports[`html-parse > custom emoji > text 1`] = `"Daniel Roe :nuxt:"`;
exports[`html-parse > emojis > html 1`] = `
"<img
src=\\"/emojis/twemoji/1f1eb-1f1f7.svg\\"
alt=\\"🇫🇷\\"
class=\\"iconify-emoji iconify-emoji--twemoji\\"
/>
<img
src=\\"/emojis/twemoji/1f468-200d-1f469-200d-1f466.svg\\"
alt=\\"👨‍👩‍👦\\"
class=\\"iconify-emoji iconify-emoji--twemoji\\"
/>
<img
src=\\"/emojis/twemoji/1f469-200d-1f692.svg\\"
alt=\\"👩‍🚒\\"
class=\\"iconify-emoji iconify-emoji--twemoji\\"
/><img
src=\\"/emojis/twemoji/1f9d1-1f3fd-200d-1f680.svg\\"
alt=\\"🧑🏽‍🚀\\"
class=\\"iconify-emoji iconify-emoji--twemoji\\"
/>
"
`;
exports[`html-parse > emojis > text 1`] = `"🇫🇷 👨‍👩‍👦 👩‍🚒🧑🏽‍🚀"`;
exports[`html-parse > empty > html 1`] = `""`;
exports[`html-parse > empty > text 1`] = `""`;
exports[`html-parse > html entities > html 1`] = `
"<p>Hello &lt;World /&gt;.</p>
"
`;
exports[`html-parse > html entities > text 1`] = `"Hello <World />."`;
exports[`html-parse > inline markdown > html 1`] = `"<p>text <code>code</code> <b>bold</b> <em>italic</em> <del>del</del></p><p></p><p><pre><code class=\\"language-js\\">code block</code></pre></p>"`;
exports[`html-parse > inline markdown > text 1`] = `
"text \`code\` **bold** *italic* ~~del~~
\`\`\`js
code block
\`\`\`"
`;
exports[`html-parse > link + mention > html 1`] = `
"<p>
Happy
<img
src=\\"/emojis/twemoji/1f917.svg\\"
alt=\\"🤗\\"
class=\\"iconify-emoji iconify-emoji--twemoji\\"
/>
were now using
<span class=\\"h-card\\"
><a
href=\\"https://webtoo.ls/@vitest\\"
class=\\"u-url mention\\"
rel=\\"nofollow noopener noreferrer\\"
target=\\"_blank\\"
>@<span>vitest</span></a
></span
>
(migrated from chai+mocha)
<a
href=\\"https://github.com/ayoayco/astro-reactive-library/pull/203\\"
rel=\\"nofollow noopener noreferrer\\"
target=\\"_blank\\"
><span class=\\"invisible\\">https://</span
><span class=\\"ellipsis\\">github.com/ayoayco/astro-react</span
><span class=\\"invisible\\">ive-library/pull/203</span></a
>
</p>
"
`;
exports[`html-parse > link + mention > text 1`] = `"Happy 🤗 were now using @vitest (migrated from chai+mocha) https://github.com/ayoayco/astro-reactive-library/pull/203"`;

View file

@ -287,15 +287,10 @@ vi.mock('shiki-es', async (importOriginal) => {
} }
}) })
vi.mock('~/components/content/ContentMentionGroup.vue', async () => { mockComponent('ContentMentionGroup', {
const { defineComponent, h } = await import('vue') setup(props, { slots }) {
return { return () => h('mention-group', null, { default: () => slots?.default?.() })
default: defineComponent({ },
setup(props, { slots }) {
return () => h('mention-group', null, { default: () => slots?.default?.() })
},
}),
}
}) })
mockComponent('AccountHoverWrapper', { mockComponent('AccountHoverWrapper', {