fix(frontend): リノートの判定が甘いのを修正 (#14396)

* fix(frontend): リノートの判定が甘いのを修正

* fix

* Update Changelog

* fix

* use type assertion

* fix + add comments

* lint

* misskey-jsに移動

* PureRenote -> Renote

* isRenote -> isPureRenote
This commit is contained in:
かっこかり 2024-08-17 11:28:22 +09:00 committed by GitHub
parent 61cc3b5642
commit 059eb6d379
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 73 additions and 46 deletions

View File

@ -12,6 +12,7 @@
- Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制 - Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制
- Fix: mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正 - Fix: mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正
- Fix: ユーザーのモデレーションページにおいてユーザー名にドットが入っているとシステムアカウントとして表示されてしまう問題を修正 - Fix: ユーザーのモデレーションページにおいてユーザー名にドットが入っているとシステムアカウントとして表示されてしまう問題を修正
- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正
### Server ### Server
- Enhance: 凍結されたアカウントのフォローリクエストを表示しないように - Enhance: 凍結されたアカウントのフォローリクエストを表示しないように

View File

@ -200,6 +200,7 @@ import { host } from '@/config.js';
import { isEnabledUrlPreview } from '@/instance.js'; import { isEnabledUrlPreview } from '@/instance.js';
import { type Keymap } from '@/scripts/hotkey.js'; import { type Keymap } from '@/scripts/hotkey.js';
import { focusPrev, focusNext } from '@/scripts/focus.js'; import { focusPrev, focusNext } from '@/scripts/focus.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -242,14 +243,7 @@ if (noteViewInterruptors.length > 0) {
}); });
} }
const isRenote = ( const isRenote = Misskey.note.isPureRenote(note.value);
note.value.renote != null &&
note.value.reply == null &&
note.value.text == null &&
note.value.cw == null &&
note.value.fileIds && note.value.fileIds.length === 0 &&
note.value.poll == null
);
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
@ -257,7 +251,7 @@ const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>();
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); const appearNote = computed(() => getAppearNote(note.value));
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>(); const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
const isMyRenote = $i && ($i.id === note.value.userId); const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(false); const showContent = ref(false);

View File

@ -235,6 +235,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { isEnabledUrlPreview } from '@/instance.js'; import { isEnabledUrlPreview } from '@/instance.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
import { type Keymap } from '@/scripts/hotkey.js'; import { type Keymap } from '@/scripts/hotkey.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -267,14 +268,7 @@ if (noteViewInterruptors.length > 0) {
}); });
} }
const isRenote = ( const isRenote = Misskey.note.isPureRenote(note.value);
note.value.renote != null &&
note.value.reply == null &&
note.value.text == null &&
note.value.cw == null &&
note.value.fileIds && note.value.fileIds.length === 0 &&
note.value.poll == null
);
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
@ -282,7 +276,7 @@ const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>();
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); const appearNote = computed(() => getAppearNote(note.value));
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>(); const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
const isMyRenote = $i && ($i.id === note.value.userId); const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(false); const showContent = ref(false);

View File

@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Misskey from 'misskey-js';
export function getAppearNote(note: Misskey.entities.Note) {
return Misskey.note.isPureRenote(note) ? note.renote : note;
}

View File

