feat: support quote post (#3443)
This commit is contained in:
parent
4d13eb9e92
commit
0198a2bf43
32 changed files with 488 additions and 69 deletions
|
|
@ -71,7 +71,6 @@ async function removeList() {
|
|||
if (confirmDelete.choice === 'confirm') {
|
||||
await nextTick()
|
||||
try {
|
||||
// @ts-expect-error this method should not take any argument, but it expects 1-2 arguments since masto.js v7.7.0 (potential issue)
|
||||
await client.v1.lists.$select(list.value.id).remove()
|
||||
emit('listRemoved', list.value.id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,10 @@ const shortcutItemGroups = computed<ShortcutItemGroup[]>(() => [
|
|||
description: t('magic_keys.groups.actions.boost'),
|
||||
shortcut: { keys: ['b'], isSequence: false },
|
||||
},
|
||||
{
|
||||
description: t('magic_keys.groups.actions.quote'),
|
||||
shortcut: { keys: ['q'], isSequence: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import {
|
|||
isConfirmDialogOpen,
|
||||
isEditHistoryDialogOpen,
|
||||
isErrorDialogOpen,
|
||||
isFavouritedBoostedByDialogOpen,
|
||||
isKeyboardShortcutsDialogOpen,
|
||||
isMediaPreviewOpen,
|
||||
isPreviewHelpOpen,
|
||||
isPublishDialogOpen,
|
||||
isReactedByDialogOpen,
|
||||
isReportDialogOpen,
|
||||
isSigninDialogOpen,
|
||||
} from '~/composables/dialog'
|
||||
|
|
@ -48,8 +48,8 @@ function handleConfirmChoice(choice: ConfirmDialogChoice) {
|
|||
isConfirmDialogOpen.value = false
|
||||
}
|
||||
|
||||
function handleFavouritedBoostedByClose() {
|
||||
isFavouritedBoostedByDialogOpen.value = false
|
||||
function handleReactedByClose() {
|
||||
isReactedByDialogOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -95,11 +95,11 @@ function handleFavouritedBoostedByClose() {
|
|||
<ModalError v-if="errorDialogData" v-bind="errorDialogData" />
|
||||
</ModalDialog>
|
||||
<ModalDialog
|
||||
v-model="isFavouritedBoostedByDialogOpen"
|
||||
v-model="isReactedByDialogOpen"
|
||||
max-w-180
|
||||
@close="handleFavouritedBoostedByClose"
|
||||
@close="handleReactedByClose"
|
||||
>
|
||||
<StatusFavouritedBoostedBy />
|
||||
<StatusReactedBy />
|
||||
</ModalDialog>
|
||||
<ModalDialog v-model="isKeyboardShortcutsDialogOpen" max-w-full sm:max-w-140 md:max-w-170 lg:max-w-220 md:min-w-160>
|
||||
<MagickeysKeyboardShortcuts @close="closeKeyboardShortcuts()" />
|
||||
|
|
|
|||
42
app/components/publish/PublishQuoteApprovalPicker.vue
Normal file
42
app/components/publish/PublishQuoteApprovalPicker.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<script setup lang="ts">
|
||||
import type { mastodon } from 'masto'
|
||||
import { statusQuoteApprovalPolicies } from '~/composables/masto/icons'
|
||||
|
||||
defineProps<{
|
||||
editing?: boolean
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string | null | undefined>({
|
||||
required: true,
|
||||
})
|
||||
|
||||
const currentQuoteApprovalPolicy = computed(() =>
|
||||
statusQuoteApprovalPolicies.find(v => v.value === modelValue.value) || statusQuoteApprovalPolicies[0],
|
||||
)
|
||||
|
||||
function chooseQuoteApprovalPolicy(quoteApprovalPolicy: mastodon.rest.v1.QuoteApprovalPolicy) {
|
||||
modelValue.value = quoteApprovalPolicy
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex items-center>
|
||||
<div i-ri:double-quotes-l me--2 />
|
||||
<CommonTooltip placement="top" :content="editing ? $t(`quote_approval_policy.${currentQuoteApprovalPolicy.value}`) : $t('tooltip.change_quote_approval_policy')">
|
||||
<CommonDropdown placement="bottom">
|
||||
<slot :quote-approval-policy="currentQuoteApprovalPolicy" />
|
||||
<template #popper>
|
||||
<CommonDropdownItem
|
||||
v-for="quoteApprovalPolicy in statusQuoteApprovalPolicies"
|
||||
:key="quoteApprovalPolicy.value"
|
||||
:icon="quoteApprovalPolicy.icon"
|
||||
:text="$t(`quote_approval_policy.${quoteApprovalPolicy.value}`)"
|
||||
:description="$t(`quote_approval_policy.${quoteApprovalPolicy.value}_desc`)"
|
||||
:checked="quoteApprovalPolicy.value === modelValue"
|
||||
@click="chooseQuoteApprovalPolicy(quoteApprovalPolicy.value)"
|
||||
/>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</CommonTooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
const { draftKey, draftItemIndex } = defineProps<{
|
||||
draftKey: string
|
||||
draftKey: DraftKey
|
||||
draftItemIndex: number
|
||||
}>()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import type { mastodon } from 'masto'
|
||||
|
||||
defineProps<{
|
||||
editing?: boolean
|
||||
}>()
|
||||
|
|
@ -11,7 +13,7 @@ const currentVisibility = computed(() =>
|
|||
statusVisibilities.find(v => v.value === modelValue.value) || statusVisibilities[0],
|
||||
)
|
||||
|
||||
function chooseVisibility(visibility: string) {
|
||||
function chooseVisibility(visibility: mastodon.v1.StatusVisibility) {
|
||||
modelValue.value = visibility
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import type { DraftItem } from '#shared/types'
|
||||
import type { DraftItem, DraftKey } from '#shared/types'
|
||||
import type { mastodon } from 'masto'
|
||||
import { EditorContent } from '@tiptap/vue-3'
|
||||
import { useNow } from '@vueuse/core'
|
||||
|
|
@ -13,7 +13,7 @@ const {
|
|||
placeholder,
|
||||
initial = getDefaultDraftItem,
|
||||
} = defineProps<{
|
||||
draftKey: string
|
||||
draftKey: DraftKey
|
||||
draftItemIndex: number
|
||||
initial?: () => DraftItem
|
||||
threadComposer?: ReturnType<typeof useThreadComposer>
|
||||
|
|
@ -251,6 +251,28 @@ const postLanguageDisplay = computed(() => languagesNameList.find(i => i.code ==
|
|||
|
||||
const isDM = computed(() => draft.value.params.visibility === 'direct')
|
||||
|
||||
const hasQuote = computed(() => !!draft.value.params.quotedStatusId)
|
||||
const quotedStatus = ref<mastodon.v1.Status | null>(null)
|
||||
const quoteFetchError = ref<string | null>(null)
|
||||
watchEffect(async () => {
|
||||
if (hasQuote.value) {
|
||||
try {
|
||||
quotedStatus.value = await fetchStatus(draft.value.params.quotedStatusId!)
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
quoteFetchError.value = (err as Error).message
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function removeQuote() {
|
||||
draft.value.params.quotedStatusId = undefined
|
||||
draft.value.params.quoteApprovalPolicy = undefined
|
||||
quotedStatus.value = null
|
||||
quoteFetchError.value = null
|
||||
}
|
||||
|
||||
async function handlePaste(evt: ClipboardEvent) {
|
||||
const files = evt.clipboardData?.files
|
||||
if (!files || files.length === 0)
|
||||
|
|
@ -552,6 +574,33 @@ const detectLanguage = useDebounceFn(async () => {
|
|||
}}</span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template v-if="hasQuote">
|
||||
<div flex justify-end mt-2>
|
||||
<button
|
||||
text-sm px-2 py-1 rounded-3 hover:bg-gray-300
|
||||
flex="~ gap1" items-center
|
||||
:aria-label="$t('action.remove_quote')"
|
||||
@click="removeQuote"
|
||||
>
|
||||
<div i-ri:close-line />
|
||||
{{ $t('action.remove_quote') }}
|
||||
</button>
|
||||
</div>
|
||||
<blockquote v-if="quotedStatus" b="~ base 1" rounded-lg overflow-hidden my-3>
|
||||
<StatusCard
|
||||
:status="quotedStatus"
|
||||
:actions="false"
|
||||
:is-nested="true"
|
||||
/>
|
||||
</blockquote>
|
||||
<div v-else-if="quoteFetchError" text-danger b="base 1" rounded-lg hover:bg-active my-3 p-3>
|
||||
{{ $t('error.quote_fetch_error') }} ({{ quoteFetchError }})
|
||||
</div>
|
||||
<StatusCardSkeleton v-else b="base 1" rounded-lg hover:bg-active my-3 />
|
||||
</template>
|
||||
|
||||
<!-- toolbar -->
|
||||
<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')">
|
||||
|
|
@ -685,6 +734,18 @@ const detectLanguage = useDebounceFn(async () => {
|
|||
</template>
|
||||
</PublishVisibilityPicker>
|
||||
|
||||
<PublishQuoteApprovalPicker v-if="hasQuote" v-model="draft.params.quoteApprovalPolicy" :editing="!!draft.editingStatus">
|
||||
<template #default="{ quoteApprovalPolicy }">
|
||||
<button
|
||||
:disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')"
|
||||
btn-action-icon :class="{ 'w-12': !draft.editingStatus }"
|
||||
>
|
||||
<div :class="quoteApprovalPolicy.icon" />
|
||||
<div v-if="!draft.editingStatus" i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</template>
|
||||
</PublishQuoteApprovalPicker>
|
||||
|
||||
<PublishThreadTools :draft-item-index="draftItemIndex" :draft-key="draftKey" />
|
||||
|
||||
<CommonTooltip
|
||||
|
|
|
|||
|
|
@ -6,16 +6,25 @@ const route = useRoute()
|
|||
const { formatNumber } = useHumanReadableNumber()
|
||||
const timeAgoOptions = useTimeAgoOptions()
|
||||
|
||||
const draftKey = ref('home')
|
||||
const draftKey = ref<DraftKey>('home')
|
||||
|
||||
const draftKeys = computed(() => Object.keys(currentUserDrafts.value))
|
||||
const draftKeys = computed<DraftKey[]>(() => Object.keys(currentUserDrafts.value) as DraftKey[])
|
||||
const nonEmptyDrafts = computed(() => draftKeys.value
|
||||
.filter(i => i !== draftKey.value && !isEmptyDraft(currentUserDrafts.value[i]))
|
||||
.map(i => [i, currentUserDrafts.value[i]] as const),
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
draftKey.value = route.query.draft?.toString() || 'home'
|
||||
const quotedStatusId = route.query.quote?.toString()
|
||||
if (quotedStatusId) {
|
||||
draftKey.value = 'quote'
|
||||
currentUserDrafts.value[draftKey.value] = [getDefaultDraftItem({ quotedStatusId })]
|
||||
}
|
||||
else {
|
||||
const key = route.query.draft?.toString() || 'home'
|
||||
if (isDraftKey(key))
|
||||
draftKey.value = key
|
||||
}
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const {
|
|||
initial = getDefaultDraftItem,
|
||||
expanded = false,
|
||||
} = defineProps<{
|
||||
draftKey: string
|
||||
draftKey: DraftKey
|
||||
initial?: () => DraftItem
|
||||
placeholder?: string
|
||||
inReplyToId?: string
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@ const {
|
|||
status,
|
||||
isLoading,
|
||||
canReblog,
|
||||
canQuote,
|
||||
toggleBookmark,
|
||||
toggleFavourite,
|
||||
toggleReblog,
|
||||
composeWithQuote,
|
||||
} = useStatusActions({ status: props.status })
|
||||
|
||||
function reply() {
|
||||
|
|
@ -73,6 +75,27 @@ function reply() {
|
|||
</StatusActionButton>
|
||||
</div>
|
||||
|
||||
<div flex-1>
|
||||
<StatusActionButton
|
||||
:content="$t('action.quote')"
|
||||
:text="!getPreferences(userSettings, 'hideQuoteCount') && status.quotesCount ? status.quotesCount : ''"
|
||||
color="text-purple" hover="text-purple" elk-group-hover="bg-purple/10"
|
||||
icon="i-ri:double-quotes-l"
|
||||
active-icon="i-ri:double-quotes-l"
|
||||
inactive-icon="i-ri:double-quotes-l"
|
||||
:disabled="!canQuote"
|
||||
:command="command"
|
||||
@click="composeWithQuote()"
|
||||
>
|
||||
<template v-if="status.quotesCount && !getPreferences(userSettings, 'hideQuoteCount')" #text>
|
||||
<CommonLocalizedNumber
|
||||
keypath="action.quote_count"
|
||||
:count="status.quotesCount"
|
||||
/>
|
||||
</template>
|
||||
</StatusActionButton>
|
||||
</div>
|
||||
|
||||
<div flex-1>
|
||||
<StatusActionButton
|
||||
:content="$t(status.favourited ? 'action.favourited' : 'action.favourite')"
|
||||
|
|
|
|||
|
|
@ -126,8 +126,8 @@ async function editStatus() {
|
|||
emit('afterEdit')
|
||||
}
|
||||
|
||||
function showFavoritedAndBoostedBy() {
|
||||
openFavoridedBoostedByDialog(status.value.id)
|
||||
function showReactedBy() {
|
||||
openReactedByDialog(status.value.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -194,10 +194,10 @@ function showFavoritedAndBoostedBy() {
|
|||
|
||||
<CommonDropdownItem
|
||||
is="button"
|
||||
:text="$t('menu.show_favourited_and_boosted_by')"
|
||||
:text="$t('menu.show_reacted_by')"
|
||||
icon="i-ri:hearts-line"
|
||||
:command="command"
|
||||
@click="showFavoritedAndBoostedBy()"
|
||||
@click="showReactedBy()"
|
||||
/>
|
||||
|
||||
<CommonDropdownItem
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ const {
|
|||
status,
|
||||
newer,
|
||||
withAction = true,
|
||||
isNested = false,
|
||||
} = defineProps<{
|
||||
status: mastodon.v1.Status | mastodon.v1.StatusEdit
|
||||
newer?: mastodon.v1.Status
|
||||
withAction?: boolean
|
||||
isNested?: boolean
|
||||
}>()
|
||||
|
||||
const { translation } = await useTranslation(status, getLanguageCode())
|
||||
|
|
@ -38,6 +40,7 @@ const vnode = computed(() => {
|
|||
<component :is="vnode" v-if="vnode" />
|
||||
</span>
|
||||
<div v-else />
|
||||
<StatusQuote :status="status" :is-nested="isNested" />
|
||||
<template v-if="translation.visible">
|
||||
<div my2 h-px border="b base" bg-base />
|
||||
<ContentRich v-if="translation.success" class="line-compact" :content="translation.text" :emojis="status.emojis" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { mastodon } from 'masto'
|
||||
|
||||
const { actions = true, older, newer, hasOlder, hasNewer, main, account, ...props } = defineProps<{
|
||||
const { actions = true, isNested = false, older, newer, hasOlder, hasNewer, main, account, ...props } = defineProps<{
|
||||
status: mastodon.v1.Status
|
||||
followedTag?: string | null
|
||||
actions?: boolean
|
||||
|
|
@ -9,6 +9,7 @@ const { actions = true, older, newer, hasOlder, hasNewer, main, account, ...prop
|
|||
hover?: boolean
|
||||
inNotification?: boolean
|
||||
isPreview?: boolean
|
||||
isNested?: boolean
|
||||
|
||||
// If we know the prev and next status in the timeline, we can simplify the card
|
||||
older?: mastodon.v1.Status
|
||||
|
|
@ -211,6 +212,7 @@ const forceShow = ref(false)
|
|||
:context="context"
|
||||
:is-preview="isPreview"
|
||||
:in-notification="inNotification"
|
||||
:is-nested="isNested"
|
||||
mb2 :class="{ 'mt-2 mb1': isDM }"
|
||||
/>
|
||||
<StatusActions v-if="actions !== false" v-show="!getPreferences(userSettings, 'zenMode')" :status="status" />
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const { status, context } = defineProps<{
|
|||
context?: mastodon.v2.FilterContext | 'details'
|
||||
isPreview?: boolean
|
||||
inNotification?: boolean
|
||||
isNested: boolean
|
||||
}>()
|
||||
|
||||
const isDM = computed(() => status.visibility === 'direct')
|
||||
|
|
@ -41,7 +42,7 @@ const allowEmbeddedMedia = computed(() => status.card?.html && embeddedMediaPref
|
|||
'ms--3.5 mt--1 ms--1': isDM && context !== 'details',
|
||||
}"
|
||||
>
|
||||
<StatusBody v-if="(!isFiltered && isSensitiveNonSpoiler) || hideAllMedia" :status="status" :newer="newer" :with-action="!isDetails" :class="isDetails ? 'text-xl' : ''" />
|
||||
<StatusBody v-if="(!isFiltered && isSensitiveNonSpoiler) || hideAllMedia" :status="status" :newer="newer" :with-action="!isDetails" :is-nested="isNested" :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>
|
||||
|
|
@ -51,7 +52,7 @@ const allowEmbeddedMedia = computed(() => status.card?.html && embeddedMediaPref
|
|||
<template v-else-if="filterPhrase" #spoiler>
|
||||
<p>{{ `${$t('status.filter_hidden_phrase')}: ${filterPhrase}` }}</p>
|
||||
</template>
|
||||
<StatusBody v-if="!(isSensitiveNonSpoiler || hideAllMedia)" :status="status" :newer="newer" :with-action="!isDetails" :class="isDetails ? 'text-xl' : ''" />
|
||||
<StatusBody v-if="!(isSensitiveNonSpoiler || hideAllMedia)" :status="status" :newer="newer" :with-action="!isDetails" :is-nested="isNested" :class="isDetails ? 'text-xl' : ''" />
|
||||
<StatusTranslation :status="status" />
|
||||
<StatusPoll v-if="status.poll" :status="status" />
|
||||
<StatusMedia
|
||||
|
|
@ -60,7 +61,7 @@ const allowEmbeddedMedia = computed(() => status.card?.html && embeddedMediaPref
|
|||
:is-preview="isPreview"
|
||||
/>
|
||||
<StatusPreviewCard
|
||||
v-if="status.card && !allowEmbeddedMedia"
|
||||
v-if="status.card && !allowEmbeddedMedia && !isNested"
|
||||
:card="status.card"
|
||||
:small-picture-only="status.mediaAttachments?.length > 0"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import type { mastodon } from 'masto'
|
||||
|
||||
const { actions = true, ...props } = defineProps<{
|
||||
const { actions = true, isNested = false, ...props } = defineProps<{
|
||||
status: mastodon.v1.Status
|
||||
newer?: mastodon.v1.Status
|
||||
command?: boolean
|
||||
actions?: boolean
|
||||
isNested?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
|
|
@ -35,7 +36,7 @@ useHydratedHead({
|
|||
<AccountInfo :account="status.account" />
|
||||
</AccountHoverWrapper>
|
||||
</NuxtLink>
|
||||
<StatusContent :status="status" :newer="newer" context="details" />
|
||||
<StatusContent :status="status" :newer="newer" context="details" :is-nested="isNested" />
|
||||
<div flex="~ gap-1" items-center text-secondary text-sm>
|
||||
<div flex>
|
||||
<div>{{ createdAt }}</div>
|
||||
|
|
|
|||
134
app/components/status/StatusQuote.vue
Normal file
134
app/components/status/StatusQuote.vue
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<script setup lang="ts">
|
||||
import type { mastodon } from 'masto'
|
||||
|
||||
const {
|
||||
status,
|
||||
isNested = false,
|
||||
} = defineProps<{
|
||||
status: mastodon.v1.Status | mastodon.v1.StatusEdit
|
||||
isNested?: boolean
|
||||
}>()
|
||||
|
||||
function isQuoteType(quote: mastodon.v1.Status['quote']): quote is mastodon.v1.Quote | mastodon.v1.ShallowQuote {
|
||||
return !!quote
|
||||
}
|
||||
|
||||
function isShallowQuoteType(quote: mastodon.v1.Quote | mastodon.v1.ShallowQuote): quote is mastodon.v1.ShallowQuote {
|
||||
return 'quotedStatusId' in quote
|
||||
}
|
||||
|
||||
const quoteState = computed<mastodon.v1.QuoteState | null>(() => {
|
||||
if (!isQuoteType(status.quote)) {
|
||||
return null
|
||||
}
|
||||
return status.quote.state
|
||||
})
|
||||
const shallowQuotedStatus = ref<mastodon.v1.Status | null>(null)
|
||||
watchEffect(async () => {
|
||||
if (!isQuoteType(status.quote) || !isShallowQuoteType(status.quote) || quoteState.value === 'deleted' || !status.quote.quotedStatusId) {
|
||||
shallowQuotedStatus.value = null
|
||||
return
|
||||
}
|
||||
shallowQuotedStatus.value = await fetchStatus(status.quote.quotedStatusId)
|
||||
})
|
||||
|
||||
const quotedStatus = computed(() => {
|
||||
if (!isQuoteType(status.quote)) {
|
||||
return null
|
||||
}
|
||||
if (isShallowQuoteType(status.quote)) {
|
||||
if (!status.quote.quotedStatusId) {
|
||||
return null
|
||||
}
|
||||
return shallowQuotedStatus.value
|
||||
}
|
||||
return status.quote.quotedStatus
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="quotedStatus">
|
||||
<template v-if="isNested && quoteState">
|
||||
<div
|
||||
v-if="quoteState === 'pending'"
|
||||
flex b="~ 1" rounded-lg bg-card mt-3 p-3
|
||||
>
|
||||
Post pending for approval by author
|
||||
</div>
|
||||
<div
|
||||
v-else-if="quoteState === 'revoked'"
|
||||
flex b="~ 1" rounded-lg bg-card mt-3 p-3
|
||||
>
|
||||
Post removed by author
|
||||
</div>
|
||||
<div
|
||||
v-else-if="quoteState === 'blocked_account'"
|
||||
flex b="~ 1" rounded-lg bg-card mt-3 p-3
|
||||
>
|
||||
Post by blocked author
|
||||
</div>
|
||||
<div
|
||||
v-else-if="quoteState === 'blocked_domain'"
|
||||
flex b="~ 1" rounded-lg bg-card mt-3 p-3
|
||||
>
|
||||
Post from blocked server
|
||||
</div>
|
||||
<div
|
||||
v-else-if="quoteState === 'muted_account'"
|
||||
flex b="~ 1" rounded-lg bg-card mt-3 p-3
|
||||
>
|
||||
Post by muted author
|
||||
</div>
|
||||
<div
|
||||
v-else-if="quoteState === 'deleted' || quoteState === 'rejected' || quoteState === 'unauthorized'"
|
||||
flex b="~ 1" rounded-lg bg-card mt-3 p-3
|
||||
>
|
||||
Post is unavailable
|
||||
</div>
|
||||
<div
|
||||
v-else-if="quoteState === 'accepted'"
|
||||
flex b="~ 1" rounded-lg bg-card mt-3 p-3
|
||||
>
|
||||
Post by
|
||||
<AccountInlineInfo :account="quotedStatus.account" :link="false" mx-1 />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="quoteState === 'pending'"
|
||||
flex b="~ 1" rounded-lg bg-card mt-3 p-3
|
||||
>
|
||||
Post pending for approval by author
|
||||
</div>
|
||||
<div
|
||||
v-else-if="quoteState === 'revoked'"
|
||||
flex b="~ 1" rounded-lg bg-card mt-3 p-3
|
||||
>
|
||||
Post removed by author
|
||||
</div>
|
||||
<div
|
||||
v-else-if="quoteState === 'deleted' || quoteState === 'rejected' || quoteState === 'unauthorized'"
|
||||
flex b="~ 1" rounded-lg bg-card mt-3 p-3
|
||||
>
|
||||
Post is unavailable
|
||||
</div>
|
||||
<blockquote
|
||||
v-else-if="quoteState === 'accepted'"
|
||||
:cite="quotedStatus.uri"
|
||||
>
|
||||
<StatusCard
|
||||
:status="quotedStatus"
|
||||
:actions="false"
|
||||
:is-nested="true"
|
||||
b="base 1" rounded-lg hover:bg-active my-3
|
||||
/>
|
||||
</blockquote>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,12 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import { favouritedBoostedByStatusId } from '~/composables/dialog'
|
||||
import { reactedByStatusId } from '~/composables/dialog'
|
||||
|
||||
const type = ref<'favourited-by' | 'boosted-by'>('favourited-by')
|
||||
const type = ref<'favourited-by' | 'boosted-by' | 'quoted-by'>('favourited-by')
|
||||
|
||||
const { client } = useMasto()
|
||||
|
||||
function load() {
|
||||
return client.value.v1.statuses.$select(favouritedBoostedByStatusId.value!)[type.value === 'favourited-by' ? 'favouritedBy' : 'rebloggedBy'].list()
|
||||
if (type.value !== 'quoted-by') {
|
||||
const accounts = client.value.v1.statuses.$select(reactedByStatusId.value!)[type.value === 'favourited-by' ? 'favouritedBy' : 'rebloggedBy'].list()
|
||||
return accounts
|
||||
}
|
||||
else {
|
||||
const quotes = client.value.v1.statuses.$select(reactedByStatusId.value!).quotes.list()
|
||||
// @ts-expect-error waiting for masto.js v7.9.0 release (quotes)
|
||||
return quotes.map(quote => quote.account)
|
||||
}
|
||||
}
|
||||
|
||||
const paginator = computed(() => load())
|
||||
|
|
@ -19,6 +27,10 @@ function showRebloggedBy() {
|
|||
type.value = 'boosted-by'
|
||||
}
|
||||
|
||||
function showQuotedBy() {
|
||||
type.value = 'quoted-by'
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const tabs = [
|
||||
{
|
||||
|
|
@ -31,6 +43,11 @@ const tabs = [
|
|||
display: t('status.boosted_by'),
|
||||
onClick: showRebloggedBy,
|
||||
},
|
||||
{
|
||||
name: 'quoted-by',
|
||||
display: t('status.quoted_by'),
|
||||
onClick: showQuotedBy,
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ConfirmDialogChoice, ConfirmDialogOptions, DraftItem, ErrorDialogData } from '#shared/types'
|
||||
import type { ConfirmDialogChoice, ConfirmDialogOptions, DraftItem, DraftKey, ErrorDialogData } from '#shared/types'
|
||||
import type { mastodon } from 'masto'
|
||||
|
||||
export const confirmDialogChoice = ref<ConfirmDialogChoice>()
|
||||
|
|
@ -9,7 +9,7 @@ export const mediaPreviewList = ref<mastodon.v1.MediaAttachment[]>([])
|
|||
export const mediaPreviewIndex = ref(0)
|
||||
|
||||
export const statusEdit = ref<mastodon.v1.StatusEdit>()
|
||||
export const dialogDraftKey = ref<string>()
|
||||
export const dialogDraftKey = ref<DraftKey>()
|
||||
|
||||
export const reportAccount = ref<mastodon.v1.Account>()
|
||||
export const reportStatus = ref<mastodon.v1.Status>()
|
||||
|
|
@ -25,12 +25,12 @@ export const isPreviewHelpOpen = ref(false)
|
|||
export const isCommandPanelOpen = ref(false)
|
||||
export const isConfirmDialogOpen = ref(false)
|
||||
export const isErrorDialogOpen = ref(false)
|
||||
export const isFavouritedBoostedByDialogOpen = ref(false)
|
||||
export const isReactedByDialogOpen = ref(false)
|
||||
export const isReportDialogOpen = ref(false)
|
||||
|
||||
export const lastPublishDialogStatus = ref<mastodon.v1.Status | null>(null)
|
||||
|
||||
export const favouritedBoostedByStatusId = ref<string | null>(null)
|
||||
export const reactedByStatusId = ref<string | null>(null)
|
||||
|
||||
export function openSigninDialog() {
|
||||
isSigninDialogOpen.value = true
|
||||
|
|
@ -46,7 +46,7 @@ export async function openConfirmDialog(label: ConfirmDialogOptions | string): P
|
|||
return confirmDialogChoice.value!
|
||||
}
|
||||
|
||||
export async function openPublishDialog(draftKey = 'dialog', draft?: DraftItem, overwrite = false): Promise<void> {
|
||||
export async function openPublishDialog(draftKey: DraftKey = 'dialog', draft?: DraftItem, overwrite = false): Promise<void> {
|
||||
dialogDraftKey.value = draftKey
|
||||
|
||||
if (draft) {
|
||||
|
|
@ -69,9 +69,9 @@ export async function openPublishDialog(draftKey = 'dialog', draft?: DraftItem,
|
|||
await until(isPublishDialogOpen).toBe(false)
|
||||
}
|
||||
|
||||
export async function openFavoridedBoostedByDialog(statusId: string) {
|
||||
isFavouritedBoostedByDialogOpen.value = true
|
||||
favouritedBoostedByStatusId.value = statusId
|
||||
export async function openReactedByDialog(statusId: string) {
|
||||
isReactedByDialogOpen.value = true
|
||||
reactedByStatusId.value = statusId
|
||||
}
|
||||
|
||||
function restoreMediaPreviewFromState() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// @unocss-include
|
||||
import type { mastodon } from 'masto'
|
||||
|
||||
export const accountFieldIcons: Record<string, string> = Object.fromEntries(Object.entries({
|
||||
Alipay: 'i-ri:alipay-line',
|
||||
Bilibili: 'i-ri:bilibili-line',
|
||||
|
|
@ -65,7 +67,10 @@ export function getAccountFieldIcon(value: string) {
|
|||
return accountFieldIconsLowercase[name] || undefined
|
||||
}
|
||||
|
||||
export const statusVisibilities = [
|
||||
export const statusVisibilities: {
|
||||
value: mastodon.v1.StatusVisibility
|
||||
icon: `i-ri:${string}`
|
||||
}[] = [
|
||||
{
|
||||
value: 'public',
|
||||
icon: 'i-ri:global-line',
|
||||
|
|
@ -83,3 +88,21 @@ export const statusVisibilities = [
|
|||
icon: 'i-ri:at-line',
|
||||
},
|
||||
] as const
|
||||
|
||||
export const statusQuoteApprovalPolicies: {
|
||||
value: mastodon.rest.v1.QuoteApprovalPolicy
|
||||
icon: `i-ri:${string}`
|
||||
}[] = [
|
||||
{
|
||||
value: 'public',
|
||||
icon: 'i-ri:global-line',
|
||||
},
|
||||
{
|
||||
value: 'followers',
|
||||
icon: 'i-ri:group-line',
|
||||
},
|
||||
{
|
||||
value: 'nobody',
|
||||
icon: 'i-ri:lock-line',
|
||||
},
|
||||
] as const
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ export function usePublish(options: {
|
|||
language: draftItem.value.params.language || preferredLanguage.value,
|
||||
poll,
|
||||
scheduledAt,
|
||||
quotedStatusId: draftItem.value.params.quotedStatusId,
|
||||
quoteApprovalPolicy: draftItem.value.params.quoteApprovalPolicy,
|
||||
...(isGlitchEdition.value ? { 'content-type': 'text/markdown' } : {}),
|
||||
} as mastodon.rest.v1.CreateScheduledStatusParams
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { mastodon } from 'masto'
|
||||
|
||||
type Action = 'reblogged' | 'favourited' | 'bookmarked' | 'pinned' | 'muted'
|
||||
type CountField = 'reblogsCount' | 'favouritesCount'
|
||||
type CountField = 'reblogsCount' | 'favouritesCount' | 'quotesCount'
|
||||
|
||||
export interface StatusActionsProps {
|
||||
status: mastodon.v1.Status
|
||||
|
|
@ -54,6 +54,12 @@ export function useStatusActions(props: StatusActionsProps) {
|
|||
status.value[countField] += status.value[action] ? 1 : -1
|
||||
}
|
||||
|
||||
const toggleFavourite = () => toggleStatusAction(
|
||||
'favourited',
|
||||
() => client.value.v1.statuses.$select(status.value.id)[status.value.favourited ? 'unfavourite' : 'favourite'](),
|
||||
'favouritesCount',
|
||||
)
|
||||
|
||||
const canReblog = computed(() =>
|
||||
status.value.visibility !== 'direct'
|
||||
&& (status.value.visibility !== 'private' || status.value.account.id === currentUser.value?.account.id),
|
||||
|
|
@ -61,7 +67,6 @@ export function useStatusActions(props: StatusActionsProps) {
|
|||
|
||||
const toggleReblog = () => toggleStatusAction(
|
||||
'reblogged',
|
||||
// @ts-expect-error this method should not take any argument, but it expects 1-2 arguments since masto.js v7.7.0 (potential issue)
|
||||
() => client.value.v1.statuses.$select(status.value.id)[status.value.reblogged ? 'unreblog' : 'reblog']().then((res) => {
|
||||
if (status.value.reblogged)
|
||||
// returns the original status
|
||||
|
|
@ -71,11 +76,14 @@ export function useStatusActions(props: StatusActionsProps) {
|
|||
'reblogsCount',
|
||||
)
|
||||
|
||||
const toggleFavourite = () => toggleStatusAction(
|
||||
'favourited',
|
||||
() => client.value.v1.statuses.$select(status.value.id)[status.value.favourited ? 'unfavourite' : 'favourite'](),
|
||||
'favouritesCount',
|
||||
)
|
||||
const canQuote = computed(() => {
|
||||
if (status.value.visibility === 'private' || status.value.visibility === 'direct')
|
||||
return false
|
||||
|
||||
return status.value.quoteApproval?.currentUser === 'automatic' || status.value.quoteApproval?.currentUser === 'manual'
|
||||
})
|
||||
|
||||
const composeWithQuote = () => navigateTo(`/compose?quote=${status.value.id}`)
|
||||
|
||||
const toggleBookmark = () => toggleStatusAction(
|
||||
'bookmarked',
|
||||
|
|
@ -95,11 +103,13 @@ export function useStatusActions(props: StatusActionsProps) {
|
|||
return {
|
||||
status,
|
||||
isLoading,
|
||||
canQuote,
|
||||
canReblog,
|
||||
toggleMute,
|
||||
toggleReblog,
|
||||
toggleFavourite,
|
||||
toggleBookmark,
|
||||
togglePin,
|
||||
composeWithQuote,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
import type { DraftItem, DraftMap } from '#shared/types'
|
||||
import type { DraftItem, DraftKey, DraftMap } from '#shared/types'
|
||||
import type { Mutable } from '#shared/types/utils'
|
||||
import type { mastodon } from 'masto'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { STORAGE_KEY_DRAFTS } from '~/constants'
|
||||
|
||||
export const currentUserDrafts = (import.meta.server || process.test)
|
||||
? computed<DraftMap>(() => ({}))
|
||||
: useUserLocalStorage<DraftMap>(STORAGE_KEY_DRAFTS, () => ({}))
|
||||
? computed<DraftMap>(() => ({ home: [], dialog: [], intent: [], quote: [] }))
|
||||
: useUserLocalStorage<DraftMap>(STORAGE_KEY_DRAFTS, () => ({ home: [], dialog: [], intent: [], quote: [] }))
|
||||
|
||||
export const builtinDraftKeys = [
|
||||
'dialog',
|
||||
'home',
|
||||
]
|
||||
|
||||
const ALL_VISIBILITY = ['public', 'unlisted', 'private', 'direct'] as const
|
||||
const ALL_VISIBILITY: readonly mastodon.v1.StatusVisibility[] = ['public', 'unlisted', 'private', 'direct'] as const
|
||||
|
||||
function getDefaultVisibility(currentVisibility: mastodon.v1.StatusVisibility) {
|
||||
// The default privacy only should be taken into account if it makes
|
||||
|
|
@ -25,6 +20,18 @@ function getDefaultVisibility(currentVisibility: mastodon.v1.StatusVisibility) {
|
|||
: preferredVisibility
|
||||
}
|
||||
|
||||
const ALL_QUOTE_APPROVAL_POLICY: readonly mastodon.rest.v1.QuoteApprovalPolicy[] = ['public', 'followers', 'nobody'] as const
|
||||
|
||||
function getDefaultQuoteApprovalPolicy(currentQuoteApprovalPolicy: mastodon.rest.v1.QuoteApprovalPolicy) {
|
||||
// The default quote policy only should be taken into account if it makes
|
||||
// the quote permission more restricted
|
||||
const preferredQuoteApprovalPolicy = currentUser.value?.account.source.quotePolicy || 'public'
|
||||
return ALL_QUOTE_APPROVAL_POLICY.indexOf(currentQuoteApprovalPolicy)
|
||||
> ALL_QUOTE_APPROVAL_POLICY.indexOf(preferredQuoteApprovalPolicy)
|
||||
? currentQuoteApprovalPolicy
|
||||
: preferredQuoteApprovalPolicy
|
||||
}
|
||||
|
||||
export function getDefaultDraftItem(options: Partial<Mutable<mastodon.rest.v1.CreateScheduledStatusParams> & Omit<DraftItem, 'params'>> = {}): DraftItem {
|
||||
const {
|
||||
attachments = [],
|
||||
|
|
@ -38,6 +45,8 @@ export function getDefaultDraftItem(options: Partial<Mutable<mastodon.rest.v1.Cr
|
|||
mentions,
|
||||
poll,
|
||||
scheduledAt,
|
||||
quotedStatusId,
|
||||
quoteApprovalPolicy,
|
||||
} = options
|
||||
|
||||
return {
|
||||
|
|
@ -48,6 +57,8 @@ export function getDefaultDraftItem(options: Partial<Mutable<mastodon.rest.v1.Cr
|
|||
poll,
|
||||
scheduledAt,
|
||||
inReplyToId,
|
||||
quotedStatusId,
|
||||
quoteApprovalPolicy: getDefaultQuoteApprovalPolicy(quoteApprovalPolicy || 'public'),
|
||||
visibility: getDefaultVisibility(visibility || 'public'),
|
||||
sensitive: sensitive ?? false,
|
||||
spoilerText: spoilerText || '',
|
||||
|
|
@ -99,7 +110,7 @@ function getAccountsToMention(status: mastodon.v1.Status) {
|
|||
export function getReplyDraft(status: mastodon.v1.Status) {
|
||||
const accountsToMention = getAccountsToMention(status)
|
||||
return {
|
||||
key: `reply-${status.id}`,
|
||||
key: `reply-${status.id}` satisfies DraftKey,
|
||||
draft: () => {
|
||||
return getDefaultDraftItem({
|
||||
initialText: '',
|
||||
|
|
@ -127,9 +138,11 @@ export function isEmptyDraft(drafts: Array<DraftItem> | DraftItem | null | undef
|
|||
const { params, attachments } = draft
|
||||
const status = params.status ?? ''
|
||||
const text = htmlToText(status).trim().replace(/^(@\S+\s?)+/, '').replaceAll(/```/g, '').trim()
|
||||
const hasQuote = !!params.quotedStatusId
|
||||
|
||||
return (text.length > 0)
|
||||
|| (attachments.length > 0)
|
||||
|| hasQuote
|
||||
})
|
||||
|
||||
return !anyDraftHasContent
|
||||
|
|
@ -141,7 +154,7 @@ export interface UseDraft {
|
|||
}
|
||||
|
||||
export function useDraft(
|
||||
draftKey: string,
|
||||
draftKey: DraftKey,
|
||||
initial: () => DraftItem = () => getDefaultDraftItem({}),
|
||||
): UseDraft {
|
||||
const draftItems = computed({
|
||||
|
|
@ -184,11 +197,24 @@ export function privateMentionUser(account: mastodon.v1.Account) {
|
|||
}))
|
||||
}
|
||||
|
||||
export const builtinDraftKeys = [
|
||||
'home',
|
||||
'dialog',
|
||||
'intent',
|
||||
'quote',
|
||||
]
|
||||
|
||||
export function clearEmptyDrafts() {
|
||||
for (const key in currentUserDrafts.value) {
|
||||
if (builtinDraftKeys.includes(key) && !isEmptyDraft(currentUserDrafts.value[key]))
|
||||
continue
|
||||
if (isEmptyDraft(currentUserDrafts.value[key]))
|
||||
delete currentUserDrafts.value[key]
|
||||
if (isDraftKey(key)) {
|
||||
if (builtinDraftKeys.includes(key) && !isEmptyDraft(currentUserDrafts.value[key]))
|
||||
continue
|
||||
if (isEmptyDraft(currentUserDrafts.value[key]))
|
||||
delete currentUserDrafts.value[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isDraftKey(key: string): key is DraftKey {
|
||||
return builtinDraftKeys.includes(key) || key.startsWith('reply-')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface PreferencesSettings {
|
|||
hideAltIndicatorOnPosts: boolean
|
||||
hideGifIndicatorOnPosts: boolean
|
||||
hideBoostCount: boolean
|
||||
hideQuoteCount: boolean
|
||||
hideReplyCount: boolean
|
||||
hideFavoriteCount: boolean
|
||||
hideFollowerCount: boolean
|
||||
|
|
@ -70,6 +71,7 @@ export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = {
|
|||
hideAltIndicatorOnPosts: false,
|
||||
hideGifIndicatorOnPosts: false,
|
||||
hideBoostCount: false,
|
||||
hideQuoteCount: false,
|
||||
hideReplyCount: false,
|
||||
hideFavoriteCount: false,
|
||||
hideFollowerCount: false,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { DraftItem } from '#shared/types'
|
||||
import type { DraftItem, DraftKey } from '#shared/types'
|
||||
import type { mastodon } from 'masto'
|
||||
|
||||
const maxThreadLength = 99
|
||||
|
||||
export function useThreadComposer(draftKey: string, initial?: () => DraftItem) {
|
||||
export function useThreadComposer(draftKey: DraftKey, initial?: () => DraftItem) {
|
||||
const { draftItems } = useDraft(draftKey, initial)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ onMounted(async () => {
|
|||
spoilerText: route.query.spoiler_text as string,
|
||||
visibility: route.query.visibility as mastodon.v1.StatusVisibility,
|
||||
language: route.query.language as string,
|
||||
quotedStatusId: route.query.quote as string,
|
||||
}), true)
|
||||
// TODO: need a better idea 👀
|
||||
await router.replace('/home')
|
||||
|
|
|
|||
|
|
@ -102,6 +102,12 @@ const userSettings = useUserSettings()
|
|||
>
|
||||
{{ $t('settings.preferences.hide_boost_count') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideQuoteCount')"
|
||||
@click="togglePreferences('hideQuoteCount')"
|
||||
>
|
||||
{{ $t('settings.preferences.hide_quote_count') }}
|
||||
</SettingsToggleItem>
|
||||
<SettingsToggleItem
|
||||
:checked="getPreferences(userSettings, 'hideFavoriteCount')"
|
||||
@click="togglePreferences('hideFavoriteCount')"
|
||||
|
|
|
|||
|
|
@ -71,6 +71,15 @@ export default defineNuxtPlugin(({ $scrollToTop }) => {
|
|||
}
|
||||
whenever(logicAnd(isAuthenticated, notUsingInput, keys.b), toggleBoostActiveStatus)
|
||||
|
||||
const composeWithQuote = () => {
|
||||
const quotedStatusId = document.querySelector<HTMLElement>('[aria-roledescription=status-details]')
|
||||
?.getAttribute('id')
|
||||
?.replace('status-', '')
|
||||
if (quotedStatusId)
|
||||
navigateTo(`/compose?quote=${quotedStatusId}`)
|
||||
}
|
||||
whenever(logicAnd(isAuthenticated, notUsingInput, keys.q), composeWithQuote)
|
||||
|
||||
const showNewItems = () => {
|
||||
// TODO: find a better solution than clicking buttons...
|
||||
document
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@
|
|||
"prev": "Prev",
|
||||
"publish": "Publish",
|
||||
"publish_thread": "Publish thread",
|
||||
"quote": "Quote",
|
||||
"quote_count": "{0}",
|
||||
"remove_quote": "Remove quote",
|
||||
"reply": "Reply",
|
||||
"reply_count": "{0}",
|
||||
"reset": "Reset",
|
||||
|
|
@ -189,6 +192,7 @@
|
|||
"account_not_found": "Account {0} not found",
|
||||
"explore_list_empty": "Nothing is trending right now. Check back later!",
|
||||
"file_size_cannot_exceed_n_mb": "File size cannot exceed {0}MB",
|
||||
"quote_fetch_error": "Could not fetch the quoted post",
|
||||
"sign_in_error": "Cannot connect to the server.",
|
||||
"status_not_found": "Post not found",
|
||||
"unsupported_file_format": "Unsupported file format"
|
||||
|
|
@ -240,6 +244,7 @@
|
|||
"command_mode": "Command mode",
|
||||
"compose": "Compose",
|
||||
"favourite": "Favorite",
|
||||
"quote": "Quote",
|
||||
"search": "Search",
|
||||
"show_new_items": "Show new items",
|
||||
"title": "Actions"
|
||||
|
|
@ -289,6 +294,7 @@
|
|||
"share_account": "Share {0}",
|
||||
"share_post": "Share this post",
|
||||
"show_favourited_and_boosted_by": "Show who favorited and boosted",
|
||||
"show_reacted_by": "Show who reacted",
|
||||
"show_reblogs": "Show boosts from {0}",
|
||||
"show_untranslated": "Show untranslated",
|
||||
"toggle_theme": {
|
||||
|
|
@ -402,6 +408,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"quote_approval_policy": {
|
||||
"followers": "Followers only",
|
||||
"followers_desc": "Your followers can quote",
|
||||
"nobody": "Nobody",
|
||||
"nobody_desc": "Only you can quote",
|
||||
"public": "Public",
|
||||
"public_desc": "Anyone can quote"
|
||||
},
|
||||
"report": {
|
||||
"additional_comments": "Additional comments",
|
||||
"another_server": "The user you're reporting is from another server",
|
||||
|
|
@ -557,6 +571,7 @@
|
|||
"hide_follower_count": "Hide following/follower count",
|
||||
"hide_gif_indi_on_posts": "Hide gif indicator on posts",
|
||||
"hide_news": "Hide news",
|
||||
"hide_quote_count": "Hide quote count",
|
||||
"hide_reply_count": "Hide reply count",
|
||||
"hide_tag_hover_card": "Hide tag hover card",
|
||||
"hide_translation": "Hide translation",
|
||||
|
|
@ -650,6 +665,7 @@
|
|||
"finished": "finished {0}",
|
||||
"update": "Update poll"
|
||||
},
|
||||
"quoted_by": "Quoted By",
|
||||
"replying_to": "Replying to {0}",
|
||||
"show_full_thread": "Show Full thread",
|
||||
"someone": "someone",
|
||||
|
|
@ -742,6 +758,7 @@
|
|||
"add_thread_item": "Add item to thread",
|
||||
"change_content_visibility": "Change content visibility",
|
||||
"change_language": "Change language",
|
||||
"change_quote_approval_policy": "Change quote approval policy",
|
||||
"emoji": "Emoji",
|
||||
"explore_links_intro": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
|
||||
"explore_posts_intro": "These posts from this and other servers in the decentralized network are gaining traction on this server right now.",
|
||||
|
|
|
|||
|
|
@ -77,11 +77,15 @@
|
|||
"prev": "前へ",
|
||||
"publish": "投稿",
|
||||
"publish_thread": "スレッドを投稿",
|
||||
"quote": "引用",
|
||||
"quote_count": "{0}",
|
||||
"remove_quote": "引用を削除",
|
||||
"reply": "返信",
|
||||
"reply_count": "{0}",
|
||||
"reset": "リセット",
|
||||
"save": "保存する",
|
||||
"save_changes": "変更を保存",
|
||||
"schedule": "予約",
|
||||
"sign_in": "サインイン",
|
||||
"sign_in_to": "{0}にサインイン",
|
||||
"switch_account": "アカウントの切り替え",
|
||||
|
|
@ -187,6 +191,7 @@
|
|||
"account_not_found": "アカウント {0} が見つかりません",
|
||||
"explore_list_empty": "現在トレンドには何もありません。またあとで!",
|
||||
"file_size_cannot_exceed_n_mb": "{0}MBを超えるファイルはアップロードできません",
|
||||
"quote_fetch_error": "引用投稿が取得できませんでした",
|
||||
"sign_in_error": "サーバーに接続できません。",
|
||||
"status_not_found": "投稿が見つかりません",
|
||||
"unsupported_file_format": "対応していないファイル形式です"
|
||||
|
|
@ -238,6 +243,7 @@
|
|||
"command_mode": "コマンドモード",
|
||||
"compose": "投稿",
|
||||
"favourite": "お気に入り",
|
||||
"quote": "引用",
|
||||
"search": "検索",
|
||||
"show_new_items": "新しいアイテムを表示",
|
||||
"title": "アクション"
|
||||
|
|
@ -400,6 +406,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"quote_approval_policy": {
|
||||
"followers": "フォロワーのみ",
|
||||
"followers_desc": "フォロワーの引用を許可",
|
||||
"nobody": "自分のみ",
|
||||
"nobody_desc": "自分自身の引用のみ許可",
|
||||
"public": "誰でも",
|
||||
"public_desc": "すべての人に引用を許可"
|
||||
},
|
||||
"report": {
|
||||
"additional_comments": "追加コメント",
|
||||
"another_server": "報告しようとしているユーザーは他のサーバーのユーザーです",
|
||||
|
|
@ -555,6 +569,7 @@
|
|||
"hide_follower_count": "フォロワーの数を隠す",
|
||||
"hide_gif_indi_on_posts": "投稿上のGIF表示を隠す",
|
||||
"hide_news": "ニュースを隠す",
|
||||
"hide_quote_count": "引用数を隠す",
|
||||
"hide_reply_count": "リプライの数を隠す",
|
||||
"hide_tag_hover_card": "タグのホバーカードを隠す",
|
||||
"hide_translation": "翻訳を隠す",
|
||||
|
|
@ -617,6 +632,9 @@
|
|||
"publish_failed": "投稿に失敗しました",
|
||||
"publishing": "投稿中",
|
||||
"save_failed": "保存に失敗しました",
|
||||
"schedule_failed": "投稿の予約に失敗しました",
|
||||
"schedule_time_invalid": "予約時刻は少なくとも5分後でなければなりません。{0}以降に設定してください。",
|
||||
"scheduling": "予約投稿中",
|
||||
"upload_failed": "アップロードに失敗しました",
|
||||
"uploading": "更新中..."
|
||||
},
|
||||
|
|
@ -645,6 +663,7 @@
|
|||
"finished": "{0}に完了",
|
||||
"update": "投票を更新"
|
||||
},
|
||||
"quoted_by": "引用したユーザー",
|
||||
"replying_to": "{0}さんへの返信",
|
||||
"show_full_thread": "スレッド全体を表示",
|
||||
"someone": "誰か",
|
||||
|
|
@ -737,6 +756,7 @@
|
|||
"add_thread_item": "投稿をスレッドに追加",
|
||||
"change_content_visibility": "公開範囲を変更",
|
||||
"change_language": "言語を変更",
|
||||
"change_quote_approval_policy": "引用許可ポリシーを変更",
|
||||
"emoji": "絵文字",
|
||||
"explore_links_intro": "これらのニュース記事は、分散型ネットワーク上のこのサーバーや他のサーバー上で現在話題になっています。",
|
||||
"explore_posts_intro": "これらの投稿は、分散型ネットワーク上のこのサーバーや他のサーバー上にあり、現在このサーバー上で注目を集めています。",
|
||||
|
|
@ -745,6 +765,8 @@
|
|||
"pick_an_icon": "アイコンの選択",
|
||||
"publish_failed": "投稿の再公開時にエディタ上部のエラーメッセージを閉じる",
|
||||
"remove_thread_item": "投稿をスレッドから削除",
|
||||
"schedule_failed": "投稿の予約投稿時にエディタ上部のエラーメッセージを閉じる",
|
||||
"schedule_post": "予約投稿",
|
||||
"start_thread": "スレッドを開始",
|
||||
"toggle_bold": "太字",
|
||||
"toggle_code_block": "コードブロック",
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
"js-yaml": "^4.1.0",
|
||||
"lru-cache": "^11.0.0",
|
||||
"magic-string": "^0.30.19",
|
||||
"masto": "^7.8.0",
|
||||
"masto": "^7.10.0",
|
||||
"mocked-exports": "^0.1.1",
|
||||
"node-emoji": "^2.1.3",
|
||||
"nuxt": "^4.1.2",
|
||||
|
|
|
|||
|
|
@ -198,8 +198,8 @@ importers:
|
|||
specifier: ^0.30.19
|
||||
version: 0.30.19
|
||||
masto:
|
||||
specifier: ^7.8.0
|
||||
version: 7.8.0
|
||||
specifier: ^7.10.0
|
||||
version: 7.10.0
|
||||
mocked-exports:
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1
|
||||
|
|
@ -6932,8 +6932,8 @@ packages:
|
|||
marky@1.3.0:
|
||||
resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==}
|
||||
|
||||
masto@7.8.0:
|
||||
resolution: {integrity: sha512-ZU0zQHzyV6I4hTOUv0PBvmLQDvtp+EtOm3/2hwei+CNcfA9E+XcBafThe1gSzNDBAzUbEg/O8fwGLTZV5PJNfQ==}
|
||||
masto@7.10.0:
|
||||
resolution: {integrity: sha512-BTaivOeHkb87oND2Dp/QzRP5kWJnB5khH9xSsfS0N8WzEAziuKV4DjZAHdbh2SNemqwikQeYtMPBsl52Q9sreQ==}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
|
|
@ -17586,7 +17586,7 @@ snapshots:
|
|||
|
||||
marky@1.3.0: {}
|
||||
|
||||
masto@7.8.0:
|
||||
masto@7.10.0:
|
||||
dependencies:
|
||||
change-case: 5.4.4
|
||||
events-to-async: 2.0.2
|
||||
|
|
|
|||
|
|
@ -50,10 +50,13 @@ export interface DraftItem {
|
|||
attachments: mastodon.v1.MediaAttachment[]
|
||||
lastUpdated: number
|
||||
mentions?: string[]
|
||||
quotedStatusId?: mastodon.v1.Status['id']
|
||||
}
|
||||
|
||||
export type DraftMap = Record<string, Array<DraftItem>
|
||||
// For backward compatibility we need to support single draft items
|
||||
export type DraftKey = 'home' | 'dialog' | 'intent' | 'quote' | `reply-${string}` | `edit-${string}`
|
||||
|
||||
export type DraftMap = Record<DraftKey, DraftItem[]
|
||||
// For backward compatibility to support single draft item before introducing thread
|
||||
| DraftItem>
|
||||
|
||||
export interface ConfirmDialogOptions {
|
||||
|
|
|
|||
Loading…
Reference in a new issue