feat(frontend): ノート・ユーザータイムライン埋め込み (#13929)

* fix

* navhookをbootに移動

* サーバーサイドのbootも分けるように

* 埋め込みページかどうかの判定は最初の一回だけに

* tooltipは出せるように

* fix design

* 埋め込み独自のtooltipを削除

* ロジックの分岐が多かったMkNoteDetailedを分離

* fix indent

* プレビュー用iframeにフォーカスが当たるのを修正

* popupの制御を出す側で行うように

* パラメータが逆になっていたのを修正

* Update MkEmbedCodeGenDialog.vue

* fix

* eliminate misskey-js lint warns

* fix

* add appropriate attributes to embed html

* enhance: サーバーサイドのembed系をさらに分離

* enhance: embed routerを分離(route定義をboot時に変更できるようにする改修を含む)

* type

* lint

* fix indent

* server-side styleを完全に分離

* Revert "refactor: 画面サイズのしきい値をconstにまとめる"

This reverts commit 05ca36f400.

* fix

* revert all changes in base.pug

* embedドメインをまとめた

* embedドメインをまとめた

* prevent calling contextmenu in embed page by stopping at the caller

* fix import

* fix import

* improve directory structure

* fix import

* register timeline ui as a container

* wa-

* rename

* wa-

* Update EmMediaList.vue

* Update EmMediaList.vue

* Update EmMediaList.vue

* Update EmMediaImage.vue

* Update EmNote.vue

* revert mkmedialist changes

* 戻し漏れ

* wip

* tweak embed media ui

* revert original media components

* Update boot.embed.js

* rename

* wip

* Update MkNote.vue

* wip

* Update MkSubNoteContent.vue

* Update EmNote.vue

* Update packages/frontend/src/router/definition.ts

* Revert "Update packages/frontend/src/router/definition.ts"

This reverts commit 937ae44521.

* refactor EmMediaImage

* fix import

* remove unused imports

* Update router.ts

* wip

* Update boot.ts

* wip

* wip

* wip

* wip

* Update EmNote.vue

* Update EmNote.vue

* Create EmA.vue

* Create EmAvatar.vue

* Update EmAvatar.vue

* wip

* wip

* wip

* Create EmImgWithBlurhash.vue

* Update EmImgWithBlurhash.vue

* Create EmPagination.vue

* wip

* Update boot.ts

* wip

* wip

* wi@p

* wip

* wip

* wiop

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update boot.ts

* wip

* Update MkMisskeyFlavoredMarkdown.ts

* wip

* wip

* wip

* wip

* wip

* Update post-message.ts

* wip

* Update EmNoteDetailed.vue

* Update EmNoteDetailed.vue

* Create instance.ts

* Update EmNoteDetailed.vue

* wip

* Update EmNoteDetailed.vue

* wip

* wip

* wip

* Update pnpm-lock.yaml

* wip

* wip

* wp

* wip

* Update ClientServerService.ts

* wip

* Update boot.ts

* Update vite.config.local-dev.ts

* Update vite.config.ts

* Create index.html

* wa-

* wip

* Update boot.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Create EmLink.vue

* Create EmMention.vue

* Update EmMfm.ts

* wip

* wip

* wip

* wip

* Update vite.config.ts

* Update boot.ts

* Update EmA.vue

* うぃp

* wip

* wip

* Create EmError.vue

* wip

* Update MkEmbedCodeGenDialog.vue

* Update EmNote.vue

* wip

* wip

* Update user-timeline.vue

* Update check-spdx-license-id.yml

* wip

* wip

* style(frontend-shared): lint fixes on build.js

* fix(frontend-shared): include `*.{js,json}` files in js-built

* wip

* use alias

* refactor

* refactor

* Update scroll.ts

* refactor

* refactor

* refactor

* wip

* wip

* wip

* wip

* Update roles.vue

* Update branding.vue

* wip

* wip

* wip

* Update page.vue

* wip

* fix import

* add missing css variables

* 絵文字をtwemojiに変更

クライアントデフォルトにあわせるため

* force empoll readonly

* fix compiler error

* fix broken imports

* tweak button style

* run api extractor

* fix storybook theme preloads

* fix storybook instance imports

* Update preview.ts

* Update preview.ts

* Update preview.ts

* Revert "Update preview.ts"

This reverts commit 12bab1c6fb.

* Revert "Update preview.ts"

This reverts commit 5c0ce01dbd.

* Revert "Update preview.ts"

This reverts commit f4863524d7.

* Revert "fix storybook instance imports"

This reverts commit ed8eabb246.

* Revert "wip"

This reverts commit d3c1926519.

* Revert "Update page.vue"

This reverts commit 27c7900b0c.

* Revert "Update branding.vue"

This reverts commit c08ccb65ba.

* Revert "Update roles.vue"

This reverts commit 1488b67066.

* Revert "wip"

This reverts commit aab1c76981.

* refactor: use common media proxy

* fix imports

* fix

* fix: MediaProxyの初期化を保証する(storybook対策?)

* enhance(frontend-embed): improve embedParams provide

* fix(backend): MK_DEV_PREFER=backendのときにembed viteが読み込めないのを修正

* fix

* embed-pageを共通化

* fix import

* fix import

* fix import

* const.jsを共通化

(たぶんrevertしすぎた)

* fix type error

* fix duplicated import

* fix lint

* fix

* コメントとして残す

* sharedとembedをlint対象にする

* lint

* attempt to fix eslint (frontend-shared)

* lint fixes

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
This commit is contained in:
かっこかり
2024-09-09 20:57:36 +09:00
committed by GitHub
parent 0d0cd738f8
commit 2cbe1d1210
236 changed files with 9470 additions and 454 deletions

View File

@@ -30,7 +30,7 @@ const keys = [
'd-u0',
]
await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => {
await Promise.all(keys.map((key) => readFile(new URL(`../../frontend-shared/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => {
writeFile(
new URL('./themes.ts', import.meta.url),
`export default ${JSON.stringify(

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare module '@/themes/*.json5' {
declare module '@@/themes/*.json5' {
import { Theme } from '@/scripts/theme.js';
const theme: Theme;

View File

@@ -55,6 +55,7 @@
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"frontend-shared": "workspace:*",
"photoswipe": "5.4.4",
"punycode": "2.3.1",
"rollup": "4.19.1",

View File

@@ -22,7 +22,8 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
import { deckStore } from '@/ui/deck/deck-store.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { setupRouter } from '@/router/definition.js';
import { setupRouter } from '@/router/main.js';
import { createMainRouter } from '@/router/definition.js';
export async function common(createVue: () => App<Element>) {
console.info(`Misskey v${version}`);
@@ -239,7 +240,7 @@ export async function common(createVue: () => App<Element>) {
const app = createVue();
setupRouter(app);
setupRouter(app, createMainRouter);
if (_DEV_) {
app.config.performance = true;

View File

@@ -22,6 +22,7 @@ import { deckStore } from '@/ui/deck/deck-store.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mainRouter } from '@/router/main.js';
import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp(
@@ -62,6 +63,18 @@ export async function mainBoot() {
}
});
stream.on('emojiAdded', emojiData => {
addCustomEmoji(emojiData.emoji);
});
stream.on('emojiUpdated', emojiData => {
updateCustomEmojis(emojiData.emojis);
});
stream.on('emojiDeleted', emojiData => {
removeCustomEmojis(emojiData.emojis);
});
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
import('@/plugin.js').then(async ({ install }) => {
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740

View File

@@ -46,17 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts">
import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import sanitizeHtml from 'sanitize-html';
import { emojilist, getEmojiName } from '@@/js/emojilist.js';
import contains from '@/scripts/contains.js';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
import { acct } from '@/filters/user.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js';
import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js';
const lib = emojilist.filter(x => x.category !== 'flags');

View File

@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, onMounted, onUnmounted, ref } from 'vue';
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import * as game from '@/scripts/clicker-game.js';
import number from '@/filters/number.js';
import { claimAchievement } from '@/scripts/achievements.js';

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.codeBlockRoot">
<button :class="$style.codeBlockCopyButton" class="_button" @click="copy">
<button v-if="copyButton" :class="$style.codeBlockCopyButton" class="_button" @click="copy">
<i class="ti ti-copy"></i>
</button>
<Suspense>
@@ -32,12 +32,17 @@ import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{
const props = withDefaults(defineProps<{
code: string;
forceShow?: boolean;
copyButton?: boolean;
lang?: string;
}>();
}>(), {
copyButton: true,
forceShow: false,
});
const show = ref(!defaultStore.state.dataSaver.code);
const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code);
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));

View File

@@ -0,0 +1,412 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialogEl"
:width="1000"
:height="600"
:scroll="false"
:withOkButton="false"
@close="cancel()"
@closed="$emit('closed')"
>
<template #header>{{ i18n.ts._embedCodeGen.title }}</template>
<div :class="$style.embedCodeGenRoot">
<Transition
mode="out-in"
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
:leaveToClass="$style.transition_x_leaveTo"
>
<div v-if="phase === 'input'" key="input" :class="$style.embedCodeGenInputRoot">
<div
:class="$style.embedCodeGenPreviewRoot"
>
<MkLoading v-if="iframeLoading" :class="$style.embedCodeGenPreviewSpinner"/>
<div :class="$style.embedCodeGenPreviewWrapper">
<div class="_acrylic" :class="$style.embedCodeGenPreviewTitle">{{ i18n.ts.preview }}</div>
<div ref="resizerRootEl" :class="$style.embedCodeGenPreviewResizerRoot" inert>
<div
:class="$style.embedCodeGenPreviewResizer"
:style="{ transform: iframeStyle }"
>
<iframe
ref="iframeEl"
:src="embedPreviewUrl"
:class="$style.embedCodeGenPreviewIframe"
:style="{ height: `${iframeHeight}px` }"
@load="iframeOnLoad"
></iframe>
</div>
</div>
</div>
</div>
<div :class="$style.embedCodeGenSettings" class="_gaps">
<MkInput v-if="isEmbedWithScrollbar" v-model="maxHeight" type="number" :min="0">
<template #label>{{ i18n.ts._embedCodeGen.maxHeight }}</template>
<template #suffix>px</template>
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
</MkInput>
<MkSelect v-model="colorMode">
<template #label>{{ i18n.ts.theme }}</template>
<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
<option value="light">{{ i18n.ts.light }}</option>
<option value="dark">{{ i18n.ts.dark }}</option>
</MkSelect>
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
<MkSwitch v-model="border">{{ i18n.ts._embedCodeGen.border }}</MkSwitch>
<MkInfo v-if="isEmbedWithScrollbar && (!maxHeight || maxHeight <= 0)" warn>{{ i18n.ts._embedCodeGen.maxHeightWarn }}</MkInfo>
<MkInfo v-if="typeof maxHeight === 'number' && (maxHeight <= 0 || maxHeight > 700)">{{ i18n.ts._embedCodeGen.previewIsNotActual }}</MkInfo>
<div class="_buttons">
<MkButton :disabled="iframeLoading" @click="applyToPreview">{{ i18n.ts._embedCodeGen.applyToPreview }}</MkButton>
<MkButton :disabled="iframeLoading" primary @click="generate">{{ i18n.ts._embedCodeGen.generateCode }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
<div v-else-if="phase === 'result'" key="result" :class="$style.embedCodeGenResultRoot">
<div :class="$style.embedCodeGenResultWrapper" class="_gaps">
<div class="_gaps_s">
<div :class="$style.embedCodeGenResultHeadingIcon"><i class="ti ti-check"></i></div>
<div :class="$style.embedCodeGenResultHeading">{{ i18n.ts._embedCodeGen.codeGenerated }}</div>
<div :class="$style.embedCodeGenResultDescription">{{ i18n.ts._embedCodeGen.codeGeneratedDescription }}</div>
</div>
<div class="_gaps_s">
<MkCode :code="result" lang="html" :forceShow="true" :copyButton="false" :class="$style.embedCodeGenResultCode"/>
<MkButton :class="$style.embedCodeGenResultButtons" rounded primary @click="doCopy"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
</div>
<MkButton :class="$style.embedCodeGenResultButtons" rounded transparent @click="close">{{ i18n.ts.close }}</MkButton>
</div>
</div>
</Transition>
</div>
</MkModalWindow>
</template>
<script setup lang="ts">
import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue';
import type { EmbeddableEntity, EmbedParams } from '@@/js/embed-page.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkButton from '@/components/MkButton.vue';
import MkCode from '@/components/MkCode.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { url } from '@/config.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js';
import { embedRouteWithScrollbar } from '@@/js/embed-page.js';
const emit = defineEmits<{
(ev: 'ok'): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
const props = defineProps<{
entity: EmbeddableEntity;
id: string;
params?: EmbedParams;
}>();
//#region Modalの制御
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
function cancel() {
emit('cancel');
dialogEl.value?.close();
}
function close() {
dialogEl.value?.close();
}
const phase = ref<'input' | 'result'>('input');
//#endregion
//#region 埋め込みURL生成・カスタマイズ
// 本URL生成用params
const paramsForUrl = computed<EmbedParams>(() => ({
header: header.value,
maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
rounded: rounded.value,
border: border.value,
}));
// プレビュー用params手動で更新を掛けるのでref
const paramsForPreview = ref<EmbedParams>(props.params ?? {});
const embedPreviewUrl = computed(() => {
const paramClass = new URLSearchParams(normalizeEmbedParams(paramsForPreview.value));
if (paramClass.has('maxHeight')) {
const maxHeight = parseInt(paramClass.get('maxHeight')!);
paramClass.set('maxHeight', maxHeight === 0 ? '500' : Math.min(maxHeight, 700).toString()); // プレビューであまりにも縮小されると見づらいため、700pxまでに制限
}
return `${url}/embed/${props.entity}/${props.id}${paramClass.toString() ? '?' + paramClass.toString() : ''}`;
});
const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity));
const header = ref(props.params?.header ?? true);
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500);
const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
const rounded = ref(props.params?.rounded ?? true);
const border = ref(props.params?.border ?? true);
function applyToPreview() {
const currentPreviewUrl = embedPreviewUrl.value;
paramsForPreview.value = {
header: header.value,
maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
rounded: rounded.value,
border: border.value,
};
nextTick(() => {
if (currentPreviewUrl === embedPreviewUrl.value) {
// URLが変わらなくてもリロード
iframeEl.value?.contentWindow?.location.reload();
}
});
}
const result = ref('');
function generate() {
result.value = getEmbedCode(`/embed/${props.entity}/${props.id}`, paramsForUrl.value);
phase.value = 'result';
}
function doCopy() {
copyToClipboard(result.value);
os.success();
}
//#endregion
//#region プレビューのリサイズ
const resizerRootEl = shallowRef<HTMLDivElement>();
const iframeLoading = ref(true);
const iframeEl = shallowRef<HTMLIFrameElement>();
const iframeHeight = ref(0);
const iframeScale = ref(1);
const iframeStyle = computed(() => {
return `translate(-50%, -50%) scale(${iframeScale.value})`;
});
const resizeObserver = new ResizeObserver(() => {
calcScale();
});
function iframeOnLoad() {
iframeEl.value?.contentWindow?.addEventListener('beforeunload', () => {
iframeLoading.value = true;
nextTick(() => {
iframeHeight.value = 0;
iframeScale.value = 1;
});
});
}
function windowEventHandler(event: MessageEvent) {
if (event.source !== iframeEl.value?.contentWindow) {
return;
}
if (event.data.type === 'misskey:embed:ready') {
iframeEl.value!.contentWindow?.postMessage({
type: 'misskey:embedParent:registerIframeId',
payload: {
iframeId: 'embedCodeGen', // 同じタイミングで複数のembed iframeがある際の区別用なのでここではなんでもいい
},
});
}
if (event.data.type === 'misskey:embed:changeHeight') {
iframeHeight.value = event.data.payload.height;
nextTick(() => {
calcScale();
iframeLoading.value = false; // 初回の高さ変更まで待つ
});
}
}
function calcScale() {
if (!resizerRootEl.value) return;
const previewWidth = resizerRootEl.value.clientWidth - 40; // 左右の余白 20pxずつ
const previewHeight = resizerRootEl.value.clientHeight - 40; // 上下の余白 20pxずつ
const iframeWidth = 500;
const scale = Math.min(previewWidth / iframeWidth, previewHeight / iframeHeight.value, 1); // 拡大はしないので1を上限に
iframeScale.value = scale;
}
onMounted(() => {
window.addEventListener('message', windowEventHandler);
if (!resizerRootEl.value) return;
resizeObserver.observe(resizerRootEl.value);
});
function reset() {
window.removeEventListener('message', windowEventHandler);
resizeObserver.disconnect();
// プレビューのリセット
iframeHeight.value = 0;
iframeScale.value = 1;
iframeLoading.value = true;
result.value = '';
phase.value = 'input';
}
onDeactivated(() => {
reset();
});
onUnmounted(() => {
reset();
});
//#endregion
</script>
<style module>
.transition_x_enterActive,
.transition_x_leaveActive {
transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
}
.transition_x_enterFrom {
opacity: 0;
transform: translateX(50px);
}
.transition_x_leaveTo {
opacity: 0;
transform: translateX(-50px);
}
.embedCodeGenRoot {
container-type: inline-size;
height: 100%;
}
.embedCodeGenInputRoot {
height: 100%;
display: grid;
grid-template-columns: 1fr 400px;
}
.embedCodeGenPreviewRoot {
position: relative;
background-color: var(--bg);
cursor: not-allowed;
}
.embedCodeGenPreviewWrapper {
display: flex;
flex-direction: column;
height: 100%;
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
.embedCodeGenPreviewTitle {
position: absolute;
z-index: 100;
top: 8px;
left: 8px;
padding: 6px 10px;
border-radius: 6px;
font-size: 85%;
}
.embedCodeGenPreviewSpinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
.embedCodeGenPreviewResizerRoot {
position: relative;
flex: 1 0;
}
.embedCodeGenPreviewResizer {
position: absolute;
top: 50%;
left: 50%;
}
.embedCodeGenPreviewIframe {
display: block;
border: none;
width: 500px;
color-scheme: light dark;
}
.embedCodeGenSettings {
padding: 24px;
overflow-y: scroll;
}
.embedCodeGenResultRoot {
box-sizing: border-box;
padding: 24px;
height: 100%;
max-width: 700px;
margin: 0 auto;
display: flex;
align-items: center;
}
.embedCodeGenResultHeading {
text-align: center;
font-size: 1.2em;
}
.embedCodeGenResultHeadingIcon {
margin: 0 auto;
background-color: var(--accentedBg);
color: var(--accent);
text-align: center;
height: 64px;
width: 64px;
font-size: 24px;
line-height: 64px;
border-radius: 50%;
}
.embedCodeGenResultDescription {
text-align: center;
white-space: pre-wrap;
}
.embedCodeGenResultWrapper,
.embedCodeGenResultCode {
width: 100%;
}
.embedCodeGenResultButtons {
margin: 0 auto;
}
@container (max-width: 800px) {
.embedCodeGenInputRoot {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
}
</style>

View File

@@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, Ref } from 'vue';
import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
import { CustomEmojiFolderTree, getEmojiName } from '@@/js/emojilist.js';
import { i18n } from '@/i18n.js';
import { customEmojis } from '@/custom-emojis.js';
import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';

View File

@@ -117,7 +117,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import XSection from '@/components/MkEmojiPicker.section.vue';
import {
emojilist,
emojiCharByCategory,
@@ -126,7 +125,8 @@ import {
getEmojiName,
CustomEmojiFolderTree,
getUnicodeEmoji,
} from '@/scripts/emojilist.js';
} from '@@/js/emojilist.js';
import XSection from '@/components/MkEmojiPicker.section.vue';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import * as os from '@/os.js';
import { isTouchUsing } from '@/scripts/touch.js';

View File

@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import DrawBlurhash from '@/workers/draw-blurhash?worker';
import TestWebGL2 from '@/workers/test-webgl2?worker';
import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js';
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
// テスト環境で Web Worker インスタンスは作成できない

View File

@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import { i18n } from '@/i18n.js';
import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';

View File

@@ -37,7 +37,7 @@ import XBanner from '@/components/MkMediaBanner.vue';
import XImage from '@/components/MkMediaImage.vue';
import XVideo from '@/components/MkMediaVideo.vue';
import * as os from '@/os.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js';
import { defaultStore } from '@/store.js';
import { focusParent } from '@/scripts/focus.js';

View File

@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { watch, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import tinycolor from 'tinycolor2';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
const props = defineProps<{
src: number[];

View File

@@ -627,7 +627,7 @@ function emitUpdReaction(emoji: string, delta: number) {
// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
//content-visibility: auto;
//contain-intrinsic-size: 0 128px;
//contain-intrinsic-size: 0 128px;
&:focus-visible {
outline: none;

View File

@@ -35,7 +35,7 @@ import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { notificationTypes } from '@/const.js';
import { notificationTypes } from '@@/js/const.js';
import { i18n } from '@/i18n.js';
type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>

View File

@@ -31,7 +31,7 @@ import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkNote from '@/components/MkNote.vue';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { notificationTypes } from '@/const.js';
import { notificationTypes } from '@@/js/const.js';
import { infoImageUrl } from '@/instance.js';
import { defaultStore } from '@/store.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';

View File

@@ -40,7 +40,7 @@ import { i18n } from '@/i18n.js';
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { openingWindowsCount } from '@/os.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { getScrollContainer } from '@/scripts/scroll.js';
import { getScrollContainer } from '@@/js/scroll.js';
import { useRouterFactory } from '@/router/supplier.js';
import { mainRouter } from '@/router/main.js';

View File

@@ -45,10 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts">
import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js';
import { useDocumentVisibility } from '@/scripts/use-document-visibility.js';
import { defaultStore } from '@/store.js';
import { MisskeyEntity } from '@/types/date-separated-list.js';
import { i18n } from '@/i18n.js';

View File

@@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { sum } from '@/scripts/array.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { host } from '@/config.js';
import { useInterval } from '@/scripts/use-interval.js';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { useInterval } from '@@/js/use-interval.js';
const props = defineProps<{
noteId: string;
@@ -83,10 +83,10 @@ if (props.poll.expiresAt) {
}
const vote = async (id) => {
pleaseLogin(undefined, pleaseLoginContext.value);
if (props.readOnly || closed.value || isVoted.value) return;
pleaseLogin(undefined, pleaseLoginContext.value);
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.voteConfirm({ choice: props.poll.choices[id].text }),
@@ -145,7 +145,7 @@ const vote = async (id) => {
.done {
.choice {
cursor: default;
cursor: initial;
}
}
</style>

View File

@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@/scripts/scroll.js';
import { getScrollContainer } from '@@/js/scroll.js';
import { isHorizontalSwipeSwiping } from '@/scripts/touch.js';
const SCROLL_STOP = 10;

View File

@@ -23,9 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { } from 'vue';
import { getEmojiName } from '@@/js/emojilist.js';
import MkTooltip from './MkTooltip.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import { getEmojiName } from '@/scripts/emojilist.js';
defineProps<{
showing: boolean;

View File

@@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, inject, onMounted, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { getUnicodeEmoji } from '@@/js/emojilist.js';
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
import XDetails from '@/components/MkReactionsViewer.details.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
@@ -34,7 +35,6 @@ import { i18n } from '@/i18n.js';
import * as sound from '@/scripts/sound.js';
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
import { customEmojisMap } from '@/custom-emojis.js';
import { getUnicodeEmoji } from '@/scripts/emojilist.js';
const props = defineProps<{
reaction: string;

View File

@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import { i18n } from '@/i18n.js';
import { MenuItem } from '@/types/menu.js';

View File

@@ -66,6 +66,7 @@ import { defineAsyncComponent, ref } from 'vue';
import { toUnicode } from 'punycode/';
import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import { query, extractDomain } from '@@/js/url.js';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import MkButton from '@/components/MkButton.vue';
@@ -74,7 +75,6 @@ import MkInfo from '@/components/MkInfo.vue';
import { host as configHost } from '@/config.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { query, extractDomain } from '@/scripts/url.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';

View File

@@ -42,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
import MkA from './MkA.vue';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js';
import { acct, userPage } from '@/filters/user.js';
import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
import { defaultStore } from '@/store.js';

View File

@@ -10,9 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, inject } from 'vue';
import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@/scripts/emoji-base.js';
import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js';
import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@@/js/emoji-base.js';
import { defaultStore } from '@/store.js';
import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
import * as os from '@/os.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import * as sound from '@/scripts/sound.js';

View File

@@ -19,8 +19,13 @@ import MkSparkle from '@/components/MkSparkle.vue';
import MkA, { MkABehavior } from '@/components/global/MkA.vue';
import { host } from '@/config.js';
import { defaultStore } from '@/store.js';
import { nyaize as doNyaize } from '@/scripts/nyaize.js';
import { safeParseFloat } from '@/scripts/safe-parse.js';
function safeParseFloat(str: unknown): number | null {
if (typeof str !== 'string' || str === '') return null;
const num = parseFloat(str);
if (isNaN(num)) return null;
return num;
}
const QUOTE_STYLE = `
display: block;
@@ -86,7 +91,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
case 'text': {
let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (!disableNyaize && shouldNyaize) {
text = doNyaize(text);
text = Misskey.nyaize(text);
}
if (!props.plain) {
@@ -281,14 +286,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
const child = token.children[0];
let text = child.type === 'text' ? child.props.text : '';
if (!disableNyaize && shouldNyaize) {
text = doNyaize(text);
text = Misskey.nyaize(text);
}
return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]);
} else {
const rt = token.children.at(-1)!;
let text = rt.type === 'text' ? rt.props.text : '';
if (!disableNyaize && shouldNyaize) {
text = doNyaize(text);
text = Misskey.nyaize(text);
}
return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
}
@@ -400,7 +405,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
}
case 'emojiCode': {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (props.author?.host == null) {
return [h(MkCustomEmoji, {
key: Math.random(),

View File

@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue';
import tinycolor from 'tinycolor2';
import XTabs, { Tab } from './MkPageHeader.tabs.vue';
import { scrollToTop } from '@/scripts/scroll.js';
import { scrollToTop } from '@@/js/scroll.js';
import { globalEvents } from '@/events.js';
import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';

View File

@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue';
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const.js';
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js';
const rootEl = shallowRef<HTMLElement>();
const headerEl = shallowRef<HTMLElement>();

View File

@@ -30,10 +30,17 @@ import { toUnicode as decodePunycode } from 'punycode/';
import { url as local } from '@/config.js';
import * as os from '@/os.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { MkABehavior } from '@/components/global/MkA.vue';
function safeURIDecode(str: string): string {
try {
return decodeURIComponent(str);
} catch {
return str;
}
}
const props = withDefaults(defineProps<{
url: string;
rel?: string;

View File

@@ -1,137 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// ブラウザで直接表示することを許可するファイルの種類のリスト
// ここに含まれないものは application/octet-stream としてレスポンスされる
// SVGはXSSを生むので許可しない
export const FILE_TYPE_BROWSERSAFE = [
// Images
'image/png',
'image/gif',
'image/jpeg',
'image/webp',
'image/avif',
'image/apng',
'image/bmp',
'image/tiff',
'image/x-icon',
// OggS
'audio/opus',
'video/ogg',
'audio/ogg',
'application/ogg',
// ISO/IEC base media file format
'video/quicktime',
'video/mp4',
'audio/mp4',
'video/x-m4v',
'audio/x-m4a',
'video/3gpp',
'video/3gpp2',
'video/mpeg',
'audio/mpeg',
'video/webm',
'audio/webm',
'audio/aac',
// see https://github.com/misskey-dev/misskey/pull/10686
'audio/flac',
'audio/wav',
// backward compatibility
'audio/x-flac',
'audio/vnd.wave',
];
/*
https://github.com/sindresorhus/file-type/blob/main/supported.js
https://github.com/sindresorhus/file-type/blob/main/core.js
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
*/
export const notificationTypes = [
'note',
'follow',
'mention',
'reply',
'renote',
'quote',
'reaction',
'pollEnded',
'receiveFollowRequest',
'followRequestAccepted',
'roleAssigned',
'achievementEarned',
'app',
] as const;
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
export const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
'mentionLimit',
'canInvite',
'inviteLimit',
'inviteLimitCycle',
'inviteExpirationTime',
'canManageCustomEmojis',
'canManageAvatarDecorations',
'canSearchNotes',
'canUseTranslator',
'canHideAds',
'driveCapacityMb',
'alwaysMarkNsfw',
'canUpdateBioMedia',
'pinLimit',
'antennaLimit',
'wordMuteLimit',
'webhookLimit',
'clipLimit',
'noteEachClipsLimit',
'userListLimit',
'userEachUserListsLimit',
'rateLimitFactor',
'avatarDecorationLimit',
] as const;
// なんか動かない
//export const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP');
//export const CURRENT_STICKY_BOTTOM = Symbol('CURRENT_STICKY_BOTTOM');
export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM';
export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg';
export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg';
export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg';
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
tada: ['speed=', 'delay='],
jelly: ['speed=', 'delay='],
twitch: ['speed=', 'delay='],
shake: ['speed=', 'delay='],
spin: ['speed=', 'delay=', 'left', 'alternate', 'x', 'y'],
jump: ['speed=', 'delay='],
bounce: ['speed=', 'delay='],
flip: ['h', 'v'],
x2: [],
x3: [],
x4: [],
scale: ['x=', 'y='],
position: ['x=', 'y='],
fg: ['color='],
bg: ['color='],
border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'],
blur: [],
rainbow: ['speed=', 'delay='],
rotate: ['deg='],
ruby: [],
unixtime: [],
};

View File

@@ -6,7 +6,6 @@
import { shallowRef, computed, markRaw, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
import { get, set } from '@/scripts/idb-proxy.js';
const storageCache = await get('emojis');
@@ -29,23 +28,20 @@ watch(customEmojis, emojis => {
}
}, { immediate: true });
// TODO: ここら辺副作用なのでいい感じにする
const stream = useStream();
stream.on('emojiAdded', emojiData => {
customEmojis.value = [emojiData.emoji, ...customEmojis.value];
export function addCustomEmoji(emoji: Misskey.entities.EmojiSimple) {
customEmojis.value = [emoji, ...customEmojis.value];
set('emojis', customEmojis.value);
});
}
stream.on('emojiUpdated', emojiData => {
customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.EmojiSimple ?? item);
export function updateCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) {
customEmojis.value = customEmojis.value.map(item => emojis.find(search => search.name === item.name) ?? item);
set('emojis', customEmojis.value);
});
}
stream.on('emojiDeleted', emojiData => {
customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name));
export function removeCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) {
customEmojis.value = customEmojis.value.filter(item => !emojis.some(search => search.name === item.name));
set('emojis', customEmojis.value);
});
}
export async function fetchCustomEmojis(force = false) {
const now = Date.now();

View File

@@ -4,7 +4,7 @@
*/
import { Directive } from 'vue';
import { getScrollContainer, getScrollPosition } from '@/scripts/scroll.js';
import { getScrollContainer, getScrollPosition } from '@@/js/scroll.js';
export default {
mounted(src, binding, vn) {

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,11 @@
*/
import { markRaw } from 'vue';
import { I18n } from '@@/js/i18n.js';
import type { Locale } from '../../../locales/index.js';
import { locale } from '@/config.js';
import { I18n } from '@/scripts/i18n.js';
export const i18n = markRaw(new I18n<Locale>(locale));
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
export function updateI18n(newLocale: Locale) {
i18n.locale = newLocale;

View File

@@ -7,7 +7,7 @@ import { computed, reactive } from 'vue';
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { miLocalStorage } from '@/local-storage.js';
import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const.js';
import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@@/js/const.js';
// TODO: 他のタブと永続化されたstateを同期

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
type Keys =
export type Keys =
'v' |
'lastVersion' |
'instance' |
@@ -38,12 +38,22 @@ type Keys =
`aiscript:${string}` |
'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~);
`channelLastReadedAt:${string}`
`channelLastReadedAt:${string}` |
`idbfallback::${string}`
// セッション毎に廃棄されるLocalStorage代替セーフモードなどで使用できそう
//const safeSessionStorage = new Map<Keys, string>();
export const miLocalStorage = {
getItem: (key: Keys): string | null => window.localStorage.getItem(key),
setItem: (key: Keys, value: string): void => window.localStorage.setItem(key, value),
removeItem: (key: Keys): void => window.localStorage.removeItem(key),
getItem: (key: Keys): string | null => {
return window.localStorage.getItem(key);
},
setItem: (key: Keys, value: string): void => {
window.localStorage.setItem(key, value);
},
removeItem: (key: Keys): void => {
window.localStorage.removeItem(key);
},
getItemAsJson: (key: Keys): any | undefined => {
const item = miLocalStorage.getItem(key);
if (item === null) {
@@ -51,5 +61,7 @@ export const miLocalStorage = {
}
return JSON.parse(item);
},
setItemAsJson: (key: Keys, value: any): void => window.localStorage.setItem(key, JSON.stringify(value)),
setItemAsJson: (key: Keys, value: any): void => {
miLocalStorage.setItem(key, JSON.stringify(value));
},
};

View File

@@ -7,7 +7,14 @@
import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
import { EventEmitter } from 'eventemitter3';
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
function safeURIDecode(str: string): string {
try {
return decodeURIComponent(str);
} catch {
return str;
}
}
interface RouteDefBase {
path: string;

View File

@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, onMounted, onUnmounted, ref, shallowRef, watch, nextTick } from 'vue';
import tinycolor from 'tinycolor2';
import { popupMenu } from '@/os.js';
import { scrollToTop } from '@/scripts/scroll.js';
import { scrollToTop } from '@@/js/scroll.js';
import MkButton from '@/components/MkButton.vue';
import { globalEvents } from '@/events.js';
import { injectReactiveMetadata } from '@/scripts/page-metadata.js';

View File

@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import * as Misskey from 'misskey-js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
import { defaultStore } from '@/store.js';

View File

@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import * as Misskey from 'misskey-js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { defaultStore } from '@/store.js';

View File

@@ -608,7 +608,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import FormSlot from '@/components/form/slot.vue';
import { i18n } from '@/i18n.js';
import { ROLE_POLICIES } from '@/const.js';
import { ROLE_POLICIES } from '@@/js/const.js';
import { instance } from '@/instance.js';
import { deepClone } from '@/scripts/clone.js';

View File

@@ -253,7 +253,7 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { instance, fetchInstance } from '@/instance.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { ROLE_POLICIES } from '@/const.js';
import { ROLE_POLICIES } from '@@/js/const.js';
import { useRouter } from '@/router/supplier.js';
const router = useRouter();

View File

@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, watch, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkTimeline from '@/components/MkTimeline.vue';
import { scroll } from '@/scripts/scroll.js';
import { scroll } from '@@/js/scroll.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';

View File

@@ -44,6 +44,7 @@ import MkButton from '@/components/MkButton.vue';
import { clipsCache } from '@/cache.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { genEmbedCode } from '@/scripts/get-embed-code.js';
const props = defineProps<{
clipId: string,
@@ -127,21 +128,33 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
clipsCache.delete();
},
}, ...(clip.value.isPublic ? [{
icon: 'ti ti-link',
text: i18n.ts.copyUrl,
handler: async (): Promise<void> => {
copyToClipboard(`${url}/clips/${clip.value.id}`);
os.success();
},
}] : []), ...(clip.value.isPublic && isSupportShare() ? [{
icon: 'ti ti-share',
text: i18n.ts.share,
handler: async (): Promise<void> => {
navigator.share({
title: clip.value.name,
text: clip.value.description,
url: `${url}/clips/${clip.value.id}`,
});
handler: (ev: MouseEvent): void => {
os.popupMenu([{
icon: 'ti ti-link',
text: i18n.ts.copyUrl,
action: () => {
copyToClipboard(`${url}/clips/${clip.value!.id}`);
os.success();
},
}, {
icon: 'ti ti-code',
text: i18n.ts.genEmbedCode,
action: () => {
genEmbedCode('clips', clip.value!.id);
},
}, ...(isSupportShare() ? [{
icon: 'ti ti-share',
text: i18n.ts.share,
action: async () => {
navigator.share({
title: clip.value!.name,
text: clip.value!.description ?? '',
url: `${url}/clips/${clip.value!.id}`,
});
},
}] : [])], ev.currentTarget ?? ev.target);
},
}] : []), {
icon: 'ti ti-trash',

View File

@@ -99,12 +99,12 @@ const file = ref<Misskey.entities.DriveFile>();
const folderHierarchy = computed(() => {
if (!file.value) return [i18n.ts.drive];
const folderNames = [i18n.ts.drive];
function get(folder: Misskey.entities.DriveFolder) {
if (folder.parent) get(folder.parent);
folderNames.push(folder.name);
}
if (file.value.folder) get(file.value.folder);
return folderNames;
});

View File

@@ -205,7 +205,7 @@ import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import { apiUrl } from '@/config.js';
import { $i } from '@/account.js';
import * as sound from '@/scripts/sound.js';

View File

@@ -30,7 +30,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { notificationTypes } from '@/const.js';
import { notificationTypes } from '@@/js/const.js';
const tab = ref('all');
const includeTypes = ref<string[] | null>(null);

View File

@@ -149,7 +149,7 @@ import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { deepClone } from '@/scripts/clone.js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import { signinRequired } from '@/account.js';
import { url } from '@/config.js';
import { i18n } from '@/i18n.js';

View File

@@ -22,7 +22,7 @@ import { useRouter } from '@/router/supplier.js';
import * as os from '@/os.js';
import { url } from '@/config.js';
import { i18n } from '@/i18n.js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
const $i = signinRequired();

View File

@@ -117,7 +117,7 @@ import { $i } from '@/account.js';
import MkPagination from '@/components/MkPagination.vue';
import { useRouter } from '@/router/supplier.js';
import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
import { useInterval } from '@@/js/use-interval.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import * as sound from '@/scripts/sound.js';

View File

@@ -69,7 +69,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import { notificationTypes } from '@/const.js';
import { notificationTypes } from '@@/js/const.js';
const $i = signinRequired();

View File

@@ -28,6 +28,7 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import { genEmbedCode } from '@/scripts/get-embed-code.js';
const props = defineProps<{
tag: string;
@@ -51,7 +52,19 @@ async function post() {
notes.value?.pagingComponent?.reload();
}
const headerActions = computed(() => []);
const headerActions = computed(() => [{
icon: 'ti ti-dots',
label: i18n.ts.more,
handler: (ev: MouseEvent) => {
os.popupMenu([{
text: i18n.ts.genEmbedCode,
icon: 'ti ti-code',
action: () => {
genEmbedCode('tags', props.tag);
},
}], ev.currentTarget ?? ev.target);
}
}]);
const headerTabs = computed(() => []);

View File

@@ -79,6 +79,8 @@ import tinycolor from 'tinycolor2';
import { v4 as uuid } from 'uuid';
import JSON5 from 'json5';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import MkButton from '@/components/MkButton.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@@ -86,8 +88,6 @@ import MkFolder from '@/components/MkFolder.vue';
import { $i } from '@/account.js';
import { Theme, applyTheme } from '@/scripts/theme.js';
import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5';
import { host } from '@/config.js';
import * as os from '@/os.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js';

View File

@@ -40,7 +40,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { scroll } from '@/scripts/scroll.js';
import { scroll } from '@@/js/scroll.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
@@ -54,7 +54,7 @@ import { MenuItem } from '@/types/menu.js';
import { miLocalStorage } from '@/local-storage.js';
import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
import type { BasicTimelineType } from '@/timelines.js';
provide('shouldOmitHeaderTitle', true);
const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();

View File

@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, watch, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkTimeline from '@/components/MkTimeline.vue';
import { scroll } from '@/scripts/scroll.js';
import { scroll } from '@@/js/scroll.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';

View File

@@ -161,7 +161,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkOmit from '@/components/MkOmit.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkButton from '@/components/MkButton.vue';
import { getScrollPosition } from '@/scripts/scroll.js';
import { getScrollPosition } from '@@/js/scroll.js';
import { getUserMenu } from '@/scripts/get-user-menu.js';
import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js';

View File

@@ -24,7 +24,7 @@ import * as Misskey from 'misskey-js';
import { onUpdated, ref, shallowRef } from 'vue';
import XNote from '@/pages/welcome.timeline.note.vue';
import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { getScrollContainer } from '@/scripts/scroll.js';
import { getScrollContainer } from '@@/js/scroll.js';
const notes = ref<Misskey.entities.Note[]>([]);
const isScrolling = ref(false);

View File

@@ -3,15 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue';
import type { RouteDef } from '@/nirax.js';
import { IRouter, Router } from '@/nirax.js';
import { AsyncComponentLoader, defineAsyncComponent } from 'vue';
import type { IRouter, RouteDef } from '@/nirax.js';
import { Router } from '@/nirax.js';
import { $i, iAmModerator } from '@/account.js';
import MkLoading from '@/pages/_loading_.vue';
import MkError from '@/pages/_error_.vue';
import { setMainRouter } from '@/router/main.js';
const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
loader: loader,
loadingComponent: MkLoading,
errorComponent: MkError,
@@ -240,7 +239,7 @@ const routes: RouteDef[] = [{
origin: 'origin',
},
}, {
// Legacy Compatibility
// Legacy Compatibility
path: '/authorize-follow',
redirect: '/lookup',
loginRequired: true,
@@ -597,32 +596,6 @@ const routes: RouteDef[] = [{
component: page(() => import('@/pages/not-found.vue')),
}];
function createRouterImpl(path: string): IRouter {
export function createMainRouter(path: string): IRouter {
return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue')));
}
/**
* {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。
* また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能)
*/
export function setupRouter(app: App) {
app.provide('routerFactory', createRouterImpl);
const mainRouter = createRouterImpl(location.pathname + location.search + location.hash);
window.addEventListener('popstate', (event) => {
mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
});
mainRouter.addListener('push', ctx => {
window.history.pushState({ key: ctx.key }, '', ctx.path);
});
mainRouter.addListener('replace', ctx => {
window.history.replaceState({ key: ctx.key }, '', ctx.path);
});
mainRouter.init();
setMainRouter(mainRouter);
}

View File

@@ -3,10 +3,37 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ShallowRef } from 'vue';
import { EventEmitter } from 'eventemitter3';
import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
import type { App, ShallowRef } from 'vue';
/**
* {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。
* また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能)
*/
export function setupRouter(app: App, routerFactory: ((path: string) => IRouter)): void {
app.provide('routerFactory', routerFactory);
const mainRouter = routerFactory(location.pathname + location.search + location.hash);
window.addEventListener('popstate', (event) => {
mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
});
mainRouter.addListener('push', ctx => {
window.history.pushState({ key: ctx.key }, '', ctx.path);
});
mainRouter.addListener('replace', ctx => {
window.history.replaceState({ key: ctx.key }, '', ctx.path);
});
mainRouter.init();
setMainRouter(mainRouter);
}
function getMainRouter(): IRouter {
const router = mainRouterHolder;
if (!router) {

View File

@@ -4,13 +4,13 @@
*/
import { utils, values } from '@syuilo/aiscript';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { $i } from '@/account.js';
import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js';
import { url, lang } from '@/config.js';
import { nyaize } from '@/scripts/nyaize.js';
export function aiScriptReadline(q: string): Promise<string> {
return new Promise(ok => {
@@ -87,7 +87,7 @@ export function createAiScriptEnv(opts) {
}),
'Mk:nyaize': values.FN_NATIVE(([text]) => {
utils.assertString(text);
return values.STR(nyaize(text.value));
return values.STR(Misskey.nyaize(text.value));
}),
};
}

View File

@@ -4,7 +4,7 @@
*/
import * as Misskey from 'misskey-js';
import { UnicodeEmojiDef } from './emojilist.js';
import { UnicodeEmojiDef } from '@@/js/emojilist.js';
export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean {
if (typeof emoji === 'string') return true; // UnicodeEmojiDefにも無い絵文字であれば文字列で来る。Unicode絵文字であることには変わりないので常にリアクション可能とする;

View File

@@ -7,13 +7,13 @@ 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 lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import { unique } from './array.js';
import { deepClone } from './clone.js';
import { deepMerge } from './merge.js';
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: HighlighterCore | null = null;

View File

@@ -1,25 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
const twemojiSvgBase = '/twemoji';
const fluentEmojiPngBase = '/fluent-emoji';
export function char2twemojiFilePath(char: string): string {
let codes = Array.from(char, x => x.codePointAt(0)?.toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
codes = codes.filter(x => x && x.length);
const fileName = codes.join('-');
return `${twemojiSvgBase}/${fileName}.svg`;
}
export function char2fluentEmojiFilePath(char: string): string {
let codes = Array.from(char, x => x.codePointAt(0)?.toString(16));
// Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25
if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char);
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
codes = codes.filter(x => x && x.length);
const fileName = codes.map(x => x!.padStart(4, '0')).join('-');
return `${fluentEmojiPngBase}/${fileName}.png`;
}

View File

@@ -1,73 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const;
export type UnicodeEmojiDef = {
name: string;
char: string;
category: typeof unicodeEmojiCategories[number];
}
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
import _emojilist from '../emojilist.json';
export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({
name: x[1] as string,
char: x[0] as string,
category: unicodeEmojiCategories[x[2]],
}));
const unicodeEmojisMap = new Map<string, UnicodeEmojiDef>(
emojilist.map(x => [x.char, x]),
);
const _indexByChar = new Map<string, number>();
const _charGroupByCategory = new Map<string, string[]>();
for (let i = 0; i < emojilist.length; i++) {
const emo = emojilist[i];
_indexByChar.set(emo.char, i);
if (_charGroupByCategory.has(emo.category)) {
_charGroupByCategory.get(emo.category)?.push(emo.char);
} else {
_charGroupByCategory.set(emo.category, [emo.char]);
}
}
export const emojiCharByCategory = _charGroupByCategory;
export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string {
// Colorize it because emojilist.json assumes that
return unicodeEmojisMap.get(colorizeEmoji(char))
// カラースタイル絵文字がjsonに無い場合はテキストスタイル絵文字にフォールバックする
?? unicodeEmojisMap.get(char)
// それでも見つからない場合はそのまま返す絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する
?? char;
}
export function getEmojiName(char: string): string {
// Colorize it because emojilist.json assumes that
const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char);
if (idx === undefined) {
// 絵文字情報がjsonに無い場合は名前の取得が出来ないのでそのまま返すしか無い
return char;
} else {
return emojilist[idx].name;
}
}
/**
* テキストスタイル絵文字U+260Eなどの1文字で表現される絵文字をカラースタイル絵文字に変換しますVS16:U+FE0Fを付与
*/
export function colorizeEmoji(char: string) {
return char.length === 1 ? `${char}\uFE0F` : char;
}
export interface CustomEmojiFolderTree {
value: string;
category: string;
children: CustomEmojiFolderTree[];
}

View File

@@ -1,14 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function extractAvgColorFromBlurhash(hash: string) {
return typeof hash === 'string'
? '#' + [...hash.slice(2, 6)]
.map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x))
.reduce((a, c) => a * 83 + c, 0)
.toString(16)
.padStart(6, '0')
: undefined;
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@/scripts/scroll.js';
import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js';
import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement;

View File

@@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineAsyncComponent } from 'vue';
import { v4 as uuid } from 'uuid';
import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js';
import { url } from '@/config.js';
import * as os from '@/os.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js';
const MOBILE_THRESHOLD = 500;
/**
* パラメータを正規化する(埋め込みコード作成用)
* @param params パラメータ
* @returns 正規化されたパラメータ
*/
export function normalizeEmbedParams(params: EmbedParams): Record<string, string> {
// paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す
const normalizedParams: Record<string, string> = {};
for (const key in params) {
// デフォルトの値と同じならparamsに含めない
if (params[key] == null || params[key] === defaultEmbedParams[key]) {
continue;
}
switch (typeof params[key]) {
case 'number':
normalizedParams[key] = params[key].toString();
break;
case 'boolean':
normalizedParams[key] = params[key] ? 'true' : 'false';
break;
default:
normalizedParams[key] = params[key];
break;
}
}
return normalizedParams;
}
/**
* 埋め込みコードを生成iframe IDの発番もやる
*/
export function getEmbedCode(path: string, params?: EmbedParams): string {
const iframeId = 'v1_' + uuid(); // 将来embed.jsのバージョンが上がったとき用にv1_を付けておく
let paramString = '';
if (params) {
const searchParams = new URLSearchParams(normalizeEmbedParams(params));
paramString = searchParams.toString() === '' ? '' : '?' + searchParams.toString();
}
const iframeCode = [
`<iframe src="${url + path + paramString}" data-misskey-embed-id="${iframeId}" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" style="border: none; width: 100%; max-width: 500px; height: 300px; color-scheme: light dark;"></iframe>`,
`<script defer src="${url}/embed.js"></script>`,
];
return iframeCode.join('\n');
}
/**
* 埋め込みコードを生成してコピーする(カスタマイズ機能つき)
*
* カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください
*/
export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) {
const _params = { ...params };
if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) {
_params.maxHeight = 700;
}
// PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー
if (window.innerWidth < MOBILE_THRESHOLD) {
copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params));
os.success();
} else {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), {
entity,
id,
params: _params,
}, {
closed: () => dispose(),
});
}
}

View File

@@ -21,6 +21,7 @@ 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';
import { genEmbedCode } from '@/scripts/get-embed-code.js';
export async function getNoteClipMenu(props: {
note: Misskey.entities.Note;
@@ -156,6 +157,19 @@ export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string):
};
}
function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined {
if (note.url != null || note.uri != null) return undefined;
if (['specified', 'followers'].includes(note.visibility)) return undefined;
return {
icon: 'ti ti-code',
text,
action: (): void => {
genEmbedCode('notes', note.id);
},
};
}
export function getNoteMenu(props: {
note: Misskey.entities.Note;
translation: Ref<Misskey.entities.NotesTranslateResponse | null>;
@@ -310,7 +324,7 @@ export function getNoteMenu(props: {
action: () => {
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
},
} : undefined,
} : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode),
...(isSupportShare() ? [{
icon: 'ti ti-share',
text: i18n.ts.share,
@@ -443,14 +457,14 @@ export function getNoteMenu(props: {
icon: 'ti ti-copy',
text: i18n.ts.copyContent,
action: copyContent,
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
, (appearNote.url || appearNote.uri) ? {
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink),
(appearNote.url || appearNote.uri) ? {
icon: 'ti ti-external-link',
text: i18n.ts.showOnRemote,
action: () => {
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
},
} : undefined]
} : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)]
.filter(x => x !== undefined);
}

View File

@@ -17,6 +17,7 @@ import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-pe
import { IRouter } from '@/nirax.js';
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
import { mainRouter } from '@/router/main.js';
import { genEmbedCode } from '@/scripts/get-embed-code.js';
import { MenuItem } from '@/types/menu.js';
export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
@@ -179,7 +180,17 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
if (user.url == null) return;
window.open(user.url, '_blank', 'noopener');
},
}] : []), {
}] : [{
icon: 'ti ti-code',
text: i18n.ts.genEmbedCode,
type: 'parent' as const,
children: [{
text: i18n.ts.noteOfThisUser,
action: () => {
genEmbedCode('user-timeline', user.id);
},
}], // TODO: ユーザーカードの埋め込みなど
}]), {
icon: 'ti ti-share',
text: i18n.ts.copyProfileUrl,
action: () => {

View File

@@ -1,245 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ILocale, ParameterizedString } from '../../../../locales/index.js';
type FlattenKeys<T extends ILocale, TPrediction> = keyof {
[K in keyof T as T[K] extends ILocale
? FlattenKeys<T[K], TPrediction> extends infer C extends string
? `${K & string}.${C}`
: never
: T[K] extends TPrediction
? K
: never]: T[K];
};
type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString>> = TKey extends `${infer K}.${infer C}`
// @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。
? ParametersOf<T[K], C>
: TKey extends keyof T
? T[TKey] extends ParameterizedString<infer P>
? P
: never
: never;
type Tsx<T extends ILocale> = {
readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString<infer P>
? (arg: { readonly [_ in P]: string | number }) => string
// @ts-expect-error -- 証明省略
: Tsx<T[K]>;
};
export class I18n<T extends ILocale> {
private tsxCache?: Tsx<T>;
constructor(public locale: T) {
//#region BIND
this.t = this.t.bind(this);
//#endregion
}
public get ts(): T {
if (_DEV_) {
class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> {
get(target: TTarget, p: string | symbol): unknown {
const value = target[p as keyof TTarget];
if (typeof value === 'object') {
return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>());
}
if (typeof value === 'string') {
const parameters = Array.from(value.matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter);
if (parameters.length) {
console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`);
}
return value;
}
console.error(`Unexpected locale key: ${String(p)}`);
return p;
}
}
return new Proxy(this.locale, new Handler());
}
return this.locale;
}
public get tsx(): Tsx<T> {
if (_DEV_) {
if (this.tsxCache) {
return this.tsxCache;
}
class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> {
get(target: TTarget, p: string | symbol): unknown {
const value = target[p as keyof TTarget];
if (typeof value === 'object') {
return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>());
}
if (typeof value === 'string') {
const quasis: string[] = [];
const expressions: string[] = [];
let cursor = 0;
while (~cursor) {
const start = value.indexOf('{', cursor);
if (!~start) {
quasis.push(value.slice(cursor));
break;
}
quasis.push(value.slice(cursor, start));
const end = value.indexOf('}', start);
expressions.push(value.slice(start + 1, end));
cursor = end + 1;
}
if (!expressions.length) {
console.error(`Unexpected locale key: ${String(p)}`);
return () => value;
}
return (arg) => {
let str = quasis[0];
for (let i = 0; i < expressions.length; i++) {
if (!Object.hasOwn(arg, expressions[i])) {
console.error(`Missing locale parameters: ${expressions[i]} at ${String(p)}`);
}
str += arg[expressions[i]] + quasis[i + 1];
}
return str;
};
}
console.error(`Unexpected locale key: ${String(p)}`);
return p;
}
}
return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx<T>;
}
if (this.tsxCache) {
return this.tsxCache;
}
function build(target: ILocale): Tsx<T> {
const result = {} as Tsx<T>;
for (const k in target) {
if (!Object.hasOwn(target, k)) {
continue;
}
const value = target[k as keyof typeof target];
if (typeof value === 'object') {
result[k] = build(value as ILocale);
} else if (typeof value === 'string') {
const quasis: string[] = [];
const expressions: string[] = [];
let cursor = 0;
while (~cursor) {
const start = value.indexOf('{', cursor);
if (!~start) {
quasis.push(value.slice(cursor));
break;
}
quasis.push(value.slice(cursor, start));
const end = value.indexOf('}', start);
expressions.push(value.slice(start + 1, end));
cursor = end + 1;
}
if (!expressions.length) {
continue;
}
result[k] = (arg) => {
let str = quasis[0];
for (let i = 0; i < expressions.length; i++) {
str += arg[expressions[i]] + quasis[i + 1];
}
return str;
};
}
}
return result;
}
return this.tsxCache = build(this.locale);
}
/**
* @deprecated なるべくこのメソッド使うよりも ts 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも
*/
public t<TKey extends FlattenKeys<T, string>>(key: TKey): string;
/**
* @deprecated なるべくこのメソッド使うよりも tsx 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも
*/
public t<TKey extends FlattenKeys<T, ParameterizedString>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string;
public t(key: string, args?: { readonly [_: string]: string | number }) {
let str: string | ParameterizedString | ILocale = this.locale;
for (const k of key.split('.')) {
str = str[k];
if (_DEV_) {
if (typeof str === 'undefined') {
console.error(`Unexpected locale key: ${key}`);
return key;
}
}
}
if (args) {
if (_DEV_) {
const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter));
if (missing.length) {
console.error(`Missing locale parameters: ${missing.join(', ')} at ${key}`);
}
}
for (const [k, v] of Object.entries(args)) {
const search = `{${k}}`;
if (_DEV_) {
if (!(str as string).includes(search)) {
console.error(`Unexpected locale parameter: ${k} at ${key}`);
}
}
str = (str as string).replace(search, v.toString());
}
}
return str;
}
}

View File

@@ -10,10 +10,11 @@ import {
set as iset,
del as idel,
} from 'idb-keyval';
import { miLocalStorage } from '@/local-storage.js';
const fallbackName = (key: string) => `idbfallback::${key}`;
const PREFIX = 'idbfallback::';
let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true;
let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true;
// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。
// バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと
@@ -38,15 +39,15 @@ if (idbAvailable) {
export async function get(key: string) {
if (idbAvailable) return iget(key);
return JSON.parse(window.localStorage.getItem(fallbackName(key)));
return miLocalStorage.getItemAsJson(`${PREFIX}${key}`);
}
export async function set(key: string, val: any) {
if (idbAvailable) return iset(key, val);
return window.localStorage.setItem(fallbackName(key), JSON.stringify(val));
return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val);
}
export async function del(key: string) {
if (idbAvailable) return idel(key);
return window.localStorage.removeItem(fallbackName(key));
return miLocalStorage.removeItem(`${PREFIX}${key}`);
}

View File

@@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function isLink(el: HTMLElement) {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
return false;
}

View File

@@ -3,51 +3,32 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { query } from '@/scripts/url.js';
import { MediaProxy } from '@@/js/media-proxy.js';
import { url } from '@/config.js';
import { instance } from '@/instance.js';
export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
const localProxy = `${url}/proxy`;
let _mediaProxy: MediaProxy | null = null;
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
// もう既にproxyっぽそうだったらurlを取り出す
imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
export function getProxiedImageUrl(...args: Parameters<MediaProxy['getProxiedImageUrl']>): string {
if (_mediaProxy == null) {
_mediaProxy = new MediaProxy(instance, url);
}
return `${mustOrigin ? localProxy : instance.mediaProxy}/${
type === 'preview' ? 'preview.webp'
: 'image.webp'
}?${query({
url: imageUrl,
...(!noFallback ? { 'fallback': '1' } : {}),
...(type ? { [type]: '1' } : {}),
...(mustOrigin ? { origin: '1' } : {}),
})}`;
return _mediaProxy.getProxiedImageUrl(...args);
}
export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
if (imageUrl == null) return null;
return getProxiedImageUrl(imageUrl, type);
}
export function getStaticImageUrl(baseUrl: string): string {
const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url);
if (u.href.startsWith(`${url}/emoji/`)) {
// もう既にemojiっぽそうだったらsearchParams付けるだけ
u.searchParams.set('static', '1');
return u.href;
export function getProxiedImageUrlNullable(...args: Parameters<MediaProxy['getProxiedImageUrlNullable']>): string | null {
if (_mediaProxy == null) {
_mediaProxy = new MediaProxy(instance, url);
}
if (u.href.startsWith(instance.mediaProxy + '/')) {
// もう既にproxyっぽそうだったらsearchParams付けるだけ
u.searchParams.set('static', '1');
return u.href;
return _mediaProxy.getProxiedImageUrlNullable(...args);
}
export function getStaticImageUrl(...args: Parameters<MediaProxy['getStaticImageUrl']>): string {
if (_mediaProxy == null) {
_mediaProxy = new MediaProxy(instance, url);
}
return `${instance.mediaProxy}/static.webp?${query({
url: u.href,
static: '1',
})}`;
return _mediaProxy.getStaticImageUrl(...args);
}

View File

@@ -6,7 +6,7 @@
import { Ref, nextTick } from 'vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { MFM_TAGS } from '@/const.js';
import { MFM_TAGS } from '@@/js/const.js';
import type { MenuItem } from '@/types/menu.js';
/**

View File

@@ -1,27 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
const enRegex1 = /(?<=n)a/gi;
const enRegex2 = /(?<=morn)ing/gi;
const enRegex3 = /(?<=every)one/gi;
const koRegex1 = /[나-낳]/g;
const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm;
const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm;
export function nyaize(text: string): string {
return text
// ja-JP
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
// en-US
.replace(enRegex1, x => x === 'A' ? 'YA' : 'ya')
.replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan')
.replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan')
// ko-KR
.replace(koRegex1, match => String.fromCharCode(
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
))
.replace(koRegex2, '다냥')
.replace(koRegex3, '냥');
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { appendQuery } from './url.js';
import { appendQuery } from '@@/js/url.js';
import * as config from '@/config.js';
export function popout(path: string, w?: HTMLElement) {

View File

@@ -18,7 +18,7 @@ export type MiPostMessageEvent = {
* 親フレームにイベントを送信
*/
export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void {
window.postMessage({
window.parent.postMessage({
type,
payload,
}, '*');

View File

@@ -1,11 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function safeParseFloat(str: unknown): number | null {
if (typeof str !== 'string' || str === '') return null;
const num = parseFloat(str);
if (isNaN(num)) return null;
return num;
}

View File

@@ -1,12 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function safeURIDecode(str: string): string {
try {
return decodeURIComponent(str);
} catch {
return str;
}
}

View File

@@ -1,144 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
type ScrollBehavior = 'auto' | 'smooth' | 'instant';
export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
if (el == null || el.tagName === 'HTML') return null;
const overflow = window.getComputedStyle(el).getPropertyValue('overflow-y');
if (overflow === 'scroll' || overflow === 'auto') {
return el;
} else {
return getScrollContainer(el.parentElement);
}
}
export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top = 0) {
if (!el.parentElement) return top;
const data = el.dataset.stickyContainerHeaderHeight;
const newTop = data ? Number(data) + top : top;
if (el === container) return newTop;
return getStickyTop(el.parentElement, container, newTop);
}
export function getStickyBottom(el: HTMLElement, container: HTMLElement | null = null, bottom = 0) {
if (!el.parentElement) return bottom;
const data = el.dataset.stickyContainerFooterHeight;
const newBottom = data ? Number(data) + bottom : bottom;
if (el === container) return newBottom;
return getStickyBottom(el.parentElement, container, newBottom);
}
export function getScrollPosition(el: HTMLElement | null): number {
const container = getScrollContainer(el);
return container == null ? window.scrollY : container.scrollTop;
}
export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) {
// とりあえず評価してみる
if (el.isConnected && isTopVisible(el)) {
cb();
if (once) return null;
}
const container = getScrollContainer(el) ?? window;
const onScroll = ev => {
if (!document.body.contains(el)) return;
if (isTopVisible(el, tolerance)) {
cb();
if (once) removeListener();
}
};
function removeListener() { container.removeEventListener('scroll', onScroll); }
container.addEventListener('scroll', onScroll, { passive: true });
return removeListener;
}
export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) {
const container = getScrollContainer(el);
// とりあえず評価してみる
if (el.isConnected && isBottomVisible(el, tolerance, container)) {
cb();
if (once) return null;
}
const containerOrWindow = container ?? window;
const onScroll = ev => {
if (!document.body.contains(el)) return;
if (isBottomVisible(el, 1, container)) {
cb();
if (once) removeListener();
}
};
function removeListener() {
containerOrWindow.removeEventListener('scroll', onScroll);
}
containerOrWindow.addEventListener('scroll', onScroll, { passive: true });
return removeListener;
}
export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) {
const container = getScrollContainer(el);
if (container == null) {
window.scroll(options);
} else {
container.scroll(options);
}
}
/**
* Scroll to Top
* @param el Scroll container element
* @param options Scroll options
*/
export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) {
scroll(el, { top: 0, ...options });
}
/**
* Scroll to Bottom
* @param el Content element
* @param options Scroll options
* @param container Scroll container element
*/
export function scrollToBottom(
el: HTMLElement,
options: ScrollToOptions = {},
container = getScrollContainer(el),
) {
if (container) {
container.scroll({ top: el.scrollHeight - container.clientHeight + getStickyTop(el, container) || 0, ...options });
} else {
window.scroll({
top: (el.scrollHeight - window.innerHeight + getStickyTop(el, container) + (window.innerWidth <= 500 ? 96 : 0)) || 0,
...options,
});
}
}
export function isTopVisible(el: HTMLElement, tolerance = 1): boolean {
const scrollTop = getScrollPosition(el);
return scrollTop <= tolerance;
}
export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) {
if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance;
return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance;
}
// https://ja.javascript.info/size-and-scroll-window#ref-932
export function getBodyScrollHeight() {
return Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight,
);
}

View File

@@ -0,0 +1,81 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js';
type AnyOf<T extends Record<any, any>> = T[keyof T];
type OmitFirst<T extends any[]> = T extends [any, ...infer R] ? R : never;
/**
* Websocket無効化時に使うStreamのモックなにもしない
*/
export class StreamMock extends EventEmitter<StreamEvents> implements IStream {
public readonly state = 'initializing';
constructor(...args: ConstructorParameters<typeof Misskey.Stream>) {
super();
// do nothing
}
public useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock<Channels[C]> {
return new ChannelConnectionMock(this, channel, name);
}
public removeSharedConnection(connection: any): void {
// do nothing
}
public removeSharedConnectionPool(pool: any): void {
// do nothing
}
public disconnectToChannel(): void {
// do nothing
}
public send(typeOrPayload: string): void
public send(typeOrPayload: string, payload: any): void
public send(typeOrPayload: Record<string, any> | any[]): void
public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void {
// do nothing
}
public ping(): void {
// do nothing
}
public heartbeat(): void {
// do nothing
}
public close(): void {
// do nothing
}
}
class ChannelConnectionMock<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> {
public id = '';
public name?: string; // for debug
public inCount = 0; // for debug
public outCount = 0; // for debug
public channel: string;
constructor(stream: IStream, ...args: OmitFirst<ConstructorParameters<typeof Misskey.ChannelConnection<Channel>>>) {
super();
this.channel = args[0];
this.name = args[1];
}
public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void {
// do nothing
}
public dispose(): void {
// do nothing
}
}

View File

@@ -5,11 +5,11 @@
import { ref } from 'vue';
import tinycolor from 'tinycolor2';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import { deepClone } from './clone.js';
import type { BundledTheme } from 'shiki/themes';
import { globalEvents } from '@/events.js';
import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5';
import { miLocalStorage } from '@/local-storage.js';
export type Theme = {

View File

@@ -1,28 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* objを検査して
* 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)
.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
return Object.entries(params)
.map((p) => `${p[0]}=${encodeURIComponent(p[1])}`)
.join('&');
}
export function appendQuery(url: string, query: string): string {
return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
}
export function extractDomain(url: string) {
const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im);
return match ? match[1] : null;
}

View File

@@ -1,24 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { onMounted, onUnmounted, ref, Ref } from 'vue';
export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
const visibility = ref(document.visibilityState);
const onChange = (): void => {
visibility.value = document.visibilityState;
};
onMounted(() => {
document.addEventListener('visibilitychange', onChange);
});
onUnmounted(() => {
document.removeEventListener('visibilitychange', onChange);
});
return visibility;
}

View File

@@ -1,46 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
export function useInterval(fn: () => void, interval: number, options: {
immediate: boolean;
afterMounted: boolean;
}): (() => void) | undefined {
if (Number.isNaN(interval)) return;
let intervalId: number | null = null;
if (options.afterMounted) {
onMounted(() => {
if (options.immediate) fn();
intervalId = window.setInterval(fn, interval);
});
} else {
if (options.immediate) fn();
intervalId = window.setInterval(fn, interval);
}
const clear = () => {
if (intervalId) window.clearInterval(intervalId);
intervalId = null;
};
onActivated(() => {
if (intervalId) return;
if (options.immediate) fn();
intervalId = window.setInterval(fn, interval);
});
onDeactivated(() => {
clear();
});
onUnmounted(() => {
clear();
});
return clear;
}

View File

@@ -458,10 +458,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
contextMenu: {
contextMenu: {
where: 'device',
default: 'app' as 'app' | 'appWithShift' | 'native',
},
},
sound_masterVolume: {
where: 'device',
@@ -520,8 +520,8 @@ interface Watcher {
/**
* 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
*/
import lightTheme from '@/themes/l-light.json5';
import darkTheme from '@/themes/d-green-lime.json5';
import lightTheme from '@@/themes/l-light.json5';
import darkTheme from '@@/themes/d-green-lime.json5';
export class ColdDeviceStorage {
public static default = {
@@ -558,7 +558,7 @@ export class ColdDeviceStorage {
public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
// 呼び出し側のバグ等で undefined が来ることがある
// undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (value === undefined) {
console.error(`attempt to store undefined value for key '${key}'`);
return;

View File

@@ -7,17 +7,20 @@ import * as Misskey from 'misskey-js';
import { markRaw } from 'vue';
import { $i } from '@/account.js';
import { wsOrigin } from '@/config.js';
// TODO: No WebsocketモードでStreamMockが使えそう
//import { StreamMock } from '@/scripts/stream-mock.js';
// heart beat interval in ms
const HEART_BEAT_INTERVAL = 1000 * 60;
let stream: Misskey.Stream | null = null;
let timeoutHeartBeat: ReturnType<typeof setTimeout> | null = null;
let stream: Misskey.IStream | null = null;
let timeoutHeartBeat: number | null = null;
let lastHeartbeatCall = 0;
export function useStream(): Misskey.Stream {
export function useStream(): Misskey.IStream {
if (stream) return stream;
// TODO: No Websocketモードもここで判定
stream = markRaw(new Misskey.Stream(wsOrigin, $i ? {
token: $i.token,
} : null));

View File

@@ -1,93 +0,0 @@
// ダークテーマのベーステーマ
// このテーマが直接使われることは無い
{
id: 'dark',
name: 'Dark',
author: 'syuilo',
desc: 'Default dark theme',
kind: 'dark',
props: {
accent: '#86b300',
accentDarken: ':darken<10<@accent',
accentLighten: ':lighten<10<@accent',
accentedBg: ':alpha<0.15<@accent',
focus: ':alpha<0.3<@accent',
bg: '#000',
acrylicBg: ':alpha<0.5<@bg',
fg: '#dadada',
fgTransparentWeak: ':alpha<0.75<@fg',
fgTransparent: ':alpha<0.5<@fg',
fgHighlighted: ':lighten<3<@fg',
fgOnAccent: '#fff',
fgOnWhite: '#333',
divider: 'rgba(255, 255, 255, 0.1)',
indicator: '@accent',
panel: ':lighten<3<@bg',
panelHighlight: ':lighten<3<@panel',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
panelBorder: '" solid 1px var(--divider)',
acrylicPanel: ':alpha<0.5<@panel',
windowHeader: ':alpha<0.85<@panel',
popup: ':lighten<3<@panel',
shadow: 'rgba(0, 0, 0, 0.3)',
header: ':alpha<0.7<@panel',
navBg: '@panel',
navFg: '@fg',
navHoverFg: ':lighten<17<@fg',
navActive: '@accent',
navIndicator: '@indicator',
link: '#44a4c1',
hashtag: '#ff9156',
mention: '@accent',
mentionMe: '@mention',
renote: '#229e82',
modalBg: 'rgba(0, 0, 0, 0.5)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
dateLabelFg: '@fg',
infoBg: '#253142',
infoFg: '#fff',
infoWarnBg: '#42321c',
infoWarnFg: '#ffbd3e',
switchBg: 'rgba(255, 255, 255, 0.15)',
buttonBg: 'rgba(255, 255, 255, 0.05)',
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
buttonGradateA: '@accent',
buttonGradateB: ':hue<20<@accent',
switchOffBg: 'rgba(255, 255, 255, 0.1)',
switchOffFg: ':alpha<0.8<@fg',
switchOnBg: '@accentedBg',
switchOnFg: '@accent',
inputBorder: 'rgba(255, 255, 255, 0.1)',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
driveFolderBg: ':alpha<0.3<@accent',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
badge: '#31b1ce',
messageBg: '@bg',
success: '#86b300',
error: '#ec4137',
warn: '#ecb637',
codeString: '#ffb675',
codeNumber: '#cfff9e',
codeBoolean: '#c59eff',
deckBg: '#000',
htmlThemeColor: '@bg',
X3: 'rgba(255, 255, 255, 0.05)',
X4: 'rgba(255, 255, 255, 0.1)',
X5: 'rgba(255, 255, 255, 0.05)',
X6: 'rgba(255, 255, 255, 0.15)',
X7: 'rgba(255, 255, 255, 0.05)',
X11: 'rgba(0, 0, 0, 0.3)',
X12: 'rgba(255, 255, 255, 0.1)',
X13: 'rgba(255, 255, 255, 0.15)',
},
codeHighlighter: {
base: 'one-dark-pro',
},
}

View File

@@ -1,93 +0,0 @@
// ライトテーマのベーステーマ
// このテーマが直接使われることは無い
{
id: 'light',
name: 'Light',
author: 'syuilo',
desc: 'Default light theme',
kind: 'light',
props: {
accent: '#86b300',
accentDarken: ':darken<10<@accent',
accentLighten: ':lighten<10<@accent',
accentedBg: ':alpha<0.15<@accent',
focus: ':alpha<0.3<@accent',
bg: '#fff',
acrylicBg: ':alpha<0.5<@bg',
fg: '#5f5f5f',
fgTransparentWeak: ':alpha<0.75<@fg',
fgTransparent: ':alpha<0.5<@fg',
fgHighlighted: ':darken<3<@fg',
fgOnAccent: '#fff',
fgOnWhite: '#333',
divider: 'rgba(0, 0, 0, 0.1)',
indicator: '@accent',
panel: ':lighten<3<@bg',
panelHighlight: ':darken<3<@panel',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
panelBorder: '" solid 1px var(--divider)',
acrylicPanel: ':alpha<0.5<@panel',
windowHeader: ':alpha<0.85<@panel',
popup: ':lighten<3<@panel',
shadow: 'rgba(0, 0, 0, 0.1)',
header: ':alpha<0.7<@panel',
navBg: '@panel',
navFg: '@fg',
navHoverFg: ':darken<17<@fg',
navActive: '@accent',
navIndicator: '@indicator',
link: '#44a4c1',
hashtag: '#ff9156',
mention: '@accent',
mentionMe: '@mention',
renote: '#229e82',
modalBg: 'rgba(0, 0, 0, 0.3)',
scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
dateLabelFg: '@fg',
infoBg: '#e5f5ff',
infoFg: '#72818a',
infoWarnBg: '#fff0db',
infoWarnFg: '#8f6e31',
switchBg: 'rgba(0, 0, 0, 0.15)',
buttonBg: 'rgba(0, 0, 0, 0.05)',
buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
buttonGradateA: '@accent',
buttonGradateB: ':hue<20<@accent',
switchOffBg: 'rgba(0, 0, 0, 0.1)',
switchOffFg: '@panel',
switchOnBg: '@accent',
switchOnFg: '@fgOnAccent',
inputBorder: 'rgba(0, 0, 0, 0.1)',
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
driveFolderBg: ':alpha<0.3<@accent',
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
badge: '#31b1ce',
messageBg: '@bg',
success: '#86b300',
error: '#ec4137',
warn: '#ecb637',
codeString: '#b98710',
codeNumber: '#0fbbbb',
codeBoolean: '#62b70c',
deckBg: ':darken<3<@bg',
htmlThemeColor: '@bg',
X3: 'rgba(0, 0, 0, 0.05)',
X4: 'rgba(0, 0, 0, 0.1)',
X5: 'rgba(0, 0, 0, 0.05)',
X6: 'rgba(0, 0, 0, 0.25)',
X7: 'rgba(0, 0, 0, 0.05)',
X11: 'rgba(0, 0, 0, 0.1)',
X12: 'rgba(0, 0, 0, 0.1)',
X13: 'rgba(0, 0, 0, 0.15)',
},
codeHighlighter: {
base: 'catppuccin-latte',
},
}

View File

@@ -1,69 +0,0 @@
{
id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
base: 'dark',
name: 'Mi Astro Dark',
author: 'syuilo',
props: {
bg: '#232125',
fg: '#efdab9',
link: '#78b0a0',
warn: '#ecb637',
badge: '#31b1ce',
error: '#ec4137',
focus: ':alpha<0.3<@accent',
navBg: '@panel',
navFg: '@fg',
panel: '#2a272b',
accent: '#81c08b',
header: ':alpha<0.7<@bg',
infoBg: '#253142',
infoFg: '#fff',
renote: '#659CC8',
shadow: 'rgba(0, 0, 0, 0.3)',
divider: 'rgba(255, 255, 255, 0.1)',
hashtag: '#ff9156',
mention: '#ffd152',
modalBg: 'rgba(0, 0, 0, 0.5)',
success: '#86b300',
buttonBg: 'rgba(255, 255, 255, 0.05)',
acrylicBg: ':alpha<0.5<@bg',
indicator: '@accent',
mentionMe: '#fb5d38',
messageBg: '@bg',
navActive: '@accent',
infoWarnBg: '#42321c',
infoWarnFg: '#ffbd3e',
navHoverFg: ':lighten<17<@fg',
dateLabelFg: '@fg',
inputBorder: 'rgba(255, 255, 255, 0.1)',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
panelBorder: '" solid 1px var(--divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@accent',
accentLighten: ':lighten<10<@accent',
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
buttonGradateA: '@accent',
buttonGradateB: ':hue<-20<@accent',
driveFolderBg: ':alpha<0.3<@accent',
fgHighlighted: ':lighten<3<@fg',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
htmlThemeColor: '@bg',
fgOnWhite: '@accent',
panelHighlight: ':lighten<3<@panel',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
X3: 'rgba(255, 255, 255, 0.05)',
X4: 'rgba(255, 255, 255, 0.1)',
X5: 'rgba(255, 255, 255, 0.05)',
X6: 'rgba(255, 255, 255, 0.15)',
X7: 'rgba(255, 255, 255, 0.05)',
X11: 'rgba(0, 0, 0, 0.3)',
X12: 'rgba(255, 255, 255, 0.1)',
X13: 'rgba(255, 255, 255, 0.15)',
},
}

View File

@@ -1,26 +0,0 @@
{
id: '504debaf-4912-6a4c-5059-1db08a76b737',
name: 'Mi Botanical Dark',
author: 'syuilo',
base: 'dark',
props: {
accent: 'rgb(148, 179, 0)',
bg: 'rgb(37, 38, 36)',
fg: 'rgb(216, 212, 199)',
fgHighlighted: '#fff',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.14)',
panel: 'rgb(47, 47, 44)',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
header: ':alpha<0.7<@panel',
navBg: '#363636',
renote: '@accent',
mention: 'rgb(212, 153, 76)',
mentionMe: 'rgb(212, 210, 76)',
hashtag: '#5bcbb0',
link: '@accent',
},
}

View File

@@ -1,21 +0,0 @@
{
id: '679b3b87-a4e9-4789-8696-b56c15cc33b0',
name: 'Mi Cherry Dark',
author: 'syuilo',
base: 'dark',
props: {
accent: 'rgb(255, 89, 117)',
bg: 'rgb(28, 28, 37)',
fg: 'rgb(236, 239, 244)',
fgOnWhite: '@accent',
panel: 'rgb(35, 35, 47)',
renote: '@accent',
link: '@accent',
mention: '@accent',
hashtag: '@accent',
divider: 'rgb(63, 63, 80)',
},
}

View File

@@ -1,26 +0,0 @@
{
id: '8050783a-7f63-445a-b270-36d0f6ba1677',
name: 'Mi Dark',
author: 'syuilo',
desc: 'Default light theme',
base: 'dark',
props: {
bg: '#232323',
fg: 'rgb(199, 209, 216)',
fgHighlighted: '#fff',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.14)',
panel: '#2d2d2d',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
header: ':alpha<0.7<@panel',
navBg: '#363636',
renote: '@accent',
mention: '#da6d35',
mentionMe: '#d44c4c',
hashtag: '#4cb8d4',
link: '@accent',
},
}

View File

@@ -1,27 +0,0 @@
{
id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
name: 'Mi Future Dark',
author: 'syuilo',
base: 'dark',
props: {
accent: '#63e2b7',
bg: '#101014',
fg: '#D5D5D6',
fgHighlighted: '#fff',
fgOnAccent: '#000',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.1)',
panel: '#18181c',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
renote: '@accent',
mention: '#f2c97d',
mentionMe: '@accent',
hashtag: '#70c0e8',
link: '#e88080',
buttonGradateA: '@accent',
buttonGradateB: ':saturate<30<:hue<30<@accent',
},
}

View File

@@ -1,24 +0,0 @@
{
id: '02816013-8107-440f-877e-865083ffe194',
name: 'Mi Green+Lime Dark',
author: 'syuilo',
base: 'dark',
props: {
accent: '#b4e900',
bg: '#0C1210',
fg: '#dee7e4',
fgHighlighted: '#fff',
fgOnAccent: '#192320',
fgOnWhite: '@accent',
divider: '#e7fffb24',
panel: '#192320',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
popup: '#293330',
renote: '@accent',
mentionMe: '#ffaa00',
link: '#24d7ce',
},
}

View File

@@ -1,24 +0,0 @@
{
id: 'dc489603-27b5-424a-9b25-1ff6aec9824a',
name: 'Mi Green+Orange Dark',
author: 'syuilo',
base: 'dark',
props: {
accent: '#e97f00',
bg: '#0C1210',
fg: '#dee7e4',
fgHighlighted: '#fff',
fgOnAccent: '#192320',
fgOnWhite: '@accent',
divider: '#e7fffb24',
panel: '#192320',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
popup: '#293330',
renote: '@accent',
mentionMe: '#b4e900',
link: '#24d7ce',
},
}

View File

@@ -1,14 +0,0 @@
{
id: '66e7e5a9-cd43-42cd-837d-12f47841fa34',
name: 'Mi Ice Dark',
author: 'syuilo',
base: 'dark',
props: {
accent: '#47BFE8',
fgOnWhite: '@accent',
bg: '#212526',
},
}

Some files were not shown because too many files have changed in this diff Show More