feat: compose editor as a page (#804)
parent
9d5269e0c0
commit
272fb4a13d
|
@ -21,11 +21,11 @@ const { notifications } = useNotifications()
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</NavSideItem>
|
</NavSideItem>
|
||||||
|
<NavSideItem :text="$t('action.compose')" to="/compose" icon="i-ri:quill-pen-line" user-only :command="command" />
|
||||||
|
|
||||||
<!-- Use Search for small screens once the right sidebar is collapsed -->
|
<!-- Use Search for small screens once the right sidebar is collapsed -->
|
||||||
<NavSideItem :text="$t('nav.search')" to="/search" icon="i-ri:search-line" sm:hidden :command="command" />
|
<NavSideItem :text="$t('nav.search')" to="/search" icon="i-ri:search-line" sm:hidden :command="command" />
|
||||||
<NavSideItem :text="$t('nav.explore')" :to="`/${currentServer}/explore`" icon="i-ri:hashtag" :command="command" />
|
<NavSideItem :text="$t('nav.explore')" :to="`/${currentServer}/explore`" icon="i-ri:hashtag" :command="command" />
|
||||||
|
|
||||||
<NavSideItem :text="$t('nav.local')" :to="`/${currentServer}/public/local`" icon="i-ri:group-2-line " :command="command" />
|
<NavSideItem :text="$t('nav.local')" :to="`/${currentServer}/public/local`" icon="i-ri:group-2-line " :command="command" />
|
||||||
<NavSideItem :text="$t('nav.federated')" :to="`/${currentServer}/public`" icon="i-ri:earth-line" :command="command" />
|
<NavSideItem :text="$t('nav.federated')" :to="`/${currentServer}/public`" icon="i-ri:earth-line" :command="command" />
|
||||||
<NavSideItem :text="$t('nav.conversations')" to="/conversations" icon="i-ri:at-line" user-only :command="command" />
|
<NavSideItem :text="$t('nav.conversations')" to="/conversations" icon="i-ri:at-line" user-only :command="command" />
|
||||||
|
|
|
@ -14,7 +14,7 @@ const {
|
||||||
placeholder,
|
placeholder,
|
||||||
dialogLabelledBy,
|
dialogLabelledBy,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
draftKey: string
|
draftKey?: string
|
||||||
initial?: () => Draft
|
initial?: () => Draft
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
inReplyToId?: string
|
inReplyToId?: string
|
||||||
|
@ -38,7 +38,10 @@ const shouldExpanded = $computed(() => _expanded || isExpanded || !isEmpty)
|
||||||
const { editor } = useTiptap({
|
const { editor } = useTiptap({
|
||||||
content: computed({
|
content: computed({
|
||||||
get: () => draft.params.status,
|
get: () => draft.params.status,
|
||||||
set: newVal => draft.params.status = newVal,
|
set: (newVal) => {
|
||||||
|
draft.params.status = newVal
|
||||||
|
draft.lastUpdated = Date.now()
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
placeholder: computed(() => placeholder ?? draft.params.inReplyToId ? t('placeholder.replying') : t('placeholder.default_1')),
|
placeholder: computed(() => placeholder ?? draft.params.inReplyToId ? t('placeholder.replying') : t('placeholder.default_1')),
|
||||||
autofocus: shouldExpanded,
|
autofocus: shouldExpanded,
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { formatTimeAgo } from '@vueuse/core'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
let draftKey = $ref('home')
|
||||||
|
|
||||||
|
const draftKeys = $computed(() => Object.keys(currentUserDrafts.value))
|
||||||
|
const nonEmptyDrafts = $computed(() => draftKeys
|
||||||
|
.filter(i => i !== draftKey && !isEmptyDraft(currentUserDrafts.value[i]))
|
||||||
|
.map(i => [i, currentUserDrafts.value[i]] as const),
|
||||||
|
)
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
draftKey = route.query.draft?.toString() || 'home'
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
clearEmptyDrafts()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div flex="~ col" pt-6 h-screen>
|
||||||
|
<div text-right h-8>
|
||||||
|
<VDropdown v-if="nonEmptyDrafts.length" placement="bottom-end">
|
||||||
|
<button btn-text flex="inline center">
|
||||||
|
Drafts ({{ nonEmptyDrafts.length }}) <div 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()"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div flex="~ gap-1" items-center>
|
||||||
|
Draft <code>{{ key }}</code>
|
||||||
|
<span v-if="draft.lastUpdated" text-secondary text-sm>
|
||||||
|
· {{ formatTimeAgo(new Date(draft.lastUpdated)) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div text-secondary>
|
||||||
|
{{ htmlToText(draft.params.status).slice(0, 50) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<PublishWidget :key="draftKey" expanded class="min-h-100!" :draft-key="draftKey" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -5,6 +5,11 @@ import type { Mutable } from '~/types/utils'
|
||||||
|
|
||||||
export const currentUserDrafts = process.server ? computed<DraftMap>(() => ({})) : useUserLocalStorage<DraftMap>(STORAGE_KEY_DRAFTS, () => ({}))
|
export const currentUserDrafts = process.server ? computed<DraftMap>(() => ({})) : useUserLocalStorage<DraftMap>(STORAGE_KEY_DRAFTS, () => ({}))
|
||||||
|
|
||||||
|
export const builtinDraftKeys = [
|
||||||
|
'dialog',
|
||||||
|
'home',
|
||||||
|
]
|
||||||
|
|
||||||
export function getDefaultDraft(options: Partial<Mutable<CreateStatusParams> & Omit<Draft, 'params'>> = {}): Draft {
|
export function getDefaultDraft(options: Partial<Mutable<CreateStatusParams> & Omit<Draft, 'params'>> = {}): Draft {
|
||||||
const {
|
const {
|
||||||
attachments = [],
|
attachments = [],
|
||||||
|
@ -21,7 +26,6 @@ export function getDefaultDraft(options: Partial<Mutable<CreateStatusParams> & O
|
||||||
return {
|
return {
|
||||||
attachments,
|
attachments,
|
||||||
initialText,
|
initialText,
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
status: status || '',
|
status: status || '',
|
||||||
inReplyToId,
|
inReplyToId,
|
||||||
|
@ -30,6 +34,7 @@ export function getDefaultDraft(options: Partial<Mutable<CreateStatusParams> & O
|
||||||
spoilerText: spoilerText || '',
|
spoilerText: spoilerText || '',
|
||||||
language: language || 'en',
|
language: language || 'en',
|
||||||
},
|
},
|
||||||
|
lastUpdated: Date.now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,25 +83,27 @@ export const isEmptyDraft = (draft: Draft | null | undefined) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDraft(
|
export function useDraft(
|
||||||
draftKey: string,
|
draftKey?: string,
|
||||||
initial: () => Draft = () => getDefaultDraft({}),
|
initial: () => Draft = () => getDefaultDraft({}),
|
||||||
) {
|
) {
|
||||||
const draft = computed({
|
const draft = draftKey
|
||||||
get() {
|
? computed({
|
||||||
if (!currentUserDrafts.value[draftKey])
|
get() {
|
||||||
currentUserDrafts.value[draftKey] = initial()
|
if (!currentUserDrafts.value[draftKey])
|
||||||
return currentUserDrafts.value[draftKey]
|
currentUserDrafts.value[draftKey] = initial()
|
||||||
},
|
return currentUserDrafts.value[draftKey]
|
||||||
set(val) {
|
},
|
||||||
currentUserDrafts.value[draftKey] = val
|
set(val) {
|
||||||
},
|
currentUserDrafts.value[draftKey] = val
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
: ref(initial())
|
||||||
|
|
||||||
const isEmpty = computed(() => isEmptyDraft(draft.value))
|
const isEmpty = computed(() => isEmptyDraft(draft.value))
|
||||||
|
|
||||||
onUnmounted(async () => {
|
onUnmounted(async () => {
|
||||||
// Remove draft if it's empty
|
// Remove draft if it's empty
|
||||||
if (isEmpty.value) {
|
if (isEmpty.value && draftKey) {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
delete currentUserDrafts.value[draftKey]
|
delete currentUserDrafts.value[draftKey]
|
||||||
}
|
}
|
||||||
|
@ -117,3 +124,12 @@ export function directMessageUser(account: Account) {
|
||||||
visibility: 'direct',
|
visibility: 'direct',
|
||||||
}), true)
|
}), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearEmptyDrafts() {
|
||||||
|
for (const key in currentUserDrafts.value) {
|
||||||
|
if (builtinDraftKeys.includes(key))
|
||||||
|
continue
|
||||||
|
if (!currentUserDrafts.value[key].params || isEmptyDraft(currentUserDrafts.value[key]))
|
||||||
|
delete currentUserDrafts.value[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ const wideLayout = computed(() => route.meta.wideLayout ?? false)
|
||||||
<div flex flex-col gap-2>
|
<div flex flex-col gap-2>
|
||||||
<NavTitle />
|
<NavTitle />
|
||||||
<NavSide command />
|
<NavSide command />
|
||||||
<PublishButton ms5.5 mt4 xl:me8 xl:ms4 />
|
<!-- <PublishButton ms5.5 mt4 xl:me8 xl:ms4 /> -->
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isMastoInitialised" flex flex-col>
|
<div v-if="isMastoInitialised" flex flex-col>
|
||||||
<div hidden xl:block>
|
<div hidden xl:block>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<PublishWidgetFull />
|
||||||
|
</template>
|
|
@ -61,6 +61,7 @@ export interface Draft {
|
||||||
initialText?: string
|
initialText?: string
|
||||||
params: MarkNonNullable<Mutable<CreateStatusParams>, 'status' | 'language' | 'sensitive' | 'spoilerText' | 'visibility'>
|
params: MarkNonNullable<Mutable<CreateStatusParams>, 'status' | 'language' | 'sensitive' | 'spoilerText' | 'visibility'>
|
||||||
attachments: Attachment[]
|
attachments: Attachment[]
|
||||||
|
lastUpdated: number
|
||||||
}
|
}
|
||||||
export type DraftMap = Record<string, Draft>
|
export type DraftMap = Record<string, Draft>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue