feat: compose editor as a page (#804)

zio/stable
Anthony Fu 2023-01-05 16:42:36 +01:00 committed by GitHub
parent 9d5269e0c0
commit 272fb4a13d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 100 additions and 17 deletions

View File

@ -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" />

View File

@ -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,

View File

@ -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>
&middot; {{ 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>

View File

@ -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]
}
}

View File

@ -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>

View File

@ -0,0 +1,3 @@
<template>
<PublishWidgetFull />
</template>

View File

@ -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>