Merge upstream
This commit is contained in:
commit
67f9e3efd1
28 changed files with 448 additions and 126 deletions
|
@ -1341,6 +1341,13 @@ endingCreditMembersDescription: "IDs of users to display on the server informati
|
||||||
emailAddressLogin: "Login with email address"
|
emailAddressLogin: "Login with email address"
|
||||||
usernameLogin: "Login with username"
|
usernameLogin: "Login with username"
|
||||||
unarchive: "Unarchive"
|
unarchive: "Unarchive"
|
||||||
|
autoloadDrafts: "Automatically load drafts when opening the posting form"
|
||||||
|
drafts: "Drafts"
|
||||||
|
abuseReportCategory: "Type of Report"
|
||||||
|
selectCategory: "Select Category"
|
||||||
|
reportComplete: "Report Completed"
|
||||||
|
blockThisUser: "Block This User"
|
||||||
|
muteThisUser: "Mute This User"
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "How to play"
|
howToPlay: "How to play"
|
||||||
hold: "Hold"
|
hold: "Hold"
|
||||||
|
@ -1356,11 +1363,6 @@ _bubbleGame:
|
||||||
section1: "Adjust the position and drop the object into the box."
|
section1: "Adjust the position and drop the object into the box."
|
||||||
section2: "When two objects of the same type touch each other, they will change into a different object and you score points."
|
section2: "When two objects of the same type touch each other, they will change into a different object and you score points."
|
||||||
section3: "The game is over when objects overflow from the box. Aim for a high score by fusing objects together while you avoid overflowing the box!"
|
section3: "The game is over when objects overflow from the box. Aim for a high score by fusing objects together while you avoid overflowing the box!"
|
||||||
abuseReportCategory: "Type of Report"
|
|
||||||
selectCategory: "Select Category"
|
|
||||||
reportComplete: "Report Completed"
|
|
||||||
blockThisUser: "Block This User"
|
|
||||||
muteThisUser: "Mute This User"
|
|
||||||
_abuseReportCategory:
|
_abuseReportCategory:
|
||||||
nsfw: "Sensitive Content Violating NSFW Guidelines"
|
nsfw: "Sensitive Content Violating NSFW Guidelines"
|
||||||
nsfw_description: "Media posts without an NSFW (Not Safe For Work / Sensitive) flag, text posts not hidden by CW (Content Warning), media showing real-life genitalia, etc"
|
nsfw_description: "Media posts without an NSFW (Not Safe For Work / Sensitive) flag, text posts not hidden by CW (Content Warning), media showing real-life genitalia, etc"
|
||||||
|
|
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
|
@ -5532,6 +5532,14 @@ export interface Locale extends ILocale {
|
||||||
* アーカイブ解除
|
* アーカイブ解除
|
||||||
*/
|
*/
|
||||||
"unarchive": string;
|
"unarchive": string;
|
||||||
|
/**
|
||||||
|
* 投稿フォームを開いたときに下書きを自動で読み込む
|
||||||
|
*/
|
||||||
|
"autoloadDrafts": string;
|
||||||
|
/**
|
||||||
|
* 下書き
|
||||||
|
*/
|
||||||
|
"drafts": string;
|
||||||
"_bubbleGame": {
|
"_bubbleGame": {
|
||||||
/**
|
/**
|
||||||
* 遊び方
|
* 遊び方
|
||||||
|
|
|
@ -1376,6 +1376,8 @@ endingCreditMembersDescription: "サーバー情報ページに表示するユ
|
||||||
emailAddressLogin: "メールアドレスでログイン"
|
emailAddressLogin: "メールアドレスでログイン"
|
||||||
usernameLogin: "ユーザー名でログイン"
|
usernameLogin: "ユーザー名でログイン"
|
||||||
unarchive: "アーカイブ解除"
|
unarchive: "アーカイブ解除"
|
||||||
|
autoloadDrafts: "投稿フォームを開いたときに下書きを自動で読み込む"
|
||||||
|
drafts: "下書き"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "遊び方"
|
howToPlay: "遊び方"
|
||||||
|
|
|
@ -1366,6 +1366,8 @@ endingCreditMembersDescription: "서버 정보 페이지에 표시할 유저의
|
||||||
emailAddressLogin: "이메일 주소로 로그인"
|
emailAddressLogin: "이메일 주소로 로그인"
|
||||||
usernameLogin: "유저명으로 로그인"
|
usernameLogin: "유저명으로 로그인"
|
||||||
unarchive: "아카이브 해제"
|
unarchive: "아카이브 해제"
|
||||||
|
autoloadDrafts: "글 작성 시 자동으로 임시 저장된 글 불러오기"
|
||||||
|
drafts: "임시 저장"
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "설명"
|
howToPlay: "설명"
|
||||||
hold: "홀드"
|
hold: "홀드"
|
||||||
|
|
|
@ -274,12 +274,8 @@ export class AccountMoveService {
|
||||||
|
|
||||||
if (!srcprofile || !dstprofile) return;
|
if (!srcprofile || !dstprofile) return;
|
||||||
|
|
||||||
await this.userProfilesRepository.update({ userId: dst.id }, {
|
await this.userProfilesRepository.update({ userId: In([src.id, dst.id]) }, {
|
||||||
moderationNote: srcprofile.moderationNote + '\n' + dstprofile.moderationNote,
|
moderationNote: (srcprofile.moderationNote + '\n' + dstprofile.moderationNote).trim(),
|
||||||
});
|
|
||||||
|
|
||||||
await this.userProfilesRepository.update({ userId: src.id }, {
|
|
||||||
moderationNote: srcprofile.moderationNote + '\n' + dstprofile.moderationNote,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,9 +58,9 @@ export class ChartManagementService implements OnApplicationShutdown {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async start() {
|
public async start() {
|
||||||
// 20分おきにメモリ情報をDBに書き込み
|
// 20分おきにメモリ情報をDBに書き込み
|
||||||
this.saveIntervalId = setInterval(() => {
|
this.saveIntervalId = setInterval(async () => {
|
||||||
for (const chart of this.charts) {
|
for (const chart of this.charts) {
|
||||||
chart.save();
|
await chart.save();
|
||||||
}
|
}
|
||||||
}, 1000 * 60 * 20);
|
}, 1000 * 60 * 20);
|
||||||
}
|
}
|
||||||
|
@ -69,9 +69,9 @@ export class ChartManagementService implements OnApplicationShutdown {
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
clearInterval(this.saveIntervalId);
|
clearInterval(this.saveIntervalId);
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
await Promise.all(
|
for (const chart of this.charts) {
|
||||||
this.charts.map(chart => chart.save()),
|
await chart.save();
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,20 +48,18 @@ export class CleanChartsProcessorService {
|
||||||
public async process(): Promise<void> {
|
public async process(): Promise<void> {
|
||||||
this.logger.info('Clean charts...');
|
this.logger.info('Clean charts...');
|
||||||
|
|
||||||
await Promise.all([
|
await this.federationChart.clean();
|
||||||
this.federationChart.clean(),
|
await this.notesChart.clean();
|
||||||
this.notesChart.clean(),
|
await this.usersChart.clean();
|
||||||
this.usersChart.clean(),
|
await this.activeUsersChart.clean();
|
||||||
this.activeUsersChart.clean(),
|
await this.instanceChart.clean();
|
||||||
this.instanceChart.clean(),
|
await this.perUserNotesChart.clean();
|
||||||
this.perUserNotesChart.clean(),
|
await this.perUserPvChart.clean();
|
||||||
this.perUserPvChart.clean(),
|
await this.driveChart.clean();
|
||||||
this.driveChart.clean(),
|
await this.perUserReactionsChart.clean();
|
||||||
this.perUserReactionsChart.clean(),
|
await this.perUserFollowingChart.clean();
|
||||||
this.perUserFollowingChart.clean(),
|
await this.perUserDriveChart.clean();
|
||||||
this.perUserDriveChart.clean(),
|
await this.apRequestChart.clean();
|
||||||
this.apRequestChart.clean(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.logger.succ('All charts successfully cleaned.');
|
this.logger.succ('All charts successfully cleaned.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,11 +31,9 @@ export class ResyncChartsProcessorService {
|
||||||
|
|
||||||
// TODO: ユーザーごとのチャートも更新する
|
// TODO: ユーザーごとのチャートも更新する
|
||||||
// TODO: インスタンスごとのチャートも更新する
|
// TODO: インスタンスごとのチャートも更新する
|
||||||
await Promise.all([
|
await this.driveChart.resync();
|
||||||
this.driveChart.resync(),
|
await this.notesChart.resync();
|
||||||
this.notesChart.resync(),
|
await this.usersChart.resync();
|
||||||
this.usersChart.resync(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.logger.succ('All charts successfully resynced.');
|
this.logger.succ('All charts successfully resynced.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,20 +48,18 @@ export class TickChartsProcessorService {
|
||||||
public async process(): Promise<void> {
|
public async process(): Promise<void> {
|
||||||
this.logger.info('Tick charts...');
|
this.logger.info('Tick charts...');
|
||||||
|
|
||||||
await Promise.all([
|
await this.federationChart.tick(false);
|
||||||
this.federationChart.tick(false),
|
await this.notesChart.tick(false);
|
||||||
this.notesChart.tick(false),
|
await this.usersChart.tick(false);
|
||||||
this.usersChart.tick(false),
|
await this.activeUsersChart.tick(false);
|
||||||
this.activeUsersChart.tick(false),
|
await this.instanceChart.tick(false);
|
||||||
this.instanceChart.tick(false),
|
await this.perUserNotesChart.tick(false);
|
||||||
this.perUserNotesChart.tick(false),
|
await this.perUserPvChart.tick(false);
|
||||||
this.perUserPvChart.tick(false),
|
await this.driveChart.tick(false);
|
||||||
this.driveChart.tick(false),
|
await this.perUserReactionsChart.tick(false);
|
||||||
this.perUserReactionsChart.tick(false),
|
await this.perUserFollowingChart.tick(false);
|
||||||
this.perUserFollowingChart.tick(false),
|
await this.perUserDriveChart.tick(false);
|
||||||
this.perUserDriveChart.tick(false),
|
await this.apRequestChart.tick(false);
|
||||||
this.apRequestChart.tick(false),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.logger.succ('All charts successfully ticked.');
|
this.logger.succ('All charts successfully ticked.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -524,8 +524,8 @@ export class ActivityPubServerService {
|
||||||
},
|
},
|
||||||
deriveConstraint(request: IncomingMessage) {
|
deriveConstraint(request: IncomingMessage) {
|
||||||
const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
|
const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
|
||||||
const isAp = typeof accepted === 'string' && !accepted.match(/html/);
|
if (accepted === false) return null;
|
||||||
return isAp ? 'ap' : 'html';
|
return accepted !== 'html' ? 'ap' : 'html';
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const meta = {
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: true, nullable: false,
|
||||||
properties: {
|
properties: {
|
||||||
createdNote: {
|
createdNote: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -207,6 +207,7 @@ export const paramDef = {
|
||||||
},
|
},
|
||||||
required: ['choices'],
|
required: ['choices'],
|
||||||
},
|
},
|
||||||
|
noCreatedNote: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
// (re)note with text, files and poll are optional
|
// (re)note with text, files and poll are optional
|
||||||
if: {
|
if: {
|
||||||
|
@ -281,7 +282,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const note = await this.notesRepository.findOneBy({ id: idempotent });
|
const note = await this.notesRepository.findOneBy({ id: idempotent });
|
||||||
if (note) {
|
if (note) {
|
||||||
logger.info('The request has already been processed.', { noteId: note.id });
|
logger.info('The request has already been processed.', { noteId: note.id });
|
||||||
return { createdNote: await this.noteEntityService.pack(note, me) };
|
if (ps.noCreatedNote) return;
|
||||||
|
else return { createdNote: await this.noteEntityService.pack(note, me) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,7 +455,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
await this.redisForTimelines.set(`note:idempotent:${me.id}:${hash}`, note.id, 'EX', 60);
|
await this.redisForTimelines.set(`note:idempotent:${me.id}:${hash}`, note.id, 'EX', 60);
|
||||||
|
|
||||||
logger.info('Successfully created a note.', { noteId: note.id });
|
logger.info('Successfully created a note.', { noteId: note.id });
|
||||||
return {
|
if (ps.noCreatedNote) return;
|
||||||
|
else return {
|
||||||
createdNote: await this.noteEntityService.pack(note, me),
|
createdNote: await this.noteEntityService.pack(note, me),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
title: { type: 'string' },
|
title: { type: 'string' },
|
||||||
name: { type: 'string', minLength: 1 },
|
name: { type: 'string', minLength: 1, pattern: /^[a-zA-Z0-9_-]+$/.toString().slice(1, -1) },
|
||||||
summary: { type: 'string', nullable: true },
|
summary: { type: 'string', nullable: true },
|
||||||
content: { type: 'array', items: {
|
content: { type: 'array', items: {
|
||||||
type: 'object', additionalProperties: true,
|
type: 'object', additionalProperties: true,
|
||||||
|
|
|
@ -57,7 +57,7 @@ export const paramDef = {
|
||||||
properties: {
|
properties: {
|
||||||
pageId: { type: 'string', format: 'misskey:id' },
|
pageId: { type: 'string', format: 'misskey:id' },
|
||||||
title: { type: 'string' },
|
title: { type: 'string' },
|
||||||
name: { type: 'string', minLength: 1 },
|
name: { type: 'string', minLength: 1, pattern: /^[a-zA-Z0-9_-]+$/.toString().slice(1, -1) },
|
||||||
summary: { type: 'string', nullable: true },
|
summary: { type: 'string', nullable: true },
|
||||||
content: { type: 'array', items: {
|
content: { type: 'array', items: {
|
||||||
type: 'object', additionalProperties: true,
|
type: 'object', additionalProperties: true,
|
||||||
|
|
|
@ -534,7 +534,7 @@ export class ClientServerService {
|
||||||
|
|
||||||
vary(reply.raw, 'Accept');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
if (user != null) {
|
if (user) {
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
const me = profile.fields
|
const me = profile.fields
|
||||||
|
@ -564,11 +564,9 @@ export class ClientServerService {
|
||||||
fastify.get<{ Params: { user: string; } }>('/users/:user', async (request, reply) => {
|
fastify.get<{ Params: { user: string; } }>('/users/:user', async (request, reply) => {
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
id: request.params.user,
|
id: request.params.user,
|
||||||
host: IsNull(),
|
|
||||||
isSuspended: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user == null) {
|
if (!user || (user.isDeleted && user.isSuspended)) {
|
||||||
reply.code(404);
|
reply.code(404);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,8 @@ window.onload = async () => {
|
||||||
|
|
||||||
document.getElementById('submit').addEventListener('click', () => {
|
document.getElementById('submit').addEventListener('click', () => {
|
||||||
api('notes/create', {
|
api('notes/create', {
|
||||||
text: document.getElementById('text').value
|
text: document.getElementById('text').value,
|
||||||
|
noCreatedNote: true,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
|
|
183
packages/frontend/src/components/MkDraftsDialog.vue
Normal file
183
packages/frontend/src/components/MkDraftsDialog.vue
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
<template>
|
||||||
|
<MkModalWindow
|
||||||
|
ref="dialog"
|
||||||
|
:height="500"
|
||||||
|
:width="800"
|
||||||
|
@click="done(true)"
|
||||||
|
@close="done(true)"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
{{ i18n.ts.drafts }}
|
||||||
|
</template>
|
||||||
|
<div style="display: flex; flex-direction: column">
|
||||||
|
<div v-if="drafts.length === 0" class="empty">
|
||||||
|
<div class="_fullinfo">
|
||||||
|
<img :src="infoImageUrl" class="_ghost"/>
|
||||||
|
<div>{{ i18n.ts.nothing }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="draft in drafts" :key="draft.id" :class="$style.draftItem">
|
||||||
|
<div :class="$style.draftNote" @click="selectDraft(draft.id)">
|
||||||
|
<div :class="$style.draftNoteHeader">
|
||||||
|
<div :class="$style.draftNoteDestination">
|
||||||
|
<span v-if="draft.channel" style="opacity: 0.7; padding-right: 0.5em">
|
||||||
|
<i class="ti ti-device-tv"></i> {{ draft.channel.name }}
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.renote">
|
||||||
|
<i class="ti ti-quote"></i> <MkAcct :user="draft.renote.user" /> <span>{{ draft.renote.text }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="draft.reply">
|
||||||
|
<i class="ti ti-arrow-back-up"></i> <MkAcct :user="draft.reply.user" /> <span>{{ draft.reply.text }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<i class="ti ti-pencil"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.draftNoteInfo">
|
||||||
|
<MkTime :time="draft.createdAt" colored />
|
||||||
|
<span v-if="draft.visibility !== 'public'" :title="i18n.ts._visibility[draft.visibility]" style="margin-left: 0.5em">
|
||||||
|
<i v-if="draft.visibility === 'home'" class="ti ti-home"></i>
|
||||||
|
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
|
<i v-else-if="draft.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']" style="margin-left: 0.5em">
|
||||||
|
<i class="ti ti-rocket-off"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="draft.channel" :title="draft.channel.name" style="margin-left: 0.5em">
|
||||||
|
<i class="ti ti-device-tv"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p v-if="!!draft.cw" :class="$style.draftNoteCw">
|
||||||
|
<Mfm :text="draft.cw" />
|
||||||
|
</p>
|
||||||
|
<MkSubNoteContent :class="$style.draftNoteText" :note="draft" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button :class="$style.delete" class="_button" @click="removeDraft(draft.id)">
|
||||||
|
<i class="ti ti-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkModalWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onActivated, onMounted, ref, shallowRef } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
||||||
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
import type { NoteDraftItem } from '@/types/note-draft-item.js';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'done', v: { canceled: true } | { canceled: false; selected: string | undefined }): void;
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
|
||||||
|
const drafts = ref<(Misskey.entities.Note & { useCw: boolean })[]>([]);
|
||||||
|
|
||||||
|
onMounted(loadDrafts);
|
||||||
|
onActivated(loadDrafts);
|
||||||
|
|
||||||
|
function loadDrafts() {
|
||||||
|
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
|
||||||
|
drafts.value = Object.keys(stored).map((key) => ({
|
||||||
|
...(stored[key].data as Misskey.entities.Note & { useCw: boolean }),
|
||||||
|
id: key,
|
||||||
|
createdAt: stored[key].updatedAt,
|
||||||
|
channel: stored[key].channel as Misskey.entities.Channel,
|
||||||
|
renote: stored[key].renote as Misskey.entities.Note,
|
||||||
|
reply: stored[key].reply as Misskey.entities.Note,
|
||||||
|
user: $i as Misskey.entities.User,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDraft(draft: string) {
|
||||||
|
done(false, draft);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDraft(draft: string) {
|
||||||
|
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
|
||||||
|
|
||||||
|
delete stored[draft];
|
||||||
|
miLocalStorage.setItem('drafts', JSON.stringify(stored));
|
||||||
|
|
||||||
|
loadDrafts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function done(canceled: boolean, selected?: string): void {
|
||||||
|
emit('done', { canceled, selected } as
|
||||||
|
| { canceled: true }
|
||||||
|
| { canceled: false; selected: string | undefined });
|
||||||
|
dialog.value?.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.draftItem {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 0 8px 0;
|
||||||
|
border-bottom: 1px solid var(--divider);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
background-color: var(--accentedBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNote {
|
||||||
|
flex: 1;
|
||||||
|
width: calc(100% - 16px - 48px - 4px);
|
||||||
|
margin: 0 8px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNoteHeader {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNoteDestination {
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNoteInfo {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNoteCw {
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draftNoteText {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
width: 48px;
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-self: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--buttonBg);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -41,6 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></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>
|
<span v-else><i class="ti ti-icons"></i></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button v-if="!props.instant" v-click-anime v-tooltip="i18n.ts.drafts" class="_button" :class="$style.headerRightItem" @click="openDrafts">
|
||||||
|
<i class="ti ti-pencil"></i>
|
||||||
|
</button>
|
||||||
<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
|
<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
|
||||||
<div :class="$style.submitInner">
|
<div :class="$style.submitInner">
|
||||||
<template v-if="posted"></template>
|
<template v-if="posted"></template>
|
||||||
|
@ -110,6 +113,7 @@ import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||||
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||||
|
import MkDraftsDialog from '@/components/MkDraftsDialog.vue';
|
||||||
import { host, url } from '@/config.js';
|
import { host, url } from '@/config.js';
|
||||||
import { erase, unique } from '@/scripts/array.js';
|
import { erase, unique } from '@/scripts/array.js';
|
||||||
import { extractMentions } from '@/scripts/extract-mentions.js';
|
import { extractMentions } from '@/scripts/extract-mentions.js';
|
||||||
|
@ -130,6 +134,7 @@ import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||||
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
|
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
|
||||||
|
import type { NoteDraftItem } from '@/types/note-draft-item.js';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
|
@ -180,6 +185,10 @@ const visibilityButton = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const posting = ref(false);
|
const posting = ref(false);
|
||||||
const posted = ref(false);
|
const posted = ref(false);
|
||||||
|
const draftId = ref<string>(Date.now().toString());
|
||||||
|
const reply = ref(props.reply ?? null);
|
||||||
|
const renote = ref(props.renote ?? null);
|
||||||
|
const channel = ref(props.channel ?? null);
|
||||||
const text = ref(props.initialText ?? '');
|
const text = ref(props.initialText ?? '');
|
||||||
const files = ref(props.initialFiles ?? []);
|
const files = ref(props.initialFiles ?? []);
|
||||||
const poll = ref<PollEditorModelValue | null>(null);
|
const poll = ref<PollEditorModelValue | null>(null);
|
||||||
|
@ -210,25 +219,25 @@ const textAreaReadOnly = ref(false);
|
||||||
const nsfwGuideUrl = 'https://go.misskey.io/media-guideline';
|
const nsfwGuideUrl = 'https://go.misskey.io/media-guideline';
|
||||||
|
|
||||||
const draftKey = computed((): string => {
|
const draftKey = computed((): string => {
|
||||||
let key = props.channel ? `channel:${props.channel.id}` : '';
|
let key = channel.value ? `channel:${channel.value.id}` : '';
|
||||||
|
|
||||||
if (props.renote) {
|
if (renote.value) {
|
||||||
key += `renote:${props.renote.id}`;
|
key += `renote:${renote.value.id}`;
|
||||||
} else if (props.reply) {
|
} else if (reply.value) {
|
||||||
key += `reply:${props.reply.id}`;
|
key += `reply:${reply.value.id}`;
|
||||||
} else {
|
} else {
|
||||||
key += `note:${$i.id}`;
|
key += `note:${draftId.value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
});
|
});
|
||||||
|
|
||||||
const placeholder = computed((): string => {
|
const placeholder = computed((): string => {
|
||||||
if (props.renote) {
|
if (renote.value) {
|
||||||
return i18n.ts._postForm.quotePlaceholder;
|
return i18n.ts._postForm.quotePlaceholder;
|
||||||
} else if (props.reply) {
|
} else if (reply.value) {
|
||||||
return i18n.ts._postForm.replyPlaceholder;
|
return i18n.ts._postForm.replyPlaceholder;
|
||||||
} else if (props.channel) {
|
} else if (channel.value) {
|
||||||
return i18n.ts._postForm.channelPlaceholder;
|
return i18n.ts._postForm.channelPlaceholder;
|
||||||
} else {
|
} else {
|
||||||
const xs = [
|
const xs = [
|
||||||
|
@ -244,9 +253,9 @@ const placeholder = computed((): string => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitText = computed((): string => {
|
const submitText = computed((): string => {
|
||||||
return props.renote
|
return renote.value
|
||||||
? i18n.ts.quote
|
? i18n.ts.quote
|
||||||
: props.reply
|
: reply.value
|
||||||
? i18n.ts.reply
|
? i18n.ts.reply
|
||||||
: i18n.ts.note;
|
: i18n.ts.note;
|
||||||
});
|
});
|
||||||
|
@ -265,8 +274,8 @@ const canPost = computed((): boolean => {
|
||||||
1 <= textLength.value ||
|
1 <= textLength.value ||
|
||||||
1 <= files.value.length ||
|
1 <= files.value.length ||
|
||||||
poll.value != null ||
|
poll.value != null ||
|
||||||
props.renote != null ||
|
renote.value != null ||
|
||||||
(props.reply != null && quoteId.value != null)
|
(reply.value != null && quoteId.value != null)
|
||||||
) &&
|
) &&
|
||||||
(textLength.value <= maxTextLength.value) &&
|
(textLength.value <= maxTextLength.value) &&
|
||||||
(!poll.value || poll.value.choices.length >= 2);
|
(!poll.value || poll.value.choices.length >= 2);
|
||||||
|
@ -294,13 +303,13 @@ if (props.mention) {
|
||||||
text.value += ' ';
|
text.value += ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) {
|
if (reply.value && (reply.value.user.username !== $i.username || (reply.value.user.host != null && reply.value.user.host !== host))) {
|
||||||
text.value = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `;
|
text.value = `@${reply.value.user.username}${reply.value.user.host != null ? '@' + toASCII(reply.value.user.host) : ''} `;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.reply && props.reply.text != null) {
|
if (reply.value && reply.value.text != null) {
|
||||||
const ast = mfm.parse(props.reply.text);
|
const ast = mfm.parse(reply.value.text);
|
||||||
const otherHost = props.reply.user.host;
|
const otherHost = reply.value.user.host;
|
||||||
|
|
||||||
for (const x of extractMentions(ast)) {
|
for (const x of extractMentions(ast)) {
|
||||||
const mention = x.host ?
|
const mention = x.host ?
|
||||||
|
@ -323,32 +332,32 @@ if ($i.isSilenced && visibility.value === 'public') {
|
||||||
visibility.value = 'home';
|
visibility.value = 'home';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.channel) {
|
if (channel.value) {
|
||||||
visibility.value = 'public';
|
visibility.value = 'public';
|
||||||
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
||||||
}
|
}
|
||||||
|
|
||||||
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
|
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
|
||||||
if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) {
|
if (reply.value && ['home', 'followers', 'specified'].includes(reply.value.visibility)) {
|
||||||
if (props.reply.visibility === 'home' && visibility.value === 'followers') {
|
if (reply.value.visibility === 'home' && visibility.value === 'followers') {
|
||||||
visibility.value = 'followers';
|
visibility.value = 'followers';
|
||||||
} else if (['home', 'followers'].includes(props.reply.visibility) && visibility.value === 'specified') {
|
} else if (['home', 'followers'].includes(reply.value.visibility) && visibility.value === 'specified') {
|
||||||
visibility.value = 'specified';
|
visibility.value = 'specified';
|
||||||
} else {
|
} else {
|
||||||
visibility.value = props.reply.visibility;
|
visibility.value = reply.value.visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visibility.value === 'specified') {
|
if (visibility.value === 'specified') {
|
||||||
if (props.reply.visibleUserIds) {
|
if (reply.value.visibleUserIds) {
|
||||||
misskeyApi('users/show', {
|
misskeyApi('users/show', {
|
||||||
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply?.userId),
|
userIds: reply.value.visibleUserIds.filter(uid => uid !== $i.id && uid !== reply.value?.userId),
|
||||||
}).then(users => {
|
}).then(users => {
|
||||||
users.forEach(u => pushVisibleUser(u));
|
users.forEach(u => pushVisibleUser(u));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.reply.userId !== $i.id) {
|
if (reply.value.userId !== $i.id) {
|
||||||
misskeyApi('users/show', { userId: props.reply.userId }).then(user => {
|
misskeyApi('users/show', { userId: reply.value.userId }).then(user => {
|
||||||
pushVisibleUser(user);
|
pushVisibleUser(user);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -361,9 +370,9 @@ if (props.specified) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep cw when reply
|
// keep cw when reply
|
||||||
if (defaultStore.state.keepCw && props.reply?.cw) {
|
if (defaultStore.state.keepCw && reply.value?.cw) {
|
||||||
useCw.value = true;
|
useCw.value = true;
|
||||||
cw.value = props.reply.cw;
|
cw.value = reply.value.cw;
|
||||||
}
|
}
|
||||||
|
|
||||||
function watchForDraft() {
|
function watchForDraft() {
|
||||||
|
@ -465,7 +474,7 @@ function upload(file: File, name?: string): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVisibility() {
|
function setVisibility() {
|
||||||
if (props.channel) {
|
if (channel.value) {
|
||||||
visibility.value = 'public';
|
visibility.value = 'public';
|
||||||
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
||||||
return;
|
return;
|
||||||
|
@ -476,7 +485,7 @@ function setVisibility() {
|
||||||
isSilenced: $i.isSilenced,
|
isSilenced: $i.isSilenced,
|
||||||
localOnly: localOnly.value,
|
localOnly: localOnly.value,
|
||||||
src: visibilityButton.value,
|
src: visibilityButton.value,
|
||||||
...(props.reply ? { isReplyVisibilitySpecified: props.reply.visibility === 'specified' } : {}),
|
...(reply.value ? { isReplyVisibilitySpecified: reply.value.visibility === 'specified' } : {}),
|
||||||
}, {
|
}, {
|
||||||
changeVisibility: v => {
|
changeVisibility: v => {
|
||||||
visibility.value = v;
|
visibility.value = v;
|
||||||
|
@ -488,7 +497,7 @@ function setVisibility() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleLocalOnly() {
|
async function toggleLocalOnly() {
|
||||||
if (props.channel) {
|
if (channel.value) {
|
||||||
visibility.value = 'public';
|
visibility.value = 'public';
|
||||||
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
||||||
return;
|
return;
|
||||||
|
@ -607,7 +616,7 @@ async function onPaste(ev: ClipboardEvent) {
|
||||||
|
|
||||||
const paste = ev.clipboardData.getData('text');
|
const paste = ev.clipboardData.getData('text');
|
||||||
|
|
||||||
if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) {
|
if (!renote.value && !quoteId.value && paste.startsWith(url + '/notes/')) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
os.confirm({
|
os.confirm({
|
||||||
|
@ -681,10 +690,32 @@ function onDrop(ev: DragEvent): void {
|
||||||
function saveDraft() {
|
function saveDraft() {
|
||||||
if (props.instant || props.mock) return;
|
if (props.instant || props.mock) return;
|
||||||
|
|
||||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
|
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
|
||||||
|
|
||||||
draftData[draftKey.value] = {
|
draftData[draftKey.value] = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
channel: channel.value ? {
|
||||||
|
id: channel.value.id,
|
||||||
|
name: channel.value.name,
|
||||||
|
} : undefined,
|
||||||
|
renote: renote.value ? {
|
||||||
|
id: renote.value.id,
|
||||||
|
text: (renote.value.cw ?? renote.value.text)?.substring(0, 100),
|
||||||
|
user: {
|
||||||
|
id: renote.value.userId,
|
||||||
|
username: renote.value.user.username,
|
||||||
|
host: renote.value.user.host,
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
|
reply: reply.value ? {
|
||||||
|
id: reply.value.id,
|
||||||
|
text: (reply.value.cw ?? reply.value.text)?.substring(0, 100),
|
||||||
|
user: {
|
||||||
|
id: reply.value.userId,
|
||||||
|
username: reply.value.user.username,
|
||||||
|
host: reply.value.user.host,
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
data: {
|
data: {
|
||||||
text: text.value,
|
text: text.value,
|
||||||
useCw: useCw.value,
|
useCw: useCw.value,
|
||||||
|
@ -702,13 +733,75 @@ function saveDraft() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteDraft() {
|
function deleteDraft() {
|
||||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
|
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
|
||||||
|
|
||||||
delete draftData[draftKey.value];
|
delete draftData[draftKey.value];
|
||||||
|
|
||||||
|
draftId.value = Date.now().toString();
|
||||||
|
|
||||||
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openDrafts() {
|
||||||
|
const { canceled, selected } = await new Promise<{canceled: boolean, selected: string | undefined}>(resolve => {
|
||||||
|
os.popup(MkDraftsDialog, {}, {
|
||||||
|
done: result => {
|
||||||
|
resolve(typeof result.selected === 'string' ? result : { canceled: true, selected: undefined });
|
||||||
|
},
|
||||||
|
}, 'closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
const channelId = selected.startsWith('channel:') ? selected.match(/channel:(.+?)(renote|reply|note):/)?.[1] : undefined;
|
||||||
|
const renoteId = selected.includes('renote:') ? selected.match(/renote:(.+)/)?.[1] : undefined;
|
||||||
|
const replyId = selected.includes('reply:') ? selected.match(/reply:(.+)/)?.[1] : undefined;
|
||||||
|
|
||||||
|
channel.value = channelId ? await misskeyApi('channels/show', { channelId }) : null;
|
||||||
|
renote.value = renoteId ? await misskeyApi('notes/show', { noteId: renoteId }) : null;
|
||||||
|
reply.value = replyId ? await misskeyApi('notes/show', { noteId: replyId }) : null;
|
||||||
|
|
||||||
|
if (!renote.value && !reply.value) {
|
||||||
|
draftId.value = selected.match(/note:(.+)/)?.[1] ?? Date.now().toString();
|
||||||
|
} else {
|
||||||
|
draftId.value = Date.now().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDraft(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDraft(exactMatch: boolean = false) {
|
||||||
|
const drafts = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
|
||||||
|
const scope = exactMatch ? draftKey.value : draftKey.value.replace(`note:${draftId.value}`, 'note:');
|
||||||
|
const draft = Object.entries(drafts).filter(([k]) => k.startsWith(scope))
|
||||||
|
.map(r => ({ key: r[0], value: { ...r[1], updatedAt: new Date(r[1].updatedAt).getTime() } }))
|
||||||
|
.sort((a, b) => b.value.updatedAt - a.value.updatedAt).at(0);
|
||||||
|
|
||||||
|
if (draft) {
|
||||||
|
if (scope !== draft.key) {
|
||||||
|
draftId.value = draft.key.replace(scope, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
text.value = draft.value.data.text;
|
||||||
|
useCw.value = draft.value.data.useCw;
|
||||||
|
cw.value = draft.value.data.cw;
|
||||||
|
visibility.value = draft.value.data.visibility;
|
||||||
|
localOnly.value = draft.value.data.localOnly;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
files.value = draft.value.data.files?.filter(f => f?.id && f.type && f.name) || [];
|
||||||
|
if (draft.value.data.poll) {
|
||||||
|
poll.value = draft.value.data.poll;
|
||||||
|
}
|
||||||
|
if (draft.value.data.visibleUserIds) {
|
||||||
|
misskeyApi('users/show', { userIds: draft.value.data.visibleUserIds }).then(users => {
|
||||||
|
users.forEach(u => pushVisibleUser(u));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function post(ev?: MouseEvent) {
|
async function post(ev?: MouseEvent) {
|
||||||
if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
|
if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
|
||||||
os.alert({
|
os.alert({
|
||||||
|
@ -766,15 +859,16 @@ async function post(ev?: MouseEvent) {
|
||||||
text: text.value === '' ? null : text.value,
|
text: text.value === '' ? null : text.value,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
fileIds: files.value.length > 0 ? files.value.filter(f => f?.id).map(f => f.id) : undefined,
|
fileIds: files.value.length > 0 ? files.value.filter(f => f?.id).map(f => f.id) : undefined,
|
||||||
replyId: props.reply ? props.reply.id : undefined,
|
replyId: reply.value ? reply.value.id : undefined,
|
||||||
renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined,
|
renoteId: renote.value ? renote.value.id : quoteId.value ? quoteId.value : undefined,
|
||||||
channelId: props.channel ? props.channel.id : undefined,
|
channelId: channel.value ? channel.value.id : undefined,
|
||||||
poll: poll.value,
|
poll: poll.value,
|
||||||
cw: useCw.value ? cw.value ?? '' : null,
|
cw: useCw.value ? cw.value ?? '' : null,
|
||||||
localOnly: localOnly.value,
|
localOnly: localOnly.value,
|
||||||
visibility: visibility.value,
|
visibility: visibility.value,
|
||||||
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
|
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
|
||||||
reactionAcceptance: reactionAcceptance.value,
|
reactionAcceptance: reactionAcceptance.value,
|
||||||
|
noCreatedNote: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
|
if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
|
||||||
|
@ -855,7 +949,7 @@ async function post(ev?: MouseEvent) {
|
||||||
claimAchievement('brainDiver');
|
claimAchievement('brainDiver');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.renote && (props.renote.userId === $i.id) && text.length > 0) {
|
if (renote.value && (renote.value.userId === $i.id) && text.length > 0) {
|
||||||
claimAchievement('selfQuote');
|
claimAchievement('selfQuote');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -960,25 +1054,8 @@ onMounted(() => {
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 書きかけの投稿を復元
|
// 書きかけの投稿を復元
|
||||||
if (!props.instant && !props.mention && !props.specified && !props.mock) {
|
if (!props.instant && !props.mention && !props.specified && !props.mock && defaultStore.state.autoloadDrafts) {
|
||||||
const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey.value];
|
loadDraft();
|
||||||
if (draft) {
|
|
||||||
text.value = draft.data.text;
|
|
||||||
useCw.value = draft.data.useCw;
|
|
||||||
cw.value = draft.data.cw;
|
|
||||||
visibility.value = draft.data.visibility;
|
|
||||||
localOnly.value = draft.data.localOnly;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
files.value = draft.data.files?.filter(f => f?.id && f.type && f.name) || [];
|
|
||||||
if (draft.data.poll) {
|
|
||||||
poll.value = draft.data.poll;
|
|
||||||
}
|
|
||||||
if (draft.data.visibleUserIds) {
|
|
||||||
misskeyApi('users/show', { userIds: draft.data.visibleUserIds }).then(users => {
|
|
||||||
users.forEach(u => pushVisibleUser(u));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 削除して編集
|
// 削除して編集
|
||||||
|
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts._pages.summary }}</template>
|
<template #label>{{ i18n.ts._pages.summary }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="name">
|
<MkInput v-model="name" type="text" pattern="^[a-zA-Z0-9_-]+$" autocapitalize="off">
|
||||||
<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
|
<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
|
||||||
<template #label>{{ i18n.ts._pages.url }}</template>
|
<template #label>{{ i18n.ts._pages.url }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
@ -158,7 +158,7 @@ function save() {
|
||||||
|
|
||||||
if (pageId.value) {
|
if (pageId.value) {
|
||||||
options.pageId = pageId.value;
|
options.pageId = pageId.value;
|
||||||
misskeyApi('pages/update', options)
|
os.apiWithDialog('pages/update', options)
|
||||||
.then(page => {
|
.then(page => {
|
||||||
currentName.value = name.value.trim();
|
currentName.value = name.value.trim();
|
||||||
os.alert({
|
os.alert({
|
||||||
|
@ -167,7 +167,7 @@ function save() {
|
||||||
});
|
});
|
||||||
}).catch(onError);
|
}).catch(onError);
|
||||||
} else {
|
} else {
|
||||||
misskeyApi('pages/create', options)
|
os.apiWithDialog('pages/create', options)
|
||||||
.then(created => {
|
.then(created => {
|
||||||
pageId.value = created.id;
|
pageId.value = created.id;
|
||||||
currentName.value = name.value.trim();
|
currentName.value = name.value.trim();
|
||||||
|
|
|
@ -46,6 +46,7 @@ function start(_game: Misskey.entities.ReversiGameDetailed) {
|
||||||
misskeyApi('notes/create', {
|
misskeyApi('notes/create', {
|
||||||
text: i18n.ts._reversi.iStartedAGame + '\n' + location.href,
|
text: i18n.ts._reversi.iStartedAGame + '\n' + location.href,
|
||||||
visibility: 'home',
|
visibility: 'home',
|
||||||
|
noCreatedNote: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkButton v-if="defaultStore.reactiveState.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
|
<MkButton v-if="defaultStore.reactiveState.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
|
||||||
<MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
|
<MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
<MkSwitch v-model="autoloadDrafts">{{ i18n.ts.autoloadDrafts }}</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
@ -306,6 +307,7 @@ const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
|
||||||
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
|
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
|
||||||
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
|
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
|
||||||
const showFixedPostFormInChannel = computed(defaultStore.makeGetterSetter('showFixedPostFormInChannel'));
|
const showFixedPostFormInChannel = computed(defaultStore.makeGetterSetter('showFixedPostFormInChannel'));
|
||||||
|
const autoloadDrafts = computed(defaultStore.makeGetterSetter('autoloadDrafts'));
|
||||||
const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache'));
|
const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache'));
|
||||||
const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
|
const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
|
||||||
const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
|
const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
|
||||||
|
|
|
@ -84,6 +84,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
||||||
'useBlurEffect',
|
'useBlurEffect',
|
||||||
'showFixedPostForm',
|
'showFixedPostForm',
|
||||||
'showFixedPostFormInChannel',
|
'showFixedPostFormInChannel',
|
||||||
|
'autoloadDrafts',
|
||||||
'enableInfiniteScroll',
|
'enableInfiniteScroll',
|
||||||
'useReactionPickerForContextMenu',
|
'useReactionPickerForContextMenu',
|
||||||
'showGapBetweenNotesInTimeline',
|
'showGapBetweenNotesInTimeline',
|
||||||
|
|
|
@ -565,6 +565,7 @@ export function getRenoteMenu(props: {
|
||||||
misskeyApi('notes/create', {
|
misskeyApi('notes/create', {
|
||||||
renoteId: appearNote.id,
|
renoteId: appearNote.id,
|
||||||
channelId: appearNote.channelId,
|
channelId: appearNote.channelId,
|
||||||
|
noCreatedNote: true,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
os.toast(i18n.ts.renoted);
|
os.toast(i18n.ts.renoted);
|
||||||
});
|
});
|
||||||
|
@ -611,6 +612,7 @@ export function getRenoteMenu(props: {
|
||||||
localOnly,
|
localOnly,
|
||||||
visibility,
|
visibility,
|
||||||
renoteId: appearNote.id,
|
renoteId: appearNote.id,
|
||||||
|
noCreatedNote: true,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
os.toast(i18n.ts.renoted);
|
os.toast(i18n.ts.renoted);
|
||||||
});
|
});
|
||||||
|
@ -652,6 +654,7 @@ export function getRenoteMenu(props: {
|
||||||
misskeyApi('notes/create', {
|
misskeyApi('notes/create', {
|
||||||
renoteId: appearNote.id,
|
renoteId: appearNote.id,
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
|
noCreatedNote: true,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
os.toast(i18n.tsx.renotedToX({ name: channel.name }));
|
os.toast(i18n.tsx.renotedToX({ name: channel.name }));
|
||||||
});
|
});
|
||||||
|
|
|
@ -306,7 +306,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
||||||
const { canceled: canceled3, result: memo } = await os.inputText({
|
const { canceled: canceled3, result: memo } = await os.inputText({
|
||||||
title: i18n.ts.addMemo,
|
title: i18n.ts.addMemo,
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
placeholder: i18n.ts.memo,
|
default: '',
|
||||||
});
|
});
|
||||||
if (canceled3) return;
|
if (canceled3) return;
|
||||||
|
|
||||||
|
|
|
@ -312,6 +312,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
autoloadDrafts: {
|
||||||
|
where: 'device',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
enableInfiniteScroll: {
|
enableInfiniteScroll: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: true,
|
default: true,
|
||||||
|
|
38
packages/frontend/src/types/note-draft-item.ts
Normal file
38
packages/frontend/src/types/note-draft-item.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||||
|
|
||||||
|
export type NoteDraftItem = {
|
||||||
|
updatedAt: string;
|
||||||
|
channel?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
renote?: {
|
||||||
|
id: string;
|
||||||
|
text: string | null;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
host: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
reply?: {
|
||||||
|
id: string;
|
||||||
|
text: string | null;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
host: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
data: {
|
||||||
|
text: string;
|
||||||
|
useCw: boolean;
|
||||||
|
cw: string | null;
|
||||||
|
visibility: 'public' | 'followers' | 'home' | 'specified';
|
||||||
|
localOnly: boolean;
|
||||||
|
files: Misskey.entities.DriveFile[];
|
||||||
|
poll: PollEditorModelValue | null;
|
||||||
|
visibleUserIds?: string[];
|
||||||
|
};
|
||||||
|
};
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.top">
|
<div :class="$style.top">
|
||||||
<div v-if="instance.bannerUrl != null" :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div>
|
<div v-if="instance.bannerUrl != null" :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div>
|
||||||
<button class="_button" :class="$style.instance" @click="openInstanceMenu">
|
<button class="_button" :class="$style.instance" @click="openInstanceMenu">
|
||||||
<img v-if="miLocalStorage.getItem('kawaii')" src="/client-assets/kawaii/about-icon.png" alt="" :class="$style.instanceIconAlt"/>
|
<img v-if="kawaiiMode" src="/client-assets/kawaii/about-icon.png" alt="" :class="$style.instanceIconAlt"/>
|
||||||
<img v-else :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
|
<img v-else :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,6 +64,7 @@ import { instance } from '@/instance.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
|
||||||
|
const kawaiiMode = miLocalStorage.getItem('kawaii') === 'true';
|
||||||
const menu = toRef(defaultStore.state, 'menu');
|
const menu = toRef(defaultStore.state, 'menu');
|
||||||
const unresolvedReportAvailable = ref<boolean>(false);
|
const unresolvedReportAvailable = ref<boolean>(false);
|
||||||
const otherMenuItemIndicated = computed(() => {
|
const otherMenuItemIndicated = computed(() => {
|
||||||
|
|
|
@ -23820,6 +23820,8 @@ export type operations = {
|
||||||
expiresAt?: number | null;
|
expiresAt?: number | null;
|
||||||
expiredAfter?: number | null;
|
expiredAfter?: number | null;
|
||||||
}) | null;
|
}) | null;
|
||||||
|
/** @default false */
|
||||||
|
noCreatedNote?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -23832,6 +23834,10 @@ export type operations = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/** @description OK (without any results) */
|
||||||
|
204: {
|
||||||
|
content: never;
|
||||||
|
};
|
||||||
/** @description Client error */
|
/** @description Client error */
|
||||||
400: {
|
400: {
|
||||||
content: {
|
content: {
|
||||||
|
|
|
@ -114,7 +114,7 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
||||||
if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId);
|
if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId);
|
||||||
break;
|
break;
|
||||||
case 'renote':
|
case 'renote':
|
||||||
if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id });
|
if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id, noCreatedNote: true });
|
||||||
break;
|
break;
|
||||||
case 'accept':
|
case 'accept':
|
||||||
switch (data.body.type) {
|
switch (data.body.type) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue