From 0d508db8a7a36218d38231af4e718aff0e94d9bc Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Fri, 9 Aug 2024 12:10:51 +0900 Subject: fix(backend): check visibility of following/followers of remote users / feat: moderators can see following/followers of all users (#14375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): check visibility of following/followers of remote users Resolves https://github.com/misskey-dev/misskey/issues/13362. * test(backend): add tests for visibility of following/followers of remote users * docs(changelog): update CHANGELOG.md * feat: moderators can see following/followers of all users * docs(changelog): update CHANGELOG.md * refactor(backend): minor refactoring `createPerson`と`if`の条件を統一するとともに、異常系の 処理をearly returnに追い出すための変更。 * feat(backend): moderators can see following/followers count of all users As per https://github.com/misskey-dev/misskey/pull/14375#issuecomment-2275044908. --- packages/frontend/src/scripts/isFfVisibleForMe.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'packages/frontend/src/scripts') diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts index 406404c462..e28e5725bc 100644 --- a/packages/frontend/src/scripts/isFfVisibleForMe.ts +++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts @@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js'; import { $i } from '@/account.js'; export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean { - if ($i && $i.id === user.id) return true; + if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true; if (user.followingVisibility === 'private') return false; if (user.followingVisibility === 'followers' && !user.isFollowing) return false; @@ -15,7 +15,7 @@ export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): bo return true; } export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean { - if ($i && $i.id === user.id) return true; + if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true; if (user.followersVisibility === 'private') return false; if (user.followersVisibility === 'followers' && !user.isFollowing) return false; -- cgit v1.2.3-freya From 059eb6d379cc84ee48c76bcea88dd90423f4f929 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sat, 17 Aug 2024 11:28:22 +0900 Subject: fix(frontend): リノートの判定が甘いのを修正 (#14396) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): リノートの判定が甘いのを修正 * fix * Update Changelog * fix * use type assertion * fix + add comments * lint * misskey-jsに移動 * PureRenote -> Renote * isRenote -> isPureRenote --- CHANGELOG.md | 1 + packages/frontend/src/components/MkNote.vue | 12 ++------ .../frontend/src/components/MkNoteDetailed.vue | 12 ++------ packages/frontend/src/scripts/get-appear-note.ts | 10 +++++++ packages/frontend/src/scripts/get-note-menu.ts | 33 +++++----------------- packages/misskey-js/etc/misskey-js.api.md | 22 ++++++++++++++- packages/misskey-js/src/entities.ts | 14 +++++++++ packages/misskey-js/src/index.ts | 3 +- packages/misskey-js/src/note.ts | 12 ++++++++ 9 files changed, 73 insertions(+), 46 deletions(-) create mode 100644 packages/frontend/src/scripts/get-appear-note.ts create mode 100644 packages/misskey-js/src/note.ts (limited to 'packages/frontend/src/scripts') diff --git a/CHANGELOG.md b/CHANGELOG.md index 74e3cb99a9..caf4fab977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制 - Fix: mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正 - Fix: ユーザーのモデレーションページにおいてユーザー名にドットが入っているとシステムアカウントとして表示されてしまう問題を修正 +- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正 ### Server - Enhance: 凍結されたアカウントのフォローリクエストを表示しないように diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 13273a53b4..32d1cc5640 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -200,6 +200,7 @@ import { host } from '@/config.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { type Keymap } from '@/scripts/hotkey.js'; import { focusPrev, focusNext } from '@/scripts/focus.js'; +import { getAppearNote } from '@/scripts/get-appear-note.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -242,14 +243,7 @@ if (noteViewInterruptors.length > 0) { }); } -const isRenote = ( - 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 isRenote = Misskey.note.isPureRenote(note.value); const rootEl = shallowRef(); const menuButton = shallowRef(); @@ -257,7 +251,7 @@ const renoteButton = shallowRef(); const renoteTime = shallowRef(); const reactButton = shallowRef(); const clipButton = shallowRef(); -const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const appearNote = computed(() => getAppearNote(note.value)); const galleryEl = shallowRef>(); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(false); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 9a3e595789..2b7d2afa04 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -235,6 +235,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; import { isEnabledUrlPreview } from '@/instance.js'; +import { getAppearNote } from '@/scripts/get-appear-note.js'; import { type Keymap } from '@/scripts/hotkey.js'; const props = withDefaults(defineProps<{ @@ -267,14 +268,7 @@ if (noteViewInterruptors.length > 0) { }); } -const isRenote = ( - 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 isRenote = Misskey.note.isPureRenote(note.value); const rootEl = shallowRef(); const menuButton = shallowRef(); @@ -282,7 +276,7 @@ const renoteButton = shallowRef(); const renoteTime = shallowRef(); const reactButton = shallowRef(); const clipButton = shallowRef(); -const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const appearNote = computed(() => getAppearNote(note.value)); const galleryEl = shallowRef>(); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(false); diff --git a/packages/frontend/src/scripts/get-appear-note.ts b/packages/frontend/src/scripts/get-appear-note.ts new file mode 100644 index 0000000000..40ce80eac9 --- /dev/null +++ b/packages/frontend/src/scripts/get-appear-note.ts @@ -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; +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index ebb96d1746..2563b0baf3 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -20,6 +20,7 @@ import { clipsCache, favoritedChannelsCache } from '@/cache.js'; import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { isSupportShare } from '@/scripts/navigator.js'; +import { getAppearNote } from '@/scripts/get-appear-note.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -34,14 +35,7 @@ export async function getNoteClipMenu(props: { } } - 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 appearNote = getAppearNote(props.note); const clips = await clipsCache.fetch(); const menu: MenuItem[] = [...clips.map(clip => ({ @@ -164,14 +158,7 @@ export function getNoteMenu(props: { isDeleted: Ref; currentClip?: Misskey.entities.Clip; }) { - 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 appearNote = getAppearNote(props.note); const cleanups = [] as (() => void)[]; @@ -248,6 +235,7 @@ export function getNoteMenu(props: { } async function unclip(): Promise { + if (!props.currentClip) return; os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id }); props.isDeleted.value = true; } @@ -267,8 +255,8 @@ export function getNoteMenu(props: { function share(): void { navigator.share({ - title: i18n.tsx.noteOf({ user: appearNote.user.name }), - text: appearNote.text, + title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }), + text: appearNote.text ?? '', url: `${url}/notes/${appearNote.id}`, }); } @@ -509,14 +497,7 @@ export function getRenoteMenu(props: { renoteButton: ShallowRef; 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 appearNote = getAppearNote(props.note); const channelRenoteItems: MenuItem[] = []; const normalRenoteItems: MenuItem[] = []; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index b7bb1088bd..1edb9203c4 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1172,6 +1172,7 @@ declare namespace entities { export { ID, DateString, + PureRenote, PageEvent, ModerationLog, ServerStats, @@ -2277,6 +2278,9 @@ type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['co // @public (undocumented) type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json']; +// @public (undocumented) +function isPureRenote(note: Note): note is PureRenote; + // @public (undocumented) type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json']; @@ -2513,6 +2517,13 @@ type MyAppsResponse = operations['my___apps']['responses']['200']['content']['ap // @public (undocumented) type Note = components['schemas']['Note']; +declare namespace note { + export { + isPureRenote + } +} +export { note } + // @public (undocumented) type NoteFavorite = components['schemas']['NoteFavorite']; @@ -2753,6 +2764,15 @@ type PinnedUsersResponse = operations['pinned-users']['responses']['200']['conte // @public (undocumented) 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 & AllNullRecord> & { + files: []; + fileIds: []; +} & NonNullableRecord>; + // @public (undocumented) type QueueCount = components['schemas']['QueueCount']; @@ -3232,7 +3252,7 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody'][' // 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:230:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 544568db41..2a3625f56e 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -3,6 +3,7 @@ import { Announcement, EmojiDetailed, MeDetailed, + Note, Page, Role, RolePolicies, @@ -16,6 +17,19 @@ export * from './autogen/models.js'; export type ID = string; export type DateString = string; +type NonNullableRecord = { + [P in keyof T]-?: NonNullable; +}; +type AllNullRecord = { + [P in keyof T]: null; +}; + +export type PureRenote = + Omit + & AllNullRecord> + & { files: []; fileIds: []; } + & NonNullableRecord>; + export type PageEvent = { pageId: Page['id']; event: string; diff --git a/packages/misskey-js/src/index.ts b/packages/misskey-js/src/index.ts index 7e0165d20b..ace9738e6a 100644 --- a/packages/misskey-js/src/index.ts +++ b/packages/misskey-js/src/index.ts @@ -30,4 +30,5 @@ export const reversiUpdateKeys = consts.reversiUpdateKeys; import * as api from './api.js'; import * as entities from './entities.js'; import * as acct from './acct.js'; -export { api, entities, acct }; +import * as note from './note.js'; +export { api, entities, acct, note }; diff --git a/packages/misskey-js/src/note.ts b/packages/misskey-js/src/note.ts new file mode 100644 index 0000000000..5c8298c7e4 --- /dev/null +++ b/packages/misskey-js/src/note.ts @@ -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 + ); +} -- cgit v1.2.3-freya From b53ee54e4f1172a0d13ef0808b7236c176bf8d1a Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:18:46 +0900 Subject: fix(frontend): focustrapがzindexを考慮するように (#14431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/scripts/focus-trap.ts | 64 +++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) (limited to 'packages/frontend/src/scripts') diff --git a/packages/frontend/src/scripts/focus-trap.ts b/packages/frontend/src/scripts/focus-trap.ts index a5df36f520..fb7caea830 100644 --- a/packages/frontend/src/scripts/focus-trap.ts +++ b/packages/frontend/src/scripts/focus-trap.ts @@ -16,21 +16,57 @@ function containsFocusTrappedElements(el: HTMLElement): boolean { }); } +function getZIndex(el: HTMLElement): number { + const zIndex = parseInt(window.getComputedStyle(el).zIndex || '0', 10); + if (isNaN(zIndex)) { + return 0; + } + return zIndex; +} + +function getHighestZIndexElement(): { el: HTMLElement; zIndex: number; } | null { + let highestZIndexElement: HTMLElement | null = null; + let highestZIndex = -Infinity; + + focusTrapElements.forEach((el) => { + const zIndex = getZIndex(el); + if (zIndex > highestZIndex) { + highestZIndex = zIndex; + highestZIndexElement = el; + } + }); + + return highestZIndexElement == null ? null : { + el: highestZIndexElement, + zIndex: highestZIndex, + }; +} + function releaseFocusTrap(el: HTMLElement): void { focusTrapElements.delete(el); if (el.inert === true) { el.inert = false; } + + const highestZIndexElement = getHighestZIndexElement(); + if (el.parentElement != null && el !== document.body) { el.parentElement.childNodes.forEach((siblingNode) => { const siblingEl = getHTMLElementOrNull(siblingNode); if (!siblingEl) return; - if (siblingEl !== el && (focusTrapElements.has(siblingEl) || containsFocusTrappedElements(siblingEl) || focusTrapElements.size === 0)) { + if ( + siblingEl !== el && + ( + highestZIndexElement == null || + siblingEl === highestZIndexElement.el || + siblingEl.contains(highestZIndexElement.el) + ) + ) { siblingEl.inert = false; } else if ( - focusTrapElements.size > 0 && - !containsFocusTrappedElements(siblingEl) && - !focusTrapElements.has(siblingEl) && + highestZIndexElement != null && + siblingEl !== highestZIndexElement.el && + !siblingEl.contains(highestZIndexElement.el) && !ignoreElements.includes(siblingEl.tagName.toLowerCase()) ) { siblingEl.inert = true; @@ -45,9 +81,29 @@ function releaseFocusTrap(el: HTMLElement): void { export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void; export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; }; export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void { + const highestZIndexElement = getHighestZIndexElement(); + + const highestZIndex = highestZIndexElement == null ? -Infinity : highestZIndexElement.zIndex; + const zIndex = getZIndex(el); + + // If the element has a lower z-index than the highest z-index element, focus trap the highest z-index element instead + // Focus trapping for this element will be done in the release function + if (!parent && zIndex < highestZIndex) { + focusTrapElements.add(el); + if (highestZIndexElement) { + focusTrap(highestZIndexElement.el, hasInteractionWithOtherFocusTrappedEls); + } + return { + release: () => { + releaseFocusTrap(el); + }, + }; + } + if (el.inert === true) { el.inert = false; } + if (el.parentElement != null && el !== document.body) { el.parentElement.childNodes.forEach((siblingNode) => { const siblingEl = getHTMLElementOrNull(siblingNode); -- cgit v1.2.3-freya