@ -20,6 +20,7 @@ import { clipsCache, favoritedChannelsCache } from '@/cache.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { isSupportShare } from '@/scripts/navigator.js'; import { isSupportShare } from '@/scripts/navigator.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
export async function getNoteClipMenu(props: { export async function getNoteClipMenu(props: {
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -34,14 +35,7 @@ export async function getNoteClipMenu(props: {
} }
} }
const isRenote = ( const appearNote = getAppearNote(props.note);
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 clips = await clipsCache.fetch(); const clips = await clipsCache.fetch();
const menu: MenuItem[] = [...clips.map(clip => ({ const menu: MenuItem[] = [...clips.map(clip => ({
@ -164,14 +158,7 @@ export function getNoteMenu(props: {
isDeleted: Ref<boolean>; isDeleted: Ref<boolean>;
currentClip?: Misskey.entities.Clip; currentClip?: Misskey.entities.Clip;
}) { }) {
const isRenote = ( const appearNote = getAppearNote(props.note);
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 cleanups = [] as (() => void)[]; const cleanups = [] as (() => void)[];
@ -248,6 +235,7 @@ export function getNoteMenu(props: {
} }
async function unclip(): Promise<void> { async function unclip(): Promise<void> {
if (!props.currentClip) return;
os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id }); os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
props.isDeleted.value = true; props.isDeleted.value = true;
} }
@ -267,8 +255,8 @@ export function getNoteMenu(props: {
function share(): void { function share(): void {
navigator.share({ navigator.share({
title: i18n.tsx.noteOf({ user: appearNote.user.name }), title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }),
text: appearNote.text, text: appearNote.text ?? '',
url: `${url}/notes/${appearNote.id}`, url: `${url}/notes/${appearNote.id}`,
}); });
} }
@ -509,14 +497,7 @@ export function getRenoteMenu(props: {
renoteButton: ShallowRef<HTMLElement | undefined>; renoteButton: ShallowRef<HTMLElement | undefined>;
mock?: boolean; mock?: boolean;
}) { }) {
const isRenote = ( const appearNote = getAppearNote(props.note);
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 channelRenoteItems: MenuItem[] = [];
const normalRenoteItems: MenuItem[] = []; const normalRenoteItems: MenuItem[] = [];

View File

@ -1172,6 +1172,7 @@ declare namespace entities {
export { export {
ID, ID,
DateString, DateString,
PureRenote,
PageEvent, PageEvent,
ModerationLog, ModerationLog,
ServerStats, ServerStats,
@ -2277,6 +2278,9 @@ type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['co
// @public (undocumented) // @public (undocumented)
type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json']; type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json'];
// @public (undocumented)
function isPureRenote(note: Note): note is PureRenote;
// @public (undocumented) // @public (undocumented)
type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json']; type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json'];
@ -2513,6 +2517,13 @@ type MyAppsResponse = operations['my___apps']['responses']['200']['content']['ap
// @public (undocumented) // @public (undocumented)
type Note = components['schemas']['Note']; type Note = components['schemas']['Note'];
declare namespace note {
export {
isPureRenote
}
}
export { note }
// @public (undocumented) // @public (undocumented)
type NoteFavorite = components['schemas']['NoteFavorite']; type NoteFavorite = components['schemas']['NoteFavorite'];
@ -2753,6 +2764,15 @@ type PinnedUsersResponse = operations['pinned-users']['responses']['200']['conte
// @public (undocumented) // @public (undocumented)
type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json']; type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
// Warning: (ae-forgotten-export) The symbol "AllNullRecord" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "NonNullableRecord" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>> & {
files: [];
fileIds: [];
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
// @public (undocumented) // @public (undocumented)
type QueueCount = components['schemas']['QueueCount']; type QueueCount = components['schemas']['QueueCount'];
@ -3232,7 +3252,7 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['
// Warnings were encountered during analysis: // Warnings were encountered during analysis:
// //
// src/entities.ts:35:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/entities.ts:49:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:220:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:220:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:230:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:230:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts

View File

@ -3,6 +3,7 @@ import {
Announcement, Announcement,
EmojiDetailed, EmojiDetailed,
MeDetailed, MeDetailed,
Note,
Page, Page,
Role, Role,
RolePolicies, RolePolicies,
@ -16,6 +17,19 @@ export * from './autogen/models.js';
export type ID = string; export type ID = string;
export type DateString = string; export type DateString = string;
type NonNullableRecord<T> = {
[P in keyof T]-?: NonNullable<T[P]>;
};
type AllNullRecord<T> = {
[P in keyof T]: null;
};
export type PureRenote =
Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'>
& AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>>
& { files: []; fileIds: []; }
& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
export type PageEvent = { export type PageEvent = {
pageId: Page['id']; pageId: Page['id'];
event: string; event: string;

View File

@ -30,4 +30,5 @@ export const reversiUpdateKeys = consts.reversiUpdateKeys;
import * as api from './api.js'; import * as api from './api.js';
import * as entities from './entities.js'; import * as entities from './entities.js';
import * as acct from './acct.js'; import * as acct from './acct.js';
export { api, entities, acct }; import * as note from './note.js';
export { api, entities, acct, note };

View File

@ -0,0 +1,12 @@
import type { Note, PureRenote } from './entities.js';
export function isPureRenote(note: Note): note is PureRenote {
return (
note.renote != null &&
note.reply == null &&
note.text == null &&
note.cw == null &&
(note.fileIds == null || note.fileIds.length === 0) &&
note.poll == null
);
}