diff --git a/components/common/dropdown/DropdownItem.vue b/components/common/dropdown/DropdownItem.vue index 54f1fac9..7dc93b7e 100644 --- a/components/common/dropdown/DropdownItem.vue +++ b/components/common/dropdown/DropdownItem.vue @@ -8,10 +8,10 @@ defineProps<{ }>() const emit = defineEmits(['click']) -const { hide } = inject(dropdownContextKey)! +const { hide } = inject(dropdownContextKey, undefined) || {} const handleClick = (evt: MouseEvent) => { - hide() + hide?.() emit('click', evt) } </script> @@ -23,7 +23,7 @@ const handleClick = (evt: MouseEvent) => { > <div v-if="icon" :class="icon" /> <div flex="~ col"> - <div text-15px font-700> + <div text-15px> <slot /> </div> <div text-3 text="gray/90"> diff --git a/components/modal/ModalContainer.vue b/components/modal/ModalContainer.vue index 49122f83..9a1b5770 100644 --- a/components/modal/ModalContainer.vue +++ b/components/modal/ModalContainer.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> -import { isImagePreviewDialogOpen, isPreviewHelpOpen, isPublishDialogOpen, isSigninDialogOpen, isUserSwitcherOpen } from '~/composables/dialog' +import { isEditHistoryDialogOpen, isImagePreviewDialogOpen, isPreviewHelpOpen, isPublishDialogOpen, isSigninDialogOpen, isUserSwitcherOpen } from '~/composables/dialog' </script> <template> @@ -18,4 +18,7 @@ import { isImagePreviewDialogOpen, isPreviewHelpOpen, isPublishDialogOpen, isSig <ModalDialog v-model="isImagePreviewDialogOpen"> <img :src="imagePreview.src" :alt="imagePreview.alt" max-w-95vw max-h-95vh> </ModalDialog> + <ModalDialog v-model="isEditHistoryDialogOpen"> + <StatusEditPreview :edit="statusEdit" /> + </ModalDialog> </template> diff --git a/components/status/StatusCard.vue b/components/status/StatusCard.vue index 81af3578..def01689 100644 --- a/components/status/StatusCard.vue +++ b/components/status/StatusCard.vue @@ -34,37 +34,7 @@ function go() { } const createdAt = useFormattedDateTime(status.createdAt) -const timeago = useTimeAgo(() => status.createdAt, { - 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`, - }, -}) +const timeago = useTimeAgo(() => status.createdAt, timeAgoOptions) </script> <template> @@ -90,7 +60,7 @@ const timeago = useTimeAgo(() => status.createdAt, { </time> </a> </CommonTooltip> - <StatusEditIndicator :status="status" /> + <StatusEditIndicator :status="status" inline /> </div> </div> <StatusReplyingTo v-if="status.inReplyToAccountId" :status="status" pt1 /> diff --git a/components/status/StatusDetails.vue b/components/status/StatusDetails.vue index 0a9fda0e..4b59992b 100644 --- a/components/status/StatusDetails.vue +++ b/components/status/StatusDetails.vue @@ -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> <div>{{ createdAt }}</div> - <StatusEditIndicator :status="status" /> + <StatusEditIndicator + :status="status" + :inline="false" + > + <span ml1 font-bold cursor-pointer>(Edited)</span> + </StatusEditIndicator> </div> <div>ยท</div> <CommonTooltip :content="visibility.label" placement="bottom"> diff --git a/components/status/StatusEditIndicator.vue b/components/status/StatusEditIndicator.vue deleted file mode 100644 index 69fc5243..00000000 --- a/components/status/StatusEditIndicator.vue +++ /dev/null @@ -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 - > *</time> - </CommonTooltip> -</template> diff --git a/components/status/StatusSpoiler.vue b/components/status/StatusSpoiler.vue index 14df3326..c80cd841 100644 --- a/components/status/StatusSpoiler.vue +++ b/components/status/StatusSpoiler.vue @@ -1,6 +1,5 @@ <script setup lang="ts"> const props = defineProps<{ enabled: boolean }>() -defineSlots<'spoiler'>() const [showContent, toggleContent] = $(useToggle(!props.enabled)) </script> diff --git a/components/status/edit/StatusEditHistory.vue b/components/status/edit/StatusEditHistory.vue new file mode 100644 index 00000000..38210bd1 --- /dev/null +++ b/components/status/edit/StatusEditHistory.vue @@ -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> diff --git a/components/status/edit/StatusEditIndicator.vue b/components/status/edit/StatusEditIndicator.vue new file mode 100644 index 00000000..dde3e5eb --- /dev/null +++ b/components/status/edit/StatusEditIndicator.vue @@ -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 + > *</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> diff --git a/components/status/edit/StatusEditPreview.vue b/components/status/edit/StatusEditPreview.vue new file mode 100644 index 00000000..51867c19 --- /dev/null +++ b/components/status/edit/StatusEditPreview.vue @@ -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> diff --git a/composables/dialog.ts b/composables/dialog.ts index 848b8a51..cfba8134 100644 --- a/composables/dialog.ts +++ b/composables/dialog.ts @@ -1,7 +1,9 @@ +import type { StatusEdit } from 'masto' import type { Draft } from './statusDrafts' import { STORAGE_KEY_FIRST_VISIT, STORAGE_KEY_ZEN_MODE } from '~/constants' export const imagePreview = ref({ src: '', alt: '' }) +export const statusEdit = ref<StatusEdit>() export const isFirstVisit = useLocalStorage(STORAGE_KEY_FIRST_VISIT, true) export const isZenMode = useLocalStorage(STORAGE_KEY_ZEN_MODE, false) export const toggleZenMode = useToggle(isZenMode) @@ -10,6 +12,7 @@ export const isUserSwitcherOpen = ref(false) export const isSigninDialogOpen = ref(false) export const isPublishDialogOpen = ref(false) export const isImagePreviewDialogOpen = ref(false) +export const isEditHistoryDialogOpen = ref(false) export const isPreviewHelpOpen = ref(isFirstVisit.value) export function openUserSwitcher() { @@ -38,6 +41,11 @@ export function openImagePreviewDialog(image: { src: string; alt: string }) { isImagePreviewDialogOpen.value = true } +export function openEditHistoryDialog(edit: StatusEdit) { + statusEdit.value = edit + isEditHistoryDialogOpen.value = true +} + export function openPreviewHelp() { isPreviewHelpOpen.value = true } diff --git a/composables/time.ts b/composables/time.ts index b2dc268f..4e51597a 100644 --- a/composables/time.ts +++ b/composables/time.ts @@ -1,4 +1,4 @@ -import type { MaybeRef } from '@vueuse/core' +import type { MaybeRef, UseTimeAgoOptions } from '@vueuse/core' export const useFormattedDateTime = ( value: MaybeRef<string | Date | undefined>, @@ -10,3 +10,35 @@ export const useFormattedDateTime = ( 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`, + }, +}