kopia lustrzana https://github.com/elk-zone/elk
Porównaj commity
47 Commity
08f847f15f
...
aa171a8274
Autor | SHA1 | Data |
---|---|---|
TAKAHASHI Shuuji | aa171a8274 | |
TAKAHASHI Shuuji | facc9a8f13 | |
TAKAHASHI Shuuji | 06f8bb8388 | |
TAKAHASHI Shuuji | 9d327f17aa | |
TAKAHASHI Shuuji | f4c287481c | |
TAKAHASHI Shuuji | a988b79574 | |
TAKAHASHI Shuuji | 912d523a9b | |
TAKAHASHI Shuuji | bd8cfc7b57 | |
Joaquín Sánchez | 77f0e2c2f8 | |
lazzzis | 57ff04853b | |
Joaquín Sánchez | 1eaaa6ce9a | |
patak-dev | 1526847a18 | |
Duy | cc1d149ac8 | |
Duy | 569604646d | |
Dohány Tamás | 6f47d1aeff | |
Francesco | 9d62edf295 | |
Xabi | 7f4d8c04c6 | |
Emanuel Pina | 79c6714bac | |
lazzzis | ecd7a6f8cb | |
Andy Maloney | 4ed97dab55 | |
Andy Maloney | d4eeb7441d | |
Joaquín Sánchez | c504e14ff5 | |
Joaquín Sánchez | f78ce97f05 | |
TAKAHASHI Shuuji | c1f8e3efb5 | |
Dohány Tamás | e4c7124d28 | |
Joaquín Sánchez | 6bb9ad0511 | |
Emanuel Pina | 8697cc44e4 | |
Alan Ye | 8e5a801ef9 | |
TAKAHASHI Shuuji | 876ae4098c | |
TAKAHASHI Shuuji | 9c916e0932 | |
Joaquín Sánchez | 14162f8bcb | |
Xabi | 9fa8149f68 | |
Joaquín Sánchez | e3979c61e7 | |
Francesco | 1d817a8b69 | |
TAKAHASHI Shuuji | 2cb070c83c | |
lazzzis | 2a6a994da1 | |
Dohány Tamás | 706cffe209 | |
Joaquín Sánchez | dde907f4bb | |
Xabi | 81143de09b | |
Dohány Tamás | 8fdac7f79e | |
Duy | 7b819d116c | |
Francesco | bda2df2192 | |
Emanuel Pina | 2cada8a75c | |
TAKAHASHI Shuuji | e0280ad8c4 | |
Sebastian Di Luzio | 1234fb2dd1 | |
Joaquín Sánchez | 0538f97ada | |
Tamas | 61265a792f |
|
@ -56,7 +56,7 @@ async function removeUserNote() {
|
|||
|
||||
<template>
|
||||
<CommonDropdown :eager-mount="command">
|
||||
<button flex gap-1 items-center w-full rounded op75 hover="op100 text-purple" group aria-label="More actions">
|
||||
<button flex gap-1 items-center w-full rounded op75 hover="op100 text-purple" group :aria-label="t('actions.more')">
|
||||
<div rounded-5 p2 elk-group-hover="bg-purple/10">
|
||||
<div i-ri:more-2-fill />
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@ const vAutoFocus = (el: HTMLElement) => el.focus()
|
|||
|
||||
<template>
|
||||
<div my-8 px-3 sm:px-8 md:max-w-200 flex="~ col gap-4" relative>
|
||||
<button v-auto-focus type="button" btn-action-icon absolute top--8 right-0 m1 aria-label="Close" @click="emit('close')">
|
||||
<button v-auto-focus type="button" btn-action-icon absolute top--8 right-0 m1 :aria-label="$t('action.close')" @click="emit('close')">
|
||||
<span i-ri:close-line />
|
||||
</button>
|
||||
|
||||
|
|
|
@ -32,9 +32,9 @@ async function prepareEdit() {
|
|||
async function cancelEdit() {
|
||||
isEditing.value = false
|
||||
actionError.value = undefined
|
||||
reset()
|
||||
|
||||
await nextTick()
|
||||
reset()
|
||||
editBtn.value?.focus()
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ async function edit(listId: string) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<CommonPaginator :end-message="false" :paginator="paginator">
|
||||
<CommonPaginator :paginator="paginator">
|
||||
<template #default="{ item }">
|
||||
<div p4 hover:bg-active block w="100%" flex justify-between items-center gap-4>
|
||||
<p>{{ item.title }}</p>
|
||||
|
@ -49,5 +49,13 @@ async function edit(listId: string) {
|
|||
</CommonTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<template #done>
|
||||
<NuxtLink
|
||||
p4 hover:bg-active block w="100%" flex justify-between items-center gap-4
|
||||
to="/lists"
|
||||
>
|
||||
<p>{{ $t('list.manage') }}</p>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</CommonPaginator>
|
||||
</template>
|
||||
|
|
|
@ -14,6 +14,7 @@ interface ShortcutDef {
|
|||
interface ShortcutItem {
|
||||
description: string
|
||||
shortcut: ShortcutDef
|
||||
info?: string
|
||||
}
|
||||
|
||||
interface ShortcutItemGroup {
|
||||
|
@ -32,14 +33,16 @@ const shortcutItemGroups = computed<ShortcutItemGroup[]>(() => [
|
|||
description: t('magic_keys.groups.navigation.shortcut_help'),
|
||||
shortcut: { keys: ['?'], isSequence: false },
|
||||
},
|
||||
// {
|
||||
// description: t('magic_keys.groups.navigation.next_status'),
|
||||
// shortcut: { keys: ['j'], isSequence: false },
|
||||
// },
|
||||
// {
|
||||
// description: t('magic_keys.groups.navigation.previous_status'),
|
||||
// shortcut: { keys: ['k'], isSequence: false },
|
||||
// },
|
||||
{
|
||||
description: t('magic_keys.groups.navigation.next_status'),
|
||||
shortcut: { keys: ['j'], isSequence: false },
|
||||
info: t('magic_keys.info.disable_virtual_scrolling'),
|
||||
},
|
||||
{
|
||||
description: t('magic_keys.groups.navigation.previous_status'),
|
||||
shortcut: { keys: ['k'], isSequence: false },
|
||||
info: t('magic_keys.info.disable_virtual_scrolling'),
|
||||
},
|
||||
{
|
||||
description: t('magic_keys.groups.navigation.go_to_search'),
|
||||
shortcut: { keys: ['/'], isSequence: false },
|
||||
|
@ -147,8 +150,11 @@ const shortcutItemGroups = computed<ShortcutItemGroup[]>(() => [
|
|||
:key="item.description"
|
||||
flex my-1 lg:my-2 justify-between place-items-center max-w-full text-base
|
||||
>
|
||||
<div mr-2 break-words overflow-hidden leading-4 h-full inline-block align-middle>
|
||||
<div mr-2 break-words overflow-hidden leading-4 h-full inline-block align-middle flex flex-inline gap-1 items-center>
|
||||
{{ item.description }}
|
||||
<CommonTooltip v-if="item.info" :content="item.info">
|
||||
<div i-ri:information-line />
|
||||
</CommonTooltip>
|
||||
</div>
|
||||
<div>
|
||||
<template
|
||||
|
|
|
@ -67,7 +67,7 @@ function handleFavouritedBoostedByClose() {
|
|||
@close="handlePublishClose"
|
||||
>
|
||||
<!-- This `w-0` style is used to avoid overflow problems in flex layouts,so don't remove it unless you know what you're doing -->
|
||||
<PublishWidget
|
||||
<PublishWidgetList
|
||||
v-if="dialogDraftKey"
|
||||
:draft-key="dialogDraftKey" expanded flex-1 w-0
|
||||
@published="handlePublished"
|
||||
|
|
|
@ -39,14 +39,14 @@ onUnmounted(() => locked.value = false)
|
|||
<template>
|
||||
<div relative h-full w-full flex pt-12 @click="onClick">
|
||||
<button
|
||||
v-if="hasNext" pointer-events-auto btn-action-icon bg="black/20" :aria-label="$t('action.previous')"
|
||||
v-if="hasNext" pointer-events-auto btn-action-icon bg="black/20" :aria-label="$t('action.next')"
|
||||
hover:bg="black/40" dark:bg="white/30" dark-hover:bg="white/20" absolute top="1/2" right-1 z5
|
||||
:title="$t('action.next')" @click="next"
|
||||
>
|
||||
<div i-ri:arrow-right-s-line text-white />
|
||||
</button>
|
||||
<button
|
||||
v-if="hasPrev" pointer-events-auto btn-action-icon bg="black/20" aria-label="action.next"
|
||||
v-if="hasPrev" pointer-events-auto btn-action-icon bg="black/20" :aria-label="$t('action.prev')"
|
||||
hover:bg="black/40" dark:bg="white/30" dark:hover-bg="white/20" absolute top="1/2" left-1 z5
|
||||
:title="$t('action.prev')" @click="prev"
|
||||
>
|
||||
|
@ -71,7 +71,7 @@ onUnmounted(() => locked.value = false)
|
|||
|
||||
<div absolute top-0 w-full flex justify-end>
|
||||
<button
|
||||
btn-action-icon bg="black/30" aria-label="action.close" hover:bg="black/40" dark:bg="white/30"
|
||||
btn-action-icon bg="black/30" :aria-label="$t('action.close')" hover:bg="black/40" dark:bg="white/30"
|
||||
dark:hover-bg="white/20" pointer-events-auto shrink-0 @click="emit('close')"
|
||||
>
|
||||
<div i-ri:close-line text-white />
|
||||
|
|
|
@ -1,60 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
import type { NavButtonName } from '../../composables/settings'
|
||||
|
||||
import { STORAGE_KEY_BOTTOM_NAV_BUTTONS } from '~/constants'
|
||||
|
||||
import { NavButtonExplore, NavButtonFederated, NavButtonHome, NavButtonLocal, NavButtonMention, NavButtonMoreMenu, NavButtonNotification, NavButtonSearch } from '#components'
|
||||
|
||||
interface NavButton {
|
||||
name: string
|
||||
component: Component
|
||||
}
|
||||
|
||||
const navButtons: NavButton[] = [
|
||||
{ name: 'home', component: NavButtonHome },
|
||||
{ name: 'search', component: NavButtonSearch },
|
||||
{ name: 'notification', component: NavButtonNotification },
|
||||
{ name: 'mention', component: NavButtonMention },
|
||||
{ name: 'explore', component: NavButtonExplore },
|
||||
{ name: 'local', component: NavButtonLocal },
|
||||
{ name: 'federated', component: NavButtonFederated },
|
||||
{ name: 'moreMenu', component: NavButtonMoreMenu },
|
||||
]
|
||||
|
||||
const defaultSelectedNavButtonNames: NavButtonName[] = currentUser.value
|
||||
? ['home', 'search', 'notification', 'mention', 'moreMenu']
|
||||
: ['explore', 'local', 'federated', 'moreMenu']
|
||||
const selectedNavButtonNames = useLocalStorage<NavButtonName[]>(STORAGE_KEY_BOTTOM_NAV_BUTTONS, defaultSelectedNavButtonNames)
|
||||
|
||||
const selectedNavButtons = computed(() => selectedNavButtonNames.value.map(name => navButtons.find(navButton => navButton.name === name)))
|
||||
|
||||
// only one icon can be lit up at the same time
|
||||
import { STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE } from '~/constants'
|
||||
|
||||
const moreMenuVisible = ref(false)
|
||||
|
||||
const { notifications } = useNotifications()
|
||||
const lastAccessedNotificationRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE, '')
|
||||
const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- This weird styles above are used for scroll locking, don't change it unless you know exactly what you're doing. -->
|
||||
<nav
|
||||
h-14 border="t base" flex flex-row text-xl
|
||||
of-y-scroll scrollbar-hide overscroll-none
|
||||
class="after-content-empty after:(h-[calc(100%+0.5px)] w-0.1px pointer-events-none)"
|
||||
>
|
||||
<!-- These weird styles above are used for scroll locking, don't change it unless you know exactly what you're doing. -->
|
||||
<template v-if="currentUser">
|
||||
<NuxtLink to="/home" :aria-label="$t('nav.home')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:home-5-line />
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/search" :aria-label="$t('nav.search')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:search-line />
|
||||
</NuxtLink>
|
||||
<NuxtLink :to="`/notifications/${lastAccessedNotificationRoute}`" :aria-label="$t('nav.notifications')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div flex relative>
|
||||
<div class="i-ri:notification-4-line" text-xl />
|
||||
<div v-if="notifications" class="top-[-0.3rem] right-[-0.3rem]" absolute font-bold rounded-full h-4 w-4 text-xs bg-primary text-inverted flex items-center justify-center>
|
||||
{{ notifications < 10 ? notifications : '•' }}
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/conversations" :aria-label="$t('nav.conversations')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:at-line />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NuxtLink :to="`/${currentServer}/explore/${lastAccessedExploreRoute}`" :aria-label="$t('nav.explore')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:compass-3-line />
|
||||
</NuxtLink>
|
||||
<NuxtLink group :to="`/${currentServer}/public/local`" :aria-label="$t('nav.local')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:group-2-line />
|
||||
</NuxtLink>
|
||||
<NuxtLink :to="`/${currentServer}/public`" :aria-label="$t('nav.federated')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:earth-line />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<NavBottomMoreMenu v-slot="{ toggleVisible, show }" v-model="moreMenuVisible" flex flex-row items-center place-content-center h-full flex-1 cursor-pointer>
|
||||
<button
|
||||
flex items-center place-content-center h-full flex-1 class="select-none"
|
||||
:class="show ? '!text-primary' : ''"
|
||||
aria-label="More menu"
|
||||
@click="toggleVisible"
|
||||
>
|
||||
<span :class="show ? 'i-ri:close-fill' : 'i-ri:more-fill'" />
|
||||
</button>
|
||||
</NavBottomMoreMenu>
|
||||
<Component :is="navButton!.component" v-for="navButton in selectedNavButtons" :key="navButton!.name" :active-class="moreMenuVisible ? '' : 'text-primary'" />
|
||||
</nav>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import { STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE } from '~/constants'
|
||||
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
|
||||
const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="`/${currentServer}/explore/${lastAccessedExploreRoute}`" :aria-label="$t('nav.explore')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:compass-3-line />
|
||||
</NuxtLink>
|
||||
</template>
|
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="`/${currentServer}/public`" :aria-label="$t('nav.federated')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:earth-line />
|
||||
</NuxtLink>
|
||||
</template>
|
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink to="/home" :aria-label="$t('nav.home')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:home-5-line />
|
||||
</NuxtLink>
|
||||
</template>
|
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink group :to="`/${currentServer}/public/local`" :aria-label="$t('nav.local')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:group-2-line />
|
||||
</NuxtLink>
|
||||
</template>
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink
|
||||
to="/conversations" :aria-label="$t('nav.conversations')"
|
||||
:active-class="activeClass" flex flex-row items-center place-content-center h-full
|
||||
flex-1 class="coarse-pointer:select-none" @click="$scrollToTop"
|
||||
>
|
||||
<div i-ri:at-line />
|
||||
</NuxtLink>
|
||||
</template>
|
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
defineModel<boolean>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavBottomMoreMenu
|
||||
v-slot="{ toggleVisible, show }" v-model="modelValue!" flex flex-row items-center
|
||||
place-content-center h-full flex-1 cursor-pointer
|
||||
>
|
||||
<button
|
||||
flex items-center place-content-center h-full flex-1 class="select-none"
|
||||
:class="show ? '!text-primary' : ''"
|
||||
:aria-label="$t('nav.more_menu')"
|
||||
@click="toggleVisible"
|
||||
>
|
||||
<span :class="show ? 'i-ri:close-fill' : 'i-ri:more-fill'" />
|
||||
</button>
|
||||
</NavBottomMoreMenu>
|
||||
</template>
|
|
@ -0,0 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import { STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE } from '~/constants'
|
||||
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
const { notifications } = useNotifications()
|
||||
const lastAccessedNotificationRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE, '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="`/notifications/${lastAccessedNotificationRoute}`" :aria-label="$t('nav.notifications')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div flex relative>
|
||||
<div class="i-ri:notification-4-line" text-xl />
|
||||
<div v-if="notifications" class="top-[-0.3rem] right-[-0.3rem]" absolute font-bold rounded-full h-4 w-4 text-xs bg-primary text-inverted flex items-center justify-center>
|
||||
{{ notifications < 10 ? notifications : '•' }}
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink to="/search" :aria-label="$t('nav.search')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:search-line />
|
||||
</NuxtLink>
|
||||
</template>
|
|
@ -0,0 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
draftKey: string
|
||||
draftItemIndex: number
|
||||
}>()
|
||||
|
||||
const { threadIsActive, addThreadItem, threadItems, removeThreadItem } = useThreadComposer(props.draftKey)
|
||||
|
||||
const isRemovableItem = computed(() => threadIsActive.value && props.draftItemIndex < threadItems.value.length - 1)
|
||||
|
||||
function addOrRemoveItem() {
|
||||
if (isRemovableItem.value)
|
||||
removeThreadItem(props.draftItemIndex)
|
||||
|
||||
else
|
||||
addThreadItem()
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const label = computed(() => {
|
||||
if (!isRemovableItem.value && props.draftItemIndex === 0)
|
||||
return t('tooltip.start_thread')
|
||||
|
||||
return isRemovableItem.value ? t('tooltip.remove_thread_item') : t('tooltip.add_thread_item')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-row rounded-3 :class="{ 'bg-border': threadIsActive }">
|
||||
<div
|
||||
v-if="threadIsActive" dir="ltr" pointer-events-none pe-1 pt-2 pl-2 text-sm tabular-nums text-secondary flex
|
||||
gap="0.5"
|
||||
>
|
||||
{{ draftItemIndex + 1 }}<span text-secondary-light>/</span><span text-secondary-light>{{ threadItems.length
|
||||
}}</span>
|
||||
</div>
|
||||
<CommonTooltip placement="top" :content="label">
|
||||
<button btn-action-icon :aria-label="label" @click="addOrRemoveItem">
|
||||
<div v-if="isRemovableItem" i-ri:chat-delete-line />
|
||||
<div v-else i-ri:chat-new-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</div>
|
||||
</template>
|
|
@ -2,17 +2,19 @@
|
|||
import { EditorContent } from '@tiptap/vue-3'
|
||||
import stringLength from 'string-length'
|
||||
import type { mastodon } from 'masto'
|
||||
import type { Draft } from '~/types'
|
||||
import type { DraftItem } from '~/types'
|
||||
|
||||
const {
|
||||
draftKey,
|
||||
initial = getDefaultDraft,
|
||||
draftItemIndex,
|
||||
expanded = false,
|
||||
placeholder,
|
||||
dialogLabelledBy,
|
||||
initial = getDefaultDraftItem,
|
||||
} = defineProps<{
|
||||
draftKey?: string
|
||||
initial?: () => Draft
|
||||
draftKey: string
|
||||
draftItemIndex: number
|
||||
initial?: () => DraftItem
|
||||
placeholder?: string
|
||||
inReplyToId?: string
|
||||
inReplyToVisibility?: mastodon.v1.StatusVisibility
|
||||
|
@ -26,8 +28,17 @@ const emit = defineEmits<{
|
|||
|
||||
const { t } = useI18n()
|
||||
|
||||
const draftState = useDraft(draftKey, initial)
|
||||
const { draft } = draftState
|
||||
const { threadItems, threadIsActive, publishThread } = useThreadComposer(draftKey)
|
||||
|
||||
const draft = computed({
|
||||
get: () => threadItems.value[draftItemIndex],
|
||||
set: (updatedDraft: DraftItem) => {
|
||||
threadItems.value[draftItemIndex] = updatedDraft
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const isFinalItemOfThread = computed(() => draftItemIndex === threadItems.value.length - 1)
|
||||
|
||||
const {
|
||||
isExceedingAttachmentLimit,
|
||||
|
@ -43,8 +54,8 @@ const {
|
|||
|
||||
const { shouldExpanded, isExpanded, isSending, isPublishDisabled, publishDraft, failedMessages, preferredLanguage, publishSpoilerText } = usePublish(
|
||||
{
|
||||
draftState,
|
||||
...{ expanded: toRef(() => expanded), isUploading, initialDraft: toRef(() => initial) },
|
||||
draftItem: draft,
|
||||
...{ expanded: toRef(() => expanded), isUploading, initialDraft: initial, isPartOfThread: false },
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -156,7 +167,7 @@ const isExceedingCharacterLimit = computed(() => {
|
|||
return characterCount.value > characterLimit.value
|
||||
})
|
||||
|
||||
const postLanguageDisplay = computed(() => languagesNameList.find(i => i.code === (draft.value.params.language || preferredLanguage))?.nativeName)
|
||||
const postLanguageDisplay = computed(() => languagesNameList.find(i => i.code === (draft.value.params.language || preferredLanguage.value))?.nativeName)
|
||||
|
||||
const isDM = computed(() => draft.value.params.visibility === 'direct')
|
||||
|
||||
|
@ -181,9 +192,13 @@ async function toggleSensitive() {
|
|||
}
|
||||
|
||||
async function publish() {
|
||||
const status = await publishDraft()
|
||||
if (status)
|
||||
emit('published', status)
|
||||
const publishResult = await (threadIsActive.value ? publishThread() : publishDraft())
|
||||
if (publishResult) {
|
||||
if (Array.isArray(publishResult))
|
||||
failedMessages.value = publishResult
|
||||
else
|
||||
emit('published', publishResult)
|
||||
}
|
||||
}
|
||||
|
||||
useWebShareTarget(async ({ data: { data, action } }: any) => {
|
||||
|
@ -215,10 +230,6 @@ function stopQuestionMarkPropagation(e: KeyboardEvent) {
|
|||
if (e.key === '?')
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
onDeactivated(() => {
|
||||
clearEmptyDrafts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -228,286 +239,304 @@ onDeactivated(() => {
|
|||
{{ $t('state.editing') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div flex gap-3 flex-1>
|
||||
<NuxtLink self-start :to="getAccountRoute(currentUser.account)">
|
||||
<AccountBigAvatar :account="currentUser.account" square />
|
||||
</NuxtLink>
|
||||
<!-- This `w-0` style is used to avoid overflow problems in flex layouts,so don't remove it unless you know what you're doing -->
|
||||
<div
|
||||
ref="dropZoneRef"
|
||||
flex w-0 flex-col gap-3 flex-1
|
||||
border="2 dashed transparent"
|
||||
:class="[isSending ? 'pointer-events-none' : '', isOverDropZone ? '!border-primary' : '']"
|
||||
>
|
||||
<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)">
|
||||
{{ accountToShortHandle(m) }}
|
||||
</button>
|
||||
</ContentMentionGroup>
|
||||
|
||||
<div v-if="draft.params.sensitive">
|
||||
<input
|
||||
v-model="publishSpoilerText"
|
||||
type="text"
|
||||
:placeholder="$t('placeholder.content_warning')"
|
||||
p2 border-rounded w-full bg-transparent
|
||||
outline-none border="~ base"
|
||||
>
|
||||
</div>
|
||||
|
||||
<CommonErrorMessage v-if="failedMessages.length > 0" described-by="publish-failed">
|
||||
<header id="publish-failed" flex justify-between>
|
||||
<div flex items-center gap-x-2 font-bold>
|
||||
<div aria-hidden="true" i-ri:error-warning-fill />
|
||||
<p>{{ $t('state.publish_failed') }}</p>
|
||||
</div>
|
||||
<CommonTooltip placement="bottom" :content="$t('action.clear_publish_failed')">
|
||||
<button
|
||||
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100 :aria-label="$t('action.clear_publish_failed')"
|
||||
@click="failedMessages = []"
|
||||
>
|
||||
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</header>
|
||||
<ol ps-2 sm:ps-1>
|
||||
<li v-for="(error, i) in failedMessages" :key="i" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
|
||||
<strong>{{ i + 1 }}.</strong>
|
||||
<span>{{ error }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</CommonErrorMessage>
|
||||
|
||||
<div relative flex-1 flex flex-col min-h-30>
|
||||
<EditorContent
|
||||
:editor="editor"
|
||||
flex max-w-full
|
||||
:class="{
|
||||
'md:max-h-[calc(100vh-200px)] sm:max-h-[calc(100vh-400px)] max-h-35 of-y-auto overscroll-contain': shouldExpanded,
|
||||
'py2 px3.5 bg-dm rounded-4 me--1 ms--1 mt--1': isDM,
|
||||
}"
|
||||
@keydown="stopQuestionMarkPropagation"
|
||||
@keydown.esc.prevent="editor?.commands.blur()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isUploading" flex gap-1 items-center text-sm p1 text-primary>
|
||||
<div animate-spin preserve-3d>
|
||||
<div i-ri:loader-2-fill />
|
||||
</div>
|
||||
{{ $t('state.uploading') }}
|
||||
</div>
|
||||
<CommonErrorMessage
|
||||
v-else-if="failedAttachments.length > 0"
|
||||
:described-by="isExceedingAttachmentLimit ? 'upload-failed uploads-per-post' : 'upload-failed'"
|
||||
>
|
||||
<header id="upload-failed" flex justify-between>
|
||||
<div flex items-center gap-x-2 font-bold>
|
||||
<div aria-hidden="true" i-ri:error-warning-fill />
|
||||
<p>{{ $t('state.upload_failed') }}</p>
|
||||
</div>
|
||||
<CommonTooltip placement="bottom" :content="$t('action.clear_upload_failed')">
|
||||
<button
|
||||
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100
|
||||
:aria-label="$t('action.clear_upload_failed')" @click="failedAttachments = []"
|
||||
>
|
||||
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</header>
|
||||
<div v-if="isExceedingAttachmentLimit" id="uploads-per-post" ps-2 sm:ps-1 text-small>
|
||||
{{ $t('state.attachments_exceed_server_limit') }}
|
||||
</div>
|
||||
<ol ps-2 sm:ps-1>
|
||||
<li v-for="error in failedAttachments" :key="error[0]" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
|
||||
<strong>{{ error[1] }}:</strong>
|
||||
<span>{{ error[0] }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</CommonErrorMessage>
|
||||
|
||||
<div v-if="draft.attachments.length" flex="~ col gap-2" overflow-auto>
|
||||
<PublishAttachment
|
||||
v-for="(att, idx) in draft.attachments" :key="att.id"
|
||||
:attachment="att"
|
||||
:dialog-labelled-by="dialogLabelledBy ?? (draft.editingStatus ? 'state-editing' : undefined)"
|
||||
@remove="removeAttachment(idx)"
|
||||
@set-description="setDescription(att, $event)"
|
||||
/>
|
||||
<div>
|
||||
<NuxtLink self-start :to="getAccountRoute(currentUser.account)">
|
||||
<AccountBigAvatar :account="currentUser.account" square />
|
||||
</NuxtLink>
|
||||
<div v-if="!isFinalItemOfThread" w-full h-full flex mt--3px justify-center>
|
||||
<div w-1px border="x base" mb-6 />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div flex gap-4>
|
||||
<div w-12 h-full sm:block hidden />
|
||||
<div flex="~ col 1" max-w-full>
|
||||
<form v-if="isExpanded && draft.params.poll" my-4 flex="~ 1 col" gap-3 m="s--1">
|
||||
|
||||
<div w-full>
|
||||
<div flex gap-3 flex-1>
|
||||
<!-- This `w-0` style is used to avoid overflow problems in flex layouts,so don't remove it unless you know what you're doing -->
|
||||
<div
|
||||
v-for="(option, index) in draft.params.poll.options"
|
||||
:key="index"
|
||||
flex="~ row"
|
||||
gap-3
|
||||
ref="dropZoneRef" flex w-0 flex-col gap-3 flex-1 border="2 dashed transparent"
|
||||
:class="[isSending ? 'pointer-events-none' : '', isOverDropZone ? '!border-primary' : '']"
|
||||
>
|
||||
<input
|
||||
:value="option"
|
||||
bg-base
|
||||
border="~ base" flex-1 h10 pe-4 rounded-2 w-full flex="~ row"
|
||||
items-center relative focus-within:box-shadow-outline gap-3
|
||||
px-4 py-2
|
||||
:placeholder="$t('polls.option_placeholder', { current: index + 1, max: currentInstance?.configuration?.polls.maxOptions })"
|
||||
class="option-input"
|
||||
@input="editPollOptionDraft($event, index)"
|
||||
>
|
||||
<CommonTooltip placement="top" :content="$t('polls.remove_option')" class="delete-button">
|
||||
<ContentMentionGroup v-if="draft.mentions?.length && shouldExpanded" replying>
|
||||
<button
|
||||
btn-action-icon class="hover:bg-red/75"
|
||||
:disabled="index === draft.params.poll!.options.length - 1 && (index + 1 !== currentInstance?.configuration?.polls.maxOptions || draft.params.poll!.options[index].length === 0)"
|
||||
@click.prevent="deletePollOption(index)"
|
||||
v-for="m, i of draft.mentions" :key="m" text-primary hover:color-red
|
||||
@click="draft.mentions?.splice(i, 1)"
|
||||
>
|
||||
<div i-ri:delete-bin-line />
|
||||
{{ accountToShortHandle(m) }}
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<span
|
||||
v-if="currentInstance?.configuration?.polls.maxCharactersPerOption"
|
||||
class="char-limit-radial"
|
||||
aspect-ratio-1
|
||||
h-10
|
||||
:style="{ background: `radial-gradient(closest-side, rgba(var(--rgb-bg-base)) 79%, transparent 80% 100%), conic-gradient(${draft.params.poll!.options[index].length / currentInstance?.configuration?.polls.maxCharactersPerOption > 1 ? 'var(--c-danger)' : 'var(--c-primary)'} ${draft.params.poll!.options[index].length / currentInstance?.configuration?.polls.maxCharactersPerOption * 100}%, var(--c-primary-fade) 0)` }"
|
||||
>{{ draft.params.poll!.options[index].length }}</span>
|
||||
</ContentMentionGroup>
|
||||
|
||||
<div v-if="draft.params.sensitive">
|
||||
<input
|
||||
v-model="publishSpoilerText" type="text" :placeholder="$t('placeholder.content_warning')" p2
|
||||
border-rounded w-full bg-transparent outline-none border="~ base"
|
||||
>
|
||||
</div>
|
||||
|
||||
<CommonErrorMessage v-if="failedMessages.length > 0" described-by="publish-failed">
|
||||
<header id="publish-failed" flex justify-between>
|
||||
<div flex items-center gap-x-2 font-bold>
|
||||
<div aria-hidden="true" i-ri:error-warning-fill />
|
||||
<p>{{ $t('state.publish_failed') }}</p>
|
||||
</div>
|
||||
<CommonTooltip placement="bottom" :content="$t('action.clear_publish_failed')">
|
||||
<button
|
||||
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100
|
||||
:aria-label="$t('action.clear_publish_failed')" @click="failedMessages = []"
|
||||
>
|
||||
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</header>
|
||||
<ol ps-2 sm:ps-1>
|
||||
<li v-for="(error, i) in failedMessages" :key="i" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
|
||||
<strong>{{ i + 1 }}.</strong>
|
||||
<span>{{ error }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</CommonErrorMessage>
|
||||
|
||||
<div relative flex-1 flex flex-col :class="shouldExpanded ? 'min-h-30' : ''">
|
||||
<EditorContent
|
||||
:editor="editor" flex max-w-full
|
||||
:class="{
|
||||
'md:max-h-[calc(100vh-200px)] sm:max-h-[calc(100vh-400px)] max-h-35 of-y-auto overscroll-contain': shouldExpanded,
|
||||
'py2 px3.5 bg-dm rounded-4 me--1 ms--1 mt--1': isDM,
|
||||
}"
|
||||
@keydown="stopQuestionMarkPropagation"
|
||||
@keydown.esc.prevent="editor?.commands.blur()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isUploading" flex gap-1 items-center text-sm p1 text-primary>
|
||||
<div animate-spin preserve-3d>
|
||||
<div i-ri:loader-2-fill />
|
||||
</div>
|
||||
{{ $t('state.uploading') }}
|
||||
</div>
|
||||
<CommonErrorMessage
|
||||
v-else-if="failedAttachments.length > 0"
|
||||
:described-by="isExceedingAttachmentLimit ? 'upload-failed uploads-per-post' : 'upload-failed'"
|
||||
>
|
||||
<header id="upload-failed" flex justify-between>
|
||||
<div flex items-center gap-x-2 font-bold>
|
||||
<div aria-hidden="true" i-ri:error-warning-fill />
|
||||
<p>{{ $t('state.upload_failed') }}</p>
|
||||
</div>
|
||||
<CommonTooltip placement="bottom" :content="$t('action.clear_upload_failed')">
|
||||
<button
|
||||
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100
|
||||
:aria-label="$t('action.clear_upload_failed')" @click="failedAttachments = []"
|
||||
>
|
||||
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</header>
|
||||
<div v-if="isExceedingAttachmentLimit" id="uploads-per-post" ps-2 sm:ps-1 text-small>
|
||||
{{ $t('state.attachments_exceed_server_limit') }}
|
||||
</div>
|
||||
<ol ps-2 sm:ps-1>
|
||||
<li v-for="error in failedAttachments" :key="error[0]" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
|
||||
<strong>{{ error[1] }}:</strong>
|
||||
<span>{{ error[0] }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</CommonErrorMessage>
|
||||
|
||||
<div v-if="draft.attachments.length" flex="~ col gap-2" overflow-auto>
|
||||
<PublishAttachment
|
||||
v-for="(att, idx) in draft.attachments" :key="att.id" :attachment="att"
|
||||
:dialog-labelled-by="dialogLabelledBy ?? (draft.editingStatus ? 'state-editing' : undefined)"
|
||||
@remove="removeAttachment(idx)" @set-description="setDescription(att, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
v-if="shouldExpanded" flex="~ gap-1 1 wrap" m="s--1" pt-2 justify="end" max-w-full
|
||||
border="t base"
|
||||
>
|
||||
<PublishEmojiPicker
|
||||
@select="insertEmoji"
|
||||
@select-custom="insertCustomEmoji"
|
||||
>
|
||||
<button btn-action-icon :title="$t('tooltip.emojis')" :aria-label="$t('tooltip.add_emojis')">
|
||||
<div i-ri:emotion-line />
|
||||
</button>
|
||||
</PublishEmojiPicker>
|
||||
</div>
|
||||
|
||||
<CommonTooltip v-if="draft.params.poll === undefined" placement="top" :content="$t('tooltip.add_media')">
|
||||
<button btn-action-icon :aria-label="$t('tooltip.add_media')" @click="pickAttachments">
|
||||
<div i-ri:image-add-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<template v-if="draft.attachments.length === 0">
|
||||
<CommonTooltip v-if="!draft.params.poll" placement="top" :content="$t('polls.create')">
|
||||
<button btn-action-icon :aria-label="$t('polls.create')" @click="draft.params.poll = { options: [''], expiresIn: expiresInOptions[expiresInDefaultOptionIndex].seconds }">
|
||||
<div i-ri:chat-poll-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<div v-else rounded-full b-1 border-dark flex="~ row" gap-1>
|
||||
<CommonTooltip placement="top" :content="$t('polls.cancel')">
|
||||
<button btn-action-icon b-r border-dark :aria-label="$t('polls.cancel')" @click="draft.params.poll = undefined">
|
||||
<div i-ri:close-line />
|
||||
<div flex="~ col 1" max-w-full>
|
||||
<form v-if="isExpanded && draft.params.poll" my-4 flex="~ 1 col" gap-3 m="s--1">
|
||||
<div v-for="(option, index) in draft.params.poll.options" :key="index" flex="~ row" gap-3>
|
||||
<input
|
||||
:value="option" bg-base border="~ base" flex-1 h10 pe-4 rounded-2 w-full flex="~ row" items-center
|
||||
relative focus-within:box-shadow-outline gap-3 px-4 py-2
|
||||
:placeholder="$t('polls.option_placeholder', { current: index + 1, max: currentInstance?.configuration?.polls.maxOptions })"
|
||||
class="option-input" @input="editPollOptionDraft($event, index)"
|
||||
>
|
||||
<CommonTooltip placement="top" :content="$t('polls.remove_option')" class="delete-button">
|
||||
<button
|
||||
btn-action-icon class="hover:bg-red/75"
|
||||
:disabled="index === draft.params.poll!.options.length - 1 && (index + 1 !== currentInstance?.configuration?.polls.maxOptions || draft.params.poll!.options[index].length === 0)"
|
||||
@click.prevent="deletePollOption(index)"
|
||||
>
|
||||
<div i-ri:delete-bin-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<CommonDropdown placement="top">
|
||||
<CommonTooltip placement="top" :content="$t('polls.settings')">
|
||||
<button :aria-label="$t('polls.settings')" btn-action-icon w-12>
|
||||
<div i-ri:list-settings-line />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<template #popper>
|
||||
<div flex="~ col" gap-1 p-2>
|
||||
<CommonCheckbox v-model="draft.params.poll.multiple" :label="draft.params.poll.multiple ? $t('polls.disallow_multiple') : $t('polls.allow_multiple')" px-2 gap-3 h-9 flex justify-center hover:bg-active rounded-full icon-checked="i-ri:checkbox-multiple-blank-line" icon-unchecked="i-ri:checkbox-blank-circle-line" />
|
||||
<CommonCheckbox v-model="draft.params.poll.hideTotals" :label="draft.params.poll.hideTotals ? $t('polls.show_votes') : $t('polls.hide_votes')" px-2 gap-3 h-9 flex justify-center hover:bg-active rounded-full icon-checked="i-ri:eye-close-line" icon-unchecked="i-ri:eye-line" />
|
||||
</div>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
<CommonDropdown placement="bottom">
|
||||
<CommonTooltip placement="top" :content="$t('polls.expiration')">
|
||||
<button :aria-label="$t('polls.expiration')" btn-action-icon w-12>
|
||||
<div i-ri:hourglass-line />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<template #popper>
|
||||
<CommonDropdownItem
|
||||
v-for="expiresInOption in expiresInOptions"
|
||||
:key="expiresInOption.seconds"
|
||||
:text="expiresInOption.label"
|
||||
:checked="draft.params.poll!.expiresIn === expiresInOption.seconds"
|
||||
@click="draft.params.poll!.expiresIn = expiresInOption.seconds"
|
||||
/>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
<span
|
||||
v-if="currentInstance?.configuration?.polls.maxCharactersPerOption" class="char-limit-radial"
|
||||
aspect-ratio-1 h-10
|
||||
:style="{ background: `radial-gradient(closest-side, rgba(var(--rgb-bg-base)) 79%, transparent 80% 100%), conic-gradient(${draft.params.poll!.options[index].length / currentInstance?.configuration?.polls.maxCharactersPerOption > 1 ? 'var(--c-danger)' : 'var(--c-primary)'} ${draft.params.poll!.options[index].length / currentInstance?.configuration?.polls.maxCharactersPerOption * 100}%, var(--c-primary-fade) 0)` }"
|
||||
>{{
|
||||
draft.params.poll!.options[index].length }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<PublishEditorTools v-if="editor" :editor="editor" />
|
||||
|
||||
<div flex-auto />
|
||||
|
||||
<PublishCharacterCounter :max="characterLimit" :length="characterCount" />
|
||||
|
||||
<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 />
|
||||
</form>
|
||||
<div v-if="shouldExpanded" flex="~ gap-1 1 wrap" m="s--1" pt-2 justify="end" max-w-full border="t base">
|
||||
<PublishEmojiPicker @select="insertEmoji" @select-custom="insertCustomEmoji">
|
||||
<button btn-action-icon :title="$t('tooltip.emojis')" :aria-label="$t('tooltip.add_emojis')">
|
||||
<div i-ri:emotion-line />
|
||||
</button>
|
||||
</PublishEmojiPicker>
|
||||
|
||||
<template #popper>
|
||||
<PublishLanguagePicker v-model="draft.params.language" min-w-80 />
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</CommonTooltip>
|
||||
|
||||
<CommonTooltip placement="top" :content="$t('tooltip.add_content_warning')">
|
||||
<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-else i-ri:alarm-warning-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<PublishVisibilityPicker v-model="draft.params.visibility" :editing="!!draft.editingStatus">
|
||||
<template #default="{ visibility }">
|
||||
<button :disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon :class="{ 'w-12': !draft.editingStatus }">
|
||||
<div :class="visibility.icon" />
|
||||
<div v-if="!draft.editingStatus" i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
<CommonTooltip
|
||||
v-if="draft.params.poll === undefined" placement="top" :content="$t('tooltip.add_media')"
|
||||
>
|
||||
<button btn-action-icon :aria-label="$t('tooltip.add_media')" @click="pickAttachments">
|
||||
<div i-ri:image-add-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<template v-if="draft.attachments.length === 0">
|
||||
<CommonTooltip v-if="!draft.params.poll" placement="top" :content="$t('polls.create')">
|
||||
<button
|
||||
btn-action-icon :aria-label="$t('polls.create')"
|
||||
@click="draft.params.poll = { options: [''], expiresIn: expiresInOptions[expiresInDefaultOptionIndex].seconds }"
|
||||
>
|
||||
<div i-ri:chat-poll-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<div v-else rounded-full b-1 border-dark flex="~ row" gap-1>
|
||||
<CommonTooltip placement="top" :content="$t('polls.cancel')">
|
||||
<button
|
||||
btn-action-icon b-r border-dark :aria-label="$t('polls.cancel')"
|
||||
@click="draft.params.poll = undefined"
|
||||
>
|
||||
<div i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<CommonDropdown placement="top">
|
||||
<CommonTooltip placement="top" :content="$t('polls.settings')">
|
||||
<button :aria-label="$t('polls.settings')" btn-action-icon w-12>
|
||||
<div i-ri:list-settings-line />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<template #popper>
|
||||
<div flex="~ col" gap-1 p-2>
|
||||
<CommonCheckbox
|
||||
v-model="draft.params.poll.multiple"
|
||||
:label="draft.params.poll.multiple ? $t('polls.disallow_multiple') : $t('polls.allow_multiple')"
|
||||
px-2 gap-3 h-9 flex justify-center hover:bg-active rounded-full
|
||||
icon-checked="i-ri:checkbox-multiple-blank-line"
|
||||
icon-unchecked="i-ri:checkbox-blank-circle-line"
|
||||
/>
|
||||
<CommonCheckbox
|
||||
v-model="draft.params.poll.hideTotals"
|
||||
:label="draft.params.poll.hideTotals ? $t('polls.show_votes') : $t('polls.hide_votes')" px-2 gap-3
|
||||
h-9 flex justify-center hover:bg-active rounded-full icon-checked="i-ri:eye-close-line"
|
||||
icon-unchecked="i-ri:eye-line"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
<CommonDropdown placement="bottom">
|
||||
<CommonTooltip placement="top" :content="$t('polls.expiration')">
|
||||
<button :aria-label="$t('polls.expiration')" btn-action-icon w-12>
|
||||
<div i-ri:hourglass-line />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<template #popper>
|
||||
<CommonDropdownItem
|
||||
v-for="expiresInOption in expiresInOptions" :key="expiresInOption.seconds"
|
||||
:text="expiresInOption.label" :checked="draft.params.poll!.expiresIn === expiresInOption.seconds"
|
||||
@click="draft.params.poll!.expiresIn = expiresInOption.seconds"
|
||||
/>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</PublishVisibilityPicker>
|
||||
|
||||
<CommonTooltip v-if="failedMessages.length > 0" id="publish-failed-tooltip" placement="top" :content="$t('tooltip.publish_failed')">
|
||||
<button
|
||||
btn-danger rounded-3 text-sm w-full flex="~ gap1" items-center md:w-fit aria-describedby="publish-failed-tooltip"
|
||||
>
|
||||
<span block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<span>{{ $t('state.publish_failed') }}</span>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<PublishEditorTools v-if="editor" :editor="editor" />
|
||||
|
||||
<CommonTooltip v-else id="publish-tooltip" placement="top" :content="$t('tooltip.add_publishable_content')" :disabled="!(isPublishDisabled || isExceedingCharacterLimit)">
|
||||
<button
|
||||
btn-solid rounded-3 text-sm w-full flex="~ gap1" items-center
|
||||
md:w-fit
|
||||
class="publish-button"
|
||||
:aria-disabled="isPublishDisabled || isExceedingCharacterLimit"
|
||||
aria-describedby="publish-tooltip"
|
||||
@click="publish"
|
||||
<div flex-auto />
|
||||
|
||||
<PublishCharacterCounter :max="characterLimit" :length="characterCount" />
|
||||
|
||||
<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')">
|
||||
<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-else i-ri:alarm-warning-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<PublishVisibilityPicker v-model="draft.params.visibility" :editing="!!draft.editingStatus">
|
||||
<template #default="{ visibility }">
|
||||
<button
|
||||
:disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')"
|
||||
btn-action-icon :class="{ 'w-12': !draft.editingStatus }"
|
||||
>
|
||||
<div :class="visibility.icon" />
|
||||
<div v-if="!draft.editingStatus" i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</template>
|
||||
</PublishVisibilityPicker>
|
||||
|
||||
<PublishThreadTools :draft-item-index="draftItemIndex" :draft-key="draftKey" />
|
||||
|
||||
<CommonTooltip
|
||||
v-if="failedMessages.length > 0" id="publish-failed-tooltip" placement="top"
|
||||
:content="$t('tooltip.publish_failed')"
|
||||
>
|
||||
<span v-if="isSending" block animate-spin preserve-3d>
|
||||
<div block i-ri:loader-2-fill />
|
||||
</span>
|
||||
<span v-if="failedMessages.length" block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span>
|
||||
<span v-else-if="draft.params.inReplyToId">{{ $t('action.reply') }}</span>
|
||||
<span v-else>{{ !isSending ? $t('action.publish') : $t('state.publishing') }}</span>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<button
|
||||
btn-danger rounded-3 text-sm w-full flex="~ gap1" items-center md:w-fit
|
||||
aria-describedby="publish-failed-tooltip"
|
||||
>
|
||||
<span block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<span>{{ $t('state.publish_failed') }}</span>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<CommonTooltip
|
||||
v-else id="publish-tooltip" placement="top" :content="$t('tooltip.add_publishable_content')"
|
||||
:disabled="!(isPublishDisabled || isExceedingCharacterLimit)"
|
||||
>
|
||||
<button
|
||||
v-if="!threadIsActive || isFinalItemOfThread"
|
||||
btn-solid rounded-3 text-sm w-full flex="~ gap1" items-center md:w-fit class="publish-button"
|
||||
:aria-disabled="isPublishDisabled || isExceedingCharacterLimit" aria-describedby="publish-tooltip"
|
||||
@click="publish"
|
||||
>
|
||||
<span v-if="isSending" block animate-spin preserve-3d>
|
||||
<div block i-ri:loader-2-fill />
|
||||
</span>
|
||||
<span v-if="failedMessages.length" block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<template v-if="threadIsActive">
|
||||
<span>{{ $t('action.publish_thread') }} </span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span>
|
||||
<span v-else-if="draft.params.inReplyToId">{{ $t('action.reply') }}</span>
|
||||
<span v-else>{{ !isSending ? $t('action.publish') : $t('state.publishing') }}</span>
|
||||
</template>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -515,27 +544,29 @@ onDeactivated(() => {
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.publish-button[aria-disabled=true] {
|
||||
cursor: not-allowed;
|
||||
background-color: var(--c-bg-btn-disabled);
|
||||
color: var(--c-text-btn-disabled);
|
||||
}
|
||||
.publish-button[aria-disabled=true]:hover {
|
||||
background-color: var(--c-bg-btn-disabled);
|
||||
color: var(--c-text-btn-disabled);
|
||||
}
|
||||
.option-input:focus + .delete-button {
|
||||
display: none;
|
||||
}
|
||||
.publish-button[aria-disabled=true] {
|
||||
cursor: not-allowed;
|
||||
background-color: var(--c-bg-btn-disabled);
|
||||
color: var(--c-text-btn-disabled);
|
||||
}
|
||||
|
||||
.option-input:not(:focus) + .delete-button + .char-limit-radial {
|
||||
display: none;
|
||||
}
|
||||
.publish-button[aria-disabled=true]:hover {
|
||||
background-color: var(--c-bg-btn-disabled);
|
||||
color: var(--c-text-btn-disabled);
|
||||
}
|
||||
|
||||
.char-limit-radial {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.option-input:focus+.delete-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-input:not(:focus)+.delete-button+.char-limit-radial {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.char-limit-radial {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { formatTimeAgo } from '@vueuse/core'
|
||||
import type { DraftItem } from '~/types'
|
||||
|
||||
const route = useRoute()
|
||||
const { formatNumber } = useHumanReadableNumber()
|
||||
|
@ -20,35 +21,39 @@ watchEffect(() => {
|
|||
onDeactivated(() => {
|
||||
clearEmptyDrafts()
|
||||
})
|
||||
|
||||
function firstDraftItemOf(drafts: DraftItem | Array<DraftItem>): DraftItem {
|
||||
if (Array.isArray(drafts))
|
||||
return drafts[0]
|
||||
return drafts
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ col" pt-6 h-screen>
|
||||
<div flex="~ col" pb-6>
|
||||
<div inline-flex justify-end h-8>
|
||||
<VDropdown v-if="nonEmptyDrafts.length" placement="bottom-end">
|
||||
<button btn-text flex="inline center">
|
||||
{{ $t('compose.drafts', nonEmptyDrafts.length, { named: { v: formatNumber(nonEmptyDrafts.length) } }) }} <div aria-hidden="true" i-ri:arrow-down-s-line />
|
||||
{{ $t('compose.drafts', nonEmptyDrafts.length, { named: { v: formatNumber(nonEmptyDrafts.length) } }) }} 
|
||||
<div aria-hidden="true" i-ri:arrow-down-s-line />
|
||||
</button>
|
||||
<template #popper="{ hide }">
|
||||
<div flex="~ col">
|
||||
<NuxtLink
|
||||
v-for="[key, draft] of nonEmptyDrafts" :key="key"
|
||||
border="b base" text-left py2 px4 hover:bg-active
|
||||
:replace="true"
|
||||
:to="`/compose?draft=${encodeURIComponent(key)}`"
|
||||
@click="hide()"
|
||||
v-for="[key, drafts] of nonEmptyDrafts" :key="key" border="b base" text-left py2 px4
|
||||
hover:bg-active :replace="true" :to="`/compose?draft=${encodeURIComponent(key)}`" @click="hide()"
|
||||
>
|
||||
<div>
|
||||
<div flex="~ gap-1" items-center>
|
||||
<i18n-t keypath="compose.draft_title">
|
||||
<code>{{ key }}</code>
|
||||
</i18n-t>
|
||||
<span v-if="draft.lastUpdated" text-secondary text-sm>
|
||||
· {{ formatTimeAgo(new Date(draft.lastUpdated), timeAgoOptions) }}
|
||||
<span v-if="firstDraftItemOf(drafts).lastUpdated" text-secondary text-sm>
|
||||
· {{ formatTimeAgo(new Date(firstDraftItemOf(drafts).lastUpdated), timeAgoOptions) }}
|
||||
</span>
|
||||
</div>
|
||||
<div text-secondary>
|
||||
{{ htmlToText(draft.params.status).slice(0, 50) }}
|
||||
{{ htmlToText(firstDraftItemOf(drafts).params.status).slice(0, 50) }}
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
@ -57,7 +62,7 @@ onDeactivated(() => {
|
|||
</VDropdown>
|
||||
</div>
|
||||
<div>
|
||||
<PublishWidget :key="draftKey" expanded class="min-h-100!" :draft-key="draftKey" />
|
||||
<PublishWidgetList expanded class="min-h-100!" :draft-key="draftKey" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import type { mastodon } from 'masto'
|
||||
import type { DraftItem } from '~/types'
|
||||
|
||||
const {
|
||||
draftKey,
|
||||
initial = getDefaultDraftItem,
|
||||
expanded = false,
|
||||
placeholder,
|
||||
dialogLabelledBy,
|
||||
inReplyToId,
|
||||
inReplyToVisibility,
|
||||
} = defineProps<{
|
||||
draftKey: string
|
||||
initial?: () => DraftItem
|
||||
placeholder?: string
|
||||
inReplyToId?: string
|
||||
inReplyToVisibility?: mastodon.v1.StatusVisibility
|
||||
expanded?: boolean
|
||||
dialogLabelledBy?: string
|
||||
}>()
|
||||
|
||||
const threadItems = computed(() =>
|
||||
useThreadComposer(draftKey, initial).threadItems.value,
|
||||
)
|
||||
|
||||
onDeactivated(() => {
|
||||
clearEmptyDrafts()
|
||||
})
|
||||
|
||||
function isFirstItem(index: number) {
|
||||
return index === 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="isHydrated && currentUser">
|
||||
<PublishWidget
|
||||
v-for="(_, index) in threadItems" :key="`${draftKey}-${index}`"
|
||||
:draft-key="draftKey"
|
||||
:draft-item-index="index"
|
||||
:expanded="isFirstItem(index) ? expanded : true"
|
||||
:placeholder="placeholder"
|
||||
:dialog-labelled-by="dialogLabelledBy"
|
||||
:in-reply-to-id="isFirstItem(index) ? inReplyToId : undefined"
|
||||
:in-reply-to-visibility="inReplyToVisibility"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
|
@ -97,7 +97,7 @@ function resetModal() {
|
|||
<b text-primary>@{{ account.acct }}</b>
|
||||
</i18n-t>
|
||||
</h2>
|
||||
<button ref="dismissButton" btn-action-icon absolute top--8 right-0 m1 aria-label="Close" @click="emit('close')">
|
||||
<button ref="dismissButton" btn-action-icon absolute top--8 right-0 m1 :aria-label="$t('action.close')" @click="emit('close')">
|
||||
<div i-ri:close-line />
|
||||
</button>
|
||||
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
<script setup lang="ts">
|
||||
import type { NavButtonName } from '~/composables/settings'
|
||||
import { STORAGE_KEY_BOTTOM_NAV_BUTTONS } from '~/constants'
|
||||
|
||||
interface NavButton {
|
||||
name: NavButtonName
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const availableNavButtons: NavButton[] = [
|
||||
{ name: 'home', label: 'nav.home', icon: 'i-ri:home-5-line' },
|
||||
{ name: 'search', label: 'nav.search', icon: 'i-ri:search-line' },
|
||||
{ name: 'notification', label: 'nav.notifications', icon: 'i-ri:notification-4-line' },
|
||||
{ name: 'mention', label: 'nav.conversations', icon: 'i-ri:at-line' },
|
||||
{ name: 'explore', label: 'nav.explore', icon: 'i-ri:compass-3-line' },
|
||||
{ name: 'local', label: 'nav.local', icon: 'i-ri:group-2-line' },
|
||||
{ name: 'federated', label: 'nav.federated', icon: 'i-ri:earth-line' },
|
||||
{ name: 'moreMenu', label: 'nav.more_menu', icon: 'i-ri:more-fill' },
|
||||
] as const
|
||||
|
||||
const defaultSelectedNavButtonNames = computed<NavButtonName[]>(() =>
|
||||
currentUser.value
|
||||
? ['home', 'search', 'notification', 'mention', 'moreMenu']
|
||||
: ['explore', 'local', 'federated', 'moreMenu'],
|
||||
)
|
||||
const navButtonNamesSetting = useLocalStorage<NavButtonName[]>(STORAGE_KEY_BOTTOM_NAV_BUTTONS, defaultSelectedNavButtonNames.value)
|
||||
const selectedNavButtonNames = ref<NavButtonName[]>([])
|
||||
|
||||
const selectedNavButtons = computed<NavButton[]>(() =>
|
||||
selectedNavButtonNames.value.map(name =>
|
||||
availableNavButtons.find(navButton => navButton.name === name)!,
|
||||
),
|
||||
)
|
||||
|
||||
const canSave = computed(() =>
|
||||
selectedNavButtonNames.value.length > 0
|
||||
&& selectedNavButtonNames.value.includes('moreMenu')
|
||||
&& JSON.stringify(selectedNavButtonNames.value) !== JSON.stringify(navButtonNamesSetting.value),
|
||||
)
|
||||
|
||||
function isAdded(name: NavButtonName) {
|
||||
return selectedNavButtonNames.value.includes(name)
|
||||
}
|
||||
|
||||
function append(navButtonName: NavButtonName) {
|
||||
const maxButtonNumber = 5
|
||||
if (selectedNavButtonNames.value.length < maxButtonNumber)
|
||||
selectedNavButtonNames.value = [...selectedNavButtonNames.value, navButtonName]
|
||||
}
|
||||
|
||||
function remove(navButtonName: NavButtonName) {
|
||||
selectedNavButtonNames.value = selectedNavButtonNames.value.filter(name => name !== navButtonName)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
selectedNavButtonNames.value = []
|
||||
}
|
||||
|
||||
function reset() {
|
||||
selectedNavButtonNames.value = defaultSelectedNavButtonNames.value
|
||||
}
|
||||
|
||||
function save() {
|
||||
navButtonNamesSetting.value = selectedNavButtonNames.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section space-y-2>
|
||||
<h2 id="interface-bn" font-medium>
|
||||
{{ $t('settings.interface.bottom_nav') }}
|
||||
</h2>
|
||||
<form aria-labelledby="interface-bn" aria-describedby="interface-bn-desc" @submit.prevent="save">
|
||||
<p id="interface-bn-desc" pb-2>
|
||||
{{ $t('settings.interface.bottom_nav_instructions') }}
|
||||
</p>
|
||||
<!-- preview -->
|
||||
<div aria-hidden="true" flex="~ gap4 wrap" items-center select-settings h-14 p0>
|
||||
<nav
|
||||
v-for="availableNavButton in selectedNavButtons" :key="availableNavButton.name"
|
||||
flex="~ 1" items-center justify-center text-xl
|
||||
scrollbar-hide overscroll-none
|
||||
>
|
||||
<button btn-base :class="availableNavButton.icon" mx-4 tabindex="-1" />
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- button selection -->
|
||||
<div flex="~ gap4 wrap" py4>
|
||||
<button
|
||||
v-for="{ name, label, icon } in availableNavButtons"
|
||||
:key="name"
|
||||
btn-text flex="~ gap-2" items-center p2 border="~ base rounded" bg-base ws-nowrap
|
||||
:class="isAdded(name) ? 'text-secondary hover:text-second bg-auto' : ''"
|
||||
type="button"
|
||||
role="switch"
|
||||
:aria-checked="isAdded(name)"
|
||||
@click="isAdded(name) ? remove(name) : append(name)"
|
||||
>
|
||||
<span :class="icon" />
|
||||
{{ label ? $t(label) : 'More menu' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div flex="~ col" gap-y-4 gap-x-2 py-1 sm="~ justify-end flex-row">
|
||||
<button
|
||||
btn-outline font-bold py2 full-w sm-wa flex="~ gap2 center"
|
||||
type="button"
|
||||
:disabled="selectedNavButtonNames.length === 0"
|
||||
:class="selectedNavButtonNames.length === 0 ? 'border-none' : undefined"
|
||||
@click="clear"
|
||||
>
|
||||
<span aria-hidden="true" class="block i-ri:delete-bin-line" />
|
||||
{{ $t('action.clear') }}
|
||||
</button>
|
||||
<button
|
||||
btn-outline font-bold py2 full-w sm-wa flex="~ gap2 center"
|
||||
type="reset"
|
||||
@click="reset"
|
||||
>
|
||||
<span aria-hidden="true" class="block i-ri:repeat-line" />
|
||||
{{ $t('action.reset') }}
|
||||
</button>
|
||||
<button
|
||||
btn-solid font-bold py2 full-w sm-wa flex="~ gap2 center"
|
||||
:disabled="!canSave"
|
||||
>
|
||||
<span aria-hidden="true" i-ri:save-2-fill />
|
||||
{{ $t('action.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
|
@ -27,17 +27,23 @@ const modes = [
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ gap4 wrap" w-full>
|
||||
<button
|
||||
v-for="{ icon, label, mode } in modes"
|
||||
:key="mode"
|
||||
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base ws-nowrap
|
||||
:tabindex="colorMode.preference === mode ? 0 : -1"
|
||||
:class="colorMode.preference === mode ? 'pointer-events-none' : 'filter-saturate-0'"
|
||||
@click="setColorMode(mode)"
|
||||
>
|
||||
<span :class="`${icon}`" />
|
||||
{{ $t(label) }}
|
||||
</button>
|
||||
</div>
|
||||
<section space-y-2>
|
||||
<h2 id="interface-cm" font-medium>
|
||||
{{ $t('settings.interface.color_mode') }}
|
||||
</h2>
|
||||
<div flex="~ gap4 wrap" w-full role="group" aria-labelledby="interface-cm">
|
||||
<button
|
||||
v-for="{ icon, label, mode } in modes"
|
||||
:key="mode"
|
||||
type="button"
|
||||
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base ws-nowrap
|
||||
:aria-pressed="colorMode.preference === mode ? 'true' : 'false'"
|
||||
:class="colorMode.preference === mode ? 'pointer-events-none' : 'filter-saturate-0'"
|
||||
@click="setColorMode(mode)"
|
||||
>
|
||||
<span :class="`${icon}`" />
|
||||
{{ $t(label) }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
|
|
@ -13,45 +13,63 @@ function setFontSize(e: Event) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div flex items-center space-x-4>
|
||||
<span text-xs text-secondary>Aa</span>
|
||||
<div flex-1 relative flex items-center>
|
||||
<input
|
||||
:value="sizes.indexOf(userSettings.fontSize)"
|
||||
:aria-valuetext="`${userSettings.fontSize}${userSettings.fontSize === DEFAULT_FONT_SIZE ? ` ${$t('settings.interface.default')}` : ''}`"
|
||||
:min="0"
|
||||
:max="sizes.length - 1"
|
||||
:step="1"
|
||||
type="range"
|
||||
focus:outline-none
|
||||
appearance-none bg-transparent
|
||||
w-full cursor-pointer
|
||||
@change="setFontSize"
|
||||
>
|
||||
<div flex items-center justify-between absolute w-full pointer-events-none>
|
||||
<div
|
||||
v-for="i in sizes.length" :key="i"
|
||||
h-3 w-3
|
||||
rounded-full bg-secondary-light
|
||||
relative
|
||||
<section space-y-2>
|
||||
<h2 id="interface-fs" font-medium>
|
||||
{{ $t('settings.interface.font_size') }}
|
||||
</h2>
|
||||
<div flex items-center space-x-4 select-settings>
|
||||
<span text-xs text-secondary>Aa</span>
|
||||
<div flex-1 relative flex items-center>
|
||||
<input
|
||||
aria-labelledby="interface-fs"
|
||||
:value="sizes.indexOf(userSettings.fontSize)"
|
||||
:aria-valuetext="`${userSettings.fontSize}${userSettings.fontSize === DEFAULT_FONT_SIZE ? ` ${$t('settings.interface.default')}` : ''}`"
|
||||
:min="0"
|
||||
:max="sizes.length - 1"
|
||||
:step="1"
|
||||
type="range"
|
||||
focus:outline-none
|
||||
appearance-none bg-transparent
|
||||
w-full cursor-pointer
|
||||
@change="setFontSize"
|
||||
>
|
||||
<div flex items-center justify-between absolute w-full pointer-events-none>
|
||||
<div
|
||||
v-if="(sizes.indexOf(userSettings.fontSize)) === i - 1"
|
||||
absolute rounded-full class="-top-1 -left-1"
|
||||
bg-primary h-5 w-5
|
||||
/>
|
||||
v-for="i in sizes.length" :key="i"
|
||||
class="container-marker"
|
||||
h-3 w-3
|
||||
rounded-full bg-secondary-light
|
||||
relative
|
||||
>
|
||||
<div
|
||||
v-if="(sizes.indexOf(userSettings.fontSize)) === i - 1"
|
||||
absolute rounded-full class="-top-1 -left-1"
|
||||
bg-primary h-5 w-5
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span text-xl text-secondary>Aa</span>
|
||||
</div>
|
||||
<span text-xl text-secondary>Aa</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
input:focus + div .container-marker:has(> div)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 2px solid var(--c-primary);
|
||||
border-radius: 50%;
|
||||
}
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
--at-apply: bg-secondary-light rounded-full h1 op60;
|
||||
}
|
||||
input[type=range]:focus:-webkit-slider-runnable-track {
|
||||
input[type=range]:focus::-webkit-slider-runnable-track {
|
||||
--at-apply: outline-2 outline-red;
|
||||
}
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
|
|
|
@ -12,18 +12,25 @@ function updateTheme(theme: ThemeColors) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ gap4 wrap" p2>
|
||||
<button
|
||||
v-for="[key, theme] in themes" :key="key"
|
||||
:style="{
|
||||
'background': key,
|
||||
'--local-ring-color': key,
|
||||
}"
|
||||
:class="currentTheme === theme['--theme-color-name'] ? 'ring-2' : 'scale-90'"
|
||||
:title="theme['--theme-color-name']"
|
||||
w-8 h-8 rounded-full transition-all
|
||||
ring="$local-ring-color offset-3 offset-$c-bg-base"
|
||||
@click="updateTheme(theme)"
|
||||
/>
|
||||
</div>
|
||||
<section space-y-2>
|
||||
<h2 id="interface-tc" font-medium>
|
||||
{{ $t('settings.interface.theme_color') }}
|
||||
</h2>
|
||||
<div flex="~ gap4 wrap" p2 role="group" aria-labelledby="interface-tc">
|
||||
<button
|
||||
v-for="[key, theme] in themes" :key="key"
|
||||
:style="{
|
||||
'background': key,
|
||||
'--local-ring-color': key,
|
||||
}"
|
||||
type="button"
|
||||
:class="currentTheme === theme['--theme-color-name'] ? 'ring-2' : 'scale-90'"
|
||||
:aria-pressed="currentTheme === theme['--theme-color-name'] ? 'true' : 'false'"
|
||||
:title="theme['--theme-color-name']"
|
||||
w-8 h-8 rounded-full transition-all
|
||||
ring="$local-ring-color offset-3 offset-$c-bg-base"
|
||||
@click="updateTheme(theme)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
|
|
@ -211,7 +211,7 @@ watch(shouldLoadAttachment, () => {
|
|||
rounded-lg
|
||||
h-full
|
||||
w-full
|
||||
aria-label="Open image preview dialog"
|
||||
:aria-label="$t('action.open_image_preview_dialog')"
|
||||
relative
|
||||
@click="!shouldLoadAttachment ? loadAttachment() : openMediaPreview(attachments ? attachments : [attachment], attachments?.indexOf(attachment) || 0)"
|
||||
>
|
||||
|
|
|
@ -44,7 +44,9 @@ const allowEmbeddedMedia = computed(() => status.card?.html && embeddedMediaPref
|
|||
<StatusBody v-if="(!isFiltered && isSensitiveNonSpoiler) || hideAllMedia" :status="status" :newer="newer" :with-action="!isDetails" :class="isDetails ? 'text-xl' : ''" />
|
||||
<StatusSpoiler :enabled="hasSpoilerOrSensitiveMedia || isFiltered" :filter="isFiltered" :sensitive-non-spoiler="isSensitiveNonSpoiler || hideAllMedia" :is-d-m="isDM">
|
||||
<template v-if="spoilerTextPresent" #spoiler>
|
||||
<p>{{ status.spoilerText }}</p>
|
||||
<p>
|
||||
<ContentRich :content="status.spoilerText" :emojis="status.emojis" :markdown="false" />
|
||||
</p>
|
||||
</template>
|
||||
<template v-else-if="filterPhrase" #spoiler>
|
||||
<p>{{ `${$t('status.filter_hidden_phrase')}: ${filterPhrase}` }}</p>
|
||||
|
|
|
@ -6,10 +6,20 @@ const { status, isPreview = false } = defineProps<{
|
|||
fullSize?: boolean
|
||||
isPreview?: boolean
|
||||
}>()
|
||||
|
||||
const gridColumnNumber = computed(() => {
|
||||
const num = status.mediaAttachments.length
|
||||
if (num <= 1)
|
||||
return 1
|
||||
else if (num <= 4)
|
||||
return 2
|
||||
else
|
||||
return 3
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="status-media-container" :class="`status-media-container-${status.mediaAttachments.length}`">
|
||||
<div class="status-media-container">
|
||||
<template v-for="attachment of status.mediaAttachments" :key="attachment.id">
|
||||
<StatusAttachment
|
||||
:attachment="attachment"
|
||||
|
@ -25,25 +35,12 @@ const { status, isPreview = false } = defineProps<{
|
|||
|
||||
<style lang="postcss">
|
||||
.status-media-container {
|
||||
--grid-cols: v-bind(gridColumnNumber);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--grid-cols, 1), 1fr);
|
||||
--at-apply: gap-2;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.status-media-container-1 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.status-media-container-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.status-media-container-3 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.status-media-container-4 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,7 +10,8 @@ function reorderAndFilter(items: mastodon.v1.Status[]) {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<PublishWidget draft-key="home" border="b base" />
|
||||
<PublishWidgetList draft-key="home" />
|
||||
<div h="1px" w-auto bg-border mb-3 />
|
||||
<TimelinePaginator v-bind="{ paginator, stream }" :preprocess="reorderAndFilter" context="home" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -20,7 +20,7 @@ function clickUser(user: UserLogin) {
|
|||
<button
|
||||
flex rounded
|
||||
cursor-pointer
|
||||
aria-label="Switch user"
|
||||
:aria-label="$t('action.switch_account')"
|
||||
:class="user.account.acct === currentUser?.account.acct ? '' : 'op25 grayscale'"
|
||||
hover="filter-none op100"
|
||||
@click="clickUser(user)"
|
||||
|
|
|
@ -36,7 +36,7 @@ function processSignIn() {
|
|||
<button
|
||||
flex rounded px4 py3 text-left
|
||||
hover:bg-active cursor-pointer transition-100
|
||||
aria-label="Switch user"
|
||||
:aria-label="$t('action.switch_account')"
|
||||
@click="clickUser(user)"
|
||||
>
|
||||
<AccountInfo :account="user.account" :hover-card="false" square />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { mastodon } from 'masto'
|
||||
import type { ConfirmDialogChoice, ConfirmDialogOptions, Draft, ErrorDialogData } from '~/types'
|
||||
import type { ConfirmDialogChoice, ConfirmDialogOptions, DraftItem, ErrorDialogData } from '~/types'
|
||||
import { STORAGE_KEY_FIRST_VISIT } from '~/constants'
|
||||
|
||||
export const confirmDialogChoice = ref<ConfirmDialogChoice>()
|
||||
|
@ -49,7 +49,7 @@ export async function openConfirmDialog(label: ConfirmDialogOptions | string): P
|
|||
return confirmDialogChoice.value!
|
||||
}
|
||||
|
||||
export async function openPublishDialog(draftKey = 'dialog', draft?: Draft, overwrite = false): Promise<void> {
|
||||
export async function openPublishDialog(draftKey = 'dialog', draft?: DraftItem, overwrite = false): Promise<void> {
|
||||
dialogDraftKey.value = draftKey
|
||||
|
||||
if (draft) {
|
||||
|
@ -65,7 +65,7 @@ export async function openPublishDialog(draftKey = 'dialog', draft?: Draft, over
|
|||
}
|
||||
|
||||
if (overwrite || !currentUserDrafts.value[draftKey])
|
||||
currentUserDrafts.value[draftKey] = draft
|
||||
currentUserDrafts.value[draftKey] = [draft]
|
||||
}
|
||||
isPublishDialogOpen.value = true
|
||||
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import { fileOpen } from 'browser-fs-access'
|
||||
import type { Ref } from 'vue'
|
||||
import type { mastodon } from 'masto'
|
||||
import type { UseDraft } from './statusDrafts'
|
||||
import type { Draft } from '~~/types'
|
||||
import type { DraftItem } from '~~/types'
|
||||
|
||||
export function usePublish(options: {
|
||||
draftState: UseDraft
|
||||
draftItem: Ref<DraftItem>
|
||||
expanded: Ref<boolean>
|
||||
isUploading: Ref<boolean>
|
||||
initialDraft: Ref<() => Draft>
|
||||
isPartOfThread: boolean
|
||||
initialDraft: () => DraftItem
|
||||
}) {
|
||||
const { draft, isEmpty } = options.draftState
|
||||
const { draftItem } = options
|
||||
|
||||
const isEmpty = computed(() => isEmptyDraft([draftItem.value]))
|
||||
|
||||
const { client } = useMasto()
|
||||
const settings = useUserSettings()
|
||||
|
||||
|
@ -22,18 +25,18 @@ export function usePublish(options: {
|
|||
|
||||
const publishSpoilerText = computed({
|
||||
get() {
|
||||
return draft.value.params.sensitive ? draft.value.params.spoilerText : ''
|
||||
return draftItem.value.params.sensitive ? draftItem.value.params.spoilerText : ''
|
||||
},
|
||||
set(val) {
|
||||
if (!draft.value.params.sensitive)
|
||||
if (!draftItem.value.params.sensitive)
|
||||
return
|
||||
draft.value.params.spoilerText = val
|
||||
draftItem.value.params.spoilerText = val
|
||||
},
|
||||
})
|
||||
|
||||
const shouldExpanded = computed(() => options.expanded.value || isExpanded.value || !isEmpty.value)
|
||||
const isPublishDisabled = computed(() => {
|
||||
const { params, attachments } = draft.value
|
||||
const { params, attachments } = draftItem.value
|
||||
const firstEmptyInputIndex = params.poll?.options.findIndex(option => option.trim().length === 0)
|
||||
return isEmpty.value
|
||||
|| options.isUploading.value
|
||||
|
@ -54,7 +57,7 @@ export function usePublish(options: {
|
|||
))
|
||||
})
|
||||
|
||||
watch(draft, () => {
|
||||
watch(draftItem, () => {
|
||||
if (failedMessages.value.length > 0)
|
||||
failedMessages.value.length = 0
|
||||
}, { deep: true })
|
||||
|
@ -63,14 +66,14 @@ export function usePublish(options: {
|
|||
if (isPublishDisabled.value)
|
||||
return
|
||||
|
||||
let content = htmlToText(draft.value.params.status || '')
|
||||
if (draft.value.mentions?.length)
|
||||
content = `${draft.value.mentions.map(i => `@${i}`).join(' ')} ${content}`
|
||||
let content = htmlToText(draftItem.value.params.status || '')
|
||||
if (draftItem.value.mentions?.length)
|
||||
content = `${draftItem.value.mentions.map(i => `@${i}`).join(' ')} ${content}`
|
||||
|
||||
let poll
|
||||
|
||||
if (draft.value.params.poll) {
|
||||
let options = draft.value.params.poll.options
|
||||
if (draftItem.value.params.poll) {
|
||||
let options = draftItem.value.params.poll.options
|
||||
|
||||
if (currentInstance.value?.configuration !== undefined
|
||||
&& (
|
||||
|
@ -80,15 +83,15 @@ export function usePublish(options: {
|
|||
)
|
||||
options = options.slice(0, options.length - 1)
|
||||
|
||||
poll = { ...draft.value.params.poll, options }
|
||||
poll = { ...draftItem.value.params.poll, options }
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...draft.value.params,
|
||||
...draftItem.value.params,
|
||||
spoilerText: publishSpoilerText.value,
|
||||
status: content,
|
||||
mediaIds: draft.value.attachments.map(a => a.id),
|
||||
language: draft.value.params.language || preferredLanguage.value,
|
||||
mediaIds: draftItem.value.attachments.map(a => a.id),
|
||||
language: draftItem.value.params.language || preferredLanguage.value,
|
||||
poll,
|
||||
...(isGlitchEdition.value ? { 'content-type': 'text/markdown' } : {}),
|
||||
} as mastodon.rest.v1.CreateStatusParams
|
||||
|
@ -96,7 +99,7 @@ export function usePublish(options: {
|
|||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info({
|
||||
raw: draft.value.params.status,
|
||||
raw: draftItem.value.params.status,
|
||||
...payload,
|
||||
})
|
||||
// eslint-disable-next-line no-alert
|
||||
|
@ -109,23 +112,23 @@ export function usePublish(options: {
|
|||
isSending.value = true
|
||||
|
||||
let status: mastodon.v1.Status
|
||||
if (!draft.value.editingStatus) {
|
||||
if (!draftItem.value.editingStatus) {
|
||||
status = await client.value.v1.statuses.create(payload)
|
||||
}
|
||||
|
||||
else {
|
||||
status = await client.value.v1.statuses.$select(draft.value.editingStatus.id).update({
|
||||
status = await client.value.v1.statuses.$select(draftItem.value.editingStatus.id).update({
|
||||
...payload,
|
||||
mediaAttributes: draft.value.attachments.map(media => ({
|
||||
mediaAttributes: draftItem.value.attachments.map(media => ({
|
||||
id: media.id,
|
||||
description: media.description,
|
||||
})),
|
||||
})
|
||||
}
|
||||
if (draft.value.params.inReplyToId)
|
||||
if (draftItem.value.params.inReplyToId && !options.isPartOfThread)
|
||||
navigateToStatus({ status })
|
||||
|
||||
draft.value = options.initialDraft.value()
|
||||
draftItem.value = options.initialDraft()
|
||||
|
||||
return status
|
||||
}
|
||||
|
@ -152,7 +155,7 @@ export function usePublish(options: {
|
|||
|
||||
export type MediaAttachmentUploadError = [filename: string, message: string]
|
||||
|
||||
export function useUploadMediaAttachment(draft: Ref<Draft>) {
|
||||
export function useUploadMediaAttachment(draft: Ref<DraftItem>) {
|
||||
const { client } = useMasto()
|
||||
const { t } = useI18n()
|
||||
const { formatFileSize } = useFileSizeFormatter()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { mastodon } from 'masto'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { STORAGE_KEY_DRAFTS } from '~/constants'
|
||||
import type { Draft, DraftMap } from '~/types'
|
||||
import type { DraftItem, DraftMap } from '~/types'
|
||||
import type { Mutable } from '~/types/utils'
|
||||
|
||||
export const currentUserDrafts = (import.meta.server || process.test)
|
||||
|
@ -25,7 +25,7 @@ function getDefaultVisibility(currentVisibility: mastodon.v1.StatusVisibility) {
|
|||
: preferredVisibility
|
||||
}
|
||||
|
||||
export function getDefaultDraft(options: Partial<Mutable<mastodon.rest.v1.CreateStatusParams> & Omit<Draft, 'params'>> = {}): Draft {
|
||||
export function getDefaultDraftItem(options: Partial<Mutable<mastodon.rest.v1.CreateStatusParams> & Omit<DraftItem, 'params'>> = {}): DraftItem {
|
||||
const {
|
||||
attachments = [],
|
||||
initialText = '',
|
||||
|
@ -56,7 +56,7 @@ export function getDefaultDraft(options: Partial<Mutable<mastodon.rest.v1.Create
|
|||
}
|
||||
}
|
||||
|
||||
export async function getDraftFromStatus(status: mastodon.v1.Status): Promise<Draft> {
|
||||
export async function getDraftFromStatus(status: mastodon.v1.Status): Promise<DraftItem> {
|
||||
const info = {
|
||||
status: await convertMastodonHTML(status.content),
|
||||
visibility: status.visibility,
|
||||
|
@ -67,7 +67,7 @@ export async function getDraftFromStatus(status: mastodon.v1.Status): Promise<Dr
|
|||
inReplyToId: status.inReplyToId,
|
||||
}
|
||||
|
||||
return getDefaultDraft((status.mediaAttachments !== undefined && status.mediaAttachments.length > 0)
|
||||
return getDefaultDraftItem((status.mediaAttachments !== undefined && status.mediaAttachments.length > 0)
|
||||
? { ...info, mediaIds: status.mediaAttachments.map(att => att.id) }
|
||||
: {
|
||||
...info,
|
||||
|
@ -99,7 +99,7 @@ export function getReplyDraft(status: mastodon.v1.Status) {
|
|||
return {
|
||||
key: `reply-${status.id}`,
|
||||
draft: () => {
|
||||
return getDefaultDraft({
|
||||
return getDefaultDraftItem({
|
||||
initialText: '',
|
||||
inReplyToId: status!.id,
|
||||
sensitive: status.sensitive,
|
||||
|
@ -112,40 +112,51 @@ export function getReplyDraft(status: mastodon.v1.Status) {
|
|||
}
|
||||
}
|
||||
|
||||
export function isEmptyDraft(draft: Draft | null | undefined) {
|
||||
if (!draft)
|
||||
export function isEmptyDraft(drafts: Array<DraftItem> | DraftItem | null | undefined) {
|
||||
if (!drafts)
|
||||
return true
|
||||
const { params, attachments } = draft
|
||||
const status = params.status || ''
|
||||
const text = htmlToText(status).trim().replace(/^(@\S+\s?)+/, '').replaceAll(/```/g, '').trim()
|
||||
|
||||
return (text.length === 0)
|
||||
&& attachments.length === 0
|
||||
const draftsArray: Array<DraftItem> = Array.isArray(drafts) ? drafts : [drafts]
|
||||
|
||||
if (draftsArray.length === 0)
|
||||
return true
|
||||
|
||||
const anyDraftHasContent = draftsArray.some((draft) => {
|
||||
const { params, attachments } = draft
|
||||
const status = params.status ?? ''
|
||||
const text = htmlToText(status).trim().replace(/^(@\S+\s?)+/, '').replaceAll(/```/g, '').trim()
|
||||
|
||||
return (text.length > 0)
|
||||
|| (attachments.length > 0)
|
||||
})
|
||||
|
||||
return !anyDraftHasContent
|
||||
}
|
||||
|
||||
export interface UseDraft {
|
||||
draft: Ref<Draft>
|
||||
isEmpty: ComputedRef<boolean>
|
||||
draftItems: Ref<Array<DraftItem>>
|
||||
isEmpty: ComputedRef<boolean> | Ref<boolean>
|
||||
}
|
||||
|
||||
export function useDraft(
|
||||
draftKey?: string,
|
||||
initial: () => Draft = () => getDefaultDraft({}),
|
||||
draftKey: string,
|
||||
initial: () => DraftItem = () => getDefaultDraftItem({}),
|
||||
): UseDraft {
|
||||
const draft = draftKey
|
||||
? computed({
|
||||
get() {
|
||||
if (!currentUserDrafts.value[draftKey])
|
||||
currentUserDrafts.value[draftKey] = initial()
|
||||
return currentUserDrafts.value[draftKey]
|
||||
},
|
||||
set(val) {
|
||||
currentUserDrafts.value[draftKey] = val
|
||||
},
|
||||
})
|
||||
: ref(initial())
|
||||
const draftItems = computed({
|
||||
get() {
|
||||
if (!currentUserDrafts.value[draftKey])
|
||||
currentUserDrafts.value[draftKey] = [initial()]
|
||||
const drafts = currentUserDrafts.value[draftKey]
|
||||
if (Array.isArray(drafts))
|
||||
return drafts
|
||||
return [drafts]
|
||||
},
|
||||
set(val) {
|
||||
currentUserDrafts.value[draftKey] = val
|
||||
},
|
||||
})
|
||||
|
||||
const isEmpty = computed(() => isEmptyDraft(draft.value))
|
||||
const isEmpty = computed(() => isEmptyDraft(draftItems.value))
|
||||
|
||||
onUnmounted(async () => {
|
||||
// Remove draft if it's empty
|
||||
|
@ -155,17 +166,17 @@ export function useDraft(
|
|||
}
|
||||
})
|
||||
|
||||
return { draft, isEmpty }
|
||||
return { draftItems, isEmpty }
|
||||
}
|
||||
|
||||
export function mentionUser(account: mastodon.v1.Account) {
|
||||
openPublishDialog('dialog', getDefaultDraft({
|
||||
openPublishDialog('dialog', getDefaultDraftItem({
|
||||
status: `@${account.acct} `,
|
||||
}))
|
||||
}
|
||||
|
||||
export function directMessageUser(account: mastodon.v1.Account) {
|
||||
openPublishDialog('dialog', getDefaultDraft({
|
||||
openPublishDialog('dialog', getDefaultDraftItem({
|
||||
status: `@${account.acct} `,
|
||||
visibility: 'direct',
|
||||
}))
|
||||
|
@ -175,7 +186,7 @@ export function clearEmptyDrafts() {
|
|||
for (const key in currentUserDrafts.value) {
|
||||
if (builtinDraftKeys.includes(key) && !isEmptyDraft(currentUserDrafts.value[key]))
|
||||
continue
|
||||
if (!currentUserDrafts.value[key].params || isEmptyDraft(currentUserDrafts.value[key]))
|
||||
if (isEmptyDraft(currentUserDrafts.value[key]))
|
||||
delete currentUserDrafts.value[key]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ export type OldFontSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|||
|
||||
export type ColorMode = 'light' | 'dark' | 'system'
|
||||
|
||||
export type NavButtonName = 'home' | 'search' | 'notification' | 'mention' | 'explore' | 'local' | 'federated' | 'moreMenu'
|
||||
|
||||
export interface PreferencesSettings {
|
||||
hideAltIndicatorOnPosts: boolean
|
||||
hideGifIndicatorOnPosts: boolean
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import type { mastodon } from 'masto'
|
||||
import type { DraftItem } from '~/types'
|
||||
|
||||
const maxThreadLength = 99
|
||||
|
||||
export function useThreadComposer(draftKey: string, initial?: () => DraftItem) {
|
||||
const { draftItems } = useDraft(draftKey, initial)
|
||||
|
||||
/**
|
||||
* Whether the thread is active (has more than one item)
|
||||
*/
|
||||
const threadIsActive = computed<boolean>(() => draftItems.value.length > 1)
|
||||
|
||||
/**
|
||||
* Add an item to the thread
|
||||
*/
|
||||
function addThreadItem() {
|
||||
if (draftItems.value.length >= maxThreadLength) {
|
||||
// TODO handle with error message that tells the user what's wrong
|
||||
// For now just fail silently without breaking anything
|
||||
return
|
||||
}
|
||||
|
||||
const lastItem = draftItems.value[draftItems.value.length - 1]
|
||||
draftItems.value.push(getDefaultDraftItem({
|
||||
language: lastItem.params.language,
|
||||
sensitive: lastItem.params.sensitive,
|
||||
spoilerText: lastItem.params.spoilerText,
|
||||
visibility: lastItem.params.visibility,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param index index of the draft to remove from the thread
|
||||
*/
|
||||
function removeThreadItem(index: number) {
|
||||
draftItems.value.splice(index, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish all items in the thread in order
|
||||
*/
|
||||
async function publishThread() {
|
||||
const allFailedMessages: Array<string> = []
|
||||
const isAReplyThread = Boolean(draftItems.value[0].params.inReplyToId)
|
||||
|
||||
let lastPublishedStatus: mastodon.v1.Status | null = null
|
||||
let amountPublished = 0
|
||||
for (const draftItem of draftItems.value) {
|
||||
if (lastPublishedStatus)
|
||||
draftItem.params.inReplyToId = lastPublishedStatus.id
|
||||
|
||||
const { publishDraft, failedMessages } = usePublish({
|
||||
draftItem: ref(draftItem),
|
||||
expanded: computed(() => true),
|
||||
isUploading: ref(false),
|
||||
initialDraft: () => draftItem,
|
||||
isPartOfThread: true,
|
||||
})
|
||||
|
||||
const status = await publishDraft()
|
||||
if (status) {
|
||||
lastPublishedStatus = status
|
||||
amountPublished++
|
||||
}
|
||||
else {
|
||||
allFailedMessages.push(...failedMessages.value)
|
||||
// Stop publishing if one fails
|
||||
break
|
||||
}
|
||||
}
|
||||
// Remove all published items from the thread
|
||||
draftItems.value.splice(0, amountPublished)
|
||||
|
||||
// If we have errors, return them
|
||||
if (allFailedMessages.length > 0)
|
||||
return allFailedMessages
|
||||
|
||||
// If the thread was a reply and all items were published, jump to it
|
||||
if (isAReplyThread && lastPublishedStatus && draftItems.value.length === 0)
|
||||
navigateToStatus({ status: lastPublishedStatus })
|
||||
|
||||
return lastPublishedStatus
|
||||
}
|
||||
|
||||
return {
|
||||
threadItems: draftItems,
|
||||
threadIsActive,
|
||||
addThreadItem,
|
||||
removeThreadItem,
|
||||
publishThread,
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ export const countryLocaleVariants: Record<string, (LocaleObjectData & { country
|
|||
en: [
|
||||
// en.json contains en-US translations
|
||||
{ country: true, code: 'en-US', name: 'English (US)' },
|
||||
{ code: 'en-CA', name: 'English (Canada)' },
|
||||
{ code: 'en-GB', name: 'English (UK)' },
|
||||
],
|
||||
ca: [
|
||||
|
|
|
@ -24,6 +24,7 @@ export const STORAGE_KEY_NOTIFICATION_POLICY = 'elk-notification-policy'
|
|||
export const STORAGE_KEY_PWA_HIDE_INSTALL = 'elk-pwa-hide-install'
|
||||
export const STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE = 'elk-last-accessed-notification-route'
|
||||
export const STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE = 'elk-last-accessed-explore-route'
|
||||
export const STORAGE_KEY_BOTTOM_NAV_BUTTONS = 'elk-bottom-nav-buttons'
|
||||
|
||||
export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
"next": "Nächster",
|
||||
"prev": "Vorheriger",
|
||||
"publish": "Veröffentlichen",
|
||||
"publish_thread": "Thread veröffentlichen",
|
||||
"reply": "Antworten",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Zurücksetzen",
|
||||
|
@ -668,6 +669,7 @@
|
|||
"add_emojis": "Emojis hinzufügen",
|
||||
"add_media": "Bilder, ein Video oder eine Audiodatei hinzufügen",
|
||||
"add_publishable_content": "Füge Inhalte zum Veröffentlichen hinzu",
|
||||
"add_thread_item": "Weiteres Element zum Thread hinzufügen",
|
||||
"change_content_visibility": "Sichtbarkeit von Inhalten ändern",
|
||||
"change_language": "Sprache ändern",
|
||||
"emoji": "Emoji",
|
||||
|
@ -677,6 +679,8 @@
|
|||
"open_editor_tools": "Bearbeitungswerkzeuge",
|
||||
"pick_an_icon": "Wähle ein Symbol",
|
||||
"publish_failed": "Schließe fehlgeschlagene Nachrichten oben im Editor, um Beiträge erneut zu veröffentlichen",
|
||||
"remove_thread_item": "Element aus dem Thread entfernen",
|
||||
"start_thread": "Thread starten",
|
||||
"toggle_bold": "Fett darstellen umschalten",
|
||||
"toggle_code_block": "Codeblock umschalten",
|
||||
"toggle_italic": "Kursiv umschalten"
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"account": {
|
||||
"favourites": "Favourites"
|
||||
},
|
||||
"action": {
|
||||
"favourite": "Favourite",
|
||||
"favourited": "Favourited"
|
||||
},
|
||||
"magic_keys": {
|
||||
"groups": {
|
||||
"actions": {
|
||||
"favourite": "Favourite"
|
||||
},
|
||||
"navigation": {
|
||||
"go_to_favourites": "Favourites"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"show_favourited_and_boosted_by": "Show who favourited and boosted"
|
||||
},
|
||||
"nav": {
|
||||
"favourites": "Favourites"
|
||||
},
|
||||
"notification": {
|
||||
"favourited_post": "favourited your post"
|
||||
},
|
||||
"settings": {
|
||||
"interface": {
|
||||
"bottom_nav_instructions": "Choose your favourite navigation buttons up to five for the bottom navigation. Must include the \"More menu\" button.",
|
||||
"color_mode": "Colour Mode",
|
||||
"theme_color": "Theme Colour"
|
||||
},
|
||||
"notifications": {
|
||||
"push_notifications": {
|
||||
"alerts": {
|
||||
"favourite": "Favourites"
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"hide_favorite_count": "Hide favourite count",
|
||||
"use_star_favorite_icon": "Use star favourite icon"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"favourited_by": "Favourited By"
|
||||
},
|
||||
"tab": {
|
||||
"notifications_favourite": "Favourite"
|
||||
},
|
||||
"user": {
|
||||
"sign_in_desc": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.",
|
||||
"single_instance_sign_in_desc": "Sign in to follow profiles or hashtags, favourite, share and reply to posts."
|
||||
}
|
||||
}
|
|
@ -1,11 +1,23 @@
|
|||
{
|
||||
"account": {
|
||||
"authorize": "Authorise to follow",
|
||||
"authorized": "You have Authorised the request",
|
||||
"favourites": "Favourites"
|
||||
},
|
||||
"action": {
|
||||
"favourite": "Favourite",
|
||||
"favourited": "Favourited"
|
||||
},
|
||||
"magic_keys": {
|
||||
"groups": {
|
||||
"actions": {
|
||||
"favourite": "Favourite"
|
||||
},
|
||||
"navigation": {
|
||||
"go_to_favourites": "Favourites"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"show_favourited_and_boosted_by": "Show who favourited and boosted"
|
||||
},
|
||||
|
@ -15,7 +27,41 @@
|
|||
"notification": {
|
||||
"favourited_post": "favourited your post"
|
||||
},
|
||||
"report": {
|
||||
"forward_question": "Do you want to send an anonymised copy of this report to that server as well?"
|
||||
},
|
||||
"settings": {
|
||||
"interface": {
|
||||
"bottom_nav_instructions": "Choose your favourite navigation buttons up to five for the bottom navigation. Must include the \"More menu\" button.",
|
||||
"color_mode": "Colour Mode",
|
||||
"theme_color": "Theme Colour"
|
||||
},
|
||||
"notifications": {
|
||||
"push_notifications": {
|
||||
"alerts": {
|
||||
"favourite": "Favourites"
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"hide_favorite_count": "Hide favourite count",
|
||||
"optimize_for_low_performance_device": "Optimise for low performance device",
|
||||
"use_star_favorite_icon": "Use star favourite icon"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"favourited_by": "Favourited By"
|
||||
},
|
||||
"tab": {
|
||||
"notifications_favourite": "Favourite"
|
||||
},
|
||||
"tooltip": {
|
||||
"explore_links_intro": "These news stories are being talked about by people on this and other servers of the decentralised network right now.",
|
||||
"explore_posts_intro": "These posts from this and other servers in the decentralised network are gaining traction on this server right now.",
|
||||
"explore_tags_intro": "These hashtags are gaining traction among people on this and other servers of the decentralised network right now."
|
||||
},
|
||||
"user": {
|
||||
"sign_in_desc": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server."
|
||||
"sign_in_desc": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.",
|
||||
"single_instance_sign_in_desc": "Sign in to follow profiles or hashtags, favourite, share and reply to posts."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
"boost": "Boost",
|
||||
"boost_count": "{0}",
|
||||
"boosted": "Boosted",
|
||||
"clear": "Clear",
|
||||
"clear_publish_failed": "Clear publish errors",
|
||||
"clear_save_failed": "Clear save errors",
|
||||
"clear_upload_failed": "Clear file upload errors",
|
||||
|
@ -72,8 +73,10 @@
|
|||
"favourited": "Favorited",
|
||||
"more": "More",
|
||||
"next": "Next",
|
||||
"open_image_preview_dialog": "Open image preview dialog",
|
||||
"prev": "Prev",
|
||||
"publish": "Publish",
|
||||
"publish_thread": "Publish thread",
|
||||
"reply": "Reply",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Reset",
|
||||
|
@ -220,6 +223,7 @@
|
|||
"error": "There was an error while creating the list",
|
||||
"error_prefix": "Error: ",
|
||||
"list_title_placeholder": "List title",
|
||||
"manage": "Manage lists",
|
||||
"modify_account": "Modify lists with account",
|
||||
"remove_account": "Remove account from list",
|
||||
"save": "Save changes"
|
||||
|
@ -258,6 +262,9 @@
|
|||
"title": "Navigation"
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"disable_virtual_scrolling": "Disable the experimental \"Virtual Scrolling\" setting to use this shortcut."
|
||||
},
|
||||
"sequence_then": "then"
|
||||
},
|
||||
"menu": {
|
||||
|
@ -315,6 +322,7 @@
|
|||
"list": "List",
|
||||
"lists": "Lists",
|
||||
"local": "Local",
|
||||
"more_menu": "More menu",
|
||||
"muted_users": "Muted users",
|
||||
"notifications": "Notifications",
|
||||
"privacy": "Privacy",
|
||||
|
@ -449,6 +457,8 @@
|
|||
"label": "Account settings"
|
||||
},
|
||||
"interface": {
|
||||
"bottom_nav": "Bottom Navigation",
|
||||
"bottom_nav_instructions": "Choose your favorite navigation buttons up to five for the bottom navigation. Must include the \"More menu\" button.",
|
||||
"color_mode": "Color Mode",
|
||||
"dark_mode": "Dark",
|
||||
"default": " (default)",
|
||||
|
@ -656,7 +666,7 @@
|
|||
"sign_up": "Sign-Up"
|
||||
},
|
||||
"notifications_all": "All",
|
||||
"notifications_favourite": "Favourite",
|
||||
"notifications_favourite": "Favorite",
|
||||
"notifications_follow": "Follow",
|
||||
"notifications_follow_request": "Follow request",
|
||||
"notifications_mention": "Mentions",
|
||||
|
@ -718,6 +728,7 @@
|
|||
"add_emojis": "Add emojis",
|
||||
"add_media": "Add images, a video or an audio file",
|
||||
"add_publishable_content": "Add content to publish",
|
||||
"add_thread_item": "Add item to thread",
|
||||
"change_content_visibility": "Change content visibility",
|
||||
"change_language": "Change language",
|
||||
"emoji": "Emoji",
|
||||
|
@ -727,6 +738,8 @@
|
|||
"open_editor_tools": "Editor tools",
|
||||
"pick_an_icon": "Pick an icon",
|
||||
"publish_failed": "Close failed messages at the top of editor to republish posts",
|
||||
"remove_thread_item": "Remove item from thread",
|
||||
"start_thread": "Start thread",
|
||||
"toggle_bold": "Toggle bold",
|
||||
"toggle_code_block": "Toggle code block",
|
||||
"toggle_italic": "Toggle italic"
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
"boost": "Retootear",
|
||||
"boost_count": "{0}",
|
||||
"boosted": "Retooteado",
|
||||
"clear": "Limpiar",
|
||||
"clear_publish_failed": "Limpiar errores de publicación",
|
||||
"clear_save_failed": "Limpiar errores al guardar",
|
||||
"clear_upload_failed": "Limpiar errores de subida de archivos",
|
||||
|
@ -74,6 +75,7 @@
|
|||
"next": "Siguiente",
|
||||
"prev": "Anterior",
|
||||
"publish": "Publicar",
|
||||
"publish_thread": "Publicar hilo",
|
||||
"reply": "Responder",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Reestablecer",
|
||||
|
@ -215,11 +217,12 @@
|
|||
"create": "Crear",
|
||||
"delete": "Eliminar esta lista",
|
||||
"delete_error": "Se produjo un error eliminando la lista",
|
||||
"edit": "Ediar esta lista",
|
||||
"edit": "Editar esta lista",
|
||||
"edit_error": "Se produjo un error modificando la lista",
|
||||
"error": "Se produjo un error creando la lista",
|
||||
"error_prefix": "Error: ",
|
||||
"list_title_placeholder": "Título de la lista",
|
||||
"manage": "Administrar listas",
|
||||
"modify_account": "Modificar listas con cuenta",
|
||||
"remove_account": "Eliminar cuenta de la lista",
|
||||
"save": "Guardar"
|
||||
|
@ -315,6 +318,7 @@
|
|||
"list": "Lista",
|
||||
"lists": "Listas",
|
||||
"local": "Local",
|
||||
"more_menu": "Más opciones",
|
||||
"muted_users": "Usuarios silenciados",
|
||||
"notifications": "Notificaciones",
|
||||
"privacy": "Privacidad",
|
||||
|
@ -449,6 +453,8 @@
|
|||
"label": "Ajustes de cuenta"
|
||||
},
|
||||
"interface": {
|
||||
"bottom_nav": "Navegación inferior",
|
||||
"bottom_nav_instructions": "Elige tus botones de navegación favoritos, hasta cinco para la navegación inferior. Debes incluir el botón \"Más opciones\".",
|
||||
"color_mode": "Modos de color",
|
||||
"dark_mode": "Modo oscuro",
|
||||
"default": " (por defecto)",
|
||||
|
@ -718,6 +724,7 @@
|
|||
"add_emojis": "Agregar emojis",
|
||||
"add_media": "Añadir imágenes, vídeo o audio",
|
||||
"add_publishable_content": "Publicar contenido",
|
||||
"add_thread_item": "Agregar publicación al hilo",
|
||||
"change_content_visibility": "Cambiar visibilidad de contenido",
|
||||
"change_language": "Cambiar idioma",
|
||||
"emoji": "Emoji",
|
||||
|
@ -727,6 +734,8 @@
|
|||
"open_editor_tools": "Herramientas de edición",
|
||||
"pick_an_icon": "Selecciona un icono",
|
||||
"publish_failed": "Cierra los mensajes fallidos en la parte superior del editor para volver a publicar",
|
||||
"remove_thread_item": "Eliminar publicación del hilo",
|
||||
"start_thread": "Iniciar hilo",
|
||||
"toggle_bold": "Cambiar a negrita",
|
||||
"toggle_code_block": "Cambiar a bloque de código",
|
||||
"toggle_italic": "Cambiar a cursiva"
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
"boost": "Bultzatu",
|
||||
"boost_count": "{0}",
|
||||
"boosted": "Bultzatuta",
|
||||
"clear": "Garbitu",
|
||||
"clear_publish_failed": "Garbitu argitalpen erroreak",
|
||||
"clear_save_failed": "Garbitu gordetzerakoan gertatutako erroreak",
|
||||
"clear_upload_failed": "Garbitu fitxategi-igoeren erroreak",
|
||||
|
@ -74,6 +75,7 @@
|
|||
"next": "Hurrengoa",
|
||||
"prev": "Aurrekoa",
|
||||
"publish": "Argitaratu",
|
||||
"publish_thread": "Argitaratu haria",
|
||||
"reply": "Erantzun",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Berrezarri",
|
||||
|
@ -220,6 +222,7 @@
|
|||
"error": "Errorea gertatu da zerrenda sortzerakoan",
|
||||
"error_prefix": "Errorea: ",
|
||||
"list_title_placeholder": "Zerrendaren izena",
|
||||
"manage": "Kudeatu zerrendak",
|
||||
"modify_account": "Aldatu honako kontua duten zerrendak:",
|
||||
"remove_account": "Kendu kontua zerrendatik",
|
||||
"save": "Gorde aldaketak"
|
||||
|
@ -306,6 +309,7 @@
|
|||
"built_at": "Biltze-data: {0}",
|
||||
"compose": "Idatzi",
|
||||
"conversations": "Elkarrizketak",
|
||||
"docs": "Dokumentazioa",
|
||||
"explore": "Arakatu",
|
||||
"favourites": "Gogokoak",
|
||||
"federated": "Federatua",
|
||||
|
@ -314,6 +318,7 @@
|
|||
"list": "Zerrenda",
|
||||
"lists": "Zerrendak",
|
||||
"local": "Lokala",
|
||||
"more_menu": "Hobespen gehiago",
|
||||
"muted_users": "Mutututako erabiltzaileak",
|
||||
"notifications": "Jakinarazpenak",
|
||||
"privacy": "Pribatutasuna",
|
||||
|
@ -448,6 +453,8 @@
|
|||
"label": "Kontuaren ezarpenak"
|
||||
},
|
||||
"interface": {
|
||||
"bottom_nav": "Beheko nabigazioa",
|
||||
"bottom_nav_instructions": "Aukeratu botoi gogokoenak, bost gehienez, beheko nabigaziorako. \"Hobespen gehiago\" botoia barne izan behar du.",
|
||||
"color_mode": "Kolorea",
|
||||
"dark_mode": "Iluna",
|
||||
"default": " (defektuzkoa)",
|
||||
|
@ -541,6 +548,7 @@
|
|||
"hide_boost_count": "Ezkutatu bultzaden kopurua",
|
||||
"hide_favorite_count": "Ezkutatu gogokoen kopurua",
|
||||
"hide_follower_count": "Ezkutatu jarraitzaile kopurua",
|
||||
"hide_gif_indi_on_posts": "Ezkutatu GIF adierazlea bidalketetan",
|
||||
"hide_news": "Ezkutatu berriak",
|
||||
"hide_reply_count": "Ezkutatu erantzunen kopurua",
|
||||
"hide_tag_hover_card": "Ezkutatu traolen aurrebista-txartelak sagua gainetik pasatzean",
|
||||
|
@ -592,7 +600,11 @@
|
|||
},
|
||||
"state": {
|
||||
"attachments_exceed_server_limit": "Erantsitakoen kopuruak bidalketaren muga gainditu du.",
|
||||
"attachments_limit_audio_error": "Audioaren gehieneko neurria gainditu da: {0}",
|
||||
"attachments_limit_error": "Bidalketaren muga gainditu da",
|
||||
"attachments_limit_image_error": "Irudiaren gehieneko neurria gainditu da: {0}",
|
||||
"attachments_limit_unknown_error": "Fitxategiaren gehieneko neurria gainditu da: {0}",
|
||||
"attachments_limit_video_error": "Bideoaren gehieneko neurria gainditu da: {0}",
|
||||
"edited": "(Editatua)",
|
||||
"editing": "Editatzen",
|
||||
"loading": "Kargatzen…",
|
||||
|
@ -613,6 +625,7 @@
|
|||
"favourited_by": "Gogoko egin dute:",
|
||||
"filter_hidden_phrase": "Iragazia:",
|
||||
"filter_show_anyway": "Erakutsi edonola ere",
|
||||
"gif": "GIF",
|
||||
"img_alt": {
|
||||
"ALT": "ALT",
|
||||
"desc": "Deskribapena",
|
||||
|
@ -695,7 +708,7 @@
|
|||
"short_year_past": "{n}u",
|
||||
"week_future": "0 astetan|datorren astean|datozen {n} asteetan",
|
||||
"week_past": "duela 0 aste|aurreko astean|duela {n} aste",
|
||||
"year_future": "0 urtetan|datorren urtean|datozen {n} urteetan",
|
||||
"year_future": "0 urtetan|geurtz|datozen {n} urteetan",
|
||||
"year_past": "duela 0 urte|aurreko urtean|duela {n} urte"
|
||||
},
|
||||
"timeline": {
|
||||
|
@ -711,6 +724,7 @@
|
|||
"add_emojis": "Gehitu emojiak",
|
||||
"add_media": "Gehitu irudiak, bideoak edo audio fitxategi bat",
|
||||
"add_publishable_content": "Gehitu argitaratzeko edukia",
|
||||
"add_thread_item": "Gehitu elementua harira",
|
||||
"change_content_visibility": "Aldatu edukiaren ikusgaitasuna",
|
||||
"change_language": "Aldatu hizkuntza",
|
||||
"emoji": "Emojia",
|
||||
|
@ -720,6 +734,8 @@
|
|||
"open_editor_tools": "Editatzeko tresnak",
|
||||
"pick_an_icon": "Hautatu ikonoa",
|
||||
"publish_failed": "Itxi editorearen goikaldeko huts-egiteen mezuak bidalketa berrargitaratzeko",
|
||||
"remove_thread_item": "Kendu elementua haritik",
|
||||
"start_thread": "Hasi haria",
|
||||
"toggle_bold": "Lodia",
|
||||
"toggle_code_block": "Kodea",
|
||||
"toggle_italic": "Etzana"
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"bot": "BOT",
|
||||
"copy_account_name": "Fiók nevének másolása",
|
||||
"favourites": "Kedvencek",
|
||||
"follow": "Követők",
|
||||
"follow": "Követem",
|
||||
"follow_back": "Visszakövetés",
|
||||
"follow_requested": "Követés kérve",
|
||||
"followers": "Követők",
|
||||
|
@ -45,7 +45,7 @@
|
|||
"request_follow": "Követés kérése",
|
||||
"requested": "{0} kérte, hogy kövessen",
|
||||
"unblock": "Ne legyen blokkolva",
|
||||
"unfollow": "Nem követem tovább",
|
||||
"unfollow": "Nem követem",
|
||||
"unmute": "Némítás megszűntetve",
|
||||
"view_other_followers": "A másik instancekról származó követések nem feltétlenül lesznek megjelenítve.",
|
||||
"view_other_following": "A másik instancekról származó követők nem feltétlenül lesznek megjelenítve.",
|
||||
|
@ -58,7 +58,8 @@
|
|||
"boost": "Kiemel",
|
||||
"boost_count": "{0}",
|
||||
"boosted": "Kiemelt",
|
||||
"clear_publish_failed": "Publikálási hibák tisztítása",
|
||||
"clear": "Tisztítás",
|
||||
"clear_publish_failed": "Közzétételi hibák tisztítása",
|
||||
"clear_save_failed": "Mentési hibák tisztítása",
|
||||
"clear_upload_failed": "Fájl feltöltési hibák tisztítása",
|
||||
"close": "Bezár",
|
||||
|
@ -73,7 +74,8 @@
|
|||
"more": "Tovább",
|
||||
"next": "Következő",
|
||||
"prev": "Előző",
|
||||
"publish": "Publikál",
|
||||
"publish": "Közzététel",
|
||||
"publish_thread": "Szál közzététele",
|
||||
"reply": "Válaszol",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Visszaállít",
|
||||
|
@ -84,7 +86,7 @@
|
|||
"switch_account": "Fiók váltás",
|
||||
"vote": "Szavazás"
|
||||
},
|
||||
"app_desc_short": "A fürge Mastodon kliens",
|
||||
"app_desc_short": "Egy fürge Mastodon kliens",
|
||||
"app_logo": "Elk Logó",
|
||||
"app_name": "Elk",
|
||||
"attachment": {
|
||||
|
@ -165,9 +167,9 @@
|
|||
},
|
||||
"unfollow": {
|
||||
"cancel": "Mégsem",
|
||||
"confirm": "Követés leállítása",
|
||||
"confirm": "Nem követem",
|
||||
"description": "Biztosan leállítod a követését?",
|
||||
"title": "Követés leállítása"
|
||||
"title": "Nem követem"
|
||||
}
|
||||
},
|
||||
"conversation": {
|
||||
|
@ -190,9 +192,9 @@
|
|||
},
|
||||
"help": {
|
||||
"build_preview": {
|
||||
"desc1": "Jelenleg az Elk előzetes változatát nézi, a közösség jóvoltából - {0}.",
|
||||
"desc2": "Tartalmazhat áttekintés mentes vagy kártékony változtatásokat is.",
|
||||
"desc3": "Ne lépjen be valódi hozzáféréseddel.",
|
||||
"desc1": "Jelenleg az Elk előzetes változatát látja, a közösség jóvoltából - {0}.",
|
||||
"desc2": "Tartalmazhat áttekintésmentes vagy kártékony változtatásokat is.",
|
||||
"desc3": "Ne lépj be valódi hozzáféréssel.",
|
||||
"title": "Előzetes változatát telepítése"
|
||||
},
|
||||
"desc_highlight": "Számíthat néhány hibára és hiányzó funkcióra itt és ott.",
|
||||
|
@ -203,7 +205,7 @@
|
|||
"desc_para5": "elér minket a GitHub-on",
|
||||
"desc_para6": "és legyen elkötelezett.",
|
||||
"footer_team": "Az Elk csapat",
|
||||
"title": "Az Elk Előzetes változata"
|
||||
"title": "Üdvözöl az Elk!"
|
||||
},
|
||||
"language": {
|
||||
"search": "Keresés"
|
||||
|
@ -220,6 +222,7 @@
|
|||
"error": "Hiba történt a lista létrehozása közben",
|
||||
"error_prefix": "Hiba: ",
|
||||
"list_title_placeholder": "Lista címe",
|
||||
"manage": "Listák kezelése",
|
||||
"modify_account": "Listák módosítása fiókkal",
|
||||
"remove_account": "Fiók eltávolítása a listáról",
|
||||
"save": "Változtatások mentése"
|
||||
|
@ -315,6 +318,7 @@
|
|||
"list": "Lista",
|
||||
"lists": "Listák",
|
||||
"local": "Helyi",
|
||||
"more_menu": "Továbbiak",
|
||||
"muted_users": "Némított felhasználók",
|
||||
"notifications": "Értesítések",
|
||||
"privacy": "Adatvédelem",
|
||||
|
@ -449,6 +453,8 @@
|
|||
"label": "Fiók beállítások"
|
||||
},
|
||||
"interface": {
|
||||
"bottom_nav": "Alsó navigáció",
|
||||
"bottom_nav_instructions": "Válaszd ki a kedvenc navigációs gombodjaid, maximum öt darabot az alsó navigációhoz. Mindenképpen tartalmaznia kell a \"Továbbiak\" gombot.",
|
||||
"color_mode": "Szín mód",
|
||||
"dark_mode": "Sötét",
|
||||
"default": " (alapértelmezett)",
|
||||
|
@ -460,7 +466,7 @@
|
|||
},
|
||||
"language": {
|
||||
"display_language": "Megjelenítés nyelve",
|
||||
"how_to_contribute": "Hogyan tesze közzé?",
|
||||
"how_to_contribute": "Hogyan tehetsz közzé?",
|
||||
"label": "Nyelv",
|
||||
"post_language": "Bejegyzés nyelve",
|
||||
"status": "Fordítás állapota: {0}/{1} ({2}%)",
|
||||
|
@ -602,8 +608,8 @@
|
|||
"edited": "(Szerkesztve)",
|
||||
"editing": "Szerkesztés",
|
||||
"loading": "Betöltés...",
|
||||
"publish_failed": "A pubikálás sikertelen",
|
||||
"publishing": "Publikálás",
|
||||
"publish_failed": "A közzététel sikertelen",
|
||||
"publishing": "Közzétesz",
|
||||
"save_failed": "Elmentés sikertelen",
|
||||
"upload_failed": "Fetöltés sikertelen",
|
||||
"uploading": "Fetöltés..."
|
||||
|
@ -632,13 +638,13 @@
|
|||
"finished": "véget ért {0}"
|
||||
},
|
||||
"replying_to": "Válasz {0}-nak",
|
||||
"show_full_thread": "Teljes beszélgetés megjelenítése",
|
||||
"show_full_thread": "Teljes szál megjelenítése",
|
||||
"someone": "valaki",
|
||||
"spoiler_media_hidden": "Média elrejtve",
|
||||
"spoiler_show_less": "Mutass kevesebbet",
|
||||
"spoiler_show_more": "Mutass többet",
|
||||
"thread": "Szál",
|
||||
"try_original_site": "Próbálja az eredeti weblapon"
|
||||
"try_original_site": "Próbálja az eredeti webhelyen"
|
||||
},
|
||||
"status_history": {
|
||||
"created": "létrehozva {0}",
|
||||
|
@ -718,6 +724,7 @@
|
|||
"add_emojis": "Emoji hozzáadása",
|
||||
"add_media": "Képek hozzáadása, egy videó, vagy egy audió fájl",
|
||||
"add_publishable_content": "Tartalom hozzáadása publikáláshoz",
|
||||
"add_thread_item": "Tartalom hozzáadása a szálhoz",
|
||||
"change_content_visibility": "Tartalom láthatóságának megváltoztatása",
|
||||
"change_language": "nyelv megváltoztatása",
|
||||
"emoji": "Emoji",
|
||||
|
@ -727,6 +734,8 @@
|
|||
"open_editor_tools": "Szerkesztő eszközök",
|
||||
"pick_an_icon": "Ikon választása",
|
||||
"publish_failed": "A bejegyzések újbóli közzétételéhez zárja be a sikertelen üzeneteket a szerkesztő tetején",
|
||||
"remove_thread_item": "Elem eltávolítása a szálból",
|
||||
"start_thread": "Szál megkezdése",
|
||||
"toggle_bold": "Váltás félkövérre",
|
||||
"toggle_code_block": "Kódblokk váltása",
|
||||
"toggle_italic": "Váltás dőltbetűssé"
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
"boost": "Potenzia",
|
||||
"boost_count": "{0}",
|
||||
"boosted": "Potenziato",
|
||||
"clear": "Cancella",
|
||||
"clear_publish_failed": "Cancella errori di pubblicazione",
|
||||
"clear_save_failed": "Cancella errori di salvataggio",
|
||||
"clear_upload_failed": "Cancella errori di caricamento file",
|
||||
|
@ -74,6 +75,7 @@
|
|||
"next": "Successivo",
|
||||
"prev": "Precedente",
|
||||
"publish": "Pubblica",
|
||||
"publish_thread": "Pubblica discussione",
|
||||
"reply": "Rispondi",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Reset",
|
||||
|
@ -220,6 +222,7 @@
|
|||
"error": "C'è stato un errore nella creazione della lista",
|
||||
"error_prefix": "Errore: ",
|
||||
"list_title_placeholder": "Titolo lista",
|
||||
"manage": "Gestisci liste",
|
||||
"modify_account": "Modifica liste con account",
|
||||
"remove_account": "Rimuovi account dalla lista",
|
||||
"save": "Salva modifiche"
|
||||
|
@ -315,6 +318,7 @@
|
|||
"list": "Lista",
|
||||
"lists": "Liste",
|
||||
"local": "Locale",
|
||||
"more_menu": "Altro",
|
||||
"muted_users": "Utenti silenziati",
|
||||
"notifications": "Notifiche",
|
||||
"privacy": "Privacy",
|
||||
|
@ -449,6 +453,8 @@
|
|||
"label": "Impostazioni account"
|
||||
},
|
||||
"interface": {
|
||||
"bottom_nav": "Barra di navigazione",
|
||||
"bottom_nav_instructions": "Scegli non più di cinque tasti per la barra di navigazione. Il tasto \"Altro\" è obbligatorio.",
|
||||
"color_mode": "Aspetto",
|
||||
"dark_mode": "Scuro",
|
||||
"default": " (default)",
|
||||
|
@ -655,7 +661,7 @@
|
|||
"report": "Segnalazione",
|
||||
"sign_up": "Iscrizione"
|
||||
},
|
||||
"notifications_all": "Tutti",
|
||||
"notifications_all": "Tutte",
|
||||
"notifications_favourite": "Apprezzamento",
|
||||
"notifications_follow": "Nuovo seguace",
|
||||
"notifications_follow_request": "Richiesta di seguire",
|
||||
|
@ -718,6 +724,7 @@
|
|||
"add_emojis": "Aggiungi emoji",
|
||||
"add_media": "Aggiungi immagini, un video o un file audio",
|
||||
"add_publishable_content": "Aggiungi contenuto da pubblicare",
|
||||
"add_thread_item": "Aggiungi elemento alla discussione",
|
||||
"change_content_visibility": "Cambia visibilità contenuto",
|
||||
"change_language": "Cambia lingua",
|
||||
"emoji": "Emoji",
|
||||
|
@ -727,6 +734,8 @@
|
|||
"open_editor_tools": "Formattazione testo",
|
||||
"pick_an_icon": "Scegli un'icona",
|
||||
"publish_failed": "Chiudi i messaggi falliti in cima all'editor per ripubblicare i post",
|
||||
"remove_thread_item": "Rimuovi elemento dalla discussione",
|
||||
"start_thread": "Avvia discussione",
|
||||
"toggle_bold": "Grassetto",
|
||||
"toggle_code_block": "Blocco di codice",
|
||||
"toggle_italic": "Corsivo"
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
"boost": "Partilhar",
|
||||
"boost_count": "{0}",
|
||||
"boosted": "Partilhado",
|
||||
"clear": "Limpar",
|
||||
"clear_publish_failed": "Limpar erros de publicação",
|
||||
"clear_save_failed": "Limpar erros ocorridos ao tentar guardar",
|
||||
"clear_upload_failed": "Limpar erros de carregamento de ficheiro",
|
||||
|
@ -74,6 +75,7 @@
|
|||
"next": "Próximo",
|
||||
"prev": "Anterior",
|
||||
"publish": "Publicar",
|
||||
"publish_thread": "Publicar sequência",
|
||||
"reply": "Responder",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Repor",
|
||||
|
@ -220,6 +222,7 @@
|
|||
"error": "Ocorreu um erro ao criar a lista",
|
||||
"error_prefix": "Erro: ",
|
||||
"list_title_placeholder": "Título da lista",
|
||||
"manage": "Gerir listas",
|
||||
"modify_account": "Modificar listas com a conta",
|
||||
"remove_account": "Remover conta da lista",
|
||||
"save": "Salvar alterações"
|
||||
|
@ -315,6 +318,7 @@
|
|||
"list": "Lista",
|
||||
"lists": "Listas",
|
||||
"local": "Local",
|
||||
"more_menu": "Mais opções",
|
||||
"muted_users": "Utilizadores silenciados",
|
||||
"notifications": "Notificações",
|
||||
"privacy": "Privacidade",
|
||||
|
@ -449,7 +453,9 @@
|
|||
"label": "Configurações da conta"
|
||||
},
|
||||
"interface": {
|
||||
"color_mode": "Modo de cores",
|
||||
"bottom_nav": "Barra de Navegação",
|
||||
"bottom_nav_instructions": "Escolha as suas opções favoritas, até cinco, para a barra de navegação. Deve incluir o botão \"Mais opções\".",
|
||||
"color_mode": "Modo de Cores",
|
||||
"dark_mode": "Modo Escuro",
|
||||
"default": " (padrão)",
|
||||
"font_size": "Tamanho da fonte",
|
||||
|
@ -718,6 +724,7 @@
|
|||
"add_emojis": "Adicionar emojis",
|
||||
"add_media": "Adicionar imagens, um video ou um ficheiro audio",
|
||||
"add_publishable_content": "Adicionar conteúdo a publicar",
|
||||
"add_thread_item": "Adicionar item à sequência",
|
||||
"change_content_visibility": "Alterar visibilidade do conteúdo",
|
||||
"change_language": "Alterar idioma",
|
||||
"emoji": "Emoji",
|
||||
|
@ -727,6 +734,8 @@
|
|||
"open_editor_tools": "Ferramentas de edição",
|
||||
"pick_an_icon": "Escolher um ícone",
|
||||
"publish_failed": "Fechar mensagens de falha no topo do editor para republicar publicações",
|
||||
"remove_thread_item": "Remover item da sequência",
|
||||
"start_thread": "Iniciar sequência",
|
||||
"toggle_bold": "Alternar negrito",
|
||||
"toggle_code_block": "Alternar bloco de código",
|
||||
"toggle_italic": "Alternar itálico"
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
"boost": "Đăng lại",
|
||||
"boost_count": "{0}",
|
||||
"boosted": "Đã đăng lại",
|
||||
"clear": "Xóa",
|
||||
"clear_publish_failed": "Xóa lỗi khi đăng tút",
|
||||
"clear_save_failed": "Xóa lỗi khi lưu tút",
|
||||
"clear_upload_failed": "Xóa lỗi khi xóa file",
|
||||
|
@ -74,6 +75,7 @@
|
|||
"next": "Kế tiếp",
|
||||
"prev": "Trước đó",
|
||||
"publish": "Đăng",
|
||||
"publish_thread": "Đăng chuỗi tút",
|
||||
"reply": "Trả lời",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Đặt lại",
|
||||
|
@ -195,15 +197,15 @@
|
|||
"desc3": "Đừng đăng nhập bằng tài khoản chính.",
|
||||
"title": "Bản dựng"
|
||||
},
|
||||
"desc_highlight": "Sẽ có một số lỗi và tính năng bị thiếu ở đây.",
|
||||
"desc_para1": "Cảm ơn bạn đã quan tâm đến việc dùng thử Elk, ứng dụng web Mastodon đang được triển khai của chúng tôi!",
|
||||
"desc_para2": "Chúng tôi đang nỗ lực phát triển và cải thiện nó.",
|
||||
"desc_highlight": "Sẽ có lỗi và một số tính năng bị thiếu.",
|
||||
"desc_para1": "Elk là một ứng dụng web Mastodon nhanh nhẹn. Sử dụng để đăng nhập Mastodon và tương tác với Fediverse.",
|
||||
"desc_para2": "Elk là Mã Nguồn Mở và chúng tôi cải tiến nó như một dự án cộng đồng. Hãy tham gia và cùng xây dựng!",
|
||||
"desc_para3": "Để thúc đẩy sự phát triển, bạn có thể tài trợ cho Nhóm thông qua GitHub Sponsors. Chúng tôi hy vọng bạn thích Elk!",
|
||||
"desc_para4": "Elk là Mã Nguồn Mở. Nếu bạn muốn thử nghiệm, đưa ra phản hồi hoặc đóng góp,",
|
||||
"desc_para4": "Nếu bạn muốn thử nghiệm, đưa ra phản hồi hoặc đóng góp,",
|
||||
"desc_para5": "liên hệ trên GitHub",
|
||||
"desc_para6": "và tham gia.",
|
||||
"footer_team": "Elk Team",
|
||||
"title": "Elk đang Thử Nghiệm!"
|
||||
"title": "Elk xin chào!"
|
||||
},
|
||||
"language": {
|
||||
"search": "Tìm kiếm"
|
||||
|
@ -220,6 +222,7 @@
|
|||
"error": "Xảy ra lỗi khi tạo danh sách",
|
||||
"error_prefix": "Lỗi: ",
|
||||
"list_title_placeholder": "Tên danh sách",
|
||||
"manage": "Quản lý danh sách",
|
||||
"modify_account": "Sửa danh sách có người này",
|
||||
"remove_account": "Xóa người ra khỏi danh sách",
|
||||
"save": "Lưu thay đổi"
|
||||
|
@ -306,6 +309,7 @@
|
|||
"built_at": "Bản dựng {0}",
|
||||
"compose": "Soạn tút",
|
||||
"conversations": "Nhắn riêng",
|
||||
"docs": "Tài liệu",
|
||||
"explore": "Khám phá",
|
||||
"favourites": "Lượt thích",
|
||||
"federated": "Liên hợp",
|
||||
|
@ -314,6 +318,7 @@
|
|||
"list": "Danh sách",
|
||||
"lists": "Danh sách",
|
||||
"local": "Máy chủ",
|
||||
"more_menu": "Thêm",
|
||||
"muted_users": "Người đã ẩn",
|
||||
"notifications": "Thông báo",
|
||||
"privacy": "Bảo mật",
|
||||
|
@ -448,6 +453,8 @@
|
|||
"label": "Cài đặt tài khoản"
|
||||
},
|
||||
"interface": {
|
||||
"bottom_nav": "Menu dưới màn hình",
|
||||
"bottom_nav_instructions": "Chọn tối đa năm nút điều hướng yêu thích của bạn. Bao gồm nút \"Thêm\" ",
|
||||
"color_mode": "Chủ đề",
|
||||
"dark_mode": "Tối",
|
||||
"default": " (mặc định)",
|
||||
|
@ -459,6 +466,7 @@
|
|||
},
|
||||
"language": {
|
||||
"display_language": "Ngôn ngữ giao diện",
|
||||
"how_to_contribute": "Giúp một tay?",
|
||||
"label": "Ngôn ngữ",
|
||||
"post_language": "Ngôn ngữ đăng tút",
|
||||
"status": "Tình trạng dịch: {0}/{1} ({2}%)",
|
||||
|
@ -485,14 +493,14 @@
|
|||
"title": "Bạn muốn nhận những kiểu thông báo nào?"
|
||||
},
|
||||
"description": "Nhận thông báo kể cả khi bạn không sử dụng Elk.",
|
||||
"instructions": "Đừng quên lưu các thay đổi của bạn bằng cách @:settings.notifications.push_notifications.save_settings button!",
|
||||
"instructions": "Nhớ lưu các thay đổi của bạn bằng cách nhấn @:settings.notifications.push_notifications.save_settings !",
|
||||
"label": "Cài đặt thông báo đẩy",
|
||||
"policy": {
|
||||
"all": "Từ bất kỳ ai",
|
||||
"followed": "Từ người tôi theo dõi",
|
||||
"follower": "Từ người theo dõi tôi",
|
||||
"followed": "Từ người bạn theo dõi",
|
||||
"follower": "Từ người theo dõi bạn",
|
||||
"none": "Không ai cả",
|
||||
"title": "Tôi sẽ nhận thông báo từ ai?"
|
||||
"title": "Bạn muốn nhận thông báo từ ai?"
|
||||
},
|
||||
"save_settings": "Lưu cài đặt",
|
||||
"subscription_error": {
|
||||
|
@ -540,6 +548,7 @@
|
|||
"hide_boost_count": "Ẩn số lượt đăng lại",
|
||||
"hide_favorite_count": "Ẩn số lượt thích",
|
||||
"hide_follower_count": "Ẩn số lượt người theo dõi/đang theo dõi",
|
||||
"hide_gif_indi_on_posts": "Ẩn biểu tượng GIF trên tút",
|
||||
"hide_news": "Ẩn tin tức",
|
||||
"hide_reply_count": "Ẩn số lượt trả lời",
|
||||
"hide_tag_hover_card": "Ẩn xem trước hashtag",
|
||||
|
@ -591,7 +600,11 @@
|
|||
},
|
||||
"state": {
|
||||
"attachments_exceed_server_limit": "Vượt quá giới hạn số lượng file đính kèm.",
|
||||
"attachments_limit_audio_error": "Dung lượng âm thanh tối đa: {0}",
|
||||
"attachments_limit_error": "Đã vượt quá giới hạn cho mỗi tút",
|
||||
"attachments_limit_image_error": "Dung lượng ảnh tối đa: {0}",
|
||||
"attachments_limit_unknown_error": "Dung lượng tập tin tối đa: {0}",
|
||||
"attachments_limit_video_error": "Dung lượng video tối đa: {0}",
|
||||
"edited": "(Đã sửa)",
|
||||
"editing": "Sửa",
|
||||
"loading": "Đang tải...",
|
||||
|
@ -612,6 +625,7 @@
|
|||
"favourited_by": "Thích bởi",
|
||||
"filter_hidden_phrase": "Lọc theo",
|
||||
"filter_show_anyway": "Vẫn cứ hiện",
|
||||
"gif": "GIF",
|
||||
"img_alt": {
|
||||
"ALT": "ALT",
|
||||
"desc": "Mô tả",
|
||||
|
@ -710,6 +724,7 @@
|
|||
"add_emojis": "Chèn emoji",
|
||||
"add_media": "Thêm ảnh, video hoặc âm thanh",
|
||||
"add_publishable_content": "Thêm nội dung đăng",
|
||||
"add_thread_item": "Thêm tút vào chuỗi",
|
||||
"change_content_visibility": "Chọn kiểu tút",
|
||||
"change_language": "Đổi ngôn ngữ",
|
||||
"emoji": "Emoji",
|
||||
|
@ -719,6 +734,8 @@
|
|||
"open_editor_tools": "Công cụ soạn thảo",
|
||||
"pick_an_icon": "Chọn biểu tượng",
|
||||
"publish_failed": "Đóng các thông báo không thành công để đăng lại tút",
|
||||
"remove_thread_item": "Xóa tút khỏi chuỗi",
|
||||
"start_thread": "Bắt đầu chuỗi tút",
|
||||
"toggle_bold": "In đậm",
|
||||
"toggle_code_block": "Đoạn mã",
|
||||
"toggle_italic": "In nghiêng"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@elk-zone/elk",
|
||||
"type": "module",
|
||||
"version": "0.13.2",
|
||||
"version": "0.14.0",
|
||||
"packageManager": "pnpm@8.15.5",
|
||||
"license": "MIT",
|
||||
"homepage": "https://elk.zone/",
|
||||
|
@ -40,7 +40,7 @@
|
|||
"@nuxt/devtools": "^1.0.8",
|
||||
"@nuxt/test-utils": "^3.12.0",
|
||||
"@nuxtjs/color-mode": "^3.3.2",
|
||||
"@nuxtjs/i18n": "^8.2.0",
|
||||
"@nuxtjs/i18n": "^8.3.0",
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"@tiptap/core": "2.2.4",
|
||||
"@tiptap/extension-bold": "2.2.4",
|
||||
|
|
|
@ -85,7 +85,7 @@ onReactivated(() => {
|
|||
style="scroll-margin-top: 60px"
|
||||
@refetch-status="refreshStatus()"
|
||||
/>
|
||||
<PublishWidget
|
||||
<PublishWidgetList
|
||||
v-if="currentUser"
|
||||
ref="publishWidget"
|
||||
border="y base"
|
||||
|
|
|
@ -11,5 +11,13 @@ useHydratedHead({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<PublishWidgetFull />
|
||||
<MainContent>
|
||||
<template #title>
|
||||
<NuxtLink to="/compose" timeline-title-style flex items-center gap-2 @click="$scrollToTop">
|
||||
<div i-ri:quill-pen-line />
|
||||
<span>{{ $t('nav.compose') }}</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<PublishWidgetFull />
|
||||
</MainContent>
|
||||
</template>
|
||||
|
|
|
@ -6,7 +6,7 @@ const route = useRoute()
|
|||
|
||||
onMounted(async () => {
|
||||
// TODO: login check
|
||||
await openPublishDialog('intent', getDefaultDraft({
|
||||
await openPublishDialog('intent', getDefaultDraftItem({
|
||||
status: route.query.text as string,
|
||||
sensitive: route.query.sensitive === 'true' || route.query.sensitive === null,
|
||||
spoilerText: route.query.spoiler_text as string,
|
||||
|
|
|
@ -13,23 +13,11 @@ useHydratedHead({
|
|||
<span>{{ $t('settings.interface.label') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div p6 flex="~ col gap6">
|
||||
<label space-y-2>
|
||||
<p font-medium>{{ $t('settings.interface.font_size') }}</p>
|
||||
<SettingsFontSize select-settings />
|
||||
</label>
|
||||
<div space-y-2>
|
||||
<p font-medium>
|
||||
{{ $t('settings.interface.color_mode') }}
|
||||
</p>
|
||||
<SettingsColorMode />
|
||||
</div>
|
||||
<div space-y-2>
|
||||
<p font-medium>
|
||||
{{ $t('settings.interface.theme_color') }}
|
||||
</p>
|
||||
<SettingsThemeColors />
|
||||
</div>
|
||||
<div px-6 pt-3 pb-6 flex="~ col gap6">
|
||||
<SettingsFontSize />
|
||||
<SettingsColorMode />
|
||||
<SettingsThemeColors />
|
||||
<SettingsBottomNav />
|
||||
</div>
|
||||
</MainContent>
|
||||
</template>
|
||||
|
|
|
@ -15,162 +15,172 @@ const userSettings = useUserSettings()
|
|||
{{ $t('settings.preferences.label') }}
|
||||
</h1>
|
||||
</template>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideAltIndicatorOnPosts')"
|
||||
@click="togglePreferences('hideAltIndicatorOnPosts')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_alt_indi_on_posts') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideGifIndicatorOnPosts')"
|
||||
@click="togglePreferences('hideGifIndicatorOnPosts')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_gif_indi_on_posts') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideAccountHoverCard')"
|
||||
@click="togglePreferences('hideAccountHoverCard')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_account_hover_card') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideTagHoverCard')"
|
||||
@click="togglePreferences('hideTagHoverCard')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_tag_hover_card') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'enableAutoplay')"
|
||||
:disabled="getPreferences(userSettings, 'enableDataSaving')"
|
||||
@click="togglePreferences('enableAutoplay')"
|
||||
>
|
||||
{{ $t('settings.preferences.enable_autoplay') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'optimizeForLowPerformanceDevice')"
|
||||
@click="togglePreferences('optimizeForLowPerformanceDevice')"
|
||||
>
|
||||
{{ $t('settings.preferences.optimize_for_low_performance_device') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'enableDataSaving')"
|
||||
@click="togglePreferences('enableDataSaving')"
|
||||
>
|
||||
{{ $t("settings.preferences.enable_data_saving") }}
|
||||
<template #description>
|
||||
{{ $t("settings.preferences.enable_data_saving_description") }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'enablePinchToZoom')"
|
||||
@click="togglePreferences('enablePinchToZoom')"
|
||||
>
|
||||
{{ $t('settings.preferences.enable_pinch_to_zoom') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'useStarFavoriteIcon')"
|
||||
@click="togglePreferences('useStarFavoriteIcon')"
|
||||
>
|
||||
{{ $t('settings.preferences.use_star_favorite_icon') }}
|
||||
</SettingsToggleItem>
|
||||
<h2 px6 py4 mt2 font-bold text-xl flex="~ gap-1" items-center>
|
||||
<div i-ri-hearts-line />
|
||||
{{ $t('settings.preferences.wellbeing') }}
|
||||
</h2>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'grayscaleMode')"
|
||||
@click="togglePreferences('grayscaleMode')"
|
||||
>
|
||||
{{ $t('settings.preferences.grayscale_mode') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideBoostCount')"
|
||||
@click="togglePreferences('hideBoostCount')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_boost_count') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideFavoriteCount')"
|
||||
@click="togglePreferences('hideFavoriteCount')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_favorite_count') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideReplyCount')"
|
||||
@click="togglePreferences('hideReplyCount')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_reply_count') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideFollowerCount')"
|
||||
@click="togglePreferences('hideFollowerCount')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_follower_count') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideUsernameEmojis')"
|
||||
@click="togglePreferences('hideUsernameEmojis')"
|
||||
>
|
||||
{{ $t("settings.preferences.hide_username_emojis") }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.hide_username_emojis_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideNews')"
|
||||
@click="togglePreferences('hideNews')"
|
||||
>
|
||||
{{ $t("settings.preferences.hide_news") }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'zenMode')"
|
||||
@click="togglePreferences('zenMode')"
|
||||
>
|
||||
{{ $t("settings.preferences.zen_mode") }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.zen_mode_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<h2 px6 py4 mt2 font-bold text-xl flex="~ gap-1" items-center>
|
||||
<div i-ri-flask-line />
|
||||
{{ $t('settings.preferences.title') }}
|
||||
</h2>
|
||||
<!-- Embedded Media -->
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'experimentalEmbeddedMedia')"
|
||||
@click="togglePreferences('experimentalEmbeddedMedia')"
|
||||
>
|
||||
{{ $t('settings.preferences.embedded_media') }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.embedded_media_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'experimentalVirtualScroller')"
|
||||
@click="togglePreferences('experimentalVirtualScroller')"
|
||||
>
|
||||
{{ $t('settings.preferences.virtual_scroll') }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.virtual_scroll_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'experimentalGitHubCards')"
|
||||
@click="togglePreferences('experimentalGitHubCards')"
|
||||
>
|
||||
{{ $t('settings.preferences.github_cards') }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.github_cards_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'experimentalUserPicker')"
|
||||
@click="togglePreferences('experimentalUserPicker')"
|
||||
>
|
||||
{{ $t('settings.preferences.user_picker') }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.user_picker_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<section>
|
||||
<h2 px6 py4 mt2 font-bold text-xl flex="~ gap-1" items-center sr-only>
|
||||
<span aria-hidden="true" block i-ri-equalizer-line />
|
||||
{{ $t('settings.preferences.label') }}
|
||||
</h2>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideAltIndicatorOnPosts')"
|
||||
@click="togglePreferences('hideAltIndicatorOnPosts')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_alt_indi_on_posts') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideGifIndicatorOnPosts')"
|
||||
@click="togglePreferences('hideGifIndicatorOnPosts')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_gif_indi_on_posts') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideAccountHoverCard')"
|
||||
@click="togglePreferences('hideAccountHoverCard')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_account_hover_card') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideTagHoverCard')"
|
||||
@click="togglePreferences('hideTagHoverCard')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_tag_hover_card') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'enableAutoplay')"
|
||||
:disabled="getPreferences(userSettings, 'enableDataSaving')"
|
||||
@click="togglePreferences('enableAutoplay')"
|
||||
>
|
||||
{{ $t('settings.preferences.enable_autoplay') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'optimizeForLowPerformanceDevice')"
|
||||
@click="togglePreferences('optimizeForLowPerformanceDevice')"
|
||||
>
|
||||
{{ $t('settings.preferences.optimize_for_low_performance_device') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'enableDataSaving')"
|
||||
@click="togglePreferences('enableDataSaving')"
|
||||
>
|
||||
{{ $t("settings.preferences.enable_data_saving") }}
|
||||
<template #description>
|
||||
{{ $t("settings.preferences.enable_data_saving_description") }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'enablePinchToZoom')"
|
||||
@click="togglePreferences('enablePinchToZoom')"
|
||||
>
|
||||
{{ $t('settings.preferences.enable_pinch_to_zoom') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'useStarFavoriteIcon')"
|
||||
@click="togglePreferences('useStarFavoriteIcon')"
|
||||
>
|
||||
{{ $t('settings.preferences.use_star_favorite_icon') }}
|
||||
</SettingsToggleItem>
|
||||
</section>
|
||||
<section>
|
||||
<h2 px6 py4 mt2 font-bold text-xl flex="~ gap-1" items-center>
|
||||
<span aria-hidden="true" block i-ri-hearts-line />
|
||||
{{ $t('settings.preferences.wellbeing') }}
|
||||
</h2>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'grayscaleMode')"
|
||||
@click="togglePreferences('grayscaleMode')"
|
||||
>
|
||||
{{ $t('settings.preferences.grayscale_mode') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideBoostCount')"
|
||||
@click="togglePreferences('hideBoostCount')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_boost_count') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideFavoriteCount')"
|
||||
@click="togglePreferences('hideFavoriteCount')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_favorite_count') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideReplyCount')"
|
||||
@click="togglePreferences('hideReplyCount')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_reply_count') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideFollowerCount')"
|
||||
@click="togglePreferences('hideFollowerCount')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_follower_count') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideUsernameEmojis')"
|
||||
@click="togglePreferences('hideUsernameEmojis')"
|
||||
>
|
||||
{{ $t("settings.preferences.hide_username_emojis") }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.hide_username_emojis_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideNews')"
|
||||
@click="togglePreferences('hideNews')"
|
||||
>
|
||||
{{ $t("settings.preferences.hide_news") }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'zenMode')"
|
||||
@click="togglePreferences('zenMode')"
|
||||
>
|
||||
{{ $t("settings.preferences.zen_mode") }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.zen_mode_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
</section>
|
||||
<section>
|
||||
<h2 px6 py4 mt2 font-bold text-xl flex="~ gap-1" items-center>
|
||||
<span aria-hidden="true" block i-ri-flask-line />
|
||||
{{ $t('settings.preferences.title') }}
|
||||
</h2>
|
||||
<!-- Embedded Media -->
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'experimentalEmbeddedMedia')"
|
||||
@click="togglePreferences('experimentalEmbeddedMedia')"
|
||||
>
|
||||
{{ $t('settings.preferences.embedded_media') }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.embedded_media_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'experimentalVirtualScroller')"
|
||||
@click="togglePreferences('experimentalVirtualScroller')"
|
||||
>
|
||||
{{ $t('settings.preferences.virtual_scroll') }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.virtual_scroll_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'experimentalGitHubCards')"
|
||||
@click="togglePreferences('experimentalGitHubCards')"
|
||||
>
|
||||
{{ $t('settings.preferences.github_cards') }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.github_cards_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'experimentalUserPicker')"
|
||||
@click="togglePreferences('experimentalUserPicker')"
|
||||
>
|
||||
{{ $t('settings.preferences.user_picker') }}
|
||||
<template #description>
|
||||
{{ $t('settings.preferences.user_picker_description') }}
|
||||
</template>
|
||||
</SettingsToggleItem>
|
||||
</section>
|
||||
</MainContent>
|
||||
</template>
|
||||
|
|
|
@ -6,6 +6,8 @@ export default defineNuxtPlugin(({ $scrollToTop }) => {
|
|||
const keys = useMagicKeys()
|
||||
const router = useRouter()
|
||||
const i18n = useNuxtApp().$i18n
|
||||
const { y } = useWindowScroll({ behavior: 'instant' })
|
||||
const virtualScroller = usePreferences('experimentalVirtualScroller')
|
||||
|
||||
// disable shortcuts when focused on inputs (https://vueuse.org/core/usemagickeys/#conditionally-disable)
|
||||
const activeElement = useActiveElement()
|
||||
|
@ -31,7 +33,7 @@ export default defineNuxtPlugin(({ $scrollToTop }) => {
|
|||
// TODO: bugfix -> create PR for vueuse, reset `current` ref on window focus|blur
|
||||
if (!current.has('shift') && !current.has('meta') && !current.has('control') && !current.has('alt')) {
|
||||
// TODO: is this the correct way of using openPublishDialog()?
|
||||
openPublishDialog('dialog', getDefaultDraft())
|
||||
openPublishDialog('dialog', getDefaultDraftItem())
|
||||
}
|
||||
}
|
||||
whenever(logicAnd(isAuthenticated, notUsingInput, keys.c), defaultPublishDialog)
|
||||
|
@ -76,4 +78,54 @@ export default defineNuxtPlugin(({ $scrollToTop }) => {
|
|||
?.click()
|
||||
}
|
||||
whenever(logicAnd(isAuthenticated, notUsingInput, keys['.']), showNewItems)
|
||||
|
||||
// TODO: virtual scroller cannot load off-screen post
|
||||
// that prevents focusing next post properly
|
||||
// we disabled this shortcut when enabled virtual scroller
|
||||
if (!virtualScroller.value) {
|
||||
const statusSelector = '[aria-roledescription="status-card"]'
|
||||
|
||||
// find the nearest status element id traversing up from the current active element
|
||||
// `activeElement` can be some of an element within a status element
|
||||
// otherwise, reach to the root `<html>`
|
||||
function getActiveStatueId(element: HTMLElement): string | undefined {
|
||||
if (element.nodeName === 'HTML')
|
||||
return undefined
|
||||
|
||||
if (element.matches(statusSelector))
|
||||
return element.id
|
||||
|
||||
return getActiveStatueId(element.parentNode as HTMLElement)
|
||||
}
|
||||
|
||||
function focusNextOrPreviousStatus(direction: 'next' | 'previous') {
|
||||
const activeStatusId = activeElement.value ? getActiveStatueId(activeElement.value) : undefined
|
||||
const nextOrPreviousStatusId = getNextOrPreviousStatusId(activeStatusId, direction)
|
||||
if (nextOrPreviousStatusId) {
|
||||
const status = document.getElementById(nextOrPreviousStatusId)
|
||||
if (status) {
|
||||
status.focus({ preventScroll: true })
|
||||
const topBarHeight = 58
|
||||
y.value += status.getBoundingClientRect().top - topBarHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNextOrPreviousStatusId(currentStatusId: string | undefined, direction: 'next' | 'previous'): string | undefined {
|
||||
const statusIds = [...document.querySelectorAll(statusSelector)].map(s => s.id)
|
||||
if (currentStatusId === undefined) {
|
||||
// if there is no selection, always focus on the first status
|
||||
return statusIds[0]
|
||||
}
|
||||
|
||||
const currentIndex = statusIds.findIndex(id => id === currentStatusId)
|
||||
const statusId = direction === 'next'
|
||||
? statusIds[Math.min(currentIndex + 1, statusIds.length)]
|
||||
: statusIds[Math.max(0, currentIndex - 1)]
|
||||
return statusId
|
||||
}
|
||||
|
||||
whenever(logicAnd(notUsingInput, keys.j), () => focusNextOrPreviousStatus('next'))
|
||||
whenever(logicAnd(notUsingInput, keys.k), () => focusNextOrPreviousStatus('previous'))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -43,8 +43,8 @@ importers:
|
|||
specifier: ^3.3.2
|
||||
version: 3.3.2(rollup@4.14.0)
|
||||
'@nuxtjs/i18n':
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0(rollup@4.14.0)(vue@3.4.21)
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0(rollup@4.14.0)(vue@3.4.21)
|
||||
'@pinia/nuxt':
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1(rollup@4.14.0)(typescript@5.4.4)(vue@3.4.21)
|
||||
|
@ -2745,8 +2745,8 @@ packages:
|
|||
engines: {node: '>= 16'}
|
||||
dev: false
|
||||
|
||||
/@intlify/unplugin-vue-i18n@2.0.0(rollup@4.14.0)(vue-i18n@9.9.1):
|
||||
resolution: {integrity: sha512-1oKvm92L9l2od2H9wKx2ZvR4tzn7gUtd7bPLI7AWUmm7U9H1iEypndt5d985ypxGsEs0gToDaKTrytbBIJwwSg==}
|
||||
/@intlify/unplugin-vue-i18n@3.0.1(rollup@4.14.0)(vue-i18n@9.9.1):
|
||||
resolution: {integrity: sha512-q1zJhA/WpoLBzAAuKA5/AEp0e+bMOM10ll/HxT4g1VAw/9JhC4TTobP9KobKH90JMZ4U2daLFlYQfKNd29lpqw==}
|
||||
engines: {node: '>= 14.16'}
|
||||
peerDependencies:
|
||||
petite-vue-i18n: '*'
|
||||
|
@ -3700,13 +3700,13 @@ packages:
|
|||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@nuxtjs/i18n@8.2.0(rollup@4.14.0)(vue@3.4.21):
|
||||
resolution: {integrity: sha512-t37aF/WOD1g8CA/iCyCJrURXocjPy7diZG+kJcHLkLmJh1v4/2Zhe0AeUsjXubGgvQKLSL3b5w8rZuPkG4yhUw==}
|
||||
/@nuxtjs/i18n@8.3.0(rollup@4.14.0)(vue@3.4.21):
|
||||
resolution: {integrity: sha512-/2g4zYwBwHwIVJitu/i5zP73G4F9xH394Uq0RbfOGc34YxscN+B2kMnuPL8XXM9zThdMVj9ctHInQXXtr62CLg==}
|
||||
engines: {node: ^14.16.0 || >=16.11.0}
|
||||
dependencies:
|
||||
'@intlify/h3': 0.5.0
|
||||
'@intlify/shared': 9.9.1
|
||||
'@intlify/unplugin-vue-i18n': 2.0.0(rollup@4.14.0)(vue-i18n@9.9.1)
|
||||
'@intlify/unplugin-vue-i18n': 3.0.1(rollup@4.14.0)(vue-i18n@9.9.1)
|
||||
'@intlify/utils': 0.12.0
|
||||
'@miyaneee/rollup-plugin-json5': 1.2.0(rollup@4.14.0)
|
||||
'@nuxt/kit': 3.11.2(rollup@4.14.0)
|
||||
|
|
|
@ -45,7 +45,7 @@ export type NotificationSlot = GroupedNotifications | GroupedLikeNotifications |
|
|||
|
||||
export type TranslateFn = ReturnType<typeof useI18n>['t']
|
||||
|
||||
export interface Draft {
|
||||
export interface DraftItem {
|
||||
editingStatus?: mastodon.v1.Status
|
||||
initialText?: string
|
||||
params: MarkNonNullable<Mutable<Omit<mastodon.rest.v1.CreateStatusParams, 'poll'>>, 'status' | 'language' | 'sensitive' | 'spoilerText' | 'visibility'> & { poll: Mutable<mastodon.rest.v1.CreateStatusParams['poll']> }
|
||||
|
@ -54,7 +54,9 @@ export interface Draft {
|
|||
mentions?: string[]
|
||||
}
|
||||
|
||||
export type DraftMap = Record<string, Draft>
|
||||
export type DraftMap = Record<string, Array<DraftItem>
|
||||
// For backward compatibility we need to support single draft items
|
||||
| DraftItem>
|
||||
|
||||
export interface ConfirmDialogOptions {
|
||||
title: string
|
||||
|
|
Ładowanie…
Reference in New Issue