feat: チャンネル内→チャンネル外へのリノート制限機能追加 (#12230)
* チャンネル内→チャンネル外へのリノート制限機能追加
* fix CHANGELOG.md
* コメント対応(canRenoteSwitch→allowRenoteToExternal)
* コメント対応(別チャンネルへのリノート対策)
* コメント対応(canRenote->allowRenoteToExternal)
* fix comment
* Update misskey-js.api.md
* ✌️
---------
Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
parent
4631e6cd4a
commit
39a3f4ae98
17 changed files with 222 additions and 161 deletions
|
@ -20,6 +20,7 @@
|
||||||
- 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png
|
- 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png
|
||||||
- 最大でも黄色いエリア内にデコレーションを収めることを推奨します。
|
- 最大でも黄色いエリア内にデコレーションを収めることを推奨します。
|
||||||
- 画像は512x512pxを推奨します。
|
- 画像は512x512pxを推奨します。
|
||||||
|
- Feat: チャンネル設定にリノート/引用リノートの可否を設定できる項目を追加
|
||||||
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
|
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
|
||||||
- Enhance: 未読の通知数を表示できるように
|
- Enhance: 未読の通知数を表示できるように
|
||||||
- Enhance: ローカリゼーションの更新
|
- Enhance: ローカリゼーションの更新
|
||||||
|
|
1
locales/index.d.ts
vendored
1
locales/index.d.ts
vendored
|
@ -1832,6 +1832,7 @@ export interface Locale {
|
||||||
"notesCount": string;
|
"notesCount": string;
|
||||||
"nameAndDescription": string;
|
"nameAndDescription": string;
|
||||||
"nameOnly": string;
|
"nameOnly": string;
|
||||||
|
"allowRenoteToExternal": string;
|
||||||
};
|
};
|
||||||
"_menuDisplay": {
|
"_menuDisplay": {
|
||||||
"sideFull": string;
|
"sideFull": string;
|
||||||
|
|
|
@ -1737,6 +1737,7 @@ _channel:
|
||||||
notesCount: "{n}投稿があります"
|
notesCount: "{n}投稿があります"
|
||||||
nameAndDescription: "名前と説明"
|
nameAndDescription: "名前と説明"
|
||||||
nameOnly: "名前のみ"
|
nameOnly: "名前のみ"
|
||||||
|
allowRenoteToExternal: "チャンネル外へのリノートと引用リノートを許可する"
|
||||||
|
|
||||||
_menuDisplay:
|
_menuDisplay:
|
||||||
sideFull: "横"
|
sideFull: "横"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AddAllowRenoteToExternal1698840138000 {
|
||||||
|
name = 'AddAllowRenoteToExternal1698840138000'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel" ADD "allowRenoteToExternal" boolean NOT NULL DEFAULT true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "allowRenoteToExternal"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -85,6 +85,7 @@ export class ChannelEntityService {
|
||||||
usersCount: channel.usersCount,
|
usersCount: channel.usersCount,
|
||||||
notesCount: channel.notesCount,
|
notesCount: channel.notesCount,
|
||||||
isSensitive: channel.isSensitive,
|
isSensitive: channel.isSensitive,
|
||||||
|
allowRenoteToExternal: channel.allowRenoteToExternal,
|
||||||
|
|
||||||
...(me ? {
|
...(me ? {
|
||||||
isFollowing,
|
isFollowing,
|
||||||
|
|
|
@ -350,6 +350,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
color: channel.color,
|
color: channel.color,
|
||||||
isSensitive: channel.isSensitive,
|
isSensitive: channel.isSensitive,
|
||||||
|
allowRenoteToExternal: channel.allowRenoteToExternal,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||||
uri: note.uri ?? undefined,
|
uri: note.uri ?? undefined,
|
||||||
|
|
|
@ -93,4 +93,9 @@ export class MiChannel {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
public isSensitive: boolean;
|
public isSensitive: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
public allowRenoteToExternal: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,5 +76,9 @@ export const packedChannelSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
allowRenoteToExternal: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -50,6 +50,7 @@ export const paramDef = {
|
||||||
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
color: { type: 'string', minLength: 1, maxLength: 16 },
|
color: { type: 'string', minLength: 1, maxLength: 16 },
|
||||||
isSensitive: { type: 'boolean', nullable: true },
|
isSensitive: { type: 'boolean', nullable: true },
|
||||||
|
allowRenoteToExternal: { type: 'boolean', nullable: true },
|
||||||
},
|
},
|
||||||
required: ['name'],
|
required: ['name'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -87,6 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
bannerId: banner ? banner.id : null,
|
bannerId: banner ? banner.id : null,
|
||||||
isSensitive: ps.isSensitive ?? false,
|
isSensitive: ps.isSensitive ?? false,
|
||||||
...(ps.color !== undefined ? { color: ps.color } : {}),
|
...(ps.color !== undefined ? { color: ps.color } : {}),
|
||||||
|
allowRenoteToExternal: ps.allowRenoteToExternal ?? true,
|
||||||
} as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
|
} as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
return await this.channelEntityService.pack(channel, me);
|
return await this.channelEntityService.pack(channel, me);
|
||||||
|
|
|
@ -61,6 +61,7 @@ export const paramDef = {
|
||||||
},
|
},
|
||||||
color: { type: 'string', minLength: 1, maxLength: 16 },
|
color: { type: 'string', minLength: 1, maxLength: 16 },
|
||||||
isSensitive: { type: 'boolean', nullable: true },
|
isSensitive: { type: 'boolean', nullable: true },
|
||||||
|
allowRenoteToExternal: { type: 'boolean', nullable: true },
|
||||||
},
|
},
|
||||||
required: ['channelId'],
|
required: ['channelId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -115,6 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}),
|
...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}),
|
||||||
...(banner ? { bannerId: banner.id } : {}),
|
...(banner ? { bannerId: banner.id } : {}),
|
||||||
...(typeof ps.isSensitive === 'boolean' ? { isSensitive: ps.isSensitive } : {}),
|
...(typeof ps.isSensitive === 'boolean' ? { isSensitive: ps.isSensitive } : {}),
|
||||||
|
...(typeof ps.allowRenoteToExternal === 'boolean' ? { allowRenoteToExternal: ps.allowRenoteToExternal } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.channelEntityService.pack(channel.id, me);
|
return await this.channelEntityService.pack(channel.id, me);
|
||||||
|
|
|
@ -99,6 +99,12 @@ export const meta = {
|
||||||
code: 'NO_SUCH_FILE',
|
code: 'NO_SUCH_FILE',
|
||||||
id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
|
id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cannotRenoteOutsideOfChannel: {
|
||||||
|
message: 'Cannot renote outside of channel.',
|
||||||
|
code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL',
|
||||||
|
id: '33510210-8452-094c-6227-4a6c05d99f00',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -246,6 +252,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
// specified / direct noteはreject
|
// specified / direct noteはreject
|
||||||
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
|
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (renote.channelId && renote.channelId !== ps.channelId) {
|
||||||
|
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
|
||||||
|
// リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
|
||||||
|
const renoteChannel = await this.channelsRepository.findOneById(renote.channelId);
|
||||||
|
if (renoteChannel == null) {
|
||||||
|
// リノートしたいノートが書き込まれているチャンネルが無い
|
||||||
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
|
} else if (!renoteChannel.allowRenoteToExternal) {
|
||||||
|
// リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合
|
||||||
|
throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let reply: MiNote | null = null;
|
let reply: MiNote | null = null;
|
||||||
|
|
|
@ -159,7 +159,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js';
|
import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
|
@ -275,103 +275,14 @@ if (!props.mock) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type Visibility = 'public' | 'home' | 'followers' | 'specified';
|
|
||||||
|
|
||||||
// defaultStore.state.visibilityがstringなためstringも受け付けている
|
|
||||||
function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
|
|
||||||
if (a === 'specified' || b === 'specified') return 'specified';
|
|
||||||
if (a === 'followers' || b === 'followers') return 'followers';
|
|
||||||
if (a === 'home' || b === 'home') return 'home';
|
|
||||||
// if (a === 'public' || b === 'public')
|
|
||||||
return 'public';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renote(viaKeyboard = false) {
|
function renote(viaKeyboard = false) {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
let items = [] as MenuItem[];
|
const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock });
|
||||||
|
os.popupMenu(menu, renoteButton.value, {
|
||||||
if (appearNote.channel) {
|
|
||||||
items = items.concat([{
|
|
||||||
text: i18n.ts.inChannelRenote,
|
|
||||||
icon: 'ti ti-repeat',
|
|
||||||
action: () => {
|
|
||||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
|
||||||
if (el) {
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
|
||||||
const y = rect.top + (el.offsetHeight / 2);
|
|
||||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!props.mock) {
|
|
||||||
os.api('notes/create', {
|
|
||||||
renoteId: appearNote.id,
|
|
||||||
channelId: appearNote.channelId,
|
|
||||||
}).then(() => {
|
|
||||||
os.toast(i18n.ts.renoted);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
text: i18n.ts.inChannelQuote,
|
|
||||||
icon: 'ti ti-quote',
|
|
||||||
action: () => {
|
|
||||||
if (!props.mock) {
|
|
||||||
os.post({
|
|
||||||
renote: appearNote,
|
|
||||||
channel: appearNote.channel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}, null]);
|
|
||||||
}
|
|
||||||
|
|
||||||
items = items.concat([{
|
|
||||||
text: i18n.ts.renote,
|
|
||||||
icon: 'ti ti-repeat',
|
|
||||||
action: () => {
|
|
||||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
|
||||||
if (el) {
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
|
||||||
const y = rect.top + (el.offsetHeight / 2);
|
|
||||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
|
|
||||||
const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
|
|
||||||
|
|
||||||
let visibility = appearNote.visibility;
|
|
||||||
visibility = smallerVisibility(visibility, configuredVisibility);
|
|
||||||
if (appearNote.channel?.isSensitive) {
|
|
||||||
visibility = smallerVisibility(visibility, 'home');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!props.mock) {
|
|
||||||
os.api('notes/create', {
|
|
||||||
localOnly,
|
|
||||||
visibility,
|
|
||||||
renoteId: appearNote.id,
|
|
||||||
}).then(() => {
|
|
||||||
os.toast(i18n.ts.renoted);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}, (props.mock) ? undefined : {
|
|
||||||
text: i18n.ts.quote,
|
|
||||||
icon: 'ti ti-quote',
|
|
||||||
action: () => {
|
|
||||||
os.post({
|
|
||||||
renote: appearNote,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}]);
|
|
||||||
|
|
||||||
os.popupMenu(items, renoteButton.value, {
|
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
});
|
}).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(viaKeyboard = false): void {
|
||||||
|
|
|
@ -206,7 +206,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js';
|
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
|
@ -325,71 +325,10 @@ function renote(viaKeyboard = false) {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
let items = [] as MenuItem[];
|
const { menu } = getRenoteMenu({ note: note, renoteButton });
|
||||||
|
os.popupMenu(menu, renoteButton.value, {
|
||||||
if (appearNote.channel) {
|
|
||||||
items = items.concat([{
|
|
||||||
text: i18n.ts.inChannelRenote,
|
|
||||||
icon: 'ti ti-repeat',
|
|
||||||
action: () => {
|
|
||||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
|
||||||
if (el) {
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
|
||||||
const y = rect.top + (el.offsetHeight / 2);
|
|
||||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
|
||||||
}
|
|
||||||
|
|
||||||
os.api('notes/create', {
|
|
||||||
renoteId: appearNote.id,
|
|
||||||
channelId: appearNote.channelId,
|
|
||||||
}).then(() => {
|
|
||||||
os.toast(i18n.ts.renoted);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
text: i18n.ts.inChannelQuote,
|
|
||||||
icon: 'ti ti-quote',
|
|
||||||
action: () => {
|
|
||||||
os.post({
|
|
||||||
renote: appearNote,
|
|
||||||
channel: appearNote.channel,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}, null]);
|
|
||||||
}
|
|
||||||
|
|
||||||
items = items.concat([{
|
|
||||||
text: i18n.ts.renote,
|
|
||||||
icon: 'ti ti-repeat',
|
|
||||||
action: () => {
|
|
||||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
|
||||||
if (el) {
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
|
||||||
const y = rect.top + (el.offsetHeight / 2);
|
|
||||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
|
||||||
}
|
|
||||||
|
|
||||||
os.api('notes/create', {
|
|
||||||
renoteId: appearNote.id,
|
|
||||||
}).then(() => {
|
|
||||||
os.toast(i18n.ts.renoted);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
text: i18n.ts.quote,
|
|
||||||
icon: 'ti ti-quote',
|
|
||||||
action: () => {
|
|
||||||
os.post({
|
|
||||||
renote: appearNote,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}]);
|
|
||||||
|
|
||||||
os.popupMenu(items, renoteButton.value, {
|
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
});
|
}).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(viaKeyboard = false): void {
|
||||||
|
|
|
@ -24,6 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.sensitive }}</template>
|
<template #label>{{ i18n.ts.sensitive }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
|
<MkSwitch v-model="allowRenoteToExternal">
|
||||||
|
<template #label>{{ i18n.ts._channel.allowRenoteToExternal }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
|
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
|
||||||
<div v-else-if="bannerUrl">
|
<div v-else-if="bannerUrl">
|
||||||
|
@ -76,7 +80,7 @@ import { useRouter } from '@/router.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkSwitch from "@/components/MkSwitch.vue";
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||||
|
|
||||||
|
@ -93,6 +97,7 @@ let bannerUrl = $ref<string | null>(null);
|
||||||
let bannerId = $ref<string | null>(null);
|
let bannerId = $ref<string | null>(null);
|
||||||
let color = $ref('#000');
|
let color = $ref('#000');
|
||||||
let isSensitive = $ref(false);
|
let isSensitive = $ref(false);
|
||||||
|
let allowRenoteToExternal = $ref(true);
|
||||||
const pinnedNotes = ref([]);
|
const pinnedNotes = ref([]);
|
||||||
|
|
||||||
watch(() => bannerId, async () => {
|
watch(() => bannerId, async () => {
|
||||||
|
@ -121,6 +126,7 @@ async function fetchChannel() {
|
||||||
id,
|
id,
|
||||||
}));
|
}));
|
||||||
color = channel.color;
|
color = channel.color;
|
||||||
|
allowRenoteToExternal = channel.allowRenoteToExternal;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchChannel();
|
fetchChannel();
|
||||||
|
@ -150,6 +156,7 @@ function save() {
|
||||||
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
|
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
|
||||||
color: color,
|
color: color,
|
||||||
isSensitive: isSensitive,
|
isSensitive: isSensitive,
|
||||||
|
allowRenoteToExternal: allowRenoteToExternal,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.channelId) {
|
if (props.channelId) {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { getUserMenu } from '@/scripts/get-user-menu.js';
|
import { getUserMenu } from '@/scripts/get-user-menu.js';
|
||||||
import { clipsCache } from '@/cache.js';
|
import { clipsCache } from '@/cache.js';
|
||||||
import { MenuItem } from '@/types/menu.js';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
|
|
||||||
export async function getNoteClipMenu(props: {
|
export async function getNoteClipMenu(props: {
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -418,3 +419,122 @@ export function getNoteMenu(props: {
|
||||||
cleanup,
|
cleanup,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Visibility = 'public' | 'home' | 'followers' | 'specified';
|
||||||
|
|
||||||
|
// defaultStore.state.visibilityがstringなためstringも受け付けている
|
||||||
|
function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
|
||||||
|
if (a === 'specified' || b === 'specified') return 'specified';
|
||||||
|
if (a === 'followers' || b === 'followers') return 'followers';
|
||||||
|
if (a === 'home' || b === 'home') return 'home';
|
||||||
|
// if (a === 'public' || b === 'public')
|
||||||
|
return 'public';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRenoteMenu(props: {
|
||||||
|
note: Misskey.entities.Note;
|
||||||
|
renoteButton: Ref<HTMLElement>;
|
||||||
|
mock?: boolean;
|
||||||
|
}) {
|
||||||
|
const isRenote = (
|
||||||
|
props.note.renote != null &&
|
||||||
|
props.note.text == null &&
|
||||||
|
props.note.fileIds.length === 0 &&
|
||||||
|
props.note.poll == null
|
||||||
|
);
|
||||||
|
|
||||||
|
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
|
||||||
|
|
||||||
|
const channelRenoteItems: MenuItem[] = [];
|
||||||
|
const normalRenoteItems: MenuItem[] = [];
|
||||||
|
|
||||||
|
if (appearNote.channel) {
|
||||||
|
channelRenoteItems.push(...[{
|
||||||
|
text: i18n.ts.inChannelRenote,
|
||||||
|
icon: 'ti ti-repeat',
|
||||||
|
action: () => {
|
||||||
|
const el = props.renoteButton.value as HTMLElement | null | undefined;
|
||||||
|
if (el) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const x = rect.left + (el.offsetWidth / 2);
|
||||||
|
const y = rect.top + (el.offsetHeight / 2);
|
||||||
|
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.mock) {
|
||||||
|
os.api('notes/create', {
|
||||||
|
renoteId: appearNote.id,
|
||||||
|
channelId: appearNote.channelId,
|
||||||
|
}).then(() => {
|
||||||
|
os.toast(i18n.ts.renoted);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts.inChannelQuote,
|
||||||
|
icon: 'ti ti-quote',
|
||||||
|
action: () => {
|
||||||
|
if (!props.mock) {
|
||||||
|
os.post({
|
||||||
|
renote: appearNote,
|
||||||
|
channel: appearNote.channel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
|
||||||
|
normalRenoteItems.push(...[{
|
||||||
|
text: i18n.ts.renote,
|
||||||
|
icon: 'ti ti-repeat',
|
||||||
|
action: () => {
|
||||||
|
const el = props.renoteButton.value as HTMLElement | null | undefined;
|
||||||
|
if (el) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const x = rect.left + (el.offsetWidth / 2);
|
||||||
|
const y = rect.top + (el.offsetHeight / 2);
|
||||||
|
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
|
||||||
|
const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
|
||||||
|
|
||||||
|
let visibility = appearNote.visibility;
|
||||||
|
visibility = smallerVisibility(visibility, configuredVisibility);
|
||||||
|
if (appearNote.channel?.isSensitive) {
|
||||||
|
visibility = smallerVisibility(visibility, 'home');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.mock) {
|
||||||
|
os.api('notes/create', {
|
||||||
|
localOnly,
|
||||||
|
visibility,
|
||||||
|
renoteId: appearNote.id,
|
||||||
|
}).then(() => {
|
||||||
|
os.toast(i18n.ts.renoted);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, (props.mock) ? undefined : {
|
||||||
|
text: i18n.ts.quote,
|
||||||
|
icon: 'ti ti-quote',
|
||||||
|
action: () => {
|
||||||
|
os.post({
|
||||||
|
renote: appearNote,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// nullを挟むことで区切り線を出せる
|
||||||
|
const renoteItems = [
|
||||||
|
...normalRenoteItems,
|
||||||
|
...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [null] : [],
|
||||||
|
...channelRenoteItems,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
menu: renoteItems,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -134,6 +134,20 @@ type Blocking = {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type Channel = {
|
type Channel = {
|
||||||
id: ID;
|
id: ID;
|
||||||
|
lastNotedAt: Date | null;
|
||||||
|
userId: User['id'] | null;
|
||||||
|
user: User | null;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
bannerId: DriveFile['id'] | null;
|
||||||
|
banner: DriveFile | null;
|
||||||
|
pinnedNoteIds: string[];
|
||||||
|
color: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
notesCount: number;
|
||||||
|
usersCount: number;
|
||||||
|
isSensitive: boolean;
|
||||||
|
allowRenoteToExternal: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Warning: (ae-forgotten-export) The symbol "AnyOf" needs to be exported by the entry point index.d.ts
|
// Warning: (ae-forgotten-export) The symbol "AnyOf" needs to be exported by the entry point index.d.ts
|
||||||
|
@ -2683,6 +2697,8 @@ type Note = {
|
||||||
fileIds: DriveFile['id'][];
|
fileIds: DriveFile['id'][];
|
||||||
visibility: 'public' | 'home' | 'followers' | 'specified';
|
visibility: 'public' | 'home' | 'followers' | 'specified';
|
||||||
visibleUserIds?: User['id'][];
|
visibleUserIds?: User['id'][];
|
||||||
|
channel?: Channel;
|
||||||
|
channelId?: Channel['id'];
|
||||||
localOnly?: boolean;
|
localOnly?: boolean;
|
||||||
myReaction?: string;
|
myReaction?: string;
|
||||||
reactions: Record<string, number>;
|
reactions: Record<string, number>;
|
||||||
|
@ -3021,7 +3037,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
||||||
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:632:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:632:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||||
// src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
// src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
||||||
// src/entities.ts:612:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
// src/entities.ts:627:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||||
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||||
|
|
||||||
// (No @packageDocumentation comment for this package)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
|
@ -198,6 +198,8 @@ export type Note = {
|
||||||
fileIds: DriveFile['id'][];
|
fileIds: DriveFile['id'][];
|
||||||
visibility: 'public' | 'home' | 'followers' | 'specified';
|
visibility: 'public' | 'home' | 'followers' | 'specified';
|
||||||
visibleUserIds?: User['id'][];
|
visibleUserIds?: User['id'][];
|
||||||
|
channel?: Channel;
|
||||||
|
channelId?: Channel['id'];
|
||||||
localOnly?: boolean;
|
localOnly?: boolean;
|
||||||
myReaction?: string;
|
myReaction?: string;
|
||||||
reactions: Record<string, number>;
|
reactions: Record<string, number>;
|
||||||
|
@ -514,7 +516,20 @@ export type FollowRequest = {
|
||||||
|
|
||||||
export type Channel = {
|
export type Channel = {
|
||||||
id: ID;
|
id: ID;
|
||||||
// TODO
|
lastNotedAt: Date | null;
|
||||||
|
userId: User['id'] | null;
|
||||||
|
user: User | null;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
bannerId: DriveFile['id'] | null;
|
||||||
|
banner: DriveFile | null;
|
||||||
|
pinnedNoteIds: string[];
|
||||||
|
color: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
notesCount: number;
|
||||||
|
usersCount: number;
|
||||||
|
isSensitive: boolean;
|
||||||
|
allowRenoteToExternal: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Following = {
|
export type Following = {
|
||||||
|
|
Loading…
Reference in a new issue