feat(status): edit history

zio/stable
三咲智子 2022-11-26 13:05:44 +08:00
parent eb3f2ab771
commit 36fc189064
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
11 changed files with 152 additions and 59 deletions

View File

@ -8,10 +8,10 @@ defineProps<{
}>() }>()
const emit = defineEmits(['click']) const emit = defineEmits(['click'])
const { hide } = inject(dropdownContextKey)! const { hide } = inject(dropdownContextKey, undefined) || {}
const handleClick = (evt: MouseEvent) => { const handleClick = (evt: MouseEvent) => {
hide() hide?.()
emit('click', evt) emit('click', evt)
} }
</script> </script>
@ -23,7 +23,7 @@ const handleClick = (evt: MouseEvent) => {
> >
<div v-if="icon" :class="icon" /> <div v-if="icon" :class="icon" />
<div flex="~ col"> <div flex="~ col">
<div text-15px font-700> <div text-15px>
<slot /> <slot />
</div> </div>
<div text-3 text="gray/90"> <div text-3 text="gray/90">

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { isImagePreviewDialogOpen, isPreviewHelpOpen, isPublishDialogOpen, isSigninDialogOpen, isUserSwitcherOpen } from '~/composables/dialog' import { isEditHistoryDialogOpen, isImagePreviewDialogOpen, isPreviewHelpOpen, isPublishDialogOpen, isSigninDialogOpen, isUserSwitcherOpen } from '~/composables/dialog'
</script> </script>
<template> <template>
@ -18,4 +18,7 @@ import { isImagePreviewDialogOpen, isPreviewHelpOpen, isPublishDialogOpen, isSig
<ModalDialog v-model="isImagePreviewDialogOpen"> <ModalDialog v-model="isImagePreviewDialogOpen">
<img :src="imagePreview.src" :alt="imagePreview.alt" max-w-95vw max-h-95vh> <img :src="imagePreview.src" :alt="imagePreview.alt" max-w-95vw max-h-95vh>
</ModalDialog> </ModalDialog>
<ModalDialog v-model="isEditHistoryDialogOpen">
<StatusEditPreview :edit="statusEdit" />
</ModalDialog>
</template> </template>

View File

@ -34,37 +34,7 @@ function go() {
} }
const createdAt = useFormattedDateTime(status.createdAt) const createdAt = useFormattedDateTime(status.createdAt)
const timeago = useTimeAgo(() => status.createdAt, { const timeago = useTimeAgo(() => status.createdAt, timeAgoOptions)
showSecond: true,
messages: {
justNow: 'just now',
past: n => n,
future: n => n.match(/\d/) ? `in ${n}` : n,
month: (n, past) => n === 1
? past
? 'last month'
: 'next month'
: `${n}m`,
year: (n, past) => n === 1
? past
? 'last year'
: 'next year'
: `${n}y`,
day: (n, past) => n === 1
? past
? 'yesterday'
: 'tomorrow'
: `${n}d`,
week: (n, past) => n === 1
? past
? 'last week'
: 'next week'
: `${n} week${n > 1 ? 's' : ''}`,
hour: n => `${n}h`,
minute: n => `${n}min`,
second: n => `${n}s`,
},
})
</script> </script>
<template> <template>
@ -90,7 +60,7 @@ const timeago = useTimeAgo(() => status.createdAt, {
</time> </time>
</a> </a>
</CommonTooltip> </CommonTooltip>
<StatusEditIndicator :status="status" /> <StatusEditIndicator :status="status" inline />
</div> </div>
</div> </div>
<StatusReplyingTo v-if="status.inReplyToAccountId" :status="status" pt1 /> <StatusReplyingTo v-if="status.inReplyToAccountId" :status="status" pt1 />

View File

@ -33,7 +33,12 @@ const visibility = $computed(() => STATUS_VISIBILITIES.find(v => v.value === sta
<div flex="~ gap-1" items-center op50 text-sm> <div flex="~ gap-1" items-center op50 text-sm>
<div flex> <div flex>
<div>{{ createdAt }}</div> <div>{{ createdAt }}</div>
<StatusEditIndicator :status="status" /> <StatusEditIndicator
:status="status"
:inline="false"
>
<span ml1 font-bold cursor-pointer>(Edited)</span>
</StatusEditIndicator>
</div> </div>
<div>·</div> <div>·</div>
<CommonTooltip :content="visibility.label" placement="bottom"> <CommonTooltip :content="visibility.label" placement="bottom">

View File

@ -1,20 +0,0 @@
<script setup lang="ts">
import type { Status } from 'masto'
const { status } = defineProps<{
status: Status
}>()
const editedAt = $computed(() => status.editedAt)
const formatted = useFormattedDateTime(status.editedAt)
</script>
<template>
<CommonTooltip v-if="editedAt" :content="`Edited ${formatted}`">
<time
:title="editedAt"
:datetime="editedAt"
font-bold underline decoration-dashed
>&nbsp;*</time>
</CommonTooltip>
</template>

View File

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ enabled: boolean }>() const props = defineProps<{ enabled: boolean }>()
defineSlots<'spoiler'>()
const [showContent, toggleContent] = $(useToggle(!props.enabled)) const [showContent, toggleContent] = $(useToggle(!props.enabled))
</script> </script>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import type { Status, StatusEdit } from 'masto'
const { status } = defineProps<{
status: Status
}>()
const { data: statusEdits } = useAsyncData(`status:history:${status.id}`, () => masto.statuses.fetchHistory(status.id).then(res => res.reverse()))
const showHistory = (edit: StatusEdit) => {
openEditHistoryDialog(edit)
}
</script>
<template>
<template v-if="statusEdits">
<CommonDropdownItem
v-for="(edit, idx) in statusEdits"
:key="idx"
px="0.5"
@click="showHistory(edit)"
>
{{ getDisplayName(edit.account) }}
{{ idx === statusEdits.length - 1 ? 'created' : 'edited' }}
{{ useTimeAgo(edit.createdAt, { showSecond: true }).value }}
</CommonDropdownItem>
</template>
<template v-else>
<div i-ri:loader-2-fill animate-spin text-2xl ma />
</template>
</template>

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import type { Status } from 'masto'
const { status } = defineProps<{
status: Status
inline: boolean
}>()
const editedAt = $computed(() => status.editedAt)
const formatted = useFormattedDateTime(status.editedAt)
</script>
<template>
<template v-if="editedAt">
<CommonTooltip v-if="inline" :content="`Edited ${formatted}`">
<time
:title="editedAt"
:datetime="editedAt"
font-bold underline decoration-dashed
>&nbsp;*</time>
</CommonTooltip>
<CommonDropdown v-else>
<slot />
<template #popper>
<div text-sm p2>
<div text-center mb1>
Edited {{ formatted }}
</div>
<StatusEditHistory :status="status" />
</div>
</template>
</CommonDropdown>
</template>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import type { StatusEdit } from 'masto'
const { edit } = defineProps<{
edit: StatusEdit
}>()
</script>
<template>
<div px3 py-4 flex="~ col">
<div text-center flex="~ row gap-1">
<AccountInlineInfo :account="edit.account" />
edited {{ useFormattedDateTime(edit.createdAt).value }}
</div>
<div h1px bg="gray/20" my2 />
<StatusSpoiler :enabled="edit.sensitive">
<template #spoiler>
{{ edit.spoilerText }}
</template>
<StatusBody :status="edit" />
<StatusMedia
v-if="edit.mediaAttachments.length"
:status="edit"
/>
</StatusSpoiler>
</div>
</template>

View File

@ -1,7 +1,9 @@
import type { StatusEdit } from 'masto'
import type { Draft } from './statusDrafts' import type { Draft } from './statusDrafts'
import { STORAGE_KEY_FIRST_VISIT, STORAGE_KEY_ZEN_MODE } from '~/constants' import { STORAGE_KEY_FIRST_VISIT, STORAGE_KEY_ZEN_MODE } from '~/constants'
export const imagePreview = ref({ src: '', alt: '' }) export const imagePreview = ref({ src: '', alt: '' })
export const statusEdit = ref<StatusEdit>()
export const isFirstVisit = useLocalStorage(STORAGE_KEY_FIRST_VISIT, true) export const isFirstVisit = useLocalStorage(STORAGE_KEY_FIRST_VISIT, true)
export const isZenMode = useLocalStorage(STORAGE_KEY_ZEN_MODE, false) export const isZenMode = useLocalStorage(STORAGE_KEY_ZEN_MODE, false)
export const toggleZenMode = useToggle(isZenMode) export const toggleZenMode = useToggle(isZenMode)
@ -10,6 +12,7 @@ export const isUserSwitcherOpen = ref(false)
export const isSigninDialogOpen = ref(false) export const isSigninDialogOpen = ref(false)
export const isPublishDialogOpen = ref(false) export const isPublishDialogOpen = ref(false)
export const isImagePreviewDialogOpen = ref(false) export const isImagePreviewDialogOpen = ref(false)
export const isEditHistoryDialogOpen = ref(false)
export const isPreviewHelpOpen = ref(isFirstVisit.value) export const isPreviewHelpOpen = ref(isFirstVisit.value)
export function openUserSwitcher() { export function openUserSwitcher() {
@ -38,6 +41,11 @@ export function openImagePreviewDialog(image: { src: string; alt: string }) {
isImagePreviewDialogOpen.value = true isImagePreviewDialogOpen.value = true
} }
export function openEditHistoryDialog(edit: StatusEdit) {
statusEdit.value = edit
isEditHistoryDialogOpen.value = true
}
export function openPreviewHelp() { export function openPreviewHelp() {
isPreviewHelpOpen.value = true isPreviewHelpOpen.value = true
} }

View File

@ -1,4 +1,4 @@
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef, UseTimeAgoOptions } from '@vueuse/core'
export const useFormattedDateTime = ( export const useFormattedDateTime = (
value: MaybeRef<string | Date | undefined>, value: MaybeRef<string | Date | undefined>,
@ -10,3 +10,35 @@ export const useFormattedDateTime = (
return v ? formatter.format(new Date(v)) : '' return v ? formatter.format(new Date(v)) : ''
}) })
} }
export const timeAgoOptions: UseTimeAgoOptions<false> = {
showSecond: true,
messages: {
justNow: 'just now',
past: n => n,
future: n => n.match(/\d/) ? `in ${n}` : n,
month: (n, past) => n === 1
? past
? 'last month'
: 'next month'
: `${n}m`,
year: (n, past) => n === 1
? past
? 'last year'
: 'next year'
: `${n}y`,
day: (n, past) => n === 1
? past
? 'yesterday'
: 'tomorrow'
: `${n}d`,
week: (n, past) => n === 1
? past
? 'last week'
: 'next week'
: `${n} week${n > 1 ? 's' : ''}`,
hour: n => `${n}h`,
minute: n => `${n}min`,
second: n => `${n}s`,
},
}