diff options
Diffstat (limited to '')
21 files changed, 173 insertions, 45 deletions
diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index b6b7445b67..5453fe827d 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -11,6 +11,7 @@ export function createAiScriptEnv(opts) { USER_NAME: $i ? values.STR($i.name) : values.NULL, USER_USERNAME: $i ? values.STR($i.username) : values.NULL, CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value), + CURRENT_URL: values.STR(window.location.href), 'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { await os.alert({ type: type ? type.value : 'info', diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index 2ca1b164ae..c26ae5a4df 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -510,7 +510,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R // Ui:root.update({ children: [...] }) の糖衣構文 'Ui:render': values.FN_NATIVE(([children], opts) => { utils.assertArray(children); - + rootComponent.value.children = children.value.map(v => { utils.assertObject(v); return v.value.get('id').value; diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/scripts/array.ts index 4620c8b735..c9a146e707 100644 --- a/packages/frontend/src/scripts/array.ts +++ b/packages/frontend/src/scripts/array.ts @@ -78,8 +78,9 @@ export function maximum(xs: number[]): number { export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] { const groups = [] as T[][]; for (const x of xs) { - if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { - groups[groups.length - 1].push(x); + const lastGroup = groups.at(-1); + if (lastGroup !== undefined && f(lastGroup[0], x)) { + lastGroup.push(x); } else { groups.push([x]); } diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts index 1bae3790f5..564573ae8a 100644 --- a/packages/frontend/src/scripts/autocomplete.ts +++ b/packages/frontend/src/scripts/autocomplete.ts @@ -65,7 +65,7 @@ export class Autocomplete { */ private onInput() { const caretPos = this.textarea.selectionStart; - const text = this.text.substr(0, caretPos).split('\n').pop()!; + const text = this.text.substring(0, caretPos).split('\n').pop()!; const mentionIndex = text.lastIndexOf('@'); const hashtagIndex = text.lastIndexOf('#'); @@ -91,7 +91,7 @@ export class Autocomplete { let opened = false; if (isMention) { - const username = text.substr(mentionIndex + 1); + const username = text.substring(mentionIndex + 1); if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) { this.open('user', username); opened = true; @@ -102,7 +102,7 @@ export class Autocomplete { } if (isHashtag && !opened) { - const hashtag = text.substr(hashtagIndex + 1); + const hashtag = text.substring(hashtagIndex + 1); if (!hashtag.includes(' ')) { this.open('hashtag', hashtag); opened = true; @@ -110,7 +110,7 @@ export class Autocomplete { } if (isEmoji && !opened) { - const emoji = text.substr(emojiIndex + 1); + const emoji = text.substring(emojiIndex + 1); if (!emoji.includes(' ')) { this.open('emoji', emoji); opened = true; @@ -118,7 +118,7 @@ export class Autocomplete { } if (isMfmTag && !opened) { - const mfmTag = text.substr(mfmTagIndex + 1); + const mfmTag = text.substring(mfmTagIndex + 1); if (!mfmTag.includes(' ')) { this.open('mfmTag', mfmTag.replace('[', '')); opened = true; @@ -208,9 +208,9 @@ export class Autocomplete { if (type === 'user') { const source = this.text; - const before = source.substr(0, caret); + const before = source.substring(0, caret); const trimmedBefore = before.substring(0, before.lastIndexOf('@')); - const after = source.substr(caret); + const after = source.substring(caret); const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`; @@ -226,9 +226,9 @@ export class Autocomplete { } else if (type === 'hashtag') { const source = this.text; - const before = source.substr(0, caret); + const before = source.substring(0, caret); const trimmedBefore = before.substring(0, before.lastIndexOf('#')); - const after = source.substr(caret); + const after = source.substring(caret); // 挿入 this.text = `${trimmedBefore}#${value} ${after}`; @@ -242,9 +242,9 @@ export class Autocomplete { } else if (type === 'emoji') { const source = this.text; - const before = source.substr(0, caret); + const before = source.substring(0, caret); const trimmedBefore = before.substring(0, before.lastIndexOf(':')); - const after = source.substr(caret); + const after = source.substring(caret); // 挿入 this.text = trimmedBefore + value + after; @@ -258,9 +258,9 @@ export class Autocomplete { } else if (type === 'mfmTag') { const source = this.text; - const before = source.substr(0, caret); + const before = source.substring(0, caret); const trimmedBefore = before.substring(0, before.lastIndexOf('$')); - const after = source.substr(caret); + const after = source.substring(caret); // 挿入 this.text = `${trimmedBefore}$[${value} ]${after}`; diff --git a/packages/frontend/src/scripts/cache.ts b/packages/frontend/src/scripts/cache.ts index 858e5f03bf..a61d858353 100644 --- a/packages/frontend/src/scripts/cache.ts +++ b/packages/frontend/src/scripts/cache.ts @@ -1,7 +1,8 @@ +import { ref } from "vue"; export class Cache<T> { private cachedAt: number | null = null; - private value: T | undefined; + public value = ref<T | undefined>(); private lifetime: number; constructor(lifetime: Cache<never>['lifetime']) { @@ -10,21 +11,20 @@ export class Cache<T> { public set(value: T): void { this.cachedAt = Date.now(); - this.value = value; + this.value.value = value; } - public get(): T | undefined { + private get(): T | undefined { if (this.cachedAt == null) return undefined; if ((Date.now() - this.cachedAt) > this.lifetime) { - this.value = undefined; + this.value.value = undefined; this.cachedAt = null; return undefined; } - return this.value; + return this.value.value; } public delete() { - this.value = undefined; this.cachedAt = null; } diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts index 16fad24129..cf8fa64ba3 100644 --- a/packages/frontend/src/scripts/clone.ts +++ b/packages/frontend/src/scripts/clone.ts @@ -1,5 +1,7 @@ // structredCloneが遅いため // SEE: http://var.blog.jp/archives/86038606.html +// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった +// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045 type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[]; diff --git a/packages/frontend/src/scripts/collapsed.ts b/packages/frontend/src/scripts/collapsed.ts new file mode 100644 index 0000000000..1bf56f233b --- /dev/null +++ b/packages/frontend/src/scripts/collapsed.ts @@ -0,0 +1,19 @@ +import * as mfm from 'mfm-js'; +import * as misskey from 'misskey-js'; +import { extractUrlFromMfm } from './extract-url-from-mfm'; + +export function shouldCollapsed(note: misskey.entities.Note): boolean { + const urls = note.text ? extractUrlFromMfm(mfm.parse(note.text)) : null; + 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 && urls.length >= 4) + ); + + return collapsed; +} diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts index 7f321cc0ae..635803a2bc 100644 --- a/packages/frontend/src/scripts/form.ts +++ b/packages/frontend/src/scripts/form.ts @@ -1,3 +1,4 @@ +type EnumItem = string | {label: string; value: string;}; export type FormItem = { label?: string; type: 'string'; @@ -20,7 +21,7 @@ export type FormItem = { type: 'enum'; default: string | null; hidden?: boolean; - enum: string[]; + enum: EnumItem[]; } | { label?: string; type: 'radio'; diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts index da7d622632..956e0f35d0 100644 --- a/packages/frontend/src/scripts/gen-search-query.ts +++ b/packages/frontend/src/scripts/gen-search-query.ts @@ -5,7 +5,7 @@ export async function genSearchQuery(v: any, q: string) { let host: string; let userId: string; if (q.split(' ').some(x => x.startsWith('@'))) { - for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) { + for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substring(1))) { if (at.includes('.')) { if (at === localHost || at === '.') { host = null; diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index 060c8a1a11..9b488087e2 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -3,6 +3,8 @@ import { defineAsyncComponent } from 'vue'; import { i18n } from '@/i18n'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import * as os from '@/os'; +import { MenuItem } from '@/types/menu'; +import { defaultStore } from '@/store'; function rename(file: Misskey.entities.DriveFile) { os.inputText({ @@ -66,8 +68,10 @@ async function deleteFile(file: Misskey.entities.DriveFile) { }); } -export function getDriveFileMenu(file: Misskey.entities.DriveFile) { - return [{ +export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] { + const isImage = file.type.startsWith('image/'); + let menu; + menu = [{ text: i18n.ts.rename, icon: 'ti ti-forms', action: () => rename(file), @@ -79,7 +83,14 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile) { text: i18n.ts.describeFile, icon: 'ti ti-text-caption', action: () => describe(file), - }, null, { + }, ...isImage ? [{ + text: i18n.ts.cropImage, + icon: 'ti ti-crop', + action: () => os.cropImage(file, { + aspectRatio: NaN, + uploadFolder: folder ? folder.id : folder + }), + }] : [], null, { text: i18n.ts.createNoteFromTheFile, icon: 'ti ti-pencil', action: () => os.post({ @@ -102,4 +113,16 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile) { danger: true, action: () => deleteFile(file), }]; + + if (defaultStore.state.devMode) { + menu = menu.concat([null, { + icon: 'ti ti-id', + text: i18n.ts.copyFileId, + action: () => { + copyToClipboard(file.id); + }, + }]); + } + + return menu; } diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index b055d26473..1c93d58b44 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -1,14 +1,15 @@ +import { toUnicode } from 'punycode'; import { defineAsyncComponent } from 'vue'; import * as misskey from 'misskey-js'; import { i18n } from '@/i18n'; import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { host } from '@/config'; +import { host, url } from '@/config'; import * as os from '@/os'; import { defaultStore, userActions } from '@/store'; import { $i, iAmModerator } from '@/account'; import { mainRouter } from '@/router'; import { Router } from '@/nirax'; -import { rolesCache, userListsCache } from '@/cache'; +import { antennasCache, rolesCache, userListsCache } from '@/cache'; export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) { const meId = $i ? $i.id : null; @@ -138,6 +139,13 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router copyToClipboard(`${user.host ?? host}/@${user.username}.atom`); }, }, { + icon: 'ti ti-share', + text: i18n.ts.copyProfileUrl, + action: () => { + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; + copyToClipboard(`${url}/${canonical}`); + }, + }, { icon: 'ti ti-mail', text: i18n.ts.sendMessage, action: () => { @@ -158,11 +166,39 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router return lists.map(list => ({ text: list.name, - action: () => { - os.apiWithDialog('users/lists/push', { + action: async () => { + await os.apiWithDialog('users/lists/push', { listId: list.id, userId: user.id, }); + userListsCache.delete(); + }, + })); + }, + }, { + type: 'parent', + icon: 'ti ti-antenna', + text: i18n.ts.addToAntenna, + children: async () => { + const antennas = await antennasCache.fetch(() => os.api('antennas/list')); + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; + return antennas.filter((a) => a.src === 'users').map(antenna => ({ + text: antenna.name, + action: async () => { + await os.apiWithDialog('antennas/update', { + antennaId: antenna.id, + name: antenna.name, + keywords: antenna.keywords, + excludeKeywords: antenna.excludeKeywords, + src: antenna.src, + userListId: antenna.userListId, + users: [...antenna.users, canonical], + caseSensitive: antenna.caseSensitive, + withReplies: antenna.withReplies, + withFile: antenna.withFile, + notify: antenna.notify, + }); + antennasCache.delete(); }, })); }, @@ -196,7 +232,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router default: 'indefinitely', }); if (canceled) return; - + const expiresAt = period === 'indefinitely' ? null : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) diff --git a/packages/frontend/src/scripts/idle-render.ts b/packages/frontend/src/scripts/idle-render.ts index ccce8b02bf..a1470b82e9 100644 --- a/packages/frontend/src/scripts/idle-render.ts +++ b/packages/frontend/src/scripts/idle-render.ts @@ -1,3 +1,20 @@ +const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.requestIdleCallback ?? ((callback) => { + const start = performance.now(); + const timeoutId = setTimeout(() => { + callback({ + didTimeout: false, // polyfill でタイムアウト発火することはない + timeRemaining() { + const diff = performance.now() - start; + return Math.max(0, 50 - diff); // <https://www.w3.org/TR/requestidlecallback/#idle-periods> + }, + }); + }); + return timeoutId; +}); +const cancelIdleCallback: typeof globalThis.cancelIdleCallback = globalThis.cancelIdleCallback ?? ((timeoutId) => { + clearTimeout(timeoutId); +}); + class IdlingRenderScheduler { #renderers: Set<FrameRequestCallback>; #rafId: number; diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts new file mode 100644 index 0000000000..0ddd3f377d --- /dev/null +++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts @@ -0,0 +1,11 @@ +import * as misskey from 'misskey-js'; +import { $i } from '@/account'; + +export function isFfVisibleForMe(user: misskey.entities.UserDetailed): boolean { + if ($i && $i.id === user.id) return true; + + if (user.ffVisibility === 'private') return false; + if (user.ffVisibility === 'followers' && !user.isFollowing) return false; + + return true; +} diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts index ce5b03fc38..3f357a3c92 100644 --- a/packages/frontend/src/scripts/lookup.ts +++ b/packages/frontend/src/scripts/lookup.ts @@ -6,18 +6,19 @@ import { Router } from '@/nirax'; export async function lookup(router?: Router) { const _router = router ?? mainRouter; - const { canceled, result: query } = await os.inputText({ + const { canceled, result: temp } = await os.inputText({ title: i18n.ts.lookup, }); + const query = temp ? temp.trim() : ''; if (canceled) return; - + if (query.startsWith('@') && !query.includes(' ')) { _router.push(`/${query}`); return; } if (query.startsWith('#')) { - _router.push(`/tags/${encodeURIComponent(query.substr(1))}`); + _router.push(`/tags/${encodeURIComponent(query.substring(1))}`); return; } diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 7a5dd4dbfa..68136cdcfe 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -132,9 +132,7 @@ export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notifica } export function playFile(file: string, volume: number) { - const masterVolume = soundConfigStore.state.sound_masterVolume; - if (masterVolume === 0) return; - const audio = setVolume(getAudio(file), volume); + if (audio.volume === 0) return; audio.play(); } diff --git a/packages/frontend/src/scripts/theme-editor.ts b/packages/frontend/src/scripts/theme-editor.ts index 944875ff15..001d87381c 100644 --- a/packages/frontend/src/scripts/theme-editor.ts +++ b/packages/frontend/src/scripts/theme-editor.ts @@ -35,7 +35,7 @@ export const fromThemeString = (str?: string) : ThemeValue => { } else if (str.startsWith('"')) { return { type: 'css', - value: str.substr(1).trim(), + value: str.substring(1).trim(), }; } else { return str; diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index f2e8253565..bc61256cac 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -98,7 +98,7 @@ function compile(theme: Theme): Record<string, string> { function getColor(val: string): tinycolor.Instance { // ref (prop) if (val[0] === '@') { - return getColor(theme.props[val.substr(1)]); + return getColor(theme.props[val.substring(1)]); } // ref (const) @@ -109,7 +109,7 @@ function compile(theme: Theme): Record<string, string> { // func else if (val[0] === ':') { const parts = val.split('<'); - const func = parts.shift().substr(1); + const func = parts.shift().substring(1); const arg = parseFloat(parts.shift()); const color = getColor(parts.join('<')); diff --git a/packages/frontend/src/scripts/upload/compress-config.ts b/packages/frontend/src/scripts/upload/compress-config.ts index 793c78ad20..55d469c5e4 100644 --- a/packages/frontend/src/scripts/upload/compress-config.ts +++ b/packages/frontend/src/scripts/upload/compress-config.ts @@ -1,7 +1,15 @@ import isAnimated from 'is-file-animated'; +import { isWebpSupported } from './isWebpSupported'; import type { BrowserImageResizerConfig } from 'browser-image-resizer'; const compressTypeMap = { + 'image/jpeg': { quality: 0.90, mimeType: 'image/webp' }, + 'image/png': { quality: 1, mimeType: 'image/webp' }, + 'image/webp': { quality: 0.90, mimeType: 'image/webp' }, + 'image/svg+xml': { quality: 1, mimeType: 'image/webp' }, +} as const; + +const compressTypeMapFallback = { 'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' }, 'image/png': { quality: 1, mimeType: 'image/png' }, 'image/webp': { quality: 0.85, mimeType: 'image/jpeg' }, @@ -9,7 +17,7 @@ const compressTypeMap = { } as const; export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfig | undefined> { - const imgConfig = compressTypeMap[file.type]; + const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type]; if (!imgConfig || await isAnimated(file)) { return; } diff --git a/packages/frontend/src/scripts/upload/isWebpSupported.ts b/packages/frontend/src/scripts/upload/isWebpSupported.ts new file mode 100644 index 0000000000..cde8b9d785 --- /dev/null +++ b/packages/frontend/src/scripts/upload/isWebpSupported.ts @@ -0,0 +1,10 @@ +let isWebpSupportedCache: boolean | undefined; +export function isWebpSupported() { + if (isWebpSupportedCache === undefined) { + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + isWebpSupportedCache = canvas.toDataURL('image/webp').startsWith('data:image/webp'); + } + return isWebpSupportedCache; +} diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend/src/scripts/url.ts index b6a997449a..07737d6228 100644 --- a/packages/frontend/src/scripts/url.ts +++ b/packages/frontend/src/scripts/url.ts @@ -2,7 +2,7 @@ * 1. 配列に何も入っていない時はクエリを付けない * 2. プロパティがundefinedの時はクエリを付けない * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) - */ + */ export function query(obj: Record<string, any>): string { const params = Object.entries(obj) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index 22a01e066a..d057386b13 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -93,12 +93,12 @@ export function useNoteCapture(props: { function onStreamConnected() { capture(false); } - + capture(true); if (connection) { connection.on('_connected_', onStreamConnected); } - + onUnmounted(() => { decapture(true); if (connection) { |