diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2024-06-01 11:27:03 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-01 11:27:03 +0900 |
| commit | fce66b85b603caac79e1bfa87b5f4621b1ba9d4e (patch) | |
| tree | d22952ee3f8e30057977a99a33823f4d52990fbc /packages/frontend/src/scripts | |
| parent | Merge pull request #13493 from misskey-dev/develop (diff) | |
| parent | fix(backend): use insertOne insteadof insert/findOneOrFail combination (#13908) (diff) | |
| download | sharkey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.tar.gz sharkey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.tar.bz2 sharkey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.zip | |
Merge pull request #13917 from misskey-dev/develop
Release 2024.5.0 (master)
Diffstat (limited to 'packages/frontend/src/scripts')
18 files changed, 233 insertions, 95 deletions
diff --git a/packages/frontend/src/scripts/lookup-user.ts b/packages/frontend/src/scripts/admin-lookup.ts index efc9132e75..1b57b853c9 100644 --- a/packages/frontend/src/scripts/lookup-user.ts +++ b/packages/frontend/src/scripts/admin-lookup.ts @@ -63,3 +63,26 @@ export async function lookupUserByEmail() { } } } + +export async function lookupFile() { + const { canceled, result: q } = await os.inputText({ + title: i18n.ts.fileIdOrUrl, + minLength: 1, + }); + if (canceled) return; + + const show = (file) => { + os.pageWindow(`/admin/file/${file.id}`); + }; + + misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => { + show(file); + }).catch(err => { + if (err.code === 'NO_SUCH_FILE') { + os.alert({ + type: 'error', + text: i18n.ts.notFound, + }); + } + }); +} diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index f2493264d3..fa3fcac2e7 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -6,6 +6,7 @@ import { utils, values } from '@syuilo/aiscript'; import { v4 as uuid } from 'uuid'; import { ref, Ref } from 'vue'; +import * as Misskey from 'misskey-js'; export type AsUiComponentBase = { id: string; @@ -115,23 +116,24 @@ export type AsUiFolder = AsUiComponentBase & { opened?: boolean; }; +type PostFormPropsForAsUi = { + text: string; + cw?: string; + visibility?: (typeof Misskey.noteVisibilities)[number]; + localOnly?: boolean; +}; + export type AsUiPostFormButton = AsUiComponentBase & { type: 'postFormButton'; text?: string; primary?: boolean; rounded?: boolean; - form?: { - text: string; - cw?: string; - }; + form?: PostFormPropsForAsUi; }; export type AsUiPostForm = AsUiComponentBase & { type: 'postForm'; - form?: { - text: string; - cw?: string; - }; + form?: PostFormPropsForAsUi; }; export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm; @@ -447,6 +449,24 @@ function getFolderOptions(def: values.Value | undefined): Omit<AsUiFolder, 'id' }; } +function getPostFormProps(form: values.VObj): PostFormPropsForAsUi { + const text = form.value.get('text'); + utils.assertString(text); + const cw = form.value.get('cw'); + if (cw) utils.assertString(cw); + const visibility = form.value.get('visibility'); + if (visibility) utils.assertString(visibility); + const localOnly = form.value.get('localOnly'); + if (localOnly) utils.assertBoolean(localOnly); + + return { + text: text.value, + cw: cw?.value, + visibility: (visibility?.value && (Misskey.noteVisibilities as readonly string[]).includes(visibility.value)) ? visibility.value as typeof Misskey.noteVisibilities[number] : undefined, + localOnly: localOnly?.value, + }; +} + function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostFormButton, 'id' | 'type'> { utils.assertObject(def); @@ -459,22 +479,11 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu const form = def.value.get('form'); if (form) utils.assertObject(form); - const getForm = () => { - const text = form!.value.get('text'); - utils.assertString(text); - const cw = form!.value.get('cw'); - if (cw) utils.assertString(cw); - return { - text: text.value, - cw: cw?.value, - }; - }; - return { text: text?.value, primary: primary?.value, rounded: rounded?.value, - form: form ? getForm() : { + form: form ? getPostFormProps(form) : { text: '', }, }; @@ -486,19 +495,8 @@ function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn const form = def.value.get('form'); if (form) utils.assertObject(form); - const getForm = () => { - const text = form!.value.get('text'); - utils.assertString(text); - const cw = form!.value.get('cw'); - if (cw) utils.assertString(cw); - return { - text: text.value, - cw: cw?.value, - }; - }; - return { - form: form ? getForm() : { + form: form ? getPostFormProps(form) : { text: '', }, }; diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts index e7b473dd75..8fc857f84f 100644 --- a/packages/frontend/src/scripts/check-reaction-permissions.ts +++ b/packages/frontend/src/scripts/check-reaction-permissions.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as Misskey from 'misskey-js'; import { UnicodeEmojiDef } from './emojilist.js'; diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/scripts/clear-cache.ts index b20109ec72..71d1232710 100644 --- a/packages/frontend/src/scripts/clear-cache.ts +++ b/packages/frontend/src/scripts/clear-cache.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { unisonReload } from '@/scripts/unison-reload.js'; import * as os from '@/os.js'; import { miLocalStorage } from '@/local-storage.js'; diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index 2733897bab..e94027d302 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -1,15 +1,21 @@ -import { bundledThemesInfo } from 'shiki'; +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { getHighlighterCore, loadWasm } from 'shiki/core'; import darkPlus from 'shiki/themes/dark-plus.mjs'; +import { bundledThemesInfo } from 'shiki/themes'; +import { bundledLanguagesInfo } from 'shiki/langs'; import { unique } from './array.js'; import { deepClone } from './clone.js'; import { deepMerge } from './merge.js'; -import type { Highlighter, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki'; +import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core'; import { ColdDeviceStorage } from '@/store.js'; import lightTheme from '@/themes/_light.json5'; import darkTheme from '@/themes/_dark.json5'; -let _highlighter: Highlighter | null = null; +let _highlighter: HighlighterCore | null = null; export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>; export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>; @@ -46,16 +52,14 @@ export async function getTheme(mode: 'light' | 'dark', getName = false): Promise return darkPlus; } -export async function getHighlighter(): Promise<Highlighter> { +export async function getHighlighter(): Promise<HighlighterCore> { if (!_highlighter) { return await initHighlighter(); } return _highlighter; } -export async function initHighlighter() { - const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json'); - +async function initHighlighter() { await loadWasm(import('shiki/onig.wasm?init')); // テーマの重複を消す @@ -64,11 +68,12 @@ export async function initHighlighter() { ...(await Promise.all([getTheme('light'), getTheme('dark')])), ]); + const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript'); const highlighter = await getHighlighterCore({ themes, langs: [ - import('shiki/langs/javascript.mjs'), - aiScriptGrammar.default as unknown as LanguageRegistration, + ...(jsLangInfo ? [async () => await jsLangInfo.import()] : []), + async () => (await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json')).default as unknown as LanguageRegistration, ], }); diff --git a/packages/frontend/src/scripts/collapsed.ts b/packages/frontend/src/scripts/collapsed.ts index 237bd37c7a..4ec88a3c65 100644 --- a/packages/frontend/src/scripts/collapsed.ts +++ b/packages/frontend/src/scripts/collapsed.ts @@ -6,15 +6,16 @@ import * as Misskey from 'misskey-js'; export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean { - const collapsed = note.cw == null && note.text != null && ( - (note.text.includes('$[x2')) || - (note.text.includes('$[x3')) || - (note.text.includes('$[x4')) || - (note.text.includes('$[scale')) || - (note.text.split('\n').length > 9) || - (note.text.length > 500) || - (note.files.length >= 5) || - (urls.length >= 4) + const collapsed = note.cw == null && ( + note.text != null && ( + (note.text.includes('$[x2')) || + (note.text.includes('$[x3')) || + (note.text.includes('$[x4')) || + (note.text.includes('$[scale')) || + (note.text.split('\n').length > 9) || + (note.text.length > 500) || + (urls.length >= 4) + ) || note.files.length >= 5 ); return collapsed; diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts index b0db404f28..242a504c3b 100644 --- a/packages/frontend/src/scripts/form.ts +++ b/packages/frontend/src/scripts/form.ts @@ -3,18 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import * as Misskey from 'misskey-js'; + type EnumItem = string | { label: string; value: string; }; +type Hidden = boolean | ((v: any) => boolean); + export type FormItem = { label?: string; type: 'string'; default: string | null; description?: string; required?: boolean; - hidden?: boolean; + hidden?: Hidden; multiline?: boolean; treatAsMfm?: boolean; } | { @@ -23,27 +27,27 @@ export type FormItem = { default: number | null; description?: string; required?: boolean; - hidden?: boolean; + hidden?: Hidden; step?: number; } | { label?: string; type: 'boolean'; default: boolean | null; description?: string; - hidden?: boolean; + hidden?: Hidden; } | { label?: string; type: 'enum'; default: string | null; required?: boolean; - hidden?: boolean; + hidden?: Hidden; enum: EnumItem[]; } | { label?: string; type: 'radio'; default: unknown | null; required?: boolean; - hidden?: boolean; + hidden?: Hidden; options: { label: string; value: unknown; @@ -58,20 +62,27 @@ export type FormItem = { min: number; max: number; textConverter?: (value: number) => string; + hidden?: Hidden; } | { label?: string; type: 'object'; default: Record<string, unknown> | null; - hidden: boolean; + hidden: Hidden; } | { label?: string; type: 'array'; default: unknown[] | null; - hidden: boolean; + hidden: Hidden; } | { type: 'button'; content?: string; + hidden?: Hidden; action: (ev: MouseEvent, v: any) => void; +} | { + type: 'drive-file'; + defaultFileId?: string | null; + hidden?: Hidden; + validate?: (v: Misskey.entities.DriveFile) => Promise<boolean>; }; export type Form = Record<string, FormItem>; @@ -84,8 +95,9 @@ type GetItemType<Item extends FormItem> = Item['type'] extends 'range' ? number : Item['type'] extends 'enum' ? string : Item['type'] extends 'array' ? unknown[] : - Item['type'] extends 'object' ? Record<string, unknown> - : never; + Item['type'] extends 'object' ? Record<string, unknown> : + Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined : + never; export type GetFormResultType<F extends Form> = { [P in keyof F]: GetItemType<F[P]>; diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index b273bd36f3..71ad299f50 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -16,7 +16,7 @@ import { url } from '@/config.js'; import { defaultStore, noteActions } from '@/store.js'; import { miLocalStorage } from '@/local-storage.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; -import { clipsCache } from '@/cache.js'; +import { clipsCache, favoritedChannelsCache } from '@/cache.js'; import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { isSupportShare } from '@/scripts/navigator.js'; @@ -26,6 +26,14 @@ export async function getNoteClipMenu(props: { isDeleted: Ref<boolean>; currentClip?: Misskey.entities.Clip; }) { + function getClipName(clip: Misskey.entities.Clip) { + if ($i && clip.userId === $i.id && clip.notesCount != null) { + return `${clip.name} (${clip.notesCount}/${$i.policies.noteEachClipsLimit})`; + } else { + return clip.name; + } + } + const isRenote = ( props.note.renote != null && props.note.text == null && @@ -37,7 +45,7 @@ export async function getNoteClipMenu(props: { const clips = await clipsCache.fetch(); const menu: MenuItem[] = [...clips.map(clip => ({ - text: clip.name, + text: getClipName(clip), action: () => { claimAchievement('noteClipped1'); os.promiseDialog( @@ -50,7 +58,18 @@ export async function getNoteClipMenu(props: { text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }), }); if (!confirm.canceled) { - os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); + os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }).then(() => { + clipsCache.set(clips.map(c => { + if (c.id === clip.id) { + return { + ...c, + notesCount: Math.max(0, ((c.notesCount ?? 0) - 1)), + }; + } else { + return c; + } + })); + }); if (props.currentClip?.id === clip.id) props.isDeleted.value = true; } } else { @@ -60,7 +79,18 @@ export async function getNoteClipMenu(props: { }); } }, - ); + ).then(() => { + clipsCache.set(clips.map(c => { + if (c.id === clip.id) { + return { + ...c, + notesCount: (c.notesCount ?? 0) + 1, + }; + } else { + return c; + } + })); + }); }, })), { type: 'divider' }, { icon: 'ti ti-plus', @@ -462,10 +492,9 @@ export function getNoteMenu(props: { }; } -type Visibility = 'public' | 'home' | 'followers' | 'specified'; +type Visibility = (typeof Misskey.noteVisibilities)[number]; -// defaultStore.state.visibilityがstringなためstringも受け付けている -function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility { +function smallerVisibility(a: Visibility, b: Visibility): Visibility { if (a === 'specified' || b === 'specified') return 'specified'; if (a === 'followers' || b === 'followers') return 'followers'; if (a === 'home' || b === 'home') return 'home'; @@ -489,6 +518,7 @@ export function getRenoteMenu(props: { const channelRenoteItems: MenuItem[] = []; const normalRenoteItems: MenuItem[] = []; + const normalExternalChannelRenoteItems: MenuItem[] = []; if (appearNote.channel) { channelRenoteItems.push(...[{ @@ -567,12 +597,47 @@ export function getRenoteMenu(props: { }); }, }]); + + normalExternalChannelRenoteItems.push({ + type: 'parent', + icon: 'ti ti-repeat', + text: appearNote.channel ? i18n.ts.renoteToOtherChannel : i18n.ts.renoteToChannel, + children: async () => { + const channels = await favoritedChannelsCache.fetch(); + return channels.filter((channel) => { + if (!appearNote.channelId) return true; + return channel.id !== appearNote.channelId; + }).map((channel) => ({ + text: channel.name, + action: () => { + const el = props.renoteButton.value; + 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) { + misskeyApi('notes/create', { + renoteId: appearNote.id, + channelId: channel.id, + }).then(() => { + os.toast(i18n.tsx.renotedToX({ name: channel.name })); + }); + } + }, + })); + }, + }); } const renoteItems = [ ...normalRenoteItems, ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] as MenuItem[] : [], ...channelRenoteItems, + ...(normalExternalChannelRenoteItems.length > 0 && (normalRenoteItems.length > 0 || channelRenoteItems.length > 0)) ? [{ type: 'divider' }] as MenuItem[] : [], + ...normalExternalChannelRenoteItems, ]; return { diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index c14f75f382..3e031d232f 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -272,7 +272,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter text: r.name, action: async () => { const { canceled, result: period } = await os.select({ - title: i18n.ts.period, + title: i18n.ts.period + ': ' + r.name, items: [{ value: 'indefinitely', text: i18n.ts.indefinitely, }, { diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts index 1ca0990ba9..6b511f2a5f 100644 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -15,6 +15,16 @@ const fallbackName = (key: string) => `idbfallback::${key}`; let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true; +// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 +// バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと +// see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123 +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +if (window.Cypress) { + idbAvailable = false; + console.log('Cypress detected. It will use localStorage.'); +} + if (idbAvailable) { await iset('idb-test', 'test') .catch(err => { diff --git a/packages/frontend/src/scripts/keycode.ts b/packages/frontend/src/scripts/keycode.ts index bc1f485f5e..7ffceafada 100644 --- a/packages/frontend/src/scripts/keycode.ts +++ b/packages/frontend/src/scripts/keycode.ts @@ -15,6 +15,7 @@ export default (input: string): string[] => { export const aliases = { 'esc': 'Escape', 'enter': ['Enter', 'NumpadEnter'], + 'space': [' ', 'Spacebar'], 'up': 'ArrowUp', 'down': 'ArrowDown', 'left': 'ArrowLeft', diff --git a/packages/frontend/src/scripts/media-has-audio.ts b/packages/frontend/src/scripts/media-has-audio.ts index 3421a38a76..4bf3ee5d97 100644 --- a/packages/frontend/src/scripts/media-has-audio.ts +++ b/packages/frontend/src/scripts/media-has-audio.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export default async function hasAudio(media: HTMLMediaElement) { const cloned = media.cloneNode() as HTMLMediaElement; cloned.muted = (cloned as typeof cloned & Partial<HTMLVideoElement>).playsInline = true; diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts index 8c9e3c02c3..3dad41a8b3 100644 --- a/packages/frontend/src/scripts/popup-position.ts +++ b/packages/frontend/src/scripts/popup-position.ts @@ -26,8 +26,8 @@ export function calcPopupPosition(el: HTMLElement, props: { let top: number; if (props.anchorElement) { - left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); - top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin; + left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2); + top = (rect.top + window.scrollY - contentHeight) - props.innerMargin; } else { left = props.x; top = (props.y - contentHeight) - props.innerMargin; @@ -35,8 +35,8 @@ export function calcPopupPosition(el: HTMLElement, props: { left -= (el.offsetWidth / 2); - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; + if (left + contentWidth - window.scrollX > window.innerWidth) { + left = window.innerWidth - contentWidth + window.scrollX - 1; } return [left, top]; @@ -47,8 +47,8 @@ export function calcPopupPosition(el: HTMLElement, props: { let top: number; if (props.anchorElement) { - left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); - top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin; + left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2); + top = (rect.top + window.scrollY + props.anchorElement.offsetHeight) + props.innerMargin; } else { left = props.x; top = (props.y) + props.innerMargin; @@ -56,8 +56,8 @@ export function calcPopupPosition(el: HTMLElement, props: { left -= (el.offsetWidth / 2); - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; + if (left + contentWidth - window.scrollX > window.innerWidth) { + left = window.innerWidth - contentWidth + window.scrollX - 1; } return [left, top]; @@ -68,8 +68,8 @@ export function calcPopupPosition(el: HTMLElement, props: { let top: number; if (props.anchorElement) { - left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin; - top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); + left = (rect.left + window.scrollX - contentWidth) - props.innerMargin; + top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2); } else { left = (props.x - contentWidth) - props.innerMargin; top = props.y; @@ -77,8 +77,8 @@ export function calcPopupPosition(el: HTMLElement, props: { top -= (el.offsetHeight / 2); - if (top + contentHeight - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - contentHeight + window.pageYOffset - 1; + if (top + contentHeight - window.scrollY > window.innerHeight) { + top = window.innerHeight - contentHeight + window.scrollY - 1; } return [left, top]; @@ -89,15 +89,15 @@ export function calcPopupPosition(el: HTMLElement, props: { let top: number; if (props.anchorElement) { - left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin; + left = (rect.left + props.anchorElement.offsetWidth + window.scrollX) + props.innerMargin; if (props.align === 'top') { - top = rect.top + window.pageYOffset; + top = rect.top + window.scrollY; if (props.alignOffset != null) top += props.alignOffset; } else if (props.align === 'bottom') { // TODO } else { // center - top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); + top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2); top -= (el.offsetHeight / 2); } } else { @@ -106,8 +106,8 @@ export function calcPopupPosition(el: HTMLElement, props: { top -= (el.offsetHeight / 2); } - if (top + contentHeight - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - contentHeight + window.pageYOffset - 1; + if (top + contentHeight - window.scrollY > window.innerHeight) { + top = window.innerHeight - contentHeight + window.scrollY - 1; } return [left, top]; @@ -123,7 +123,7 @@ export function calcPopupPosition(el: HTMLElement, props: { const [left, top] = calcPosWhenTop(); // ツールチップを上に向かって表示するスペースがなければ下に向かって出す - if (top - window.pageYOffset < 0) { + if (top - window.scrollY < 0) { const [left, top] = calcPosWhenBottom(); return { left, top, transformOrigin: 'center top' }; } @@ -141,7 +141,7 @@ export function calcPopupPosition(el: HTMLElement, props: { const [left, top] = calcPosWhenLeft(); // ツールチップを左に向かって表示するスペースがなければ右に向かって出す - if (left - window.pageXOffset < 0) { + if (left - window.scrollX < 0) { const [left, top] = calcPosWhenRight(); return { left, top, transformOrigin: 'left center' }; } diff --git a/packages/frontend/src/scripts/snowfall-effect.ts b/packages/frontend/src/scripts/snowfall-effect.ts index 11fcaa0716..d88bdb6660 100644 --- a/packages/frontend/src/scripts/snowfall-effect.ts +++ b/packages/frontend/src/scripts/snowfall-effect.ts @@ -155,7 +155,9 @@ export class SnowfallEffect { max: 0.125, easing: 0.0005, }; - + /** + * @throws {Error} - Thrown when it fails to get WebGL context for the canvas + */ constructor(options: { sakura?: boolean; }) { diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index 5f7e88bd9f..c7f8b3d596 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -6,7 +6,7 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; import { deepClone } from './clone.js'; -import type { BuiltinTheme } from 'shiki'; +import type { BundledTheme } from 'shiki/themes'; import { globalEvents } from '@/events.js'; import lightTheme from '@/themes/_light.json5'; import darkTheme from '@/themes/_dark.json5'; @@ -20,7 +20,7 @@ export type Theme = { base?: 'dark' | 'light'; props: Record<string, string>; codeHighlighter?: { - base: BuiltinTheme; + base: BundledTheme; overrides?: Record<string, any>; } | { base: '_none_'; diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index 6c46b2bc1b..3e947183c9 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -5,6 +5,7 @@ import { reactive, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { v4 as uuid } from 'uuid'; import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; import { getCompressionConfig } from './upload/compress-config.js'; import { defaultStore } from '@/store.js'; @@ -39,13 +40,16 @@ export function uploadFile( if (folder && typeof folder === 'object') folder = folder.id; return new Promise((resolve, reject) => { - const id = Math.random().toString(); + const id = uuid(); const reader = new FileReader(); reader.onload = async (): Promise<void> => { + const filename = name ?? file.name ?? 'untitled'; + const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : ''; + const ctx = reactive<Uploading>({ - id: id, - name: name ?? file.name ?? 'untitled', + id, + name: defaultStore.state.keepOriginalFilename ? filename : id + extension, progressMax: undefined, progressValue: undefined, img: window.URL.createObjectURL(file), diff --git a/packages/frontend/src/scripts/use-chart-tooltip.ts b/packages/frontend/src/scripts/use-chart-tooltip.ts index 7e4bf5c9c6..bed221a622 100644 --- a/packages/frontend/src/scripts/use-chart-tooltip.ts +++ b/packages/frontend/src/scripts/use-chart-tooltip.ts @@ -53,11 +53,11 @@ export function useChartTooltip(opts: { position: 'top' | 'middle' } = { positio const rect = context.chart.canvas.getBoundingClientRect(); tooltipShowing.value = true; - tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; + tooltipX.value = rect.left + window.scrollX + context.tooltip.caretX; if (opts.position === 'top') { - tooltipY.value = rect.top + window.pageYOffset; + tooltipY.value = rect.top + window.scrollY; } else if (opts.position === 'middle') { - tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; + tooltipY.value = rect.top + window.scrollY + context.tooltip.caretY; } } diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index 524ac5d3fe..542d8ab52b 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -35,6 +35,7 @@ export function useNoteCapture(props: { const currentCount = (note.value.reactions || {})[reaction] || 0; note.value.reactions[reaction] = currentCount + 1; + note.value.reactionCount += 1; if ($i && (body.userId === $i.id)) { note.value.myReaction = reaction; @@ -49,6 +50,7 @@ export function useNoteCapture(props: { const currentCount = (note.value.reactions || {})[reaction] || 0; note.value.reactions[reaction] = Math.max(0, currentCount - 1); + note.value.reactionCount = Math.max(0, note.value.reactionCount - 1); if (note.value.reactions[reaction] === 0) delete note.value.reactions[reaction]; if ($i && (body.userId === $i.id)) { |