チャンネル内→チャンネル外へのリノート制限機能追加
This commit is contained in:
parent
5fb6847419
commit
8d5273c18b
|
|
@ -1742,6 +1742,7 @@ export interface Locale {
|
|||
"notesCount": string;
|
||||
"nameAndDescription": string;
|
||||
"nameOnly": string;
|
||||
"canRenoteSwitch": string;
|
||||
};
|
||||
"_menuDisplay": {
|
||||
"sideFull": string;
|
||||
|
|
|
|||
|
|
@ -1659,6 +1659,7 @@ _channel:
|
|||
notesCount: "{n}投稿があります"
|
||||
nameAndDescription: "名前と説明"
|
||||
nameOnly: "名前のみ"
|
||||
canRenoteSwitch: "チャンネル外へのリノートと引用リノートを許可する"
|
||||
|
||||
_menuDisplay:
|
||||
sideFull: "横"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class AddCanRenoteToChannel1698840138000 {
|
||||
name = 'AddCanRenoteToChannel1698840138000'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "channel" ADD "canRenote" boolean NOT NULL DEFAULT true`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "canRenote"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +95,7 @@ export class ChannelEntityService {
|
|||
usersCount: channel.usersCount,
|
||||
notesCount: channel.notesCount,
|
||||
isSensitive: channel.isSensitive,
|
||||
canRenote: channel.canRenote,
|
||||
|
||||
...(me ? {
|
||||
isFollowing,
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
name: channel.name,
|
||||
color: channel.color,
|
||||
isSensitive: channel.isSensitive,
|
||||
canRenote: channel.canRenote,
|
||||
} : undefined,
|
||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||
uri: note.uri ?? undefined,
|
||||
|
|
|
|||
|
|
@ -93,4 +93,9 @@ export class MiChannel {
|
|||
default: false,
|
||||
})
|
||||
public isSensitive: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public canRenote: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,5 +76,9 @@ export const packedChannelSchema = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canRenote: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export const paramDef = {
|
|||
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
color: { type: 'string', minLength: 1, maxLength: 16 },
|
||||
isSensitive: { type: 'boolean', nullable: true },
|
||||
canRenote: { type: 'boolean', nullable: true },
|
||||
},
|
||||
required: ['name'],
|
||||
} as const;
|
||||
|
|
@ -87,6 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
bannerId: banner ? banner.id : null,
|
||||
isSensitive: ps.isSensitive ?? false,
|
||||
...(ps.color !== undefined ? { color: ps.color } : {}),
|
||||
canRenote: ps.canRenote ?? true,
|
||||
} as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
return await this.channelEntityService.pack(channel, me);
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export const paramDef = {
|
|||
},
|
||||
color: { type: 'string', minLength: 1, maxLength: 16 },
|
||||
isSensitive: { type: 'boolean', nullable: true },
|
||||
canRenote: { type: 'boolean', nullable: true },
|
||||
},
|
||||
required: ['channelId'],
|
||||
} as const;
|
||||
|
|
@ -115,6 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}),
|
||||
...(banner ? { bannerId: banner.id } : {}),
|
||||
...(typeof ps.isSensitive === 'boolean' ? { isSensitive: ps.isSensitive } : {}),
|
||||
...(typeof ps.canRenote === 'boolean' ? { canRenote: ps.canRenote } : {}),
|
||||
});
|
||||
|
||||
return await this.channelEntityService.pack(channel.id, me);
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
|
@ -99,6 +99,12 @@ export const meta = {
|
|||
code: 'NO_SUCH_FILE',
|
||||
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;
|
||||
|
||||
|
|
@ -246,6 +252,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
// specified / direct noteはreject
|
||||
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
|
||||
}
|
||||
|
||||
if (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.canRenote) {
|
||||
// リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合
|
||||
throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reply: MiNote | null = null;
|
||||
|
|
|
|||
|
|
@ -272,10 +272,11 @@ function renote(viaKeyboard = false) {
|
|||
pleaseLogin();
|
||||
showMovedDialog();
|
||||
|
||||
let items = [] as MenuItem[];
|
||||
const channelRenoteItems: MenuItem[] = [];
|
||||
const normalRenoteItems: MenuItem[] = [];
|
||||
|
||||
if (appearNote.channel) {
|
||||
items = items.concat([{
|
||||
channelRenoteItems.push(...[{
|
||||
text: i18n.ts.inChannelRenote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
|
|
@ -303,20 +304,21 @@ function renote(viaKeyboard = false) {
|
|||
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');
|
||||
}
|
||||
if (!appearNote.channel || appearNote.channel?.canRenote) {
|
||||
normalRenoteItems.push(...[{
|
||||
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;
|
||||
|
|
@ -327,25 +329,33 @@ function renote(viaKeyboard = false) {
|
|||
visibility = smallerVisibility(visibility, 'home');
|
||||
}
|
||||
|
||||
os.api('notes/create', {
|
||||
localOnly,
|
||||
visibility,
|
||||
renoteId: appearNote.id,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.quote,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
os.post({
|
||||
renote: appearNote,
|
||||
});
|
||||
},
|
||||
}]);
|
||||
os.api('notes/create', {
|
||||
localOnly,
|
||||
visibility,
|
||||
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, {
|
||||
// nullを挟むことで区切り線を出せる
|
||||
const renoteItems = [
|
||||
...channelRenoteItems,
|
||||
...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [null] : [],
|
||||
...normalRenoteItems,
|
||||
];
|
||||
|
||||
os.popupMenu(renoteItems, renoteButton.value, {
|
||||
viaKeyboard,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.sensitive }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="canRenote">
|
||||
<template #label>{{ i18n.ts._channel.canRenoteSwitch }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<div>
|
||||
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
|
||||
<div v-else-if="bannerUrl">
|
||||
|
|
@ -76,7 +80,7 @@ import { useRouter } from '@/router.js';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
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));
|
||||
|
||||
|
|
@ -93,6 +97,7 @@ let bannerUrl = $ref<string | null>(null);
|
|||
let bannerId = $ref<string | null>(null);
|
||||
let color = $ref('#000');
|
||||
let isSensitive = $ref(false);
|
||||
let canRenote = $ref(true);
|
||||
const pinnedNotes = ref([]);
|
||||
|
||||
watch(() => bannerId, async () => {
|
||||
|
|
@ -121,6 +126,7 @@ async function fetchChannel() {
|
|||
id,
|
||||
}));
|
||||
color = channel.color;
|
||||
canRenote = channel.canRenote;
|
||||
}
|
||||
|
||||
fetchChannel();
|
||||
|
|
@ -150,6 +156,7 @@ function save() {
|
|||
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
|
||||
color: color,
|
||||
isSensitive: isSensitive,
|
||||
canRenote: canRenote,
|
||||
};
|
||||
|
||||
if (props.channelId) {
|
||||
|
|
|
|||
|
|
@ -198,6 +198,8 @@ export type Note = {
|
|||
fileIds: DriveFile['id'][];
|
||||
visibility: 'public' | 'home' | 'followers' | 'specified';
|
||||
visibleUserIds?: User['id'][];
|
||||
channel?: Channel;
|
||||
channelId?: Channel['id'];
|
||||
localOnly?: boolean;
|
||||
myReaction?: string;
|
||||
reactions: Record<string, number>;
|
||||
|
|
@ -514,7 +516,20 @@ export type FollowRequest = {
|
|||
|
||||
export type Channel = {
|
||||
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;
|
||||
canRenote: boolean;
|
||||
};
|
||||
|
||||
export type Following = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue