Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
@@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }]" v-html="html"></div>
|
||||
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { bundledLanguagesInfo } from 'shiki';
|
||||
import type { BuiltinLanguage } from 'shiki';
|
||||
import { getHighlighter } from '@/scripts/code-highlighter.js';
|
||||
import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
code: string;
|
||||
@@ -21,11 +22,23 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const highlighter = await getHighlighter();
|
||||
|
||||
const darkMode = defaultStore.reactiveState.darkMode;
|
||||
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
|
||||
|
||||
const [lightThemeName, darkThemeName] = await Promise.all([
|
||||
getTheme('light', true),
|
||||
getTheme('dark', true),
|
||||
]);
|
||||
|
||||
const html = computed(() => highlighter.codeToHtml(props.code, {
|
||||
lang: codeLang.value,
|
||||
theme: 'dark-plus',
|
||||
themes: {
|
||||
fallback: 'dark-plus',
|
||||
light: lightThemeName,
|
||||
dark: darkThemeName,
|
||||
},
|
||||
defaultColor: false,
|
||||
cssVariablePrefix: '--shiki-',
|
||||
}));
|
||||
|
||||
async function fetchLanguage(to: string): Promise<void> {
|
||||
@@ -63,6 +76,15 @@ watch(() => props.lang, (to) => {
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--divider);
|
||||
|
||||
color: var(--shiki-fallback);
|
||||
background-color: var(--shiki-fallback-bg);
|
||||
|
||||
& span {
|
||||
color: var(--shiki-fallback);
|
||||
background-color: var(--shiki-fallback-bg);
|
||||
}
|
||||
|
||||
& pre,
|
||||
& code {
|
||||
@@ -70,6 +92,26 @@ watch(() => props.lang, (to) => {
|
||||
}
|
||||
}
|
||||
|
||||
.light.codeBlockRoot :global(.shiki) {
|
||||
color: var(--shiki-light);
|
||||
background-color: var(--shiki-light-bg);
|
||||
|
||||
& span {
|
||||
color: var(--shiki-light);
|
||||
background-color: var(--shiki-light-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.dark.codeBlockRoot :global(.shiki) {
|
||||
color: var(--shiki-dark);
|
||||
background-color: var(--shiki-dark-bg);
|
||||
|
||||
& span {
|
||||
color: var(--shiki-dark);
|
||||
background-color: var(--shiki-dark-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.codeBlockRoot.codeEditor {
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
@@ -78,6 +120,7 @@ watch(() => props.lang, (to) => {
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
min-height: 130px;
|
||||
pointer-events: none;
|
||||
min-width: calc(100% - 24px);
|
||||
@@ -89,6 +132,11 @@ watch(() => props.lang, (to) => {
|
||||
text-rendering: inherit;
|
||||
text-transform: inherit;
|
||||
white-space: pre;
|
||||
|
||||
& span {
|
||||
display: inline-block;
|
||||
min-height: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -53,7 +53,6 @@ function copy() {
|
||||
}
|
||||
|
||||
.codeBlockCopyButton {
|
||||
color: #D4D4D4;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
@@ -67,8 +66,7 @@ function copy() {
|
||||
.codeBlockFallbackRoot {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
color: #D4D4D4;
|
||||
background: #1E1E1E;
|
||||
background: var(--bg);
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
@@ -91,8 +89,8 @@ function copy() {
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-top: 4px;
|
||||
color: #D4D4D4;
|
||||
background: #1E1E1E;
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.codePlaceholderContainer {
|
||||
|
@@ -198,10 +198,11 @@ watch(v, newValue => {
|
||||
resize: none;
|
||||
text-align: left;
|
||||
color: transparent;
|
||||
caret-color: rgb(225, 228, 232);
|
||||
caret-color: var(--fg);
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
outline: 0;
|
||||
min-width: calc(100% - 24px);
|
||||
height: 100%;
|
||||
@@ -212,6 +213,6 @@ watch(v, newValue => {
|
||||
}
|
||||
|
||||
.textarea::selection {
|
||||
color: #fff;
|
||||
color: var(--bg);
|
||||
}
|
||||
</style>
|
||||
|
@@ -18,8 +18,7 @@ const props = defineProps<{
|
||||
display: inline-block;
|
||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||
overflow-wrap: anywhere;
|
||||
color: #D4D4D4;
|
||||
background: #1E1E1E;
|
||||
background: var(--bg);
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
}
|
||||
|
@@ -118,6 +118,7 @@ import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
showPinned?: boolean;
|
||||
@@ -126,6 +127,7 @@ const props = withDefaults(defineProps<{
|
||||
asDrawer?: boolean;
|
||||
asWindow?: boolean;
|
||||
asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
|
||||
targetNote?: Misskey.entities.Note;
|
||||
}>(), {
|
||||
showPinned: true,
|
||||
});
|
||||
@@ -344,8 +346,7 @@ watch(q, () => {
|
||||
});
|
||||
|
||||
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
|
||||
return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction === undefined || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || (!!$i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) &&
|
||||
((emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction === undefined || emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction.length === 0) || (!!$i && !$i.roles.some(r => emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction?.includes(r.id))));
|
||||
return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji);
|
||||
}
|
||||
|
||||
function focus() {
|
||||
|
@@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:showPinned="showPinned"
|
||||
:pinnedEmojis="pinnedEmojis"
|
||||
:asReactionPicker="asReactionPicker"
|
||||
:targetNote="targetNote"
|
||||
:asDrawer="type === 'drawer'"
|
||||
:max-height="maxHeight"
|
||||
@chosen="chosen"
|
||||
@@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { shallowRef } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||
@@ -41,9 +43,10 @@ const props = withDefaults(defineProps<{
|
||||
manualShowing?: boolean | null;
|
||||
src?: HTMLElement;
|
||||
showPinned?: boolean;
|
||||
pinnedEmojis?: string[],
|
||||
pinnedEmojis?: string[],
|
||||
asReactionPicker?: boolean;
|
||||
choseAndClose?: boolean;
|
||||
targetNote?: Misskey.entities.Note;
|
||||
choseAndClose?: boolean;
|
||||
}>(), {
|
||||
manualShowing: null,
|
||||
showPinned: true,
|
||||
|
@@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:showPinned="showPinned"
|
||||
:pinnedEmojis="pinnedEmojis"
|
||||
:asReactionPicker="asReactionPicker"
|
||||
:targetNote="targetNote"
|
||||
asWindow
|
||||
@chosen="chosen"
|
||||
/>
|
||||
@@ -25,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||
|
||||
@@ -33,6 +35,7 @@ withDefaults(defineProps<{
|
||||
showPinned?: boolean;
|
||||
pinnedEmojis?: string[],
|
||||
asReactionPicker?: boolean;
|
||||
targetNote?: Misskey.entities.Note
|
||||
}>(), {
|
||||
showPinned: true,
|
||||
});
|
||||
|
@@ -119,6 +119,7 @@ function close() {
|
||||
margin-top: 12px;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> .indicatorWithValue {
|
||||
|
@@ -247,7 +247,7 @@ const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entiti
|
||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(false);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null);
|
||||
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
|
||||
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
||||
const collapsed = ref(appearNote.value.cw == null && isLong);
|
||||
const isDeleted = ref(false);
|
||||
@@ -379,7 +379,7 @@ function react(viaKeyboard = false): void {
|
||||
}
|
||||
} else {
|
||||
blur();
|
||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
||||
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||
sound.playMisskeySfx('reaction');
|
||||
|
||||
if (props.mock) {
|
||||
|
@@ -278,7 +278,7 @@ const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : fals
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||
const translating = ref(false);
|
||||
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
||||
const urls = parsed ? extractUrlFromMfm(parsed) : null;
|
||||
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||
const conversation = ref<Misskey.entities.Note[]>([]);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
@@ -386,7 +386,7 @@ function react(viaKeyboard = false): void {
|
||||
}
|
||||
} else {
|
||||
blur();
|
||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
||||
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||
sound.playMisskeySfx('reaction');
|
||||
|
||||
misskeyApi('notes/reactions/create', {
|
||||
|
@@ -32,6 +32,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
|
||||
const props = defineProps<{
|
||||
reaction: string;
|
||||
@@ -48,13 +50,19 @@ const emit = defineEmits<{
|
||||
|
||||
const buttonEl = shallowRef<HTMLElement>();
|
||||
|
||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||
const isCustomEmoji = computed(() => props.reaction.includes(':'));
|
||||
const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null);
|
||||
|
||||
const canToggle = computed(() => {
|
||||
return !props.reaction.match(/@\w/) && $i
|
||||
&& (emoji.value && checkReactionPermissions($i, props.note, emoji.value))
|
||||
|| !isCustomEmoji.value;
|
||||
});
|
||||
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
|
||||
|
||||
async function toggleReaction() {
|
||||
if (!canToggle.value) return;
|
||||
|
||||
// TODO: その絵文字を使う権限があるかどうか確認
|
||||
|
||||
const oldReaction = props.note.myReaction;
|
||||
if (oldReaction) {
|
||||
const confirm = await os.confirm({
|
||||
@@ -101,8 +109,8 @@ async function toggleReaction() {
|
||||
}
|
||||
|
||||
async function menu(ev) {
|
||||
if (!canToggle.value) return;
|
||||
if (!props.reaction.includes(':')) return;
|
||||
if (!canGetInfo.value) return;
|
||||
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.info,
|
||||
icon: 'ti ti-info-circle',
|
||||
|
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div :class="$style.caption"><slot name="caption"></slot></div>
|
||||
|
||||
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -141,6 +141,7 @@ function show() {
|
||||
active: computed(() => v.value === option.props?.value),
|
||||
action: () => {
|
||||
v.value = option.props?.value;
|
||||
changed.value = true;
|
||||
emit('changeByUser', v.value);
|
||||
},
|
||||
});
|
||||
@@ -291,6 +292,10 @@ function show() {
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.save {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
@@ -4,7 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<KeepAlive :max="defaultStore.state.numberOfPageCache">
|
||||
<KeepAlive
|
||||
:max="defaultStore.state.numberOfPageCache"
|
||||
:exclude="pageCacheController"
|
||||
>
|
||||
<Suspense :timeout="0">
|
||||
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
||||
|
||||
@@ -16,9 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue';
|
||||
import { IRouter, Resolved } from '@/nirax.js';
|
||||
import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue';
|
||||
import { IRouter, Resolved, RouteDef } from '@/nirax.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import MkLoadingPage from '@/pages/_loading_.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
router?: IRouter;
|
||||
@@ -46,20 +51,47 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
|
||||
}
|
||||
|
||||
const current = resolveNested(router.current)!;
|
||||
const currentPageComponent = shallowRef(current.route.component);
|
||||
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
|
||||
const currentPageProps = ref(current.props);
|
||||
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
|
||||
|
||||
function onChange({ resolved, key: newKey }) {
|
||||
const current = resolveNested(resolved);
|
||||
if (current == null) return;
|
||||
if (current == null || 'redirect' in current.route) return;
|
||||
currentPageComponent.value = current.route.component;
|
||||
currentPageProps.value = current.props;
|
||||
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
|
||||
|
||||
nextTick(() => {
|
||||
// ページ遷移完了後に再びキャッシュを有効化
|
||||
if (clearCacheRequested.value) {
|
||||
clearCacheRequested.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
router.addListener('change', onChange);
|
||||
|
||||
// #region キャッシュ制御
|
||||
|
||||
/**
|
||||
* キャッシュクリアが有効になったら、全キャッシュをクリアする
|
||||
*
|
||||
* keepAlive側にwatcherがあるのですぐ消えるとはおもうけど、念のためページ遷移完了まではキャッシュを無効化しておく。
|
||||
* キャッシュ有効時向けにexcludeを使いたい場合は、pageCacheControllerに並列に突っ込むのではなく、下に追記すること
|
||||
*/
|
||||
const pageCacheController = computed(() => clearCacheRequested.value ? /.*/ : undefined);
|
||||
const clearCacheRequested = ref(false);
|
||||
|
||||
globalEvents.on('requestClearPageCache', () => {
|
||||
if (_DEV_) console.log('clear page cache requested');
|
||||
if (!clearCacheRequested.value) {
|
||||
clearCacheRequested.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
router.removeListener('change', onChange);
|
||||
});
|
||||
|
Reference in New Issue
Block a user