This commit is contained in:
NoriDev 2024-10-31 13:52:01 +09:00 committed by Marie
parent 92ffd2a5fc
commit 2528508cff
No known key found for this signature in database
GPG key ID: 7ADF6C9CD9A28555
41 changed files with 1455 additions and 6 deletions

View file

@ -50,7 +50,10 @@ import { popupMenu } from '@/os.js';
import { defaultStore } from '@/store.js';
const props = defineProps<{
note: Misskey.entities.Note;
note: Misskey.entities.Note & {
isSchedule?: boolean
};
scheduled?: boolean;
}>();
const menuVersionsButton = shallowRef<HTMLElement>();

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<div v-show="!isDeleted" :class="$style.root" :tabindex="!isDeleted ? '-1' : undefined">
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
<div :class="$style.main">
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
@ -15,6 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</p>
<div v-show="note.cw == null || showContent">
<MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note" :expandAllCws="props.expandAllCws"/>
<div v-if="note.isSchedule" style="margin-top: 10px;">
<MkButton :class="$style.button" inline @click.stop.prevent="editScheduleNote()"><i class="ti ti-eraser"></i> {{ i18n.ts.deleteAndEdit }}</MkButton>
<MkButton :class="$style.button" inline danger @click.stop.prevent="deleteScheduleNote()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
</div>
</div>
@ -24,18 +28,60 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue';
import { defaultStore } from '@/store.js';
const props = defineProps<{
note: Misskey.entities.Note;
note: Misskey.entities.Note & {
isSchedule? : boolean,
scheduledNoteId?: string
};
expandAllCws?: boolean;
hideFiles?: boolean;
}>();
let showContent = ref(defaultStore.state.uncollapseCW);
const isDeleted = ref(false);
const emit = defineEmits<{
(ev: 'editScheduleNote'): void;
}>();
async function deleteScheduleNote() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
okText: i18n.ts.delete,
cancelText: i18n.ts.cancel,
});
if (canceled) return;
await os.apiWithDialog('notes/schedule/delete', { noteId: props.note.id })
.then(() => {
isDeleted.value = true;
});
}
async function editScheduleNote() {
try {
await misskeyApi('notes/schedule/delete', { noteId: props.note.id })
.then(() => {
isDeleted.value = true;
});
} catch (err) {
console.error(err);
}
await os.post({
initialNote: props.note,
renote: props.note.renote,
reply: props.note.reply,
channel: props.note.channel,
});
emit('editScheduleNote');
}
watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
@ -50,6 +96,11 @@ watch(() => props.expandAllCws, (expandAllCws) => {
font-size: 0.95em;
}
.button{
margin-right: var(--margin);
margin-bottom: var(--margin);
}
.avatar {
flex-shrink: 0;
display: block;

View file

@ -77,6 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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"/>
<MkScheduleEditor v-if="scheduleNote" v-model="scheduleNote" @destroyed="scheduleNote = null"/>
<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>
@ -90,6 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
<button v-tooltip="i18n.ts.otherSettings" :class="['_button', $style.footerButton]" @click="showOtherMenu"><i class="ti ti-dots"></i></button>
</div>
<div :class="$style.footerRight">
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
@ -110,6 +112,7 @@ import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
import { toASCII } from 'punycode/';
import { host, url } from '@@/js/config.js';
import type { MenuItem } from '@/types/menu.js';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
@ -133,6 +136,7 @@ import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
import MkScheduleEditor from '@/components/MkScheduleEditor.vue';
const $i = signinRequired();
@ -150,7 +154,9 @@ const props = withDefaults(defineProps<{
initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean;
initialVisibleUsers?: Misskey.entities.UserDetailed[];
initialNote?: Misskey.entities.Note;
initialNote?: Misskey.entities.Note & {
isSchedule?: boolean,
};
instant?: boolean;
fixed?: boolean;
autofocus?: boolean;
@ -206,6 +212,9 @@ const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'
const imeText = ref('');
const showingOptions = ref(false);
const textAreaReadOnly = ref(false);
const scheduleNote = ref<{
scheduledAt: number | null;
} | null>(null);
const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : '';
@ -378,6 +387,7 @@ function watchForDraft() {
watch(localOnly, () => saveDraft());
watch(quoteId, () => saveDraft());
watch(reactionAcceptance, () => saveDraft());
watch(scheduleNote, () => saveDraft());
}
function MFMWindow() {
@ -586,6 +596,7 @@ function clear() {
files.value = [];
poll.value = null;
quoteId.value = null;
scheduleNote.value = null;
}
function onKeydown(ev: KeyboardEvent) {
@ -736,6 +747,7 @@ function saveDraft() {
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
quoteId: quoteId.value,
reactionAcceptance: reactionAcceptance.value,
scheduleNote: scheduleNote.value,
},
};
@ -843,6 +855,7 @@ async function post(ev?: MouseEvent) {
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
reactionAcceptance: reactionAcceptance.value,
editId: props.editId ? props.editId : undefined,
scheduleNote: scheduleNote.value ?? undefined,
};
if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
@ -879,7 +892,7 @@ async function post(ev?: MouseEvent) {
}
posting.value = true;
misskeyApi(postData.editId ? 'notes/edit' : 'notes/create', postData, token).then(() => {
misskeyApi(postData.editId ? 'notes/edit' : (postData.scheduleNote ? 'notes/schedule/create' : 'notes/create'), postData, token).then(() => {
if (props.freezeAfterPosted) {
posted.value = true;
} else {
@ -901,6 +914,8 @@ async function post(ev?: MouseEvent) {
claimAchievement('notes1');
}
poll.value = null;
const text = postData.text ?? '';
const lowerCase = text.toLowerCase();
if ((lowerCase.includes('love') || lowerCase.includes('❤')) && lowerCase.includes('sharkey')) {
@ -1030,6 +1045,41 @@ function openAccountMenu(ev: MouseEvent) {
}, ev);
}
function toggleScheduleNote() {
if (scheduleNote.value) scheduleNote.value = null;
else {
scheduleNote.value = {
scheduledAt: null,
};
}
}
function showOtherMenu(ev: MouseEvent) {
const menuItems: MenuItem[] = [];
if ($i.policies.scheduleNoteMax > 0) {
menuItems.push({
type: 'button',
text: i18n.ts.schedulePost,
icon: 'ti ti-calendar-time',
action: toggleScheduleNote,
}, {
type: 'button',
text: i18n.ts.schedulePostList,
icon: 'ti ti-calendar-event',
action: () => {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSchedulePostListDialog.vue')), {}, {
closed: () => {
dispose();
},
});
},
});
}
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
onMounted(() => {
if (props.autofocus) {
focus();
@ -1099,6 +1149,11 @@ onMounted(() => {
}
quoteId.value = init.renote ? init.renote.id : null;
reactionAcceptance.value = init.reactionAcceptance;
if (init.isSchedule) {
scheduleNote.value = {
scheduledAt: new Date(init.createdAt).getTime(),
};
}
}
nextTick(() => watchForDraft());

View file

@ -0,0 +1,69 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div style="padding: 8px 16px;">
<section>
<MkInput v-model="atDate" small type="date" class="input">
<template #label>{{ i18n.ts._poll.deadlineDate }}</template>
</MkInput>
<MkInput v-model="atTime" small type="time" class="input">
<template #label>{{ i18n.ts._poll.deadlineTime }}</template>
</MkInput>
</section>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import MkInput from '@/components/MkInput.vue';
import { formatDateTimeString } from '@/scripts/format-time-string.js';
import { addTime } from '@/scripts/time.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
modelValue: {
scheduledAt: number | null;
};
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', v: {
scheduledAt: number | null;
}): void;
}>();
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
const atTime = ref('00:00');
if (props.modelValue.scheduledAt) {
const date = new Date(props.modelValue.scheduledAt);
atDate.value = formatDateTimeString(date, 'yyyy-MM-dd');
atTime.value = formatDateTimeString(date, 'HH:mm');
}
function get() {
const calcAt = () => {
return new Date(`${ atDate.value } ${ atTime.value }`).getTime();
};
return {
...(
{ scheduledAt: calcAt() }
),
};
}
watch([
atDate,
atTime,
], () => emit('update:modelValue', get()), {
deep: true,
});
onMounted(() => {
emit('update:modelValue', get());
});
</script>

View file

@ -0,0 +1,60 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialogEl"
:withOkButton="false"
@click="cancel()"
@close="cancel()"
>
<template #header>{{ i18n.ts.schedulePostList }}</template>
<MkSpacer :marginMin="14" :marginMax="16">
<MkPagination ref="paginationEl" :pagination="pagination">
<template #empty>
<div class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div>
</div>
</template>
<template #default="{ items }">
<div class="_gaps">
<MkNoteSimple v-for="item in items" :key="item.id" :scheduled="true" :note="item.note" @editScheduleNote="listUpdate"/>
</div>
</template>
</MkPagination>
</MkSpacer>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import type { Paging } from '@/components/MkPagination.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
const emit = defineEmits<{
(ev: 'cancel'): void;
}>();
const dialogEl = ref();
const cancel = () => {
emit('cancel');
dialogEl.value.close();
};
const paginationEl = ref();
const pagination: Paging = {
endpoint: 'notes/schedule/list',
limit: 10,
};
function listUpdate() {
paginationEl.value.reload();
}
</script>

View file

@ -733,3 +733,4 @@ export function checkExistence(fileData: ArrayBuffer): Promise<any> {
});
});
}*/

View file

@ -200,6 +200,25 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.scheduleNoteMax, 'scheduleNoteMax'])">
<template #label>{{ i18n.ts._role._options.scheduleNoteMax }}</template>
<template #suffix>
<span v-if="role.policies.scheduleNoteMax.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.scheduleNoteMax.value }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.scheduleNoteMax)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.scheduleNoteMax.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="role.policies.scheduleNoteMax.value" :disabled="role.policies.scheduleNoteMax.useDefault" type="number" :readonly="readonly">
</MkInput>
<MkRange v-model="role.policies.scheduleNoteMax.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
<template #suffix>

View file

@ -70,6 +70,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.scheduleNoteMax, 'scheduleNoteMax'])">
<template #label>{{ i18n.ts._role._options.scheduleNoteMax }}</template>
<template #suffix>{{ policies.scheduleNoteMax }}</template>
<MkInput v-model="policies.scheduleNoteMax" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
<template #suffix>{{ policies.mentionLimit }}</template>