diff options
Diffstat (limited to 'packages/frontend-embed')
17 files changed, 156 insertions, 71 deletions
diff --git a/packages/frontend-embed/@types/global.d.ts b/packages/frontend-embed/@types/global.d.ts index 1025d1bedb..c8a9bf4323 100644 --- a/packages/frontend-embed/@types/global.d.ts +++ b/packages/frontend-embed/@types/global.d.ts @@ -6,6 +6,7 @@ type FIXME = any; declare const _LANGS_: string[][]; +declare const _LANGS_VERSION_: string; declare const _VERSION_: string; declare const _ENV_: string; declare const _DEV_: boolean; @@ -13,6 +14,7 @@ declare const _PERF_PREFIX_: string; declare const _DATA_TRANSFER_DRIVE_FILE_: string; declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; declare const _DATA_TRANSFER_DECK_COLUMN_: string; +declare const _RUFFLE_VERSION_: string; // for dev-mode declare const _LANGS_FULL_: string[][]; diff --git a/packages/frontend-embed/eslint.config.js b/packages/frontend-embed/eslint.config.js index 7805256fd4..fbe91f3660 100644 --- a/packages/frontend-embed/eslint.config.js +++ b/packages/frontend-embed/eslint.config.js @@ -4,16 +4,19 @@ import parser from 'vue-eslint-parser'; import pluginVue from 'eslint-plugin-vue'; import pluginMisskey from '@misskey-dev/eslint-plugin'; import sharedConfig from '../shared/eslint.config.js'; +import localeRule from '../../eslint/locale.js'; +import { build as buildLocales } from '../../locales/index.js'; export default [ ...sharedConfig, { - files: ['src/**/*.vue'], + files: ['{src,test,js,@types}/**/*.vue'], ...pluginMisskey.configs.typescript, }, ...pluginVue.configs['flat/recommended'], { - files: ['src/**/*.{ts,vue}'], + files: ['{src,test,js,@types}/**/*.{ts,vue}'], + plugins: { sharkey: { rules: { locale: localeRule } } }, languageOptions: { globals: { ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])), @@ -44,6 +47,8 @@ export default [ }, }, rules: { + 'sharkey/locale': ['error', buildLocales()['en-US']], + '@typescript-eslint/no-empty-interface': ['error', { allowSingleExtends: true, }], @@ -93,4 +98,13 @@ export default [ 'vue/attribute-hyphenation': ['error', 'never'], }, }, + { + ignores: [ + "**/lib/", + "**/temp/", + "**/built/", + "**/coverage/", + "**/node_modules/", + ] + }, ]; diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 22b59c5a92..297a06fd8a 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -6,22 +6,22 @@ "watch": "vite", "build": "vite build", "typecheck": "vue-tsc --noEmit", - "eslint": "eslint --quiet \"src/**/*.{ts,vue}\"", + "eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache", "lint": "pnpm typecheck && pnpm eslint" }, "dependencies": { "@discordapp/twemoji": "15.1.0", + "@phosphor-icons/web": "^2.0.3", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.2", "@rollup/pluginutils": "5.1.4", - "@tabler/icons-webfont": "3.31.0", + "@transfem-org/sfm-js": "0.24.5", "@twemoji/parser": "15.1.1", "@vitejs/plugin-vue": "5.2.3", "@vue/compiler-sfc": "3.5.13", "astring": "1.9.0", "buraha": "0.0.1", "estree-walker": "3.0.3", - "mfm-js": "0.24.0", "misskey-js": "workspace:*", "frontend-shared": "workspace:*", "punycode.js": "2.3.1", diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts index c1b2b58beb..7c8336ce3f 100644 --- a/packages/frontend-embed/src/boot.ts +++ b/packages/frontend-embed/src/boot.ts @@ -6,8 +6,6 @@ // https://vitejs.dev/config/build-options.html#build-modulepreload import 'vite/modulepreload-polyfill'; -import '@tabler/icons-webfont/dist/tabler-icons.scss'; - import '@/style.scss'; import { createApp, defineAsyncComponent } from 'vue'; import defaultLightTheme from '@@/themes/l-light.json5'; @@ -17,7 +15,7 @@ import { applyTheme, assertIsTheme } from '@/theme.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; import { DI } from '@/di.js'; import { serverMetadata } from '@/server-metadata.js'; -import { url, version, locale, lang, updateLocale } from '@@/js/config.js'; +import { url, version, locale, lang, updateLocale, langsVersion } from '@@/js/config.js'; import { parseEmbedParams } from '@@/js/embed-page.js'; import { postMessageToParentWindow, setIframeId } from '@/post-message.js'; import { serverContext } from '@/server-context.js'; @@ -25,7 +23,7 @@ import { i18n, updateI18n } from '@/i18n.js'; import type { Theme } from '@/theme.js'; -console.log('Misskey Embed'); +console.log('Sharkey Embed'); //#region Embedパラメータの取得・パース const params = new URLSearchParams(location.search); @@ -73,14 +71,14 @@ if (embedParams.colorMode === 'dark') { //#region Detect language & fetch translations const localeVersion = localStorage.getItem('localeVersion'); -const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null); +const localeOutdated = (localeVersion == null || localeVersion !== langsVersion || locale == null); if (localeOutdated) { - const res = await window.fetch(`/assets/locales/${lang}.${version}.json`); + const res = await window.fetch(`/assets/locales/${lang}.${langsVersion}.json`); if (res.status === 200) { const newLocale = await res.text(); const parsedNewLocale = JSON.parse(newLocale); localStorage.setItem('locale', newLocale); - localStorage.setItem('localeVersion', version); + localStorage.setItem('localeVersion', langsVersion); updateLocale(parsedNewLocale); updateI18n(parsedNewLocale); } @@ -119,7 +117,7 @@ app.provide(DI.embedParams, embedParams); // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 // なぜか2回実行されることがあるため、mountするdivを1つに制限する const rootEl = ((): HTMLElement => { - const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; + const MISSKEY_MOUNT_DIV_ID = 'sharkey_app'; const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID); diff --git a/packages/frontend-embed/src/components/EmAvatar.vue b/packages/frontend-embed/src/components/EmAvatar.vue index 58c35c8ef0..50d46781d9 100644 --- a/packages/frontend-embed/src/components/EmAvatar.vue +++ b/packages/frontend-embed/src/components/EmAvatar.vue @@ -30,6 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only rotate: getDecorationAngle(decoration), scale: getDecorationScale(decoration), translate: getDecorationOffset(decoration), + zIndex: getDecorationZIndex(decoration), }" alt="" > @@ -86,6 +87,10 @@ function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['ava const offsetY = decoration.offsetY ?? 0; return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`; } + +function getDecorationZIndex(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { + return decoration.showBelow ? '-1' : undefined; +} </script> <style lang="scss" module> diff --git a/packages/frontend-embed/src/components/EmMediaBanner.vue b/packages/frontend-embed/src/components/EmMediaBanner.vue index cf4a4c53b5..24874940f3 100644 --- a/packages/frontend-embed/src/components/EmMediaBanner.vue +++ b/packages/frontend-embed/src/components/EmMediaBanner.vue @@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <a :href="href" target="_blank" :class="$style.root"> <div :class="$style.label"> <template v-if="media.type.startsWith('audio')"><i class="ti ti-music"></i> {{ i18n.ts.audio }}</template> + <template v-else-if="media.type.startsWith('application') && media.type.includes('flash')"><i class="ti ti-bolt"></i> {{ i18n.ts.flash }}</template> <template v-else><i class="ti ti-file"></i> {{ i18n.ts.file }}</template> </div> <div :class="$style.go"> diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue index 2c96ce3215..e9126d4665 100644 --- a/packages/frontend-embed/src/components/EmMediaImage.vue +++ b/packages/frontend-embed/src/components/EmMediaImage.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="[hide ? $style.hidden : $style.visible]" @click="onclick"> <a - :title="image.name" + :title="image.comment || image.name" :class="$style.imageContainer" :href="href ?? image.url" target="_blank" diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts index 1f9ce9d4f4..d377d492e0 100644 --- a/packages/frontend-embed/src/components/EmMfm.ts +++ b/packages/frontend-embed/src/components/EmMfm.ts @@ -5,7 +5,7 @@ import { h, provide } from 'vue'; import type { VNode, SetupContext } from 'vue'; -import * as mfm from 'mfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { host } from '@@/js/config.js'; import EmUrl from '@/components/EmUrl.vue'; @@ -42,6 +42,7 @@ type MfmProps = { rootScale?: number; nyaize?: boolean | 'respect'; parsedNodes?: mfm.MfmNode[] | null; + isBlock?: boolean; }; type MfmEvents = { @@ -51,7 +52,7 @@ type MfmEvents = { // eslint-disable-next-line import/no-default-export export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) { const isNote = props.isNote ?? true; - const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false; + const shouldNyaize = props.nyaize === 'respect' && props.author?.isCat && props.author?.speakAsCat; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (props.text == null || props.text === '') return; @@ -71,6 +72,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven const useAnim = true; + const isBlock = props.isBlock ?? false; + /** * Gen Vue Elements from MFM AST * @param ast MFM AST @@ -220,17 +223,46 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven case 'sparkle': { return genEl(token.children, scale); } + case 'fade': { + const direction = token.props.args.out + ? 'alternate-reverse' + : 'alternate'; + const speed = validTime(token.props.args.speed) ?? '1.5s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + const loop = safeParseFloat(token.props.args.loop) ?? 'infinite'; + style = `animation: mfm-fade ${speed} ${delay} linear ${loop}; animation-direction: ${direction};`; + break; + } case 'rotate': { const degrees = safeParseFloat(token.props.args.deg) ?? 90; style = `transform: rotate(${degrees}deg); transform-origin: center center;`; break; } + case 'followmouse': { + return genEl(token.children, scale); + } case 'position': { const x = safeParseFloat(token.props.args.x) ?? 0; const y = safeParseFloat(token.props.args.y) ?? 0; style = `transform: translateX(${x}em) translateY(${y}em);`; break; } + case 'crop': { + const top = Number.parseFloat( + (token.props.args.top ?? '0').toString(), + ); + const right = Number.parseFloat( + (token.props.args.right ?? '0').toString(), + ); + const bottom = Number.parseFloat( + (token.props.args.bottom ?? '0').toString(), + ); + const left = Number.parseFloat( + (token.props.args.left ?? '0').toString(), + ); + style = `clip-path: inset(${top}% ${right}% ${bottom}% ${left}%);`; + break; + } case 'scale': { const x = Math.min(safeParseFloat(token.props.args.x) ?? 1, 5); const y = Math.min(safeParseFloat(token.props.args.y) ?? 1, 5); @@ -325,63 +357,63 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven case 'center': { return [h('div', { style: 'text-align:center;', - }, genEl(token.children, scale))]; + }, h('bdi', genEl(token.children, scale)))]; } case 'url': { - return [h(EmUrl, { + return [h('bdi', h(EmUrl, { key: Math.random(), url: token.props.url, rel: 'nofollow noopener', - })]; + }))]; } case 'link': { - return [h(EmLink, { + return [h('bdi', h(EmLink, { key: Math.random(), url: token.props.url, rel: 'nofollow noopener', - }, genEl(token.children, scale, true))]; + }, genEl(token.children, scale, true)))]; } case 'mention': { - return [h(EmMention, { + return [h('bdi', h(EmMention, { key: Math.random(), host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host, username: token.props.username, - })]; + }))]; } case 'hashtag': { - return [h(EmA, { + return [h('bdi', h(EmA, { key: Math.random(), to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`, style: 'color:var(--MI_THEME-hashtag);', - }, `#${token.props.hashtag}`)]; + }, `#${token.props.hashtag}`))]; } case 'blockCode': { - return [h('code', { + return [h('bdi', { class: 'block' }, h('code', { key: Math.random(), lang: token.props.lang ?? undefined, - }, token.props.code)]; + }, token.props.code))]; } case 'inlineCode': { - return [h('code', { + return [h('bdi', h('code', { key: Math.random(), - }, token.props.code)]; + }, token.props.code))]; } case 'quote': { if (!props.nowrap) { - return [h('div', { + return [h('bdi', { class: 'block' }, h('div', { style: QUOTE_STYLE, - }, genEl(token.children, scale, true))]; + }, h('bdi', genEl(token.children, scale, true))))]; } else { return [h('span', { style: QUOTE_STYLE, - }, genEl(token.children, scale, true))]; + }, h('bdi', genEl(token.children, scale, true)))]; } } @@ -420,21 +452,21 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'mathInline': { - return [h('code', token.props.formula)]; + return [h('bdi', h('code', token.props.formula))]; } case 'mathBlock': { - return [h('code', token.props.formula)]; + return [h('bdi', h('code', token.props.formula))]; } case 'search': { - return [h('div', { + return [h('bdi', h('div', { key: Math.random(), - }, token.props.query)]; + }, token.props.query))]; } case 'plain': { - return [h('span', genEl(token.children, scale, true))]; + return [h('bdi', h('span', genEl(token.children, scale, true)))]; } default: { @@ -446,8 +478,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } }).flat(Infinity) as (VNode | string)[]; - return h('span', { + return h('bdi', { ...( isBlock ? { class: 'block' } : {}) }, h('span', { // https://codeday.me/jp/qa/20190424/690106.html style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;', - }, genEl(rootAst, props.rootScale ?? 1)); + }, genEl(rootAst, props.rootScale ?? 1))); } diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue index f5b064c293..bf96c557ea 100644 --- a/packages/frontend-embed/src/components/EmNote.vue +++ b/packages/frontend-embed/src/components/EmNote.vue @@ -46,11 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only <EmNoteHeader :note="appearNote" :mini="true"/> <EmInstanceTicker v-if="appearNote.user.instance != null" :instance="appearNote.user.instance"/> <div style="container-type: inline-size;"> - <p v-if="appearNote.cw != null" :class="$style.cw"> - <EmMfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> + <p v-if="mergedCW != null" :class="$style.cw"> + <EmMfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :author="appearNote.user" :nyaize="'respect'" :isBlock="true"/> <button style="display: block; width: 100%; margin: 4px 0;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button> </p> - <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> + <div v-show="mergedCW == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> <div :class="$style.text"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <EmA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA> @@ -61,8 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis" - :enableEmojiMenu="!true" - :enableEmojiMenuReaction="true" + :isBlock="true" /> </div> <div v-if="appearNote.files && appearNote.files.length > 0"> @@ -106,10 +105,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, inject, ref, shallowRef } from 'vue'; -import * as mfm from 'mfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { shouldCollapsed } from '@@/js/collapsed.js'; import { url } from '@@/js/config.js'; +import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import I18n from '@/components/I18n.vue'; import EmNoteSub from '@/components/EmNoteSub.vue'; import EmNoteHeader from '@/components/EmNoteHeader.vue'; @@ -155,6 +155,8 @@ const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value const isLong = shouldCollapsed(appearNote.value, []); const collapsed = ref(appearNote.value.cw == null && isLong); const isDeleted = ref(false); + +const mergedCW = computed(() => computeMergedCw(appearNote.value)); </script> <style lang="scss" module> diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue index b39b47c065..0961b36e35 100644 --- a/packages/frontend-embed/src/components/EmNoteDetailed.vue +++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue @@ -58,11 +58,11 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </header> <div :class="[$style.noteContent, { [$style.contentCollapsed]: collapsed }]"> - <p v-if="appearNote.cw != null" :class="$style.cw"> - <EmMfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> + <p v-if="mergedCW != null" :class="$style.cw"> + <EmMfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :author="appearNote.user" :nyaize="'respect'" :isBlock="true"/> <button style="display: block; width: 100%; margin: 4px 0;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button> </p> - <div v-show="appearNote.cw == null || showContent"> + <div v-show="mergedCW == null || showContent"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <EmA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA> <EmMfm @@ -72,6 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis" + :isBlock="true" /> <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a> <div v-if="appearNote.files && appearNote.files.length > 0"> @@ -127,8 +128,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, inject, ref } from 'vue'; -import * as mfm from 'mfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; +import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import I18n from '@/components/I18n.vue'; import EmMediaList from '@/components/EmMediaList.vue'; import EmNoteSub from '@/components/EmNoteSub.vue'; @@ -174,6 +176,8 @@ const isDeleted = ref(false); const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; const isLong = shouldCollapsed(appearNote.value, []); const collapsed = ref(appearNote.value.cw == null && isLong); + +const mergedCW = computed(() => computeMergedCw(appearNote.value)); </script> <style lang="scss" module> diff --git a/packages/frontend-embed/src/components/EmNoteSimple.vue b/packages/frontend-embed/src/components/EmNoteSimple.vue index b9aaf3fa4a..688758edb6 100644 --- a/packages/frontend-embed/src/components/EmNoteSimple.vue +++ b/packages/frontend-embed/src/components/EmNoteSimple.vue @@ -9,11 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.main"> <EmNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> - <p v-if="note.cw != null" :class="$style.cw"> - <EmMfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> + <p v-if="mergedCW != null" :class="$style.cw"> + <EmMfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis" :isBlock="true"/> <button style="display: block; width: 100%;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button> </p> - <div v-show="note.cw == null || showContent"> + <div v-show="mergedCW == null || showContent"> <EmSubNoteContent :class="$style.text" :note="note"/> </div> </div> @@ -22,8 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import { i18n } from '@/i18n.js'; import EmAvatar from '@/components/EmAvatar.vue'; import EmNoteHeader from '@/components/EmNoteHeader.vue'; @@ -35,6 +36,8 @@ const props = defineProps<{ }>(); const showContent = ref(false); + +const mergedCW = computed(() => computeMergedCw(props.note)); </script> <style lang="scss" module> diff --git a/packages/frontend-embed/src/components/EmNoteSub.vue b/packages/frontend-embed/src/components/EmNoteSub.vue index 59be8608e0..629f0bffcd 100644 --- a/packages/frontend-embed/src/components/EmNoteSub.vue +++ b/packages/frontend-embed/src/components/EmNoteSub.vue @@ -11,11 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.body"> <EmNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> - <p v-if="note.cw != null" :class="$style.cw"> - <EmMfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/> + <p v-if="mergedCW != null" :class="$style.cw"> + <EmMfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :author="note.user" :nyaize="'respect'" :isBlock="true"/> <button style="display: block; width: 100%;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button> </p> - <div v-show="note.cw == null || showContent"> + <div v-show="mergedCW == null || showContent"> <EmSubNoteContent :class="$style.text" :note="note"/> </div> </div> @@ -31,8 +31,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import EmA from '@/components/EmA.vue'; import EmAvatar from '@/components/EmAvatar.vue'; import EmNoteHeader from '@/components/EmNoteHeader.vue'; @@ -55,6 +56,8 @@ const props = withDefaults(defineProps<{ const showContent = ref(false); const replies = ref<Misskey.entities.Note[]>([]); +const mergedCW = computed(() => computeMergedCw(props.note)); + if (props.detail) { misskeyApi('notes/children', { noteId: props.note.id, diff --git a/packages/frontend-embed/src/components/EmSubNoteContent.vue b/packages/frontend-embed/src/components/EmSubNoteContent.vue index 61815ddfd8..e9acbcb293 100644 --- a/packages/frontend-embed/src/components/EmSubNoteContent.vue +++ b/packages/frontend-embed/src/components/EmSubNoteContent.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span> <EmA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA> - <EmMfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> + <EmMfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis" :isBlock="true"/> <EmA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</EmA> </div> <details v-if="note.files && note.files.length > 0"> diff --git a/packages/frontend-embed/src/pages/note.vue b/packages/frontend-embed/src/pages/note.vue index e879430286..4e0ea9c145 100644 --- a/packages/frontend-embed/src/pages/note.vue +++ b/packages/frontend-embed/src/pages/note.vue @@ -17,7 +17,7 @@ import EmNoteDetailed from '@/components/EmNoteDetailed.vue'; import XNotFound from '@/pages/not-found.vue'; import { DI } from '@/di.js'; import { misskeyApi } from '@/misskey-api.js'; -import { assertServerContext } from '@/server-context'; +import { assertServerContext } from '@/server-context.js'; const props = defineProps<{ noteId: string; diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss index b67f929933..ba3238cd4c 100644 --- a/packages/frontend-embed/src/style.scss +++ b/packages/frontend-embed/src/style.scss @@ -63,7 +63,7 @@ html, body { scroll-behavior: smooth; } -#misskey_app { +#sharkey_app { height: 100%; } @@ -310,6 +310,8 @@ rt { // MFM ----------------------------- +bdi.block { display: block } + ._mfm_blur_ { filter: blur(6px); transition: filter 0.3s; @@ -451,3 +453,25 @@ rt { 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); } 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); } } + +@keyframes mfm-fade { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@media (prefers-reduced-motion) { + @keyframes mfm-spin { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-spinX { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-spinY { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-jump { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-bounce { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-twitch { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-shake { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-rubberBand { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-rainbow { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-fade { 0% { transform: none; filter: none; opacity: 1 } } +} diff --git a/packages/frontend-embed/tsconfig.json b/packages/frontend-embed/tsconfig.json index 63e637c844..e0ee08188d 100644 --- a/packages/frontend-embed/tsconfig.json +++ b/packages/frontend-embed/tsconfig.json @@ -22,6 +22,7 @@ "isolatedModules": true, "useDefineForClassFields": true, "verbatimModuleSyntax": true, + "skipLibCheck": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"], @@ -50,6 +51,7 @@ "./@types/**/*.ts" ], "exclude": [ + "node_modules", ".storybook/**/*" ] } diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts index 3d628c800e..1cd47b2754 100644 --- a/packages/frontend-embed/vite.config.ts +++ b/packages/frontend-embed/vite.config.ts @@ -1,16 +1,13 @@ import path from 'path'; import pluginVue from '@vitejs/plugin-vue'; import { type UserConfig, defineConfig } from 'vite'; -import * as yaml from 'js-yaml'; -import { promises as fsp } from 'fs'; +import { localesVersion } from '../../locales/version.js'; import locales from '../../locales/index.js'; import meta from '../../package.json'; import packageInfo from './package.json' with { type: 'json' }; import pluginJson5 from './vite.json5.js'; - -const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null; -const host = url ? (new URL(url)).hostname : undefined; +import { pluginReplaceIcons } from '../frontend/vite.replaceIcons.js'; const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; @@ -67,7 +64,6 @@ export function getConfig(): UserConfig { base: '/embed_vite/', server: { - host, port: 5174, hmr: { // バックエンド経由での起動時、Viteは5174経由でアセットを参照していると思い込んでいるが実際は3000から配信される @@ -80,6 +76,7 @@ export function getConfig(): UserConfig { plugins: [ pluginVue(), pluginJson5(), + ...pluginReplaceIcons(), ], resolve: { @@ -96,11 +93,8 @@ export function getConfig(): UserConfig { modules: { generateScopedName(name, filename, _css): string { const id = (path.relative(__dirname, filename.split('?')[0]) + '-' + name).replace(/[\\\/\.\?&=]/g, '-').replace(/(src-|vue-)/g, ''); - if (process.env.NODE_ENV === 'production') { - return 'x' + toBase62(hash(id)).substring(0, 4); - } else { - return id; - } + const shortId = id.replace(/^(components(-global)?|widgets|ui(-_common_)?)-/, ''); + return shortId + '-' + toBase62(hash(id)).substring(0, 4); }, }, preprocessorOptions: { @@ -113,6 +107,7 @@ export function getConfig(): UserConfig { define: { _VERSION_: JSON.stringify(meta.version), _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), + _LANGS_VERSION_: JSON.stringify(localesVersion), _ENV_: JSON.stringify(process.env.NODE_ENV), _DEV_: process.env.NODE_ENV !== 'production', _PERF_PREFIX_: JSON.stringify('Misskey:'), |