Merge branch 'develop' into pr/12141
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
"@vue/compiler-sfc": "3.3.7",
|
||||
"astring": "1.8.6",
|
||||
"autosize": "6.0.1",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.5",
|
||||
"broadcast-channel": "5.5.1",
|
||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||
"buraha": "0.0.1",
|
||||
@@ -54,11 +55,11 @@
|
||||
"mfm-js": "0.23.3",
|
||||
"misskey-js": "workspace:*",
|
||||
"photoswipe": "5.4.2",
|
||||
"prismjs": "1.29.0",
|
||||
"punycode": "2.3.0",
|
||||
"querystring": "0.2.1",
|
||||
"rollup": "4.1.4",
|
||||
"sanitize-html": "2.11.0",
|
||||
"shiki": "^0.14.5",
|
||||
"sass": "1.69.5",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
@@ -74,7 +75,6 @@
|
||||
"vanilla-tilt": "1.8.1",
|
||||
"vite": "4.5.0",
|
||||
"vue": "3.3.7",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -8,7 +8,7 @@ import { common } from './common.js';
|
||||
import { version, ui, lang, updateLocale } from '@/config.js';
|
||||
import { i18n, updateI18n } from '@/i18n.js';
|
||||
import { confirm, alert, post, popup, toast } from '@/os.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { useStream, isReloading } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||
@@ -39,6 +39,7 @@ export async function mainBoot() {
|
||||
|
||||
let reloadDialogShowing = false;
|
||||
stream.on('_disconnected_', async () => {
|
||||
if (isReloading) return;
|
||||
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
|
||||
location.reload();
|
||||
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
||||
|
@@ -5,21 +5,90 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<code v-if="inline" :class="`language-${prismLang}`" style="overflow-wrap: anywhere;" v-html="html"></code>
|
||||
<pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
|
||||
<div :class="['codeBlockRoot', { 'codeEditor': codeEditor }]" v-html="html"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import Prism from 'prismjs';
|
||||
import 'prismjs/themes/prism-okaidia.css';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { BUNDLED_LANGUAGES } from 'shiki';
|
||||
import type { Lang as ShikiLang } from 'shiki';
|
||||
import { getHighlighter } from '@/scripts/code-highlighter.js';
|
||||
|
||||
const props = defineProps<{
|
||||
code: string;
|
||||
lang?: string;
|
||||
inline?: boolean;
|
||||
codeEditor?: boolean;
|
||||
}>();
|
||||
|
||||
const prismLang = computed(() => Prism.languages[props.lang] ? props.lang : 'js');
|
||||
const html = computed(() => Prism.highlight(props.code, Prism.languages[prismLang.value], prismLang.value));
|
||||
const highlighter = await getHighlighter();
|
||||
|
||||
const codeLang = ref<ShikiLang | 'aiscript'>('js');
|
||||
const html = computed(() => highlighter.codeToHtml(props.code, {
|
||||
lang: codeLang.value,
|
||||
theme: 'dark-plus',
|
||||
}));
|
||||
|
||||
async function fetchLanguage(to: string): Promise<void> {
|
||||
const language = to as ShikiLang;
|
||||
|
||||
// Check for the loaded languages, and load the language if it's not loaded yet.
|
||||
if (!highlighter.getLoadedLanguages().includes(language)) {
|
||||
// Check if the language is supported by Shiki
|
||||
const bundles = BUNDLED_LANGUAGES.filter((bundle) => {
|
||||
// Languages are specified by their id, they can also have aliases (i. e. "js" and "javascript")
|
||||
return bundle.id === language || bundle.aliases?.includes(language);
|
||||
});
|
||||
if (bundles.length > 0) {
|
||||
await highlighter.loadLanguage(language);
|
||||
codeLang.value = language;
|
||||
} else {
|
||||
codeLang.value = 'js';
|
||||
}
|
||||
} else {
|
||||
codeLang.value = language;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.lang, (to) => {
|
||||
if (codeLang.value === to || !to) return;
|
||||
return new Promise((resolve) => {
|
||||
fetchLanguage(to).then(() => resolve);
|
||||
});
|
||||
}, { immediate: true, });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.codeBlockRoot :deep(.shiki) {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
border-radius: .3em;
|
||||
|
||||
& pre,
|
||||
& code {
|
||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.codeBlockRoot.codeEditor {
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
|
||||
& :deep(.shiki) {
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
border-radius: 6px;
|
||||
min-height: 130px;
|
||||
pointer-events: none;
|
||||
min-width: calc(100% - 24px);
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
line-height: 1.5em;
|
||||
font-size: 1em;
|
||||
overflow: visible;
|
||||
text-rendering: inherit;
|
||||
text-transform: inherit;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -4,11 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<XCode :code="code" :lang="lang" :inline="inline"/>
|
||||
<Suspense>
|
||||
<template #fallback>
|
||||
<MkLoading v-if="!inline ?? true" />
|
||||
</template>
|
||||
<code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code>
|
||||
<XCode v-else :code="code" :lang="lang"/>
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import MkLoading from '@/components/global/MkLoading.vue';
|
||||
|
||||
defineProps<{
|
||||
code: string;
|
||||
@@ -18,3 +25,15 @@ defineProps<{
|
||||
|
||||
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.codeInlineRoot {
|
||||
display: inline-block;
|
||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||
overflow-wrap: anywhere;
|
||||
color: #D4D4D4;
|
||||
background: #1E1E1E;
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
}
|
||||
</style>
|
||||
|
166
packages/frontend/src/components/MkCodeEditor.vue
Normal file
166
packages/frontend/src/components/MkCodeEditor.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.codeEditorRoot, { [$style.disabled]: disabled, [$style.focused]: focused }]">
|
||||
<div :class="$style.codeEditorScroller">
|
||||
<textarea
|
||||
ref="inputEl"
|
||||
v-model="vModel"
|
||||
:class="[$style.textarea]"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
autocomplete="off"
|
||||
wrap="off"
|
||||
spellcheck="false"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="onKeydown($event)"
|
||||
@input="onInput"
|
||||
></textarea>
|
||||
<XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, toRefs, shallowRef, nextTick } from 'vue';
|
||||
import XCode from '@/components/MkCode.core.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: string | null;
|
||||
lang: string;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
}>(), {
|
||||
lang: 'js',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'change', _ev: KeyboardEvent): void;
|
||||
(ev: 'keydown', _ev: KeyboardEvent): void;
|
||||
(ev: 'enter'): void;
|
||||
(ev: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
|
||||
const { modelValue } = toRefs(props);
|
||||
const vModel = ref<string>(modelValue.value ?? '');
|
||||
const v = ref<string>(modelValue.value ?? '');
|
||||
const focused = ref(false);
|
||||
const changed = ref(false);
|
||||
const inputEl = shallowRef<HTMLTextAreaElement>();
|
||||
|
||||
const onInput = (ev) => {
|
||||
v.value = ev.target?.value ?? v.value;
|
||||
changed.value = true;
|
||||
emit('change', ev);
|
||||
};
|
||||
|
||||
const onKeydown = (ev: KeyboardEvent) => {
|
||||
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
|
||||
|
||||
emit('keydown', ev);
|
||||
|
||||
if (ev.code === 'Enter') {
|
||||
const pos = inputEl.value?.selectionStart ?? 0;
|
||||
const posEnd = inputEl.value?.selectionEnd ?? vModel.value.length;
|
||||
if (pos === posEnd) {
|
||||
const lines = vModel.value.slice(0, pos).split('\n');
|
||||
const currentLine = lines[lines.length - 1];
|
||||
const currentLineSpaces = currentLine.match(/^\s+/);
|
||||
const posDelta = currentLineSpaces ? currentLineSpaces[0].length : 0;
|
||||
ev.preventDefault();
|
||||
vModel.value = vModel.value.slice(0, pos) + '\n' + (currentLineSpaces ? currentLineSpaces[0] : '') + vModel.value.slice(pos);
|
||||
v.value = vModel.value;
|
||||
nextTick(() => {
|
||||
inputEl.value?.setSelectionRange(pos + 1 + posDelta, pos + 1 + posDelta);
|
||||
});
|
||||
}
|
||||
emit('enter');
|
||||
}
|
||||
|
||||
if (ev.key === 'Tab') {
|
||||
const pos = inputEl.value?.selectionStart ?? 0;
|
||||
const posEnd = inputEl.value?.selectionEnd ?? vModel.value.length;
|
||||
vModel.value = vModel.value.slice(0, pos) + '\t' + vModel.value.slice(posEnd);
|
||||
v.value = vModel.value;
|
||||
nextTick(() => {
|
||||
inputEl.value?.setSelectionRange(pos + 1, pos + 1);
|
||||
});
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const updated = () => {
|
||||
changed.value = false;
|
||||
emit('update:modelValue', v.value);
|
||||
};
|
||||
|
||||
watch(modelValue, newValue => {
|
||||
v.value = newValue ?? '';
|
||||
});
|
||||
|
||||
watch(v, () => {
|
||||
updated();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.codeEditorRoot {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--fg);
|
||||
border: solid 1px var(--panel);
|
||||
transition: border-color 0.1s ease-out;
|
||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||
&:hover {
|
||||
border-color: var(--inputBorderHover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.focused.codeEditorRoot {
|
||||
border-color: var(--accent) !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.codeEditorScroller {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: inline-block;
|
||||
appearance: none;
|
||||
resize: none;
|
||||
text-align: left;
|
||||
color: transparent;
|
||||
caret-color: rgb(225, 228, 232);
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
padding: 12px;
|
||||
line-height: 1.5em;
|
||||
font-size: 1em;
|
||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||
}
|
||||
|
||||
.textarea::selection {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
@@ -42,6 +42,7 @@ export default defineComponent({
|
||||
|
||||
setup(props, { slots, expose }) {
|
||||
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
|
||||
|
||||
function getDateText(time: string) {
|
||||
const date = new Date(time).getDate();
|
||||
const month = new Date(time).getMonth() + 1;
|
||||
@@ -121,6 +122,7 @@ export default defineComponent({
|
||||
el.style.top = `${el.offsetTop}px`;
|
||||
el.style.left = `${el.offsetLeft}px`;
|
||||
}
|
||||
|
||||
function onLeaveCanceled(el: HTMLElement) {
|
||||
el.style.top = '';
|
||||
el.style.left = '';
|
||||
|
@@ -160,6 +160,7 @@ async function ok() {
|
||||
function cancel() {
|
||||
done(true);
|
||||
}
|
||||
|
||||
/*
|
||||
function onBgClick() {
|
||||
if (props.cancelableByBgClick) cancel();
|
||||
|
@@ -505,6 +505,7 @@ function appendFile(file: Misskey.entities.DriveFile) {
|
||||
function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
|
||||
addFolder(folderToAppend);
|
||||
}
|
||||
|
||||
/*
|
||||
function prependFile(file: Misskey.entities.DriveFile) {
|
||||
addFile(file, true);
|
||||
|
@@ -84,6 +84,7 @@ onMounted(() => {
|
||||
return getParentBg(el.parentElement);
|
||||
}
|
||||
}
|
||||
|
||||
const rawBg = getParentBg(el.value);
|
||||
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
_bg.setAlpha(0.85);
|
||||
|
@@ -145,11 +145,13 @@ const onGlobalMousedown = (event: MouseEvent) => {
|
||||
};
|
||||
|
||||
let childCloseTimer: null | number = null;
|
||||
|
||||
function onItemMouseEnter(item) {
|
||||
childCloseTimer = window.setTimeout(() => {
|
||||
closeChild();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function onItemMouseLeave(item) {
|
||||
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
||||
}
|
||||
|
@@ -191,9 +191,11 @@ let note = $ref(deepClone(props.note));
|
||||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
let result = deepClone(note);
|
||||
let result:Misskey.entities.Note | null = deepClone(note);
|
||||
for (const interruptor of noteViewInterruptors) {
|
||||
result = await interruptor.handler(result);
|
||||
|
||||
if (result === null) return isDeleted.value = true;
|
||||
}
|
||||
note = result;
|
||||
});
|
||||
|
@@ -230,9 +230,11 @@ let note = $ref(deepClone(props.note));
|
||||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
let result = deepClone(note);
|
||||
let result:Misskey.entities.Note | null = deepClone(note);
|
||||
for (const interruptor of noteViewInterruptors) {
|
||||
result = await interruptor.handler(result);
|
||||
|
||||
if (result === null) return isDeleted.value = true;
|
||||
}
|
||||
note = result;
|
||||
});
|
||||
@@ -497,6 +499,7 @@ function blur() {
|
||||
}
|
||||
|
||||
const repliesLoaded = ref(false);
|
||||
|
||||
function loadReplies() {
|
||||
repliesLoaded.value = true;
|
||||
os.api('notes/children', {
|
||||
@@ -508,6 +511,7 @@ function loadReplies() {
|
||||
}
|
||||
|
||||
const conversationLoaded = ref(false);
|
||||
|
||||
function loadConversation() {
|
||||
conversationLoaded.value = true;
|
||||
os.api('notes/conversation', {
|
||||
|
@@ -166,6 +166,8 @@ defineExpose({
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
overscroll-behavior: none;
|
||||
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
|
||||
|
@@ -87,6 +87,7 @@ function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] {
|
||||
function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): MisskeyEntityMap {
|
||||
return new Map([...map, ...arrayToEntries(entities)]);
|
||||
}
|
||||
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
@@ -101,6 +102,7 @@ const props = withDefaults(defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'queue', count: number): void;
|
||||
(ev: 'status', error: boolean): void;
|
||||
}>();
|
||||
|
||||
let rootEl = $shallowRef<HTMLElement>();
|
||||
@@ -192,6 +194,11 @@ watch(queue, (a, b) => {
|
||||
emit('queue', queue.value.size);
|
||||
}, { deep: true });
|
||||
|
||||
watch(error, (n, o) => {
|
||||
if (n === o) return;
|
||||
emit('status', n);
|
||||
});
|
||||
|
||||
async function init(): Promise<void> {
|
||||
items.value = new Map();
|
||||
queue.value = new Map();
|
||||
|
@@ -59,6 +59,7 @@ function toggleSensitive(file) {
|
||||
emit('changeSensitive', file, !file.isSensitive);
|
||||
});
|
||||
}
|
||||
|
||||
async function rename(file) {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: i18n.ts.enterFileName,
|
||||
|
240
packages/frontend/src/components/MkPullToRefresh.vue
Normal file
240
packages/frontend/src/components/MkPullToRefresh.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div ref="rootEl">
|
||||
<div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR))}px;`">
|
||||
<div :class="$style.frameContent">
|
||||
<MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/>
|
||||
<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i>
|
||||
<div :class="$style.text">
|
||||
<template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template>
|
||||
<template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template>
|
||||
<template v-else>{{ i18n.ts.pullDownToRefresh }}</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="{ [$style.slotClip]: isPullStart }">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const SCROLL_STOP = 10;
|
||||
const MAX_PULL_DISTANCE = Infinity;
|
||||
const FIRE_THRESHOLD = 230;
|
||||
const RELEASE_TRANSITION_DURATION = 200;
|
||||
const PULL_BRAKE_BASE = 2;
|
||||
const PULL_BRAKE_FACTOR = 200;
|
||||
|
||||
let isPullStart = $ref(false);
|
||||
let isPullEnd = $ref(false);
|
||||
let isRefreshing = $ref(false);
|
||||
let pullDistance = $ref(0);
|
||||
|
||||
let supportPointerDesktop = false;
|
||||
let startScreenY: number | null = null;
|
||||
|
||||
const rootEl = $shallowRef<HTMLDivElement>();
|
||||
let scrollEl: HTMLElement | null = null;
|
||||
|
||||
let disabled = false;
|
||||
|
||||
const emits = defineEmits<{
|
||||
(ev: 'refresh'): void;
|
||||
}>();
|
||||
|
||||
function getScrollableParentElement(node) {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.scrollHeight > node.clientHeight) {
|
||||
return node;
|
||||
} else {
|
||||
return getScrollableParentElement(node.parentNode);
|
||||
}
|
||||
}
|
||||
|
||||
function getScreenY(event) {
|
||||
if (supportPointerDesktop) {
|
||||
return event.screenY;
|
||||
}
|
||||
return event.touches[0].screenY;
|
||||
}
|
||||
|
||||
function moveStart(event) {
|
||||
if (!isPullStart && !isRefreshing && !disabled) {
|
||||
isPullStart = true;
|
||||
startScreenY = getScreenY(event);
|
||||
pullDistance = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function moveBySystem(to: number): Promise<void> {
|
||||
return new Promise(r => {
|
||||
const startHeight = pullDistance;
|
||||
const overHeight = pullDistance - to;
|
||||
if (overHeight < 1) {
|
||||
r();
|
||||
return;
|
||||
}
|
||||
const startTime = Date.now();
|
||||
let intervalId = setInterval(() => {
|
||||
const time = Date.now() - startTime;
|
||||
if (time > RELEASE_TRANSITION_DURATION) {
|
||||
pullDistance = to;
|
||||
clearInterval(intervalId);
|
||||
r();
|
||||
return;
|
||||
}
|
||||
const nextHeight = startHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time;
|
||||
if (pullDistance < nextHeight) return;
|
||||
pullDistance = nextHeight;
|
||||
}, 1);
|
||||
});
|
||||
}
|
||||
|
||||
async function fixOverContent() {
|
||||
if (pullDistance > FIRE_THRESHOLD) {
|
||||
await moveBySystem(FIRE_THRESHOLD);
|
||||
}
|
||||
}
|
||||
|
||||
async function closeContent() {
|
||||
if (pullDistance > 0) {
|
||||
await moveBySystem(0);
|
||||
}
|
||||
}
|
||||
|
||||
function moveEnd() {
|
||||
if (isPullStart && !isRefreshing) {
|
||||
startScreenY = null;
|
||||
if (isPullEnd) {
|
||||
isPullEnd = false;
|
||||
isRefreshing = true;
|
||||
fixOverContent().then(() => emits('refresh'));
|
||||
} else {
|
||||
closeContent().then(() => isPullStart = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moving(event) {
|
||||
if (!isPullStart || isRefreshing || disabled) return;
|
||||
|
||||
if (!scrollEl) {
|
||||
scrollEl = getScrollableParentElement(rootEl);
|
||||
}
|
||||
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
|
||||
pullDistance = 0;
|
||||
isPullEnd = false;
|
||||
moveEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
if (startScreenY === null) {
|
||||
startScreenY = getScreenY(event);
|
||||
}
|
||||
const moveScreenY = getScreenY(event);
|
||||
|
||||
const moveHeight = moveScreenY - startScreenY!;
|
||||
pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
||||
|
||||
isPullEnd = pullDistance >= FIRE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* emit(refresh)が完了したことを知らせる関数
|
||||
*
|
||||
* タイムアウトがないのでこれを最終的に実行しないと出たままになる
|
||||
*/
|
||||
function refreshFinished() {
|
||||
closeContent().then(() => {
|
||||
isPullStart = false;
|
||||
isRefreshing = false;
|
||||
});
|
||||
}
|
||||
|
||||
function setDisabled(value) {
|
||||
disabled = value;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// マウス操作でpull to refreshするのは不便そう
|
||||
//supportPointerDesktop = !!window.PointerEvent && deviceKind === 'desktop';
|
||||
|
||||
if (supportPointerDesktop) {
|
||||
rootEl.addEventListener('pointerdown', moveStart);
|
||||
// ポインターの場合、ポップアップ系の動作をするとdownだけ発火されてupが発火されないため
|
||||
window.addEventListener('pointerup', moveEnd);
|
||||
rootEl.addEventListener('pointermove', moving, { passive: true });
|
||||
} else {
|
||||
rootEl.addEventListener('touchstart', moveStart);
|
||||
rootEl.addEventListener('touchend', moveEnd);
|
||||
rootEl.addEventListener('touchmove', moving, { passive: true });
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (supportPointerDesktop) window.removeEventListener('pointerup', moveEnd);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
refreshFinished,
|
||||
setDisabled,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.frame {
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
|
||||
width: 100%;
|
||||
min-height: var(--frame-min-height, 0px);
|
||||
|
||||
mask-image: linear-gradient(90deg, #000 0%, #000 80%, transparent);
|
||||
-webkit-mask-image: -webkit-linear-gradient(90deg, #000 0%, #000 80%, transparent);
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.frameContent {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
||||
> .icon, > .loader {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
transition: transform .25s;
|
||||
|
||||
&.refresh {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
> .text {
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.slotClip {
|
||||
overflow-y: clip;
|
||||
}
|
||||
</style>
|
@@ -4,13 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
|
||||
<MkPullToRefresh ref="prComponent" @refresh="() => reloadTimeline(true)">
|
||||
<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)" @status="prComponent.setDisabled($event)"/>
|
||||
</MkPullToRefresh>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, onUnmounted } from 'vue';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import { useStream, reloadStream } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { instance } from '@/instance.js';
|
||||
@@ -39,6 +42,7 @@ const emit = defineEmits<{
|
||||
|
||||
provide('inChannel', computed(() => props.src === 'channel'));
|
||||
|
||||
const prComponent: InstanceType<typeof MkPullToRefresh> = $ref();
|
||||
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
||||
|
||||
let tlNotesCount = 0;
|
||||
@@ -65,29 +69,73 @@ let connection;
|
||||
let connection2;
|
||||
|
||||
const stream = useStream();
|
||||
const connectChannel = () => {
|
||||
if (props.src === 'antenna') {
|
||||
connection = stream.useChannel('antenna', {
|
||||
antennaId: props.antenna,
|
||||
});
|
||||
} else if (props.src === 'home') {
|
||||
connection = stream.useChannel('homeTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
connection2 = stream.useChannel('main');
|
||||
} else if (props.src === 'local') {
|
||||
connection = stream.useChannel('localTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
} else if (props.src === 'social') {
|
||||
connection = stream.useChannel('hybridTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
} else if (props.src === 'global') {
|
||||
connection = stream.useChannel('globalTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
} else if (props.src === 'mentions') {
|
||||
connection = stream.useChannel('main');
|
||||
connection.on('mention', prepend);
|
||||
} else if (props.src === 'directs') {
|
||||
const onNote = note => {
|
||||
if (note.visibility === 'specified') {
|
||||
prepend(note);
|
||||
}
|
||||
};
|
||||
connection = stream.useChannel('main');
|
||||
connection.on('mention', onNote);
|
||||
} else if (props.src === 'list') {
|
||||
connection = stream.useChannel('userList', {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
});
|
||||
} else if (props.src === 'channel') {
|
||||
connection = stream.useChannel('channel', {
|
||||
channelId: props.channel,
|
||||
});
|
||||
} else if (props.src === 'role') {
|
||||
connection = stream.useChannel('roleTimeline', {
|
||||
roleId: props.role,
|
||||
});
|
||||
}
|
||||
if (props.src !== 'directs' || props.src !== 'mentions') connection.on('note', prepend);
|
||||
};
|
||||
|
||||
if (props.src === 'antenna') {
|
||||
endpoint = 'antennas/notes';
|
||||
query = {
|
||||
antennaId: props.antenna,
|
||||
};
|
||||
connection = stream.useChannel('antenna', {
|
||||
antennaId: props.antenna,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'home') {
|
||||
endpoint = 'notes/timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
connection = stream.useChannel('homeTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
|
||||
connection2 = stream.useChannel('main');
|
||||
} else if (props.src === 'local') {
|
||||
endpoint = 'notes/local-timeline';
|
||||
query = {
|
||||
@@ -95,12 +143,6 @@ if (props.src === 'antenna') {
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
connection = stream.useChannel('localTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'social') {
|
||||
endpoint = 'notes/hybrid-timeline';
|
||||
query = {
|
||||
@@ -108,68 +150,44 @@ if (props.src === 'antenna') {
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
connection = stream.useChannel('hybridTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'global') {
|
||||
endpoint = 'notes/global-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
connection = stream.useChannel('globalTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'mentions') {
|
||||
endpoint = 'notes/mentions';
|
||||
connection = stream.useChannel('main');
|
||||
connection.on('mention', prepend);
|
||||
} else if (props.src === 'directs') {
|
||||
endpoint = 'notes/mentions';
|
||||
query = {
|
||||
visibility: 'specified',
|
||||
};
|
||||
const onNote = note => {
|
||||
if (note.visibility === 'specified') {
|
||||
prepend(note);
|
||||
}
|
||||
};
|
||||
connection = stream.useChannel('main');
|
||||
connection.on('mention', onNote);
|
||||
} else if (props.src === 'list') {
|
||||
endpoint = 'notes/user-list-timeline';
|
||||
query = {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
};
|
||||
connection = stream.useChannel('userList', {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'channel') {
|
||||
endpoint = 'channels/timeline';
|
||||
query = {
|
||||
channelId: props.channel,
|
||||
};
|
||||
connection = stream.useChannel('channel', {
|
||||
channelId: props.channel,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'role') {
|
||||
endpoint = 'roles/notes';
|
||||
query = {
|
||||
roleId: props.role,
|
||||
};
|
||||
connection = stream.useChannel('roleTimeline', {
|
||||
roleId: props.role,
|
||||
}
|
||||
|
||||
if (!defaultStore.state.disableStreamingTimeline) {
|
||||
connectChannel();
|
||||
|
||||
onUnmounted(() => {
|
||||
connection.dispose();
|
||||
if (connection2) connection2.dispose();
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
}
|
||||
|
||||
const pagination = {
|
||||
@@ -178,9 +196,19 @@ const pagination = {
|
||||
params: query,
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
connection.dispose();
|
||||
if (connection2) connection2.dispose();
|
||||
const reloadTimeline = (fromPR = false) => {
|
||||
tlNotesCount = 0;
|
||||
|
||||
tlComponent.pagingComponent?.reload().then(() => {
|
||||
reloadStream();
|
||||
if (fromPR) prComponent.refreshFinished();
|
||||
});
|
||||
};
|
||||
|
||||
//const pullRefresh = () => reloadTimeline(true);
|
||||
|
||||
defineExpose({
|
||||
reloadTimeline,
|
||||
});
|
||||
|
||||
/* TODO
|
||||
|
@@ -134,9 +134,11 @@ async function enter(el: HTMLElement) {
|
||||
|
||||
setTimeout(renderTab, 170);
|
||||
}
|
||||
|
||||
function afterEnter(el: HTMLElement) {
|
||||
//el.style.width = '';
|
||||
}
|
||||
|
||||
async function leave(el: HTMLElement) {
|
||||
const elementWidth = el.getBoundingClientRect().width;
|
||||
el.style.width = elementWidth + 'px';
|
||||
@@ -145,6 +147,7 @@ async function leave(el: HTMLElement) {
|
||||
el.style.width = '0';
|
||||
el.style.paddingLeft = '0';
|
||||
}
|
||||
|
||||
function afterLeave(el: HTMLElement) {
|
||||
el.style.width = '';
|
||||
}
|
||||
|
@@ -66,6 +66,7 @@ export const ROLE_POLICIES = [
|
||||
'inviteLimitCycle',
|
||||
'inviteExpirationTime',
|
||||
'canManageCustomEmojis',
|
||||
'canManageAvatarDecorations',
|
||||
'canSearchNotes',
|
||||
'canUseTranslator',
|
||||
'canHideAds',
|
||||
|
@@ -1061,7 +1061,7 @@
|
||||
["💰", "moneybag", 6],
|
||||
["🪙", "coin", 6],
|
||||
["💳", "credit_card", 6],
|
||||
["🪫", "identification_card", 6],
|
||||
["🪪", "identification_card", 6],
|
||||
["💎", "gem", 6],
|
||||
["⚖", "balance_scale", 6],
|
||||
["🧰", "toolbox", 6],
|
||||
|
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
||||
<template #label>{{ i18n.ts._aboutMisskey.projectMembers }}</template>
|
||||
<div :class="$style.contributors">
|
||||
<a href="https://github.com/syuilo" target="_blank" :class="$style.contributor">
|
||||
<img src="https://avatars.githubusercontent.com/u/4439005?v=4" :class="$style.contributorAvatar">
|
||||
@@ -61,20 +61,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<img src="https://avatars.githubusercontent.com/u/20679825?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@acid-chicken</span>
|
||||
</a>
|
||||
<a href="https://github.com/rinsuki" target="_blank" :class="$style.contributor">
|
||||
<img src="https://avatars.githubusercontent.com/u/6533808?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@rinsuki</span>
|
||||
<a href="https://github.com/kakkokari-gtyih" target="_blank" :class="$style.contributor">
|
||||
<img src="https://avatars.githubusercontent.com/u/67428053?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@kakkokari-gtyih</span>
|
||||
</a>
|
||||
<a href="https://github.com/mei23" target="_blank" :class="$style.contributor">
|
||||
<img src="https://avatars.githubusercontent.com/u/30769358?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@mei23</span>
|
||||
</a>
|
||||
<a href="https://github.com/robflop" target="_blank" :class="$style.contributor">
|
||||
<img src="https://avatars.githubusercontent.com/u/8159402?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@robflop</span>
|
||||
<a href="https://github.com/taichanNE30" target="_blank" :class="$style.contributor">
|
||||
<img src="https://avatars.githubusercontent.com/u/40626578?v=4" :class="$style.contributorAvatar">
|
||||
<span :class="$style.contributorUsername">@taichanNE30</span>
|
||||
</a>
|
||||
</div>
|
||||
<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
||||
<MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
|
||||
@@ -95,6 +94,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div>
|
||||
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
|
||||
</div>
|
||||
<div>
|
||||
<a style="display: inline-block;" class="xserver" title="XServer" href="https://www.xserver.ne.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/xserver.png" alt="XServer"></a>
|
||||
</div>
|
||||
<div>
|
||||
<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
|
||||
</div>
|
||||
|
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<XHeader :actions="headerActions" :tabs="headerTabs" />
|
||||
<XHeader :actions="headerActions" :tabs="headerTabs"/>
|
||||
</template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<MkSwitch :modelValue="publishing" @update:modelValue="onChangePublishing">
|
||||
@@ -14,11 +14,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
<div>
|
||||
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
|
||||
<MkAd v-if="ad.url" :specify="ad" />
|
||||
<MkAd v-if="ad.url" :specify="ad"/>
|
||||
<MkInput v-model="ad.url" type="url">
|
||||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.imageUrl">
|
||||
<MkInput v-model="ad.imageUrl" type="url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="ad.place">
|
||||
@@ -51,8 +51,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span>
|
||||
{{ i18n.ts._ad.timezoneinfo }}
|
||||
<div v-for="(day, index) in daysOfWeek" :key="index">
|
||||
<input :id="`ad${ad.id}-${index}`" type="checkbox" :checked="(ad.dayOfWeek & (1 << index)) !== 0"
|
||||
@change="toggleDayOfWeek(ad, index)">
|
||||
<input
|
||||
:id="`ad${ad.id}-${index}`" type="checkbox" :checked="(ad.dayOfWeek & (1 << index)) !== 0"
|
||||
@change="toggleDayOfWeek(ad, index)"
|
||||
>
|
||||
<label :for="`ad${ad.id}-${index}`">{{ day }}</label>
|
||||
</div>
|
||||
</span>
|
||||
@@ -61,9 +63,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts.memo }}</template>
|
||||
</MkTextarea>
|
||||
<div class="buttons">
|
||||
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i
|
||||
class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton class="button" inline danger @click="remove(ad)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}
|
||||
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)">
|
||||
<i
|
||||
class="ti ti-device-floppy"
|
||||
></i> {{ i18n.ts.save }}
|
||||
</MkButton>
|
||||
<MkButton class="button" inline danger @click="remove(ad)">
|
||||
<i class="ti ti-trash"></i> {{ i18n.ts.remove }}
|
||||
</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,6 +121,7 @@ const onChangePublishing = (v) => {
|
||||
publishing = v;
|
||||
refresh();
|
||||
};
|
||||
|
||||
// 選択された曜日(index)のビットフラグを操作する
|
||||
function toggleDayOfWeek(ad, index) {
|
||||
ad.dayOfWeek ^= 1 << index;
|
||||
@@ -187,6 +194,7 @@ function save(ad) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function more() {
|
||||
os.api('admin/ad/list', { untilId: ads.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => {
|
||||
ads = ads.concat(adsResponse.map(r => {
|
||||
|
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkTextarea v-model="announcement.text">
|
||||
<template #label>{{ i18n.ts.text }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="announcement.imageUrl">
|
||||
<MkInput v-model="announcement.imageUrl" type="url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="announcement.icon">
|
||||
|
@@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="iconUrl">
|
||||
<MkInput v-model="iconUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="app192IconUrl">
|
||||
<MkInput v-model="app192IconUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
|
||||
<template #caption>
|
||||
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="app512IconUrl">
|
||||
<MkInput v-model="app512IconUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
|
||||
<template #caption>
|
||||
@@ -37,27 +37,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="bannerUrl">
|
||||
<MkInput v-model="bannerUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.bannerUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="backgroundImageUrl">
|
||||
<MkInput v-model="backgroundImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="notFoundImageUrl">
|
||||
<MkInput v-model="notFoundImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.notFoundDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="infoImageUrl">
|
||||
<MkInput v-model="infoImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.nothing }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="serverErrorImageUrl">
|
||||
<MkInput v-model="serverErrorImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.somethingHappened }}</template>
|
||||
</MkInput>
|
||||
|
@@ -118,7 +118,7 @@ const menuDef = $computed(() => [{
|
||||
}, {
|
||||
icon: 'ti ti-sparkles',
|
||||
text: i18n.ts.avatarDecorations,
|
||||
to: '/admin/avatar-decorations',
|
||||
to: '/avatar-decorations',
|
||||
active: currentPage?.route.name === 'avatarDecorations',
|
||||
}, {
|
||||
icon: 'ti ti-whirl',
|
||||
|
@@ -20,12 +20,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||
|
||||
<MkInput v-model="tosUrl">
|
||||
<MkInput v-model="tosUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.tosUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="privacyPolicyUrl">
|
||||
<MkInput v-model="privacyPolicyUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
|
||||
</MkInput>
|
||||
|
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
|
||||
|
||||
<template v-if="useObjectStorage">
|
||||
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'">
|
||||
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'" type="url">
|
||||
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
||||
</MkInput>
|
||||
|
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts.color }}</template>
|
||||
</MkColorInput>
|
||||
|
||||
<MkInput v-model="role.iconUrl">
|
||||
<MkInput v-model="role.iconUrl" type="url">
|
||||
<template #label>{{ i18n.ts._role.iconUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
@@ -259,6 +259,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.canManageAvatarDecorations.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canManageAvatarDecorations.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canManageAvatarDecorations)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.canManageAvatarDecorations.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="role.policies.canManageAvatarDecorations.value" :disabled="role.policies.canManageAvatarDecorations.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="role.policies.canManageAvatarDecorations.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])">
|
||||
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
||||
<template #suffix>
|
||||
|
@@ -79,6 +79,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template>
|
||||
<template #suffix>{{ policies.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canManageAvatarDecorations">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
|
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
|
||||
<MkInput v-model="impressumUrl">
|
||||
<MkInput v-model="impressumUrl" type="url">
|
||||
<template #label>{{ i18n.ts.impressumUrl }}</template>
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #caption>{{ i18n.ts.impressumDescription }}</template>
|
||||
|
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps">
|
||||
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
|
||||
@@ -35,7 +35,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
@@ -154,12 +154,9 @@ function save() {
|
||||
|
||||
if (props.channelId) {
|
||||
params.channelId = props.channelId;
|
||||
os.api('channels/update', params).then(() => {
|
||||
os.success();
|
||||
});
|
||||
os.apiWithDialog('channels/update', params);
|
||||
} else {
|
||||
os.api('channels/create', params).then(created => {
|
||||
os.success();
|
||||
os.apiWithDialog('channels/create', params).then(created => {
|
||||
router.push(`/channels/${created.id}`);
|
||||
});
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
<template #label>{{ i18n.ts._play.viewSource }}</template>
|
||||
|
||||
<MkCode :code="flash.script" :inline="false" class="_monospace"/>
|
||||
<MkCode :code="flash.script" lang="is" :inline="false" class="_monospace"/>
|
||||
</MkFolder>
|
||||
<div :class="$style.footer">
|
||||
<Mfm :text="`By @${flash.user.username}`"/>
|
||||
|
@@ -36,8 +36,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
|
||||
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
@@ -171,8 +171,8 @@ async function fetch(): Promise<void> {
|
||||
});
|
||||
suspended = instance.isSuspended;
|
||||
isBlocked = instance.isBlocked;
|
||||
isSilenced = instance.isSilenced;
|
||||
faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview');
|
||||
isSilenced = instance.isSilenced;
|
||||
faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview');
|
||||
}
|
||||
|
||||
async function toggleBlock(): Promise<void> {
|
||||
@@ -183,14 +183,16 @@ async function toggleBlock(): Promise<void> {
|
||||
blockedHosts: isBlocked ? meta.blockedHosts.concat([host]) : meta.blockedHosts.filter(x => x !== host),
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleSilenced(): Promise<void> {
|
||||
if (!meta) throw new Error('No meta?');
|
||||
if (!instance) throw new Error('No instance?');
|
||||
const { host } = instance;
|
||||
await os.api('admin/update-meta', {
|
||||
silencedHosts: isSilenced ? meta.silencedHosts.concat([host]) : meta.silencedHosts.filter(x => x !== host),
|
||||
});
|
||||
if (!meta) throw new Error('No meta?');
|
||||
if (!instance) throw new Error('No instance?');
|
||||
const { host } = instance;
|
||||
await os.api('admin/update-meta', {
|
||||
silencedHosts: isSilenced ? meta.silencedHosts.concat([host]) : meta.silencedHosts.filter(x => x !== host),
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleSuspend(): Promise<void> {
|
||||
if (!instance) throw new Error('No instance?');
|
||||
await os.api('admin/federation/update-instance', {
|
||||
|
@@ -4,46 +4,46 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.editor" class="_panel">
|
||||
<PrismEditor v-model="code" class="_monospace" :class="$style.code" :highlight="highlighter" :lineNumbers="false"/>
|
||||
<MkButton style="position: absolute; top: 8px; right: 8px;" primary @click="run()"><i class="ti ti-player-play"></i></MkButton>
|
||||
</div>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
|
||||
<MkContainer v-if="root && components.length > 1" :key="uiKey" :foldable="true">
|
||||
<template #header>UI</template>
|
||||
<div :class="$style.ui">
|
||||
<MkAsUi :component="root" :components="components" size="small"/>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div :class="$style.root">
|
||||
<div class="_gaps_s">
|
||||
<div :class="$style.editor" class="_panel">
|
||||
<MkCodeEditor v-model="code" lang="aiscript"/>
|
||||
</div>
|
||||
<MkButton primary @click="run()"><i class="ti ti-player-play"></i></MkButton>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<MkContainer :foldable="true" class="">
|
||||
<template #header>{{ i18n.ts.output }}</template>
|
||||
<div :class="$style.logs">
|
||||
<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
|
||||
<MkContainer v-if="root && components.length > 1" :key="uiKey" :foldable="true">
|
||||
<template #header>UI</template>
|
||||
<div :class="$style.ui">
|
||||
<MkAsUi :component="root" :components="components" size="small"/>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<MkContainer :foldable="true" class="">
|
||||
<template #header>{{ i18n.ts.output }}</template>
|
||||
<div :class="$style.logs">
|
||||
<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<div class="">
|
||||
{{ i18n.ts.scratchpadDescription }}
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<div class="">
|
||||
{{ i18n.ts.scratchpadDescription }}
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onDeactivated, onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import 'prismjs';
|
||||
import { highlight, languages } from 'prismjs/components/prism-core';
|
||||
import 'prismjs/components/prism-clike';
|
||||
import 'prismjs/components/prism-javascript';
|
||||
import 'prismjs/themes/prism-okaidia.css';
|
||||
import { PrismEditor } from 'vue-prism-editor';
|
||||
import 'vue-prism-editor/dist/prismeditor.min.css';
|
||||
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
|
||||
import * as os from '@/os.js';
|
||||
import { $i } from '@/account.js';
|
||||
@@ -152,10 +152,6 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
function highlighter(code) {
|
||||
return highlight(code, languages.js, 'javascript');
|
||||
}
|
||||
|
||||
onDeactivated(() => {
|
||||
if (aiscript) aiscript.abort();
|
||||
});
|
||||
|
@@ -151,6 +151,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
|
||||
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
|
||||
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
|
||||
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
|
||||
</div>
|
||||
<MkSelect v-model="serverDisconnectedBehavior">
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||
@@ -253,6 +254,7 @@ const notificationPosition = computed(defaultStore.makeGetterSetter('notificatio
|
||||
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
|
||||
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
|
||||
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
||||
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
|
||||
|
||||
watch(lang, () => {
|
||||
miLocalStorage.setItem('lang', lang.value as string);
|
||||
@@ -289,6 +291,7 @@ watch([
|
||||
reactionsDisplaySize,
|
||||
highlightSensitiveMedia,
|
||||
keepScreenOn,
|
||||
disableStreamingTimeline,
|
||||
], async () => {
|
||||
await reloadAsk();
|
||||
});
|
||||
@@ -298,12 +301,14 @@ const emojiIndexLangs = ['en-US'];
|
||||
function downloadEmojiIndex(lang: string) {
|
||||
async function main() {
|
||||
const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
|
||||
|
||||
function download() {
|
||||
switch (lang) {
|
||||
case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default);
|
||||
default: throw new Error('unrecognized lang: ' + lang);
|
||||
}
|
||||
}
|
||||
|
||||
currentIndexes[lang] = await download();
|
||||
await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
|
||||
}
|
||||
@@ -340,6 +345,7 @@ function removePinnedList() {
|
||||
|
||||
let smashCount = 0;
|
||||
let smashTimer: number | null = null;
|
||||
|
||||
function testNotification(): void {
|
||||
const notification: Misskey.entities.Notification = {
|
||||
id: Math.random().toString(),
|
||||
|
@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkButton inline @click="copy(plugin)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkCode :code="plugin.src ?? ''"/>
|
||||
<MkCode :code="plugin.src ?? ''" lang="is"/>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
|
@@ -108,6 +108,7 @@ async function del(): Promise<void> {
|
||||
|
||||
router.push('/settings/webhook');
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
@@ -47,6 +47,7 @@ import { $i } from '@/account.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { antennasCache, userListsCache } from '@/cache.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
|
||||
@@ -147,27 +148,36 @@ function closeTutorial(): void {
|
||||
defaultStore.set('timelineTutorials', before);
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'ti ti-dots',
|
||||
text: i18n.ts.options,
|
||||
handler: (ev) => {
|
||||
os.popupMenu([{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
icon: 'ti ti-repeat',
|
||||
ref: $$(withRenotes),
|
||||
}, src === 'local' || src === 'social' ? {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
ref: $$(withReplies),
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
icon: 'ti ti-photo',
|
||||
ref: $$(onlyFiles),
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
const headerActions = $computed(() => [
|
||||
...[deviceKind === 'desktop' ? {
|
||||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.reload,
|
||||
handler: (ev) => {
|
||||
console.log('called');
|
||||
tlComponent.reloadTimeline();
|
||||
},
|
||||
} : {}], {
|
||||
icon: 'ti ti-dots',
|
||||
text: i18n.ts.options,
|
||||
handler: (ev) => {
|
||||
os.popupMenu([{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
icon: 'ti ti-repeat',
|
||||
ref: $$(withRenotes),
|
||||
}, src === 'local' || src === 'social' ? {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
ref: $$(withReplies),
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
icon: 'ti ti-photo',
|
||||
ref: $$(onlyFiles),
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
},
|
||||
}]);
|
||||
]);
|
||||
|
||||
const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
|
||||
key: 'list:' + l.id,
|
||||
|
@@ -313,6 +313,10 @@ export const routes = [{
|
||||
}, {
|
||||
path: '/custom-emojis-manager',
|
||||
component: page(() => import('./pages/custom-emojis-manager.vue')),
|
||||
}, {
|
||||
path: '/avatar-decorations',
|
||||
name: 'avatarDecorations',
|
||||
component: page(() => import('./pages/avatar-decorations.vue')),
|
||||
}, {
|
||||
path: '/registry/keys/system/:path(*)?',
|
||||
component: page(() => import('./pages/registry.keys.vue')),
|
||||
@@ -350,7 +354,7 @@ export const routes = [{
|
||||
}, {
|
||||
path: '/avatar-decorations',
|
||||
name: 'avatarDecorations',
|
||||
component: page(() => import('./pages/admin/avatar-decorations.vue')),
|
||||
component: page(() => import('./pages/avatar-decorations.vue')),
|
||||
}, {
|
||||
path: '/queue',
|
||||
name: 'queue',
|
||||
|
31
packages/frontend/src/scripts/code-highlighter.ts
Normal file
31
packages/frontend/src/scripts/code-highlighter.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { setWasm, setCDN, Highlighter, getHighlighter as _getHighlighter } from 'shiki';
|
||||
|
||||
setWasm('/assets/shiki/dist/onig.wasm');
|
||||
setCDN('/assets/shiki/');
|
||||
|
||||
let _highlighter: Highlighter | null = null;
|
||||
|
||||
export async function getHighlighter(): Promise<Highlighter> {
|
||||
if (!_highlighter) {
|
||||
return await initHighlighter();
|
||||
}
|
||||
return _highlighter;
|
||||
}
|
||||
|
||||
export async function initHighlighter() {
|
||||
const highlighter = await _getHighlighter({
|
||||
theme: 'dark-plus',
|
||||
langs: ['js'],
|
||||
});
|
||||
|
||||
await highlighter.loadLanguage({
|
||||
path: 'languages/aiscript.tmLanguage.json',
|
||||
id: 'aiscript',
|
||||
scopeName: 'source.aiscript',
|
||||
aliases: ['is', 'ais'],
|
||||
});
|
||||
|
||||
_highlighter = highlighter;
|
||||
|
||||
return highlighter;
|
||||
}
|
@@ -56,6 +56,7 @@ function copyUrl(file: Misskey.entities.DriveFile) {
|
||||
copyToClipboard(file.url);
|
||||
os.success();
|
||||
}
|
||||
|
||||
/*
|
||||
function addApp() {
|
||||
alert('not implemented yet');
|
||||
|
@@ -114,6 +114,12 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||
return !confirm.canceled;
|
||||
}
|
||||
|
||||
async function userInfoUpdate() {
|
||||
os.apiWithDialog('federation/update-remote-user', {
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
async function invalidateFollow() {
|
||||
if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return;
|
||||
|
||||
@@ -330,6 +336,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||
}]);
|
||||
}
|
||||
|
||||
if (user.host !== null) {
|
||||
menu = menu.concat([null, {
|
||||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.updateRemoteUser,
|
||||
action: userInfoUpdate,
|
||||
}]);
|
||||
}
|
||||
|
||||
if (defaultStore.state.devMode) {
|
||||
menu = menu.concat([null, {
|
||||
icon: 'ti ti-id',
|
||||
|
@@ -46,6 +46,7 @@ export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, o
|
||||
};
|
||||
|
||||
function removeListener() { container.removeEventListener('scroll', onScroll); }
|
||||
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
return removeListener;
|
||||
}
|
||||
@@ -71,6 +72,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
|
||||
function removeListener() {
|
||||
containerOrWindow.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
|
||||
containerOrWindow.addEventListener('scroll', onScroll, { passive: true });
|
||||
return removeListener;
|
||||
}
|
||||
|
@@ -71,9 +71,11 @@ export class WorkerMultiDispatch<POST = any, RETURN = any> {
|
||||
public isTerminated() {
|
||||
return this.terminated;
|
||||
}
|
||||
|
||||
public getWorkers() {
|
||||
return this.workers;
|
||||
}
|
||||
|
||||
public getSymbol() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
@@ -374,6 +374,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'account',
|
||||
default: false,
|
||||
},
|
||||
disableStreamingTimeline: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
@@ -9,6 +9,9 @@ import { $i } from '@/account.js';
|
||||
import { url } from '@/config.js';
|
||||
|
||||
let stream: Misskey.Stream | null = null;
|
||||
let timeoutHeartBeat: number | null = null;
|
||||
|
||||
export let isReloading: boolean = false;
|
||||
|
||||
export function useStream(): Misskey.Stream {
|
||||
if (stream) return stream;
|
||||
@@ -17,7 +20,20 @@ export function useStream(): Misskey.Stream {
|
||||
token: $i.token,
|
||||
} : null));
|
||||
|
||||
window.setTimeout(heartbeat, 1000 * 60);
|
||||
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
export function reloadStream() {
|
||||
if (!stream) return useStream();
|
||||
if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat);
|
||||
isReloading = true;
|
||||
|
||||
stream.close();
|
||||
stream.once('_connected_', () => isReloading = false);
|
||||
stream.stream.reconnect();
|
||||
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||
|
||||
return stream;
|
||||
}
|
||||
@@ -26,5 +42,5 @@ function heartbeat(): void {
|
||||
if (stream != null && document.visibilityState === 'visible') {
|
||||
stream.heartbeat();
|
||||
}
|
||||
window.setTimeout(heartbeat, 1000 * 60);
|
||||
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||
}
|
||||
|
@@ -400,10 +400,6 @@ hr {
|
||||
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important;
|
||||
}
|
||||
|
||||
.prism-editor__textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
._zoom {
|
||||
transition-duration: 0.5s, 0.5s;
|
||||
transition-property: opacity, transform;
|
||||
|
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Theme } from '@/scripts/theme.js';
|
||||
import { Theme, getBuiltinThemes } from '@/scripts/theme.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { api } from '@/os.js';
|
||||
import { $i } from '@/account.js';
|
||||
@@ -29,6 +29,10 @@ export async function fetchThemes(): Promise<void> {
|
||||
|
||||
export async function addTheme(theme: Theme): Promise<void> {
|
||||
if ($i == null) return;
|
||||
const builtinThemes = await getBuiltinThemes();
|
||||
if (builtinThemes.some(t => t.id === theme.id)) {
|
||||
throw new Error('builtin theme');
|
||||
}
|
||||
await fetchThemes();
|
||||
const themes = getThemes().concat(theme);
|
||||
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
|
||||
|
@@ -68,6 +68,11 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||
to: '/custom-emojis-manager',
|
||||
text: i18n.ts.manageCustomEmojis,
|
||||
icon: 'ti ti-icons',
|
||||
} : undefined, ($i && ($i.isAdmin || $i.policies.canManageAvatarDecorations)) ? {
|
||||
type: 'link',
|
||||
to: '/avatar-decorations',
|
||||
text: i18n.ts.manageAvatarDecorations,
|
||||
icon: 'ti ti-sparkles',
|
||||
} : undefined],
|
||||
}, null, (instance.impressumUrl) ? {
|
||||
text: i18n.ts.impressum,
|
||||
|
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted } from 'vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { useStream, isReloading } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
@@ -26,6 +26,7 @@ const zIndex = os.claimZIndex('high');
|
||||
let hasDisconnected = $ref(false);
|
||||
|
||||
function onDisconnected() {
|
||||
if (isReloading) return;
|
||||
hasDisconnected = true;
|
||||
}
|
||||
|
||||
|
@@ -319,7 +319,7 @@ $widgets-hide-threshold: 1090px;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior: contain;
|
||||
overscroll-behavior: none;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user