デフォルトでノート自動削除できる機能
This commit is contained in:
parent
f0797b556b
commit
26da54d372
16
locales/index.d.ts
vendored
16
locales/index.d.ts
vendored
@ -5213,6 +5213,22 @@ export interface Locale extends ILocale {
|
||||
* 高度なMFMのピッカーを表示する
|
||||
*/
|
||||
"enableQuickAddMfmFunction": string;
|
||||
/**
|
||||
* ノートの自己消滅の初期値
|
||||
*/
|
||||
"defaultScheduledNoteDeleteTime": string;
|
||||
/**
|
||||
* ノートの自己消滅が有効になっています
|
||||
*/
|
||||
"scheduledNoteDeleteEnabled": string;
|
||||
/**
|
||||
* 1年以上先の日時を指定することはできません
|
||||
*/
|
||||
"cannotScheduleLaterThanOneYear": string;
|
||||
/**
|
||||
* デフォルトでノートが自己消滅するように
|
||||
*/
|
||||
"defaultScheduledNoteDelete": string;
|
||||
/**
|
||||
* バブルゲーム
|
||||
*/
|
||||
|
@ -1344,6 +1344,10 @@ _delivery:
|
||||
autoSuspendedForNotResponding: "サーバー応答なしのため停止中"
|
||||
scheduledNoteDelete: "時限爆弾"
|
||||
noteDeletationAt: "このノートは{time}に削除されます"
|
||||
defaultScheduledNoteDeleteTime: "ノートの自己消滅の初期値"
|
||||
scheduledNoteDeleteEnabled: "ノートの自己消滅が有効になっています"
|
||||
cannotScheduleLaterThanOneYear: "1年以上先の日時を指定することはできません"
|
||||
defaultScheduledNoteDelete: "デフォルトでノートが自己消滅するように"
|
||||
|
||||
_bubbleGame:
|
||||
howToPlay: "遊び方"
|
||||
|
@ -110,6 +110,12 @@ export const meta = {
|
||||
id: '9576c3c8-d8f3-11ee-ac15-00155d19d35d',
|
||||
},
|
||||
|
||||
cannotScheduleDeleteLaterThanOneYear: {
|
||||
message: 'Scheduled delete time is later than one year.',
|
||||
code: 'CANNOT_SCHEDULE_DELETE_LATER_THAN_ONE_YEAR',
|
||||
id: 'b02b5edb-2741-4841-b692-d9893f1e6515',
|
||||
},
|
||||
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
@ -387,6 +393,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
} else if (typeof ps.scheduledDelete.deleteAfter === 'number') {
|
||||
ps.scheduledDelete.deleteAt = Date.now() + ps.scheduledDelete.deleteAfter;
|
||||
}
|
||||
|
||||
if (ps.scheduledDelete.deleteAt && ps.scheduledDelete.deleteAt > Date.now() + ms('1year')) {
|
||||
throw new ApiError(meta.errors.cannotScheduleDeleteLaterThanOneYear);
|
||||
}
|
||||
}
|
||||
|
||||
let channel: MiChannel | null = null;
|
||||
|
@ -1,74 +1,108 @@
|
||||
<template>
|
||||
<div class="zmdxowus">
|
||||
<span>{{ i18n.ts.scheduledNoteDelete }}</span>
|
||||
<section>
|
||||
<div>
|
||||
<MkSelect v-model="expiration" small>
|
||||
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
||||
<option value="at">{{ i18n.ts._poll.at }}</option>
|
||||
<option value="after">{{ i18n.ts._poll.after }}</option>
|
||||
</MkSelect>
|
||||
<section v-if="expiration === 'at'">
|
||||
<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>
|
||||
<section v-else-if="expiration === 'after'">
|
||||
<MkInput v-model="after" small type="number" class="input">
|
||||
<template #label>{{ i18n.ts._poll.duration }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="unit" small>
|
||||
<option value="second">{{ i18n.ts._time.second }}</option>
|
||||
<option value="minute">{{ i18n.ts._time.minute }}</option>
|
||||
<option value="hour">{{ i18n.ts._time.hour }}</option>
|
||||
<option value="day">{{ i18n.ts._time.day }}</option>
|
||||
<div :class="[$style.root, { [$style.padding]: !afterOnly }]">
|
||||
<div v-if="!afterOnly" :class="[$style.label, { [$style.withAccent]: !showDetail }]" @click="showDetail = !showDetail"><i class="ti" :class="showDetail ? 'ti-chevron-up' : 'ti-chevron-down'"></i> {{ summaryText }}</div>
|
||||
<MkInfo v-if="!isValid" warn>{{ i18n.ts.cannotScheduleLaterThanOneYear }}</MkInfo>
|
||||
<section v-if="afterOnly || showDetail">
|
||||
<div>
|
||||
<MkSelect v-if="!afterOnly" v-model="expiration" small>
|
||||
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
||||
<option value="at">{{ i18n.ts._poll.at }}</option>
|
||||
<option value="after">{{ i18n.ts._poll.after }}</option>
|
||||
</MkSelect>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkSelect from './MkSelect.vue';
|
||||
import { formatDateTimeString } from '@/scripts/format-time-string.js';
|
||||
import { addTime } from '@/scripts/time.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
export type DeleteScheduleEditorModelValue = {
|
||||
<section v-if="expiration === 'at'">
|
||||
<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>
|
||||
<section v-else-if="expiration === 'after'">
|
||||
<MkInput v-model="after" small type="number" class="input">
|
||||
<template #label>{{ i18n.ts._poll.duration }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="unit" small>
|
||||
<option value="second">{{ i18n.ts._time.second }}</option>
|
||||
<option value="minute">{{ i18n.ts._time.minute }}</option>
|
||||
<option value="hour">{{ i18n.ts._time.hour }}</option>
|
||||
<option value="day">{{ i18n.ts._time.day }}</option>
|
||||
</MkSelect>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkSelect from './MkSelect.vue';
|
||||
import MkInfo from './MkInfo.vue';
|
||||
import { formatDateTimeString } from '@/scripts/format-time-string.js';
|
||||
import { addTime } from '@/scripts/time.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
export type DeleteScheduleEditorModelValue = {
|
||||
deleteAt: number | null;
|
||||
deleteAfter: number | null;
|
||||
isValid: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
const props = defineProps<{
|
||||
modelValue: DeleteScheduleEditorModelValue;
|
||||
afterOnly?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', v: DeleteScheduleEditorModelValue): void;
|
||||
}>();
|
||||
|
||||
const expiration = ref('at');
|
||||
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
|
||||
const atTime = ref('00:00');
|
||||
const after = ref(0);
|
||||
const unit = ref('second');
|
||||
const expiration = ref<'at' | 'after'>('after');
|
||||
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
|
||||
const atTime = ref('00:00');
|
||||
const after = ref(0);
|
||||
const unit = ref<'second' | 'minute' | 'hour' | 'day'>('second');
|
||||
const isValid = ref(true);
|
||||
const showDetail = ref(!defaultStore.state.defaultScheduledNoteDelete);
|
||||
const summaryText = computed(() => {
|
||||
if (showDetail.value) {
|
||||
return i18n.ts.scheduledNoteDelete;
|
||||
}
|
||||
if (expiration.value === 'at') {
|
||||
return `${i18n.ts.scheduledNoteDeleteEnabled} (${formatDateTimeString(new Date(calcAt()), 'yyyy/MM/dd HH:mm')})`;
|
||||
} else {
|
||||
const time = unit.value === 'second' ? i18n.tsx._timeIn.seconds({ n: (after.value).toString() })
|
||||
: unit.value === 'minute' ? i18n.tsx._timeIn.minutes({ n: (after.value).toString() })
|
||||
: unit.value === 'hour' ? i18n.tsx._timeIn.hours({ n: (after.value).toString() })
|
||||
: i18n.tsx._timeIn.days({ n: (after.value).toString() });
|
||||
return `${i18n.ts.scheduledNoteDeleteEnabled} (${time})`;
|
||||
}
|
||||
});
|
||||
const beautifyAfter = (base: number) => {
|
||||
let time = base;
|
||||
if (time % 60 === 0) {
|
||||
unit.value = 'minute';
|
||||
time /= 60;
|
||||
}
|
||||
if (time % 60 === 0) {
|
||||
unit.value = 'hour';
|
||||
time /= 60;
|
||||
}
|
||||
if (time % 24 === 0) {
|
||||
unit.value = 'day';
|
||||
time /= 24;
|
||||
}
|
||||
after.value = time;
|
||||
};
|
||||
beautifyAfter(defaultStore.state.defaultScheduledNoteDeleteTime / 1000);
|
||||
if (props.modelValue.deleteAt) {
|
||||
expiration.value = 'at';
|
||||
const deleteAt = new Date(props.modelValue.deleteAt);
|
||||
atDate.value = formatDateTimeString(deleteAt, 'yyyy-MM-dd');
|
||||
atTime.value = formatDateTimeString(deleteAt, 'HH:mm');
|
||||
} else if (typeof props.modelValue.deleteAfter === 'number') {
|
||||
expiration.value = 'after';
|
||||
beautifyAfter(props.modelValue.deleteAfter / 1000);
|
||||
}
|
||||
|
||||
if (props.modelValue.deleteAt) {
|
||||
expiration.value = 'at';
|
||||
const deleteAt = new Date(props.modelValue.deleteAt);
|
||||
atDate.value = formatDateTimeString(deleteAt, 'yyyy-MM-dd');
|
||||
atTime.value = formatDateTimeString(deleteAt, 'HH:mm');
|
||||
} else if (typeof props.modelValue.deleteAfter === 'number') {
|
||||
expiration.value = 'after';
|
||||
after.value = props.modelValue.deleteAfter / 1000;
|
||||
}
|
||||
|
||||
function get(): DeleteScheduleEditorModelValue {
|
||||
const calcAt = () => {
|
||||
return new Date(`${atDate.value} ${atTime.value}`).getTime();
|
||||
};
|
||||
@ -88,20 +122,35 @@ function get(): DeleteScheduleEditorModelValue {
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
deleteAt: expiration.value === 'at' ? calcAt() : null,
|
||||
deleteAfter: expiration.value === 'after' ? calcAfter() : null,
|
||||
const isValidTime = () => {
|
||||
if (expiration.value === 'at') {
|
||||
return calcAt() < Date.now() + (1000 * 60 * 60 * 24 * 365);
|
||||
} else {
|
||||
const afterMs = calcAfter();
|
||||
if (afterMs === null) return false;
|
||||
return afterMs < 1000 * 60 * 60 * 24 * 365;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
watch([expiration, atDate, atTime, after, unit], () => emit('update:modelValue', get()), {
|
||||
deep: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zmdxowus {
|
||||
padding: 8px 16px;
|
||||
isValid.value = isValidTime();
|
||||
watch([expiration, atDate, atTime, after, unit, isValid], () => {
|
||||
const isValidTimeValue = isValidTime();
|
||||
isValid.value = isValidTimeValue;
|
||||
emit('update:modelValue', {
|
||||
deleteAt: expiration.value === 'at' ? calcAt() : null,
|
||||
deleteAfter: expiration.value === 'after' ? calcAfter() : null,
|
||||
isValid: isValidTimeValue,
|
||||
});
|
||||
}, {
|
||||
deep: true,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 0px;
|
||||
|
||||
>span {
|
||||
opacity: 0.7;
|
||||
@ -131,10 +180,7 @@ watch([expiration, atDate, atTime, after, unit], () => emit('update:modelValue',
|
||||
}
|
||||
|
||||
>section {
|
||||
margin: 16px 0 0 0;
|
||||
|
||||
>div {
|
||||
margin: 0 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
@ -162,4 +208,19 @@ watch([expiration, atDate, atTime, after, unit], () => emit('update:modelValue',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.padding {
|
||||
padding: 8px 24px;
|
||||
}
|
||||
.label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 0;
|
||||
user-select: none;
|
||||
}
|
||||
.withAccent {
|
||||
color: var(--MI_THEME-accent);
|
||||
}
|
||||
.chevronOpening {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
</style>
|
||||
|
@ -185,7 +185,7 @@ const posted = ref(false);
|
||||
const text = ref(props.initialText ?? '');
|
||||
const files = ref(props.initialFiles ?? []);
|
||||
const poll = ref<PollEditorModelValue | null>(null);
|
||||
const scheduledNoteDelete = ref<DeleteScheduleEditorModelValue | null>(null);
|
||||
const scheduledNoteDelete = ref<DeleteScheduleEditorModelValue | null>(defaultStore.state.defaultScheduledNoteDelete ? { deleteAt: null, deleteAfter: defaultStore.state.defaultScheduledNoteDeleteTime, isValid: true } : null);
|
||||
const useCw = ref<boolean>(!!props.initialCw);
|
||||
const showPreview = ref(defaultStore.state.showPreview);
|
||||
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
|
||||
@ -260,6 +260,7 @@ const maxTextLength = computed((): number => {
|
||||
|
||||
const canPost = computed((): boolean => {
|
||||
return !props.mock && !posting.value && !posted.value &&
|
||||
(scheduledNoteDelete.value ? scheduledNoteDelete.value.isValid : true) &&
|
||||
(
|
||||
1 <= textLength.value ||
|
||||
1 <= files.value.length ||
|
||||
@ -433,6 +434,7 @@ function toggleScheduledNoteDelete() {
|
||||
scheduledNoteDelete.value = {
|
||||
deleteAt: null,
|
||||
deleteAfter: null,
|
||||
isValid: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1074,6 +1076,7 @@ onMounted(() => {
|
||||
scheduledNoteDelete.value = {
|
||||
deleteAt: init.deleteAt ? (new Date(init.deleteAt)).getTime() : null,
|
||||
deleteAfter: null,
|
||||
isValid: true,
|
||||
};
|
||||
}
|
||||
if (init.visibleUserIds) {
|
||||
|
@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
|
||||
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
|
||||
<FormLink to="/settings/post-form">{{ i18n.ts.defaultScheduledNoteDelete }}</FormLink>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.pinnedList }}</template>
|
||||
<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
|
||||
|
62
packages/frontend/src/pages/settings/post-form.vue
Normal file
62
packages/frontend/src/pages/settings/post-form.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<!-- <MkSwitch v-model="disableNoteDrafting">
|
||||
<template #caption>{{ i18n.ts.disableNoteDraftingDescription }}</template>
|
||||
{{ i18n.ts.disableNoteDrafting }}
|
||||
<span class="_beta">{{ i18n.ts.originalFeature }}</span>
|
||||
</MkSwitch> -->
|
||||
<div>
|
||||
<div :class="$style.label">
|
||||
{{ i18n.ts.defaultScheduledNoteDeleteTime }}
|
||||
<span class="_beta">{{ i18n.ts.originalFeature }}</span>
|
||||
</div>
|
||||
<MkDeleteScheduleEditor v-model="scheduledNoteDelete" :afterOnly="true"/>
|
||||
</div>
|
||||
<MkSwitch v-model="defaultScheduledNoteDelete">
|
||||
{{ i18n.ts.defaultScheduledNoteDelete }}
|
||||
<span class="_beta">{{ i18n.ts.originalFeature }}</span>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkDeleteScheduleEditor from '@/components/MkDeleteScheduleEditor.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
// const disableNoteDrafting = computed(defaultStore.makeGetterSetter('disableNoteDrafting'));
|
||||
const defaultScheduledNoteDelete = computed(defaultStore.makeGetterSetter('defaultScheduledNoteDelete'));
|
||||
const scheduledNoteDelete = ref({ deleteAt: null, deleteAfter: defaultStore.state.defaultScheduledNoteDeleteTime, isValid: true });
|
||||
watch(scheduledNoteDelete, () => {
|
||||
if (!scheduledNoteDelete.value.isValid) return;
|
||||
defaultStore.set('defaultScheduledNoteDeleteTime', scheduledNoteDelete.value.deleteAfter);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" module>
|
||||
.items {
|
||||
padding: 8px;
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
|
||||
grid-auto-rows: 40px;
|
||||
}
|
||||
.item {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
&:hover {
|
||||
background: var(--X5);
|
||||
}
|
||||
}
|
||||
.label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 0;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
@ -116,6 +116,10 @@ const routes: RouteDef[] = [{
|
||||
path: '/navbar',
|
||||
name: 'navbar',
|
||||
component: page(() => import('@/pages/settings/navbar.vue')),
|
||||
}, {
|
||||
path: '/post-form',
|
||||
name: 'post-form',
|
||||
component: page(() => import('@/pages/settings/post-form.vue')),
|
||||
}, {
|
||||
path: '/statusbar',
|
||||
name: 'statusbar',
|
||||
|
@ -117,6 +117,14 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'account',
|
||||
default: true,
|
||||
},
|
||||
defaultScheduledNoteDelete: {
|
||||
where: 'account',
|
||||
default: false,
|
||||
},
|
||||
defaultScheduledNoteDeleteTime: {
|
||||
where: 'account',
|
||||
default: 86400000,
|
||||
},
|
||||
uploadFolder: {
|
||||
where: 'account',
|
||||
default: null as string | null,
|
||||
@ -569,10 +577,6 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
|
||||
},
|
||||
searchEngine: {
|
||||
where: 'device',
|
||||
default: 'https://google.com/search?q=',
|
||||
},
|
||||
reactionChecksMuting: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
|
Loading…
Reference in New Issue
Block a user