Merge remote-tracking branch 'misskey-mattyatea/schedule-note' into develop

# Conflicts:
#	locales/index.d.ts
#	locales/ja-JP.yml
#	packages/backend/src/core/RoleService.ts
#	packages/frontend/src/components/MkNoteHeader.vue
#	packages/frontend/src/components/MkPostForm.vue
#	packages/frontend/src/const.ts
#	packages/frontend/src/navbar.ts
#	packages/frontend/src/pages/admin/roles.editor.vue
#	packages/frontend/src/pages/admin/roles.vue
This commit is contained in:
mattyatea 2023-12-02 00:50:04 +09:00
commit b7f9ad1944
39 changed files with 900 additions and 74 deletions

View file

@ -36,17 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="!localOnly"><i class="ti ti-rocket"></i></span>
<span v-else><i class="ti ti-rocket-off"></i></span>
</button>
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
<span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span>
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
<span v-else><i class="ti ti-icons"></i></span>
</button>
<button v-tooltip="i18n.ts.otherSettings" class="_button" :class="[$style.headerRightItem]" @click="openOtherSettingsMenu"><i class="ti ti-dots"></i></button>
<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
<div :class="[$style.submitInner ,{ [$style.gamingDark]: gamingType === 'dark',[$style.gamingLight]: gamingType === 'light' }]">
<template v-if="posted"></template>
<template v-else-if="posting"><MkEllipsis/></template>
<template v-else>{{ submitText }}</template>
<i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i>
<i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : schedule ? 'ti ti-clock-hour-4' : 'ti ti-send'"></i>
</div>
</button>
</div>
@ -72,7 +68,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<div :class="$style.postOptionsRoot">
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<MkScheduleEditor v-if="schedule" v-model="schedule" @destroyed="schedule = null"/>
</div>
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
<div v-if="showingOptions" style="padding: 8px 16px;">
</div>
@ -116,6 +115,7 @@ import { formatTimeString } from '@/scripts/format-time-string.js';
import { Autocomplete } from '@/scripts/autocomplete.js';
import * as os from '@/os.js';
import { selectFiles } from '@/scripts/select-file.js';
import { dateTimeFormat } from '@/scripts/intl-const.js';
import {
bannerDark,
bannerLight,
@ -134,6 +134,9 @@ import { deepClone } from '@/scripts/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js';
import MkScheduleEditor from '@/components/MkScheduleEditor.vue';
import { listSchedulePost } from '@/os.js';
const modal = inject('modal');
let gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
@ -188,6 +191,9 @@ let poll = $ref<{
expiresAt: string | null;
expiredAfter: string | null;
} | null>(null);
let schedule = $ref<{
scheduledAt: string | null;
}| null>(null);
let useCw = $ref(false);
let showPreview = $ref(defaultStore.state.showPreview);
watch($$(showPreview), () => defaultStore.set('showPreview', showPreview));
@ -246,7 +252,9 @@ const submitText = $computed((): string => {
? i18n.ts.quote
: props.reply
? i18n.ts.reply
: i18n.ts.note;
: schedule
? i18n.ts._schedulePost.addSchedule
: i18n.ts.note;
});
const textLength = $computed((): number => {
@ -407,6 +415,16 @@ function togglePoll() {
}
}
function toggleSchedule() {
if (schedule) {
schedule = null;
} else {
schedule = {
scheduledAt: null,
};
}
}
function addTag(tag: string) {
insertTextAtCursor(textareaEl, ` #${tag} `);
}
@ -552,7 +570,7 @@ function removeVisibleUser(user) {
function clear() {
text = '';
files = [];
schedule = null;files = [];
poll = null;
quoteId = null;
}
@ -735,7 +753,7 @@ async function post(ev?: MouseEvent) {
replyId: props.reply ? props.reply.id : undefined,
renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
channelId: props.channel ? props.channel.id : undefined,
poll: poll,
schedule, poll,
cw: useCw ? cw ?? '' : null,
localOnly: localOnly,
visibility: visibility,
@ -765,7 +783,7 @@ async function post(ev?: MouseEvent) {
if (postAccount) {
const storedAccounts = await getAccounts();
token = storedAccounts.find(x => x.id === postAccount.id)?.token;
token = storedAccounts.find(x => x.id === postAccount?.id)?.token;
}
posting = true;
@ -790,6 +808,13 @@ async function post(ev?: MouseEvent) {
if (notesCount === 1) {
claimAchievement('notes1');
}
poll = null;
if (postData.schedule?.scheduledAt) {
const d = new Date(postData.schedule.scheduledAt);
const str = dateTimeFormat.format(d);
os.toast(i18n.t('_schedulePost.willBePostedAtX', { date: str }));
}
const text = postData.text ?? '';
const lowerCase = text.toLowerCase();
@ -888,6 +913,45 @@ function openAccountMenu(ev: MouseEvent) {
}, ev);
}
function openOtherSettingsMenu(ev: MouseEvent) {
let reactionAcceptanceIcon: string;
switch (reactionAcceptance) {
case 'likeOnly':
reactionAcceptanceIcon = 'ti ti-heart';
break;
case 'likeOnlyForRemote':
reactionAcceptanceIcon = 'ti ti-heart-plus';
break;
default:
reactionAcceptanceIcon = 'ti ti-icons';
break;
}
os.popupMenu([{
type: 'button',
text: i18n.ts.reactionAcceptance,
icon: reactionAcceptanceIcon,
action: toggleReactionAcceptance,
}, ($i.policies?.canScheduleNote) ? {
type: 'button',
text: i18n.ts.schedulePost,
icon: 'ti ti-calendar-time',
indicate: (schedule != null),
action: toggleSchedule,
} : undefined, ...(($i.policies?.canScheduleNote) ? [null, {
type: 'button',
text: i18n.ts._schedulePost.list,
icon: 'ti ti-calendar-event',
action: () => {
// 稿
emit('cancel');
listSchedulePost();
},
}] : [])], ev.currentTarget ?? ev.target, {
align: 'right',
});
}
onMounted(() => {
if (props.autofocus) {
focus();
@ -926,7 +990,12 @@ onMounted(() => {
files = init.files;
cw = init.cw;
useCw = init.cw != null;
if (init.poll) {
if (init.isSchedule) {
schedule = {
scheduledAt: init.createdAt,
};
}
if (init.poll) {
poll = {
choices: init.poll.choices.map(x => x.text),
multiple: init.poll.multiple,
@ -1069,7 +1138,11 @@ defineExpose({
background: none;
}
&.danger {
&.headerRightButtonActive {
color: var(--accent);
}
&.danger {
color: #ff2a2a;
}
}
@ -1160,6 +1233,15 @@ defineExpose({
border-bottom: solid 0.5px var(--divider);
}
.postOptionsRoot {
>* {
border-bottom: solid 0.5px var(--divider);
}
>:last-child {
border-bottom: none;
}
}
.hashtags {
z-index: 1;
padding-top: 8px;