forked from Mirrors/elk
feat: poll creation (#2111)
This commit is contained in:
parent
d9add9f670
commit
1fda33848e
6 changed files with 237 additions and 88 deletions
|
@ -1,10 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
label?: string
|
||||
hover?: boolean
|
||||
iconChecked?: string
|
||||
iconUnchecked?: string
|
||||
}>()
|
||||
const { modelValue } = defineModels<{
|
||||
modelValue?: boolean
|
||||
modelValue?: boolean | null
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
@ -12,11 +14,12 @@ const { modelValue } = defineModels<{
|
|||
<label
|
||||
class="common-checkbox flex items-center cursor-pointer py-1 text-md w-full gap-y-1"
|
||||
:class="hover ? 'hover:bg-active ms--2 px-4 py-2' : null"
|
||||
v-bind="$attrs"
|
||||
@click.prevent="modelValue = !modelValue"
|
||||
>
|
||||
<span flex-1 ms-2 pointer-events-none>{{ label }}</span>
|
||||
<span v-if="label" flex-1 ms-2 pointer-events-none>{{ label }}</span>
|
||||
<span
|
||||
:class="modelValue ? 'i-ri:checkbox-line' : 'i-ri:checkbox-blank-line'"
|
||||
:class="modelValue ? (iconChecked ?? 'i-ri:checkbox-line') : (iconUnchecked ?? 'i-ri:checkbox-blank-line')"
|
||||
text-lg
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
|
|
@ -63,6 +63,41 @@ const { editor } = useTiptap({
|
|||
onPaste: handlePaste,
|
||||
})
|
||||
|
||||
function editPollOptionDraft(event: Event, index: number) {
|
||||
draft.params.poll!.options[index] = (event.target as HTMLInputElement).value
|
||||
const indexLastNonEmpty = draft.params.poll!.options.findLastIndex(option => option.trim().length > 0)
|
||||
draft.params.poll!.options = [...draft.params.poll!.options.slice(0, indexLastNonEmpty + 1), '']
|
||||
}
|
||||
|
||||
function deletePollOption(index: number) {
|
||||
draft.params.poll!.options.splice(index, 1)
|
||||
}
|
||||
|
||||
const expiresInOptions = [
|
||||
{
|
||||
seconds: 1 * 60 * 60,
|
||||
label: t('time_ago_options.hour_future', 1),
|
||||
},
|
||||
{
|
||||
seconds: 2 * 60 * 60,
|
||||
label: t('time_ago_options.hour_future', 2),
|
||||
},
|
||||
{
|
||||
seconds: 1 * 24 * 60 * 60,
|
||||
label: t('time_ago_options.day_future', 1),
|
||||
},
|
||||
{
|
||||
seconds: 2 * 24 * 60 * 60,
|
||||
label: t('time_ago_options.day_future', 2),
|
||||
},
|
||||
{
|
||||
seconds: 7 * 24 * 60 * 60,
|
||||
label: t('time_ago_options.day_future', 7),
|
||||
},
|
||||
]
|
||||
|
||||
const expiresInDefaultOptionIndex = 2
|
||||
|
||||
const characterCount = $computed(() => {
|
||||
const text = htmlToText(editor.value?.getHTML() || '')
|
||||
|
||||
|
@ -277,92 +312,167 @@ onDeactivated(() => {
|
|||
</div>
|
||||
<div flex gap-4>
|
||||
<div w-12 h-full sm:block hidden />
|
||||
<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"
|
||||
<div flex="~ col 1" max-w-full>
|
||||
<form v-if="isExpanded && draft.params.poll" my-4 flex="~ 1 col" gap-3 m="s--1">
|
||||
<div
|
||||
v-for="(option, index) in draft.params.poll.options"
|
||||
:key="index"
|
||||
flex="~ row"
|
||||
gap-3
|
||||
>
|
||||
<input
|
||||
:value="option"
|
||||
bg-base
|
||||
border="~ base" flex-1 h10 pe-4 rounded-2 w-full flex="~ row"
|
||||
items-center relative focus-within:box-shadow-outline gap-3
|
||||
px-4 py-2
|
||||
:placeholder="$t('polls.option_placeholder')"
|
||||
@input="editPollOptionDraft($event, index)"
|
||||
>
|
||||
<CommonTooltip placement="top" :content="$t('polls.remove_option')">
|
||||
<button
|
||||
btn-action-icon class="hover:bg-red/75"
|
||||
:disabled="index === draft.params.poll!.options.length - 1"
|
||||
@click.prevent="deletePollOption(index)"
|
||||
>
|
||||
<div i-ri:delete-bin-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
v-if="shouldExpanded" flex="~ gap-1 1 wrap" m="s--1" pt-2 justify="end" max-w-full
|
||||
border="t base"
|
||||
>
|
||||
<button btn-action-icon :title="$t('tooltip.emoji')">
|
||||
<div i-ri:emotion-line />
|
||||
</button>
|
||||
</PublishEmojiPicker>
|
||||
|
||||
<CommonTooltip placement="top" :content="$t('tooltip.add_media')">
|
||||
<button btn-action-icon :aria-label="$t('tooltip.add_media')" @click="pickAttachments">
|
||||
<div i-ri:image-add-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<PublishEditorTools v-if="editor" :editor="editor" />
|
||||
|
||||
<div flex-auto />
|
||||
|
||||
<PublishCharacterCounter :max="characterLimit" :length="characterCount" />
|
||||
|
||||
<CommonTooltip placement="top" :content="$t('tooltip.change_language')">
|
||||
<CommonDropdown placement="bottom" auto-boundary-max-size>
|
||||
<button btn-action-icon :aria-label="$t('tooltip.change_language')" w-max mr1>
|
||||
<span v-if="postLanguageDisplay" text-secondary text-sm ml1>{{ postLanguageDisplay }}</span>
|
||||
<div v-else i-ri:translate-2 />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
<PublishEmojiPicker
|
||||
@select="insertEmoji"
|
||||
@select-custom="insertCustomEmoji"
|
||||
>
|
||||
<button btn-action-icon :title="$t('tooltip.emoji')">
|
||||
<div i-ri:emotion-line />
|
||||
</button>
|
||||
</PublishEmojiPicker>
|
||||
|
||||
<template #popper>
|
||||
<PublishLanguagePicker v-model="draft.params.language" min-w-80 />
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</CommonTooltip>
|
||||
|
||||
<CommonTooltip placement="top" :content="$t('tooltip.add_content_warning')">
|
||||
<button btn-action-icon :aria-label="$t('tooltip.add_content_warning')" @click="toggleSensitive">
|
||||
<div v-if="draft.params.sensitive" i-ri:alarm-warning-fill text-orange />
|
||||
<div v-else i-ri:alarm-warning-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<PublishVisibilityPicker v-model="draft.params.visibility" :editing="!!draft.editingStatus">
|
||||
<template #default="{ visibility }">
|
||||
<button :disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon :class="{ 'w-12': !draft.editingStatus }">
|
||||
<div :class="visibility.icon" />
|
||||
<div v-if="!draft.editingStatus" i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
<CommonTooltip v-if="draft.params.poll === undefined" placement="top" :content="$t('tooltip.add_media')">
|
||||
<button btn-action-icon :aria-label="$t('tooltip.add_media')" @click="pickAttachments">
|
||||
<div i-ri:image-add-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<template v-if="draft.attachments.length === 0">
|
||||
<CommonTooltip v-if="!draft.params.poll" placement="top" :content="$t('polls.create')">
|
||||
<button btn-action-icon :aria-label="$t('polls.create')" @click="draft.params.poll = { options: [''], expiresIn: expiresInOptions[expiresInDefaultOptionIndex].seconds }">
|
||||
<div i-ri:chat-poll-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<div v-else rounded-full b-1 border-dark flex="~ row" gap-1>
|
||||
<CommonTooltip placement="top" :content="$t('polls.cancel')">
|
||||
<button btn-action-icon b-r border-dark :aria-label="$t('polls.cancel')" @click="draft.params.poll = undefined">
|
||||
<div i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<CommonDropdown placement="top">
|
||||
<CommonTooltip placement="top" :content="$t('polls.settings')">
|
||||
<button :aria-label="$t('polls.settings')" btn-action-icon w-12>
|
||||
<div i-ri:list-settings-line />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<template #popper>
|
||||
<div flex="~ col" gap-1 p-2>
|
||||
<CommonCheckbox v-model="draft.params.poll.multiple" :label="draft.params.poll.multiple ? $t('polls.disallow_multiple') : $t('polls.allow_multiple')" px-2 gap-3 h-9 flex justify-center hover:bg-active rounded-full icon-checked="i-ri:checkbox-multiple-blank-line" icon-unchecked="i-ri:checkbox-blank-circle-line" />
|
||||
<CommonCheckbox v-model="draft.params.poll.hideTotals" :label="draft.params.poll.hideTotals ? $t('polls.show_votes') : $t('polls.hide_votes')" px-2 gap-3 h-9 flex justify-center hover:bg-active rounded-full icon-checked="i-ri:eye-close-line" icon-unchecked="i-ri:eye-line" />
|
||||
</div>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
<CommonDropdown placement="bottom">
|
||||
<CommonTooltip placement="top" :content="$t('polls.expiration')">
|
||||
<button :aria-label="$t('polls.expiration')" btn-action-icon w-12>
|
||||
<div i-ri:hourglass-line />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<template #popper>
|
||||
<CommonDropdownItem
|
||||
v-for="expiresInOption in expiresInOptions"
|
||||
:key="expiresInOption.seconds"
|
||||
:text="expiresInOption.label"
|
||||
:checked="draft.params.poll!.expiresIn === expiresInOption.seconds"
|
||||
@click="draft.params.poll!.expiresIn = expiresInOption.seconds"
|
||||
/>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</PublishVisibilityPicker>
|
||||
|
||||
<CommonTooltip v-if="failedMessages.length > 0" id="publish-failed-tooltip" placement="top" :content="$t('tooltip.publish_failed')">
|
||||
<button
|
||||
btn-danger rounded-3 text-sm w-full flex="~ gap1" items-center md:w-fit aria-describedby="publish-failed-tooltip"
|
||||
>
|
||||
<span block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<span>{{ $t('state.publish_failed') }}</span>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<PublishEditorTools v-if="editor" :editor="editor" />
|
||||
|
||||
<CommonTooltip v-else id="publish-tooltip" placement="top" :content="$t('tooltip.add_publishable_content')" :disabled="!(isPublishDisabled || isExceedingCharacterLimit)">
|
||||
<button
|
||||
btn-solid rounded-3 text-sm w-full flex="~ gap1" items-center
|
||||
md:w-fit
|
||||
class="publish-button"
|
||||
:aria-disabled="isPublishDisabled || isExceedingCharacterLimit"
|
||||
aria-describedby="publish-tooltip"
|
||||
@click="publish"
|
||||
>
|
||||
<span v-if="isSending" block animate-spin preserve-3d>
|
||||
<div block i-ri:loader-2-fill />
|
||||
</span>
|
||||
<span v-if="failedMessages.length" block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span>
|
||||
<span v-else-if="draft.params.inReplyToId">{{ $t('action.reply') }}</span>
|
||||
<span v-else>{{ !isSending ? $t('action.publish') : $t('state.publishing') }}</span>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
<div flex-auto />
|
||||
|
||||
<PublishCharacterCounter :max="characterLimit" :length="characterCount" />
|
||||
|
||||
<CommonTooltip placement="top" :content="$t('tooltip.change_language')">
|
||||
<CommonDropdown placement="bottom" auto-boundary-max-size>
|
||||
<button btn-action-icon :aria-label="$t('tooltip.change_language')" w-max mr1>
|
||||
<span v-if="postLanguageDisplay" text-secondary text-sm ml1>{{ postLanguageDisplay }}</span>
|
||||
<div v-else i-ri:translate-2 />
|
||||
<div i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
|
||||
<template #popper>
|
||||
<PublishLanguagePicker v-model="draft.params.language" min-w-80 />
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</CommonTooltip>
|
||||
|
||||
<CommonTooltip placement="top" :content="$t('tooltip.add_content_warning')">
|
||||
<button btn-action-icon :aria-label="$t('tooltip.add_content_warning')" @click="toggleSensitive">
|
||||
<div v-if="draft.params.sensitive" i-ri:alarm-warning-fill text-orange />
|
||||
<div v-else i-ri:alarm-warning-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<PublishVisibilityPicker v-model="draft.params.visibility" :editing="!!draft.editingStatus">
|
||||
<template #default="{ visibility }">
|
||||
<button :disabled="!!draft.editingStatus" :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon :class="{ 'w-12': !draft.editingStatus }">
|
||||
<div :class="visibility.icon" />
|
||||
<div v-if="!draft.editingStatus" i-ri:arrow-down-s-line text-sm text-secondary me--1 />
|
||||
</button>
|
||||
</template>
|
||||
</PublishVisibilityPicker>
|
||||
|
||||
<CommonTooltip v-if="failedMessages.length > 0" id="publish-failed-tooltip" placement="top" :content="$t('tooltip.publish_failed')">
|
||||
<button
|
||||
btn-danger rounded-3 text-sm w-full flex="~ gap1" items-center md:w-fit aria-describedby="publish-failed-tooltip"
|
||||
>
|
||||
<span block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<span>{{ $t('state.publish_failed') }}</span>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
|
||||
<CommonTooltip v-else id="publish-tooltip" placement="top" :content="$t('tooltip.add_publishable_content')" :disabled="!(isPublishDisabled || isExceedingCharacterLimit)">
|
||||
<button
|
||||
btn-solid rounded-3 text-sm w-full flex="~ gap1" items-center
|
||||
md:w-fit
|
||||
class="publish-button"
|
||||
:aria-disabled="isPublishDisabled || isExceedingCharacterLimit"
|
||||
aria-describedby="publish-tooltip"
|
||||
@click="publish"
|
||||
>
|
||||
<span v-if="isSending" block animate-spin preserve-3d>
|
||||
<div block i-ri:loader-2-fill />
|
||||
</span>
|
||||
<span v-if="failedMessages.length" block>
|
||||
<div block i-carbon:face-dizzy-filled />
|
||||
</span>
|
||||
<span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span>
|
||||
<span v-else-if="draft.params.inReplyToId">{{ $t('action.reply') }}</span>
|
||||
<span v-else>{{ !isSending ? $t('action.publish') : $t('state.publishing') }}</span>
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -34,7 +34,15 @@ export function usePublish(options: {
|
|||
|
||||
const shouldExpanded = $computed(() => expanded || isExpanded || !isEmpty)
|
||||
const isPublishDisabled = $computed(() => {
|
||||
return isEmpty || isUploading || isSending || (draft.attachments.length === 0 && !draft.params.status) || failedMessages.length > 0
|
||||
return isEmpty
|
||||
|| isUploading
|
||||
|| isSending
|
||||
|| (draft.attachments.length === 0 && !draft.params.status)
|
||||
|| failedMessages.length > 0
|
||||
|| (draft.attachments.length > 0 && draft.params.poll !== null && draft.params.poll !== undefined)
|
||||
|| (draft.params.poll !== null && draft.params.poll !== undefined && draft.params.poll.options.length <= 1)
|
||||
|| (draft.params.poll !== null && draft.params.poll !== undefined && ![-1, draft.params.poll.options.length - 1].includes(draft.params.poll.options.findIndex(option => option.trim().length === 0)))
|
||||
|| (draft.params.poll !== null && draft.params.poll !== undefined && new Set(draft.params.poll.options).size !== draft.params.poll.options.length)
|
||||
})
|
||||
|
||||
watch(() => draft, () => {
|
||||
|
@ -56,6 +64,7 @@ export function usePublish(options: {
|
|||
status: content,
|
||||
mediaIds: draft.attachments.map(a => a.id),
|
||||
language: draft.params.language || preferredLanguage,
|
||||
poll: draft.params.poll ? { ...draft.params.poll, options: draft.params.poll.options.slice(0, draft.params.poll.options.length - 1) } : undefined,
|
||||
...(isGlitchEdition.value ? { 'content-type': 'text/markdown' } : {}),
|
||||
} as mastodon.v1.CreateStatusParams
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ export function getDefaultDraft(options: Partial<Mutable<mastodon.v1.CreateStatu
|
|||
spoilerText,
|
||||
language,
|
||||
mentions,
|
||||
poll,
|
||||
} = options
|
||||
|
||||
return {
|
||||
|
@ -43,6 +44,7 @@ export function getDefaultDraft(options: Partial<Mutable<mastodon.v1.CreateStatu
|
|||
initialText,
|
||||
params: {
|
||||
status: status || '',
|
||||
poll,
|
||||
inReplyToId,
|
||||
visibility: getDefaultVisibility(visibility || 'public'),
|
||||
sensitive: sensitive ?? false,
|
||||
|
@ -55,16 +57,29 @@ export function getDefaultDraft(options: Partial<Mutable<mastodon.v1.CreateStatu
|
|||
}
|
||||
|
||||
export async function getDraftFromStatus(status: mastodon.v1.Status): Promise<Draft> {
|
||||
return getDefaultDraft({
|
||||
const info = {
|
||||
status: await convertMastodonHTML(status.content),
|
||||
mediaIds: status.mediaAttachments.map(att => att.id),
|
||||
visibility: status.visibility,
|
||||
attachments: status.mediaAttachments,
|
||||
sensitive: status.sensitive,
|
||||
spoilerText: status.spoilerText,
|
||||
language: status.language,
|
||||
inReplyToId: status.inReplyToId,
|
||||
})
|
||||
}
|
||||
|
||||
return getDefaultDraft((status.mediaAttachments !== undefined && status.mediaAttachments.length > 0)
|
||||
? { ...info, mediaIds: status.mediaAttachments.map(att => att.id) }
|
||||
: {
|
||||
...info,
|
||||
poll: status.poll
|
||||
? {
|
||||
expiresIn: Math.abs(new Date().getTime() - new Date(status.poll.expiresAt!).getTime()) / 1000,
|
||||
options: [...status.poll.options.map(({ title }) => title), ''],
|
||||
multiple: status.poll.multiple,
|
||||
hideTotals: status.poll.options[0].votesCount === null,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
function getAccountsToMention(status: mastodon.v1.Status) {
|
||||
|
|
|
@ -307,6 +307,18 @@
|
|||
"replying": "Replying",
|
||||
"the_thread": "the thread"
|
||||
},
|
||||
"polls": {
|
||||
"allow_multiple": "Allow multiple choice",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create poll",
|
||||
"disallow_multiple": "Disallow multiple choice",
|
||||
"expiration": "Poll expiration",
|
||||
"hide_votes": "Hide vote totals until the end",
|
||||
"option_placeholder": "Poll choice",
|
||||
"remove_option": "Remove choice",
|
||||
"settings": "Poll options",
|
||||
"show_votes": "Always show vote totals"
|
||||
},
|
||||
"pwa": {
|
||||
"dismiss": "Dismiss",
|
||||
"install": "Install",
|
||||
|
|
|
@ -47,7 +47,7 @@ export type TranslateFn = ReturnType<typeof useI18n>['t']
|
|||
export interface Draft {
|
||||
editingStatus?: mastodon.v1.Status
|
||||
initialText?: string
|
||||
params: MarkNonNullable<Mutable<mastodon.v1.CreateStatusParams>, 'status' | 'language' | 'sensitive' | 'spoilerText' | 'visibility'>
|
||||
params: MarkNonNullable<Mutable<Omit<mastodon.v1.CreateStatusParams, 'poll'>>, 'status' | 'language' | 'sensitive' | 'spoilerText' | 'visibility'> & { poll: Mutable<mastodon.v1.CreateStatusParams['poll']> }
|
||||
attachments: mastodon.v1.MediaAttachment[]
|
||||
lastUpdated: number
|
||||
mentions?: string[]
|
||||
|
|
Loading…
Reference in a new issue