Merge branch 'notification-read-api' into swn

This commit is contained in:
tamaina 2022-01-01 23:04:36 +09:00
commit 7f8e3b0dee
60 changed files with 511 additions and 468 deletions

View file

@ -10,6 +10,12 @@
--> -->
## 12.101.1 (2021/12/29)
### Bugfixes
- SVG絵文字が表示できないのを修正
- エクスポートした絵文字の拡張子がfalseになることがあるのを修正
## 12.101.0 (2021/12/29) ## 12.101.0 (2021/12/29)
### Improvements ### Improvements

View file

@ -87,7 +87,7 @@ Configuration files are located in [`/.github/workflows`](/.github/workflows).
## Vue ## Vue
Misskey uses Vue(v3) as its front-end framework. Misskey uses Vue(v3) as its front-end framework.
**When creating a new component, please use the Composition API (and [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html)) instead of the Options API.** **When creating a new component, please use the Composition API (with [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html) and [ref sugar](https://github.com/vuejs/rfcs/discussions/369)) instead of the Options API.**
Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome. Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome.
## Adding MisskeyRoom items ## Adding MisskeyRoom items

View file

@ -743,7 +743,7 @@ online: "オンライン"
active: "アクティブ" active: "アクティブ"
offline: "オフライン" offline: "オフライン"
notRecommended: "非推奨" notRecommended: "非推奨"
botProtection: "Bot防御" botProtection: "Botプロテクション"
instanceBlocking: "インスタンスブロック" instanceBlocking: "インスタンスブロック"
selectAccount: "アカウントを選択" selectAccount: "アカウントを選択"
enabled: "有効" enabled: "有効"
@ -754,7 +754,7 @@ administration: "管理"
accounts: "アカウント" accounts: "アカウント"
switch: "切り替え" switch: "切り替え"
noMaintainerInformationWarning: "管理者情報が設定されていません。" noMaintainerInformationWarning: "管理者情報が設定されていません。"
noBotProtectionWarning: "Bot防御が設定されていません。" noBotProtectionWarning: "Botプロテクションが設定されていません。"
configure: "設定する" configure: "設定する"
postToGallery: "ギャラリーへ投稿" postToGallery: "ギャラリーへ投稿"
gallery: "ギャラリー" gallery: "ギャラリー"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "12.101.0", "version": "12.101.1",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -65,7 +65,8 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
for (const emoji of customEmojis) { for (const emoji of customEmojis) {
const exportId = ulid().toLowerCase(); const exportId = ulid().toLowerCase();
const emojiPath = path + '/' + exportId + '.' + mime.extension(emoji.type); const ext = mime.extension(emoji.type);
const emojiPath = path + '/' + exportId + (ext ? '.' + ext : '');
fs.writeFileSync(emojiPath, '', 'binary'); fs.writeFileSync(emojiPath, '', 'binary');
let downloaded = false; let downloaded = false;

View file

@ -19,6 +19,7 @@ export async function proxyMedia(ctx: Koa.Context) {
const { mime, ext } = await detectType(path); const { mime, ext } = await detectType(path);
if (!mime.startsWith('image/')) throw 403;
if (!FILE_TYPE_BROWSERSAFE.includes(mime)) throw 403; if (!FILE_TYPE_BROWSERSAFE.includes(mime)) throw 403;
let image: IImage; let image: IImage;

View file

@ -161,7 +161,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
} }
} }
if (!['image/jpeg', 'image/png', 'image/webp'].includes(type)) { if (!['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'].includes(type)) {
logger.debug(`web image and thumbnail not created (not an required file)`); logger.debug(`web image and thumbnail not created (not an required file)`);
return { return {
webpublic: null, webpublic: null,
@ -202,7 +202,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
webpublic = await convertSharpToJpeg(img, 2048, 2048); webpublic = await convertSharpToJpeg(img, 2048, 2048);
} else if (['image/webp'].includes(type)) { } else if (['image/webp'].includes(type)) {
webpublic = await convertSharpToWebp(img, 2048, 2048); webpublic = await convertSharpToWebp(img, 2048, 2048);
} else if (['image/png'].includes(type)) { } else if (['image/png', 'image/svg+xml'].includes(type)) {
webpublic = await convertSharpToPng(img, 2048, 2048); webpublic = await convertSharpToPng(img, 2048, 2048);
} else { } else {
logger.debug(`web image not created (not an required image)`); logger.debug(`web image not created (not an required image)`);
@ -221,7 +221,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
try { try {
if (['image/jpeg', 'image/webp'].includes(type)) { if (['image/jpeg', 'image/webp'].includes(type)) {
thumbnail = await convertSharpToJpeg(img, 498, 280); thumbnail = await convertSharpToJpeg(img, 498, 280);
} else if (['image/png'].includes(type)) { } else if (['image/png', 'image/svg+xml'].includes(type)) {
thumbnail = await convertSharpToPngOrJpeg(img, 498, 280); thumbnail = await convertSharpToPngOrJpeg(img, 498, 280);
} else { } else {
logger.debug(`thumbnail not created (not an required file)`); logger.debug(`thumbnail not created (not an required file)`);

View file

@ -1,3 +1,5 @@
/// <reference types="vue/macros-global" />
declare module '*.vue' { declare module '*.vue' {
import type { DefineComponent } from 'vue'; import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>; const component: DefineComponent<{}, {}, any>;

View file

@ -116,7 +116,7 @@
"v-debounce": "0.1.2", "v-debounce": "0.1.2",
"vanilla-tilt": "1.7.2", "vanilla-tilt": "1.7.2",
"vue": "3.2.26", "vue": "3.2.26",
"vue-loader": "16.8.3", "vue-loader": "17.0.0",
"vue-prism-editor": "2.0.0-alpha.2", "vue-prism-editor": "2.0.0-alpha.2",
"vue-router": "4.0.5", "vue-router": "4.0.5",
"vue-style-loader": "4.1.3", "vue-style-loader": "4.1.3",

View file

@ -53,6 +53,7 @@ import XFolder from './drive.folder.vue';
import XFile from './drive.file.vue'; import XFile from './drive.file.vue';
import MkButton from './ui/button.vue'; import MkButton from './ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -140,7 +141,7 @@ export default defineComponent({
}); });
} }
this.connection = markRaw(os.stream.useChannel('drive')); this.connection = markRaw(stream.useChannel('drive'));
this.connection.on('fileCreated', this.onStreamDriveFileCreated); this.connection.on('fileCreated', this.onStreamDriveFileCreated);
this.connection.on('fileUpdated', this.onStreamDriveFileUpdated); this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);

View file

@ -30,6 +30,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, markRaw } from 'vue'; import { defineComponent, markRaw } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
export default defineComponent({ export default defineComponent({
props: { props: {
@ -71,7 +72,7 @@ export default defineComponent({
}, },
mounted() { mounted() {
this.connection = markRaw(os.stream.useChannel('main')); this.connection = markRaw(stream.useChannel('main'));
this.connection.on('follow', this.onFollowChange); this.connection.on('follow', this.onFollowChange);
this.connection.on('unfollow', this.onFollowChange); this.connection.on('unfollow', this.onFollowChange);

View file

@ -7,12 +7,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue';
export default defineComponent({
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -0,0 +1,27 @@
<template>
<div class="terlnhxf _formBlock">
<slot></slot>
</div>
</template>
<script lang="ts" setup>
const props = withDefaults(defineProps<{
minWidth: number;
}>(), {
minWidth: 210,
});
const minWidth = props.minWidth + 'px';
</script>
<style lang="scss" scoped>
.terlnhxf {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(v-bind('minWidth'), 1fr));
grid-gap: 12px;
> ::v-deep(*) {
margin: 0 !important;
}
}
</style>

View file

@ -13,7 +13,8 @@
<i class="check fas fa-check"></i> <i class="check fas fa-check"></i>
</span> </span>
<span class="label"> <span class="label">
<span @click="toggle"><slot></slot></span> <!-- TODO: 無名slotの方は廃止 -->
<span @click="toggle"><slot name="label"></slot><slot></slot></span>
<p class="caption"><slot name="caption"></slot></p> <p class="caption"><slot name="caption"></slot></p>
</span> </span>
</div> </div>

View file

@ -140,6 +140,7 @@ import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import { notePage } from '@/filters/note'; import { notePage } from '@/filters/note';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import { noteActions, noteViewInterruptors } from '@/store'; import { noteActions, noteViewInterruptors } from '@/store';
import { reactionPicker } from '@/scripts/reaction-picker'; import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
@ -260,7 +261,7 @@ export default defineComponent({
async created() { async created() {
if (this.$i) { if (this.$i) {
this.connection = os.stream; this.connection = stream;
} }
this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords); this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);

View file

@ -122,6 +122,7 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
import { checkWordMute } from '@/scripts/check-word-mute'; import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import { noteActions, noteViewInterruptors } from '@/store'; import { noteActions, noteViewInterruptors } from '@/store';
import { reactionPicker } from '@/scripts/reaction-picker'; import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
@ -245,7 +246,7 @@ export default defineComponent({
async created() { async created() {
if (this.$i) { if (this.$i) {
this.connection = os.stream; this.connection = stream;
} }
this.collapsed = this.appearNote.cw == null && this.appearNote.text && ( this.collapsed = this.appearNote.cw == null && this.appearNote.text && (

View file

@ -74,6 +74,7 @@ import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import { useTooltip } from '@/scripts/use-tooltip'; import { useTooltip } from '@/scripts/use-tooltip';
export default defineComponent({ export default defineComponent({
@ -106,7 +107,7 @@ export default defineComponent({
if (!props.notification.isRead) { if (!props.notification.isRead) {
const readObserver = new IntersectionObserver((entries, observer) => { const readObserver = new IntersectionObserver((entries, observer) => {
if (!entries.some(entry => entry.isIntersecting)) return; if (!entries.some(entry => entry.isIntersecting)) return;
os.stream.send('readNotification', { stream.send('readNotification', {
id: props.notification.id id: props.notification.id
}); });
observer.disconnect(); observer.disconnect();
@ -114,7 +115,7 @@ export default defineComponent({
readObserver.observe(elRef.value); readObserver.observe(elRef.value);
const connection = os.stream.useChannel('main'); const connection = stream.useChannel('main');
connection.on('readAllNotifications', () => readObserver.disconnect()); connection.on('readAllNotifications', () => readObserver.disconnect());
watch(props.notification.isRead, () => { watch(props.notification.isRead, () => {

View file

@ -28,6 +28,7 @@ import XList from './date-separated-list.vue';
import XNote from './note.vue'; import XNote from './note.vue';
import { notificationTypes } from 'misskey-js'; import { notificationTypes } from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
export default defineComponent({ export default defineComponent({
@ -100,7 +101,7 @@ export default defineComponent({
}, },
mounted() { mounted() {
this.connection = markRaw(os.stream.useChannel('main')); this.connection = markRaw(stream.useChannel('main'));
this.connection.on('notification', this.onNotification); this.connection.on('notification', this.onNotification);
this.connection.on('readAllNotifications', () => { this.connection.on('readAllNotifications', () => {
@ -133,7 +134,7 @@ export default defineComponent({
onNotification(notification) { onNotification(notification) {
const isMuted = !this.allIncludeTypes.includes(notification.type); const isMuted = !this.allIncludeTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') { if (isMuted || document.visibilityState === 'visible') {
os.stream.send('readNotification', { stream.send('readNotification', {
id: notification.id id: notification.id
}); });
} }

View file

@ -74,11 +74,11 @@ import { formatTimeString } from '@/scripts/format-time-string';
import { Autocomplete } from '@/scripts/autocomplete'; import { Autocomplete } from '@/scripts/autocomplete';
import { noteVisibilities } from 'misskey-js'; import { noteVisibilities } from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import { selectFiles } from '@/scripts/select-file'; import { selectFiles } from '@/scripts/select-file';
import { defaultStore, notePostInterruptors, postFormActions } from '@/store'; import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
import { throttle } from 'throttle-debounce'; import { throttle } from 'throttle-debounce';
import MkInfo from '@/components/ui/info.vue'; import MkInfo from '@/components/ui/info.vue';
import { defaultStore } from '@/store';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -176,7 +176,7 @@ export default defineComponent({
imeText: '', imeText: '',
typing: throttle(3000, () => { typing: throttle(3000, () => {
if (this.channel) { if (this.channel) {
os.stream.send('typingOnChannel', { channel: this.channel.id }); stream.send('typingOnChannel', { channel: this.channel.id });
} }
}), }),
postFormActions, postFormActions,

View file

@ -83,6 +83,7 @@ import MkTab from '@/components/tab.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import follow from '@/directives/follow-append'; import follow from '@/directives/follow-append';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -104,15 +105,15 @@ export default defineComponent({
const connections = shallowRef([]); const connections = shallowRef([]);
const pools = shallowRef([]); const pools = shallowRef([]);
const refreshStreamInfo = () => { const refreshStreamInfo = () => {
console.log(os.stream.sharedConnectionPools, os.stream.sharedConnections, os.stream.nonSharedConnections); console.log(stream.sharedConnectionPools, stream.sharedConnections, stream.nonSharedConnections);
const conn = os.stream.sharedConnections.map(c => ({ const conn = stream.sharedConnections.map(c => ({
id: c.id, name: c.name, channel: c.channel, users: c.pool.users, in: c.inCount, out: c.outCount, id: c.id, name: c.name, channel: c.channel, users: c.pool.users, in: c.inCount, out: c.outCount,
})).concat(os.stream.nonSharedConnections.map(c => ({ })).concat(stream.nonSharedConnections.map(c => ({
id: c.id, name: c.name, channel: c.channel, users: null, in: c.inCount, out: c.outCount, id: c.id, name: c.name, channel: c.channel, users: null, in: c.inCount, out: c.outCount,
}))); })));
conn.sort((a, b) => (a.id > b.id) ? 1 : -1); conn.sort((a, b) => (a.id > b.id) ? 1 : -1);
connections.value = conn; connections.value = conn;
pools.value = os.stream.sharedConnectionPools; pools.value = stream.sharedConnectionPools;
}; };
const interval = setInterval(refreshStreamInfo, 1000); const interval = setInterval(refreshStreamInfo, 1000);
onBeforeUnmount(() => { onBeforeUnmount(() => {

View file

@ -6,6 +6,7 @@
import { defineComponent, markRaw } from 'vue'; import { defineComponent, markRaw } from 'vue';
import XNotes from './notes.vue'; import XNotes from './notes.vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import * as sound from '@/scripts/sound'; import * as sound from '@/scripts/sound';
export default defineComponent({ export default defineComponent({
@ -92,33 +93,33 @@ export default defineComponent({
this.query = { this.query = {
antennaId: this.antenna antennaId: this.antenna
}; };
this.connection = markRaw(os.stream.useChannel('antenna', { this.connection = markRaw(stream.useChannel('antenna', {
antennaId: this.antenna antennaId: this.antenna
})); }));
this.connection.on('note', prepend); this.connection.on('note', prepend);
} else if (this.src == 'home') { } else if (this.src == 'home') {
endpoint = 'notes/timeline'; endpoint = 'notes/timeline';
this.connection = markRaw(os.stream.useChannel('homeTimeline')); this.connection = markRaw(stream.useChannel('homeTimeline'));
this.connection.on('note', prepend); this.connection.on('note', prepend);
this.connection2 = markRaw(os.stream.useChannel('main')); this.connection2 = markRaw(stream.useChannel('main'));
this.connection2.on('follow', onChangeFollowing); this.connection2.on('follow', onChangeFollowing);
this.connection2.on('unfollow', onChangeFollowing); this.connection2.on('unfollow', onChangeFollowing);
} else if (this.src == 'local') { } else if (this.src == 'local') {
endpoint = 'notes/local-timeline'; endpoint = 'notes/local-timeline';
this.connection = markRaw(os.stream.useChannel('localTimeline')); this.connection = markRaw(stream.useChannel('localTimeline'));
this.connection.on('note', prepend); this.connection.on('note', prepend);
} else if (this.src == 'social') { } else if (this.src == 'social') {
endpoint = 'notes/hybrid-timeline'; endpoint = 'notes/hybrid-timeline';
this.connection = markRaw(os.stream.useChannel('hybridTimeline')); this.connection = markRaw(stream.useChannel('hybridTimeline'));
this.connection.on('note', prepend); this.connection.on('note', prepend);
} else if (this.src == 'global') { } else if (this.src == 'global') {
endpoint = 'notes/global-timeline'; endpoint = 'notes/global-timeline';
this.connection = markRaw(os.stream.useChannel('globalTimeline')); this.connection = markRaw(stream.useChannel('globalTimeline'));
this.connection.on('note', prepend); this.connection.on('note', prepend);
} else if (this.src == 'mentions') { } else if (this.src == 'mentions') {
endpoint = 'notes/mentions'; endpoint = 'notes/mentions';
this.connection = markRaw(os.stream.useChannel('main')); this.connection = markRaw(stream.useChannel('main'));
this.connection.on('mention', prepend); this.connection.on('mention', prepend);
} else if (this.src == 'directs') { } else if (this.src == 'directs') {
endpoint = 'notes/mentions'; endpoint = 'notes/mentions';
@ -130,14 +131,14 @@ export default defineComponent({
prepend(note); prepend(note);
} }
}; };
this.connection = markRaw(os.stream.useChannel('main')); this.connection = markRaw(stream.useChannel('main'));
this.connection.on('mention', onNote); this.connection.on('mention', onNote);
} else if (this.src == 'list') { } else if (this.src == 'list') {
endpoint = 'notes/user-list-timeline'; endpoint = 'notes/user-list-timeline';
this.query = { this.query = {
listId: this.list listId: this.list
}; };
this.connection = markRaw(os.stream.useChannel('userList', { this.connection = markRaw(stream.useChannel('userList', {
listId: this.list listId: this.list
})); }));
this.connection.on('note', prepend); this.connection.on('note', prepend);
@ -148,7 +149,7 @@ export default defineComponent({
this.query = { this.query = {
channelId: this.channel channelId: this.channel
}; };
this.connection = markRaw(os.stream.useChannel('channel', { this.connection = markRaw(stream.useChannel('channel', {
channelId: this.channel channelId: this.channel
})); }));
this.connection.on('note', prepend); this.connection.on('note', prepend);

View file

@ -1,5 +1,5 @@
<template> <template>
<XModalWindow ref="dialog" <XModalWindow ref="dialogEl"
:with-ok-button="true" :with-ok-button="true"
:ok-button-disabled="selected == null" :ok-button-disabled="selected == null"
@click="cancel()" @click="cancel()"
@ -8,20 +8,20 @@
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<template #header>{{ $ts.selectUser }}</template> <template #header>{{ $ts.selectUser }}</template>
<div class="tbhwbxda _monolithic_"> <div class="tbhwbxda">
<div class="_section"> <div class="form">
<div class="_inputSplit"> <FormSplit :min-width="170">
<MkInput ref="username" v-model="username" class="input" @update:modelValue="search"> <MkInput ref="usernameEl" v-model="username" @update:modelValue="search">
<template #label>{{ $ts.username }}</template> <template #label>{{ $ts.username }}</template>
<template #prefix>@</template> <template #prefix>@</template>
</MkInput> </MkInput>
<MkInput v-model="host" class="input" @update:modelValue="search"> <MkInput v-model="host" @update:modelValue="search">
<template #label>{{ $ts.host }}</template> <template #label>{{ $ts.host }}</template>
<template #prefix>@</template> <template #prefix>@</template>
</MkInput> </MkInput>
</FormSplit>
</div> </div>
</div> <div v-if="username != '' || host != ''" class="result" :class="{ hit: users.length > 0 }">
<div v-if="username != '' || host != ''" class="_section result" :class="{ hit: users.length > 0 }">
<div v-if="users.length > 0" class="users"> <div v-if="users.length > 0" class="users">
<div v-for="user in users" :key="user.id" class="user" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()"> <div v-for="user in users" :key="user.id" class="user" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/> <MkAvatar :user="user" class="avatar" :show-indicator="true"/>
@ -35,7 +35,7 @@
<span>{{ $ts.noUsers }}</span> <span>{{ $ts.noUsers }}</span>
</div> </div>
</div> </div>
<div v-if="username == '' && host == ''" class="_section recent"> <div v-if="username == '' && host == ''" class="recent">
<div class="users"> <div class="users">
<div v-for="user in recentUsers" :key="user.id" class="user" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()"> <div v-for="user in recentUsers" :key="user.id" class="user" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/> <MkAvatar :user="user" class="avatar" :show-indicator="true"/>
@ -50,87 +50,89 @@
</XModalWindow> </XModalWindow>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { nextTick, onMounted } from 'vue';
import MkInput from './form/input.vue'; import * as misskey from 'misskey-js';
import MkInput from '@/components/form/input.vue';
import FormSplit from '@/components/form/split.vue';
import XModalWindow from '@/components/ui/modal-window.vue'; import XModalWindow from '@/components/ui/modal-window.vue';
import * as os from '@/os'; import * as os from '@/os';
import { defaultStore } from '@/store';
export default defineComponent({ const emit = defineEmits<{
components: { (e: 'ok', selected: misskey.entities.UserDetailed): void;
MkInput, (e: 'cancel'): void;
XModalWindow, (e: 'closed'): void;
}, }>();
props: { let username = $ref('');
}, let host = $ref('');
let users: misskey.entities.UserDetailed[] = $ref([]);
let recentUsers: misskey.entities.UserDetailed[] = $ref([]);
let selected: misskey.entities.UserDetailed | null = $ref(null);
let usernameEl: HTMLElement = $ref();
let dialogEl = $ref();
emits: ['ok', 'cancel', 'closed'], const focus = () => {
if (usernameEl) {
usernameEl.focus();
}
};
data() { const search = () => {
return { if (username === '' && host === '') {
username: '', users = [];
host: '',
recentUsers: [],
users: [],
selected: null,
};
},
async mounted() {
this.focus();
this.$nextTick(() => {
this.focus();
});
this.recentUsers = await os.api('users/show', {
userIds: this.$store.state.recentlyUsedUsers
});
},
methods: {
search() {
if (this.username == '' && this.host == '') {
this.users = [];
return; return;
} }
os.api('users/search-by-username-and-host', { os.api('users/search-by-username-and-host', {
username: this.username, username: username,
host: this.host, host: host,
limit: 10, limit: 10,
detail: false detail: false
}).then(users => { }).then(_users => {
this.users = users; users = _users;
}); });
}, };
focus() { const ok = () => {
this.$refs.username.focus(); if (selected == null) return;
}, emit('ok', selected);
dialogEl.close();
ok() {
this.$emit('ok', this.selected);
this.$refs.dialog.close();
// 使 // 使
let recents = this.$store.state.recentlyUsedUsers; let recents = defaultStore.state.recentlyUsedUsers;
recents = recents.filter(x => x !== this.selected.id); recents = recents.filter(x => x !== selected.id);
recents.unshift(this.selected.id); recents.unshift(selected.id);
this.$store.set('recentlyUsedUsers', recents.splice(0, 16)); defaultStore.set('recentlyUsedUsers', recents.splice(0, 16));
}, };
cancel() { const cancel = () => {
this.$emit('cancel'); emit('cancel');
this.$refs.dialog.close(); dialogEl.close();
}, };
}
onMounted(() => {
focus();
nextTick(() => {
focus();
});
os.api('users/show', {
userIds: defaultStore.state.recentlyUsedUsers,
}).then(users => {
recentUsers = users;
});
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.tbhwbxda { .tbhwbxda {
> ._section { > .form {
padding: 0 var(--root-margin);
}
> .result, > .recent {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: auto; overflow: auto;

View file

@ -26,7 +26,8 @@ import { router } from '@/router';
import { applyTheme } from '@/scripts/theme'; import { applyTheme } from '@/scripts/theme';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { stream, confirm, alert, post, popup, toast } from '@/os'; import { confirm, alert, post, popup, toast } from '@/os';
import { stream } from '@/stream';
import * as sound from '@/scripts/sound'; import * as sound from '@/scripts/sound';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
import { defaultStore, ColdDeviceStorage } from '@/store'; import { defaultStore, ColdDeviceStorage } from '@/store';

View file

@ -12,8 +12,6 @@ import { resolve } from '@/router';
import { $i } from '@/account'; import { $i } from '@/account';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
export const stream = markRaw(new Misskey.Stream(url, $i));
export const pendingApiRequestsCount = ref(0); export const pendingApiRequestsCount = ref(0);
let apiRequestsCount = 0; // for debug let apiRequestsCount = 0; // for debug
export const apiRequests = ref([]); // for debug export const apiRequests = ref([]); // for debug

View file

@ -24,7 +24,7 @@
</FormSection> </FormSection>
<FormSection> <FormSection>
<div class="_inputSplit _formBlock"> <FormSplit>
<MkKeyValue class="_formBlock"> <MkKeyValue class="_formBlock">
<template #key>{{ $ts.administrator }}</template> <template #key>{{ $ts.administrator }}</template>
<template #value>{{ $instance.maintainerName }}</template> <template #value>{{ $instance.maintainerName }}</template>
@ -33,14 +33,14 @@
<template #key>{{ $ts.contact }}</template> <template #key>{{ $ts.contact }}</template>
<template #value>{{ $instance.maintainerEmail }}</template> <template #value>{{ $instance.maintainerEmail }}</template>
</MkKeyValue> </MkKeyValue>
</div> </FormSplit>
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink> <FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink>
</FormSection> </FormSection>
<FormSuspense :p="initStats"> <FormSuspense :p="initStats">
<FormSection> <FormSection>
<template #label>{{ $ts.statistics }}</template> <template #label>{{ $ts.statistics }}</template>
<div class="_inputSplit"> <FormSplit>
<MkKeyValue class="_formBlock"> <MkKeyValue class="_formBlock">
<template #key>{{ $ts.users }}</template> <template #key>{{ $ts.users }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template> <template #value>{{ number(stats.originalUsersCount) }}</template>
@ -49,7 +49,7 @@
<template #key>{{ $ts.notes }}</template> <template #key>{{ $ts.notes }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template> <template #value>{{ number(stats.originalNotesCount) }}</template>
</MkKeyValue> </MkKeyValue>
</div> </FormSplit>
</FormSection> </FormSection>
</FormSuspense> </FormSuspense>
@ -73,6 +73,7 @@ import { version, instanceName } from '@/config';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import MkKeyValue from '@/components/key-value.vue'; import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os'; import * as os from '@/os';
import number from '@/filters/number'; import number from '@/filters/number';
@ -85,6 +86,7 @@ export default defineComponent({
FormSection, FormSection,
FormLink, FormLink,
FormSuspense, FormSuspense,
FormSplit,
}, },
data() { data() {

View file

@ -23,14 +23,14 @@
<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio> <MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
</div> </div>
--> -->
<div class="_inputSplit"> <FormSplit>
<MkInput v-model="ad.ratio" type="number"> <MkInput v-model="ad.ratio" type="number">
<template #label>{{ $ts.ratio }}</template> <template #label>{{ $ts.ratio }}</template>
</MkInput> </MkInput>
<MkInput v-model="ad.expiresAt" type="date"> <MkInput v-model="ad.expiresAt" type="date">
<template #label>{{ $ts.expiration }}</template> <template #label>{{ $ts.expiration }}</template>
</MkInput> </MkInput>
</div> </FormSplit>
<MkTextarea v-model="ad.memo" class="_formBlock"> <MkTextarea v-model="ad.memo" class="_formBlock">
<template #label>{{ $ts.memo }}</template> <template #label>{{ $ts.memo }}</template>
</MkTextarea> </MkTextarea>
@ -49,6 +49,7 @@ import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue'; import MkTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue'; import FormRadios from '@/components/form/radios.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
@ -58,6 +59,7 @@ export default defineComponent({
MkInput, MkInput,
MkTextarea, MkTextarea,
FormRadios, FormRadios,
FormSplit,
}, },
emits: ['info'], emits: ['info'],

View file

@ -1,50 +1,55 @@
<template> <template>
<FormBase> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<FormSwitch v-model="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch> <div class="_formRoot">
<FormSwitch v-model="enableEmail" class="_formBlock">
<template #label>{{ $ts.enableEmail }}</template>
<template #caption>{{ $ts.emailConfigInfo }}</template>
</FormSwitch>
<template v-if="enableEmail"> <template v-if="enableEmail">
<FormInput v-model="email" type="email"> <FormInput v-model="email" type="email" class="_formBlock">
<span>{{ $ts.emailAddress }}</span> <template #label>{{ $ts.emailAddress }}</template>
</FormInput> </FormInput>
<div v-sticky-container class="_debobigegoItem _debobigegoNoConcat"> <FormSection>
<div class="_debobigegoLabel">{{ $ts.smtpConfig }}</div> <template #label>{{ $ts.smtpConfig }}</template>
<div class="main"> <FormSplit :min-width="280">
<FormInput v-model="smtpHost"> <FormInput v-model="smtpHost" class="_formBlock">
<span>{{ $ts.smtpHost }}</span> <template #label>{{ $ts.smtpHost }}</template>
</FormInput> </FormInput>
<FormInput v-model="smtpPort" type="number"> <FormInput v-model="smtpPort" type="number" class="_formBlock">
<span>{{ $ts.smtpPort }}</span> <template #label>{{ $ts.smtpPort }}</template>
</FormInput> </FormInput>
<FormInput v-model="smtpUser"> </FormSplit>
<span>{{ $ts.smtpUser }}</span> <FormSplit :min-width="280">
<FormInput v-model="smtpUser" class="_formBlock">
<template #label>{{ $ts.smtpUser }}</template>
</FormInput> </FormInput>
<FormInput v-model="smtpPass" type="password"> <FormInput v-model="smtpPass" type="password" class="_formBlock">
<span>{{ $ts.smtpPass }}</span> <template #label>{{ $ts.smtpPass }}</template>
</FormInput> </FormInput>
<FormInfo>{{ $ts.emptyToDisableSmtpAuth }}</FormInfo> </FormSplit>
<FormSwitch v-model="smtpSecure">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></FormSwitch> <FormInfo class="_formBlock">{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
</div> <FormSwitch v-model="smtpSecure" class="_formBlock">
</div> <template #label>{{ $ts.smtpSecure }}</template>
<template #caption>{{ $ts.smtpSecureInfo }}</template>
<FormButton @click="testEmail">{{ $ts.testEmail }}</FormButton> </FormSwitch>
</FormSection>
</template> </template>
</div>
<FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
</FormSuspense> </FormSuspense>
</FormBase> </MkSpacer>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import FormSwitch from '@/components/debobigego/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/debobigego/input.vue'; import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/debobigego/button.vue'; import FormInfo from '@/components/ui/info.vue';
import FormBase from '@/components/debobigego/base.vue'; import FormSuspense from '@/components/form/suspense.vue';
import FormGroup from '@/components/debobigego/group.vue'; import FormSplit from '@/components/form/split.vue';
import FormInfo from '@/components/debobigego/info.vue'; import FormSection from '@/components/form/section.vue';
import FormSuspense from '@/components/debobigego/suspense.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';
@ -53,9 +58,8 @@ export default defineComponent({
components: { components: {
FormSwitch, FormSwitch,
FormInput, FormInput,
FormBase, FormSplit,
FormGroup, FormSection,
FormButton,
FormInfo, FormInfo,
FormSuspense, FormSuspense,
}, },
@ -68,6 +72,16 @@ export default defineComponent({
title: this.$ts.emailServer, title: this.$ts.emailServer,
icon: 'fas fa-envelope', icon: 'fas fa-envelope',
bg: 'var(--bg)', bg: 'var(--bg)',
actions: [{
asFullButton: true,
text: this.$ts.testEmail,
handler: this.testEmail,
}, {
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
}, },
enableEmail: false, enableEmail: false,
email: null, email: null,

View file

@ -23,7 +23,7 @@
</div> </div>
<div v-else-if="tab === 'remote'" class="remote"> <div v-else-if="tab === 'remote'" class="remote">
<div class="_inputSplit"> <FormSplit>
<MkInput v-model="queryRemote" :debounce="true" type="search"> <MkInput v-model="queryRemote" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template> <template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template> <template #label>{{ $ts.search }}</template>
@ -31,7 +31,7 @@
<MkInput v-model="host" :debounce="true"> <MkInput v-model="host" :debounce="true">
<template #label>{{ $ts.host }}</template> <template #label>{{ $ts.host }}</template>
</MkInput> </MkInput>
</div> </FormSplit>
<MkPagination ref="remoteEmojis" :pagination="remotePagination"> <MkPagination ref="remoteEmojis" :pagination="remotePagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}"> <template v-slot="{items}">
@ -57,6 +57,7 @@ import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import MkTab from '@/components/tab.vue'; import MkTab from '@/components/tab.vue';
import FormSplit from '@/components/form/split.vue';
import { selectFiles } from '@/scripts/select-file'; import { selectFiles } from '@/scripts/select-file';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
@ -67,6 +68,7 @@ export default defineComponent({
MkButton, MkButton,
MkInput, MkInput,
MkPagination, MkPagination,
FormSplit,
}, },
emits: ['info'], emits: ['info'],

View file

@ -1,93 +0,0 @@
<template>
<FormBase>
<FormSuspense :p="init">
<FormSwitch v-model="cacheRemoteFiles">
{{ $ts.cacheRemoteFiles }}
<template #desc>{{ $ts.cacheRemoteFilesDescription }}</template>
</FormSwitch>
<FormSwitch v-model="proxyRemoteFiles">
{{ $ts.proxyRemoteFiles }}
<template #desc>{{ $ts.proxyRemoteFilesDescription }}</template>
</FormSwitch>
<FormInput v-model="localDriveCapacityMb" type="number">
<span>{{ $ts.driveCapacityPerLocalAccount }}</span>
<template #suffix>MB</template>
<template #desc>{{ $ts.inMb }}</template>
</FormInput>
<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
<span>{{ $ts.driveCapacityPerRemoteAccount }}</span>
<template #suffix>MB</template>
<template #desc>{{ $ts.inMb }}</template>
</FormInput>
<FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
</FormSuspense>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import FormSwitch from '@/components/debobigego/switch.vue';
import FormInput from '@/components/debobigego/input.vue';
import FormButton from '@/components/debobigego/button.vue';
import FormBase from '@/components/debobigego/base.vue';
import FormGroup from '@/components/debobigego/group.vue';
import FormSuspense from '@/components/debobigego/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormBase,
FormGroup,
FormButton,
FormSuspense,
},
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.files,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
},
cacheRemoteFiles: false,
proxyRemoteFiles: false,
localDriveCapacityMb: 0,
remoteDriveCapacityMb: 0,
}
},
async mounted() {
this.$emit('info', this[symbols.PAGE_INFO]);
},
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
this.cacheRemoteFiles = meta.cacheRemoteFiles;
this.proxyRemoteFiles = meta.proxyRemoteFiles;
this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
},
save() {
os.apiWithDialog('admin/update-meta', {
cacheRemoteFiles: this.cacheRemoteFiles,
proxyRemoteFiles: this.proxyRemoteFiles,
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
}).then(() => {
fetchInstance();
});
}
}
});
</script>

View file

@ -3,7 +3,7 @@
<div v-if="!narrow || page == null" class="nav"> <div v-if="!narrow || page == null" class="nav">
<MkHeader :info="header"></MkHeader> <MkHeader :info="header"></MkHeader>
<MkSpacer :content-max="700"> <MkSpacer :content-max="700" :margin-min="16">
<div class="lxpfedzu"> <div class="lxpfedzu">
<div class="banner"> <div class="banner">
<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/> <img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
@ -162,11 +162,6 @@ export default defineComponent({
text: i18n.locale.general, text: i18n.locale.general,
to: '/admin/settings', to: '/admin/settings',
active: page.value === 'settings', active: page.value === 'settings',
}, {
icon: 'fas fa-cloud',
text: i18n.locale.files,
to: '/admin/files-settings',
active: page.value === 'files-settings',
}, { }, {
icon: 'fas fa-envelope', icon: 'fas fa-envelope',
text: i18n.locale.emailServer, text: i18n.locale.emailServer,
@ -236,7 +231,6 @@ export default defineComponent({
case 'database': return defineAsyncComponent(() => import('./database.vue')); case 'database': return defineAsyncComponent(() => import('./database.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue')); case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue')); case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue')); case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue')); case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue')); case 'security': return defineAsyncComponent(() => import('./security.vue'));

View file

@ -101,6 +101,7 @@ const alpha = (hex, a) => {
return `rgba(${r}, ${g}, ${b}, ${a})`; return `rgba(${r}, ${g}, ${b}, ${a})`;
}; };
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -119,7 +120,7 @@ export default defineComponent({
stats: null, stats: null,
serverInfo: null, serverInfo: null,
connection: null, connection: null,
queueConnection: markRaw(os.stream.useChannel('queueStats')), queueConnection: markRaw(stream.useChannel('queueStats')),
memUsage: 0, memUsage: 0,
chartCpuMem: null, chartCpuMem: null,
chartNet: null, chartNet: null,
@ -150,7 +151,7 @@ export default defineComponent({
os.api('admin/server-info', {}).then(res => { os.api('admin/server-info', {}).then(res => {
this.serverInfo = res; this.serverInfo = res;
this.connection = markRaw(os.stream.useChannel('serverStats')); this.connection = markRaw(stream.useChannel('serverStats'));
this.connection.on('stats', this.onStats); this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog); this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', { this.connection.send('requestLog', {

View file

@ -1,76 +1,78 @@
<template> <template>
<FormBase> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<FormSwitch v-model="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch> <div class="_formRoot">
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ $ts.useObjectStorage }}</FormSwitch>
<template v-if="useObjectStorage"> <template v-if="useObjectStorage">
<FormInput v-model="objectStorageBaseUrl"> <FormInput v-model="objectStorageBaseUrl" class="_formBlock">
<span>{{ $ts.objectStorageBaseUrl }}</span> <template #label>{{ $ts.objectStorageBaseUrl }}</template>
<template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template> <template #caption>{{ $ts.objectStorageBaseUrlDesc }}</template>
</FormInput> </FormInput>
<FormInput v-model="objectStorageBucket"> <FormInput v-model="objectStorageBucket" class="_formBlock">
<span>{{ $ts.objectStorageBucket }}</span> <template #label>{{ $ts.objectStorageBucket }}</template>
<template #desc>{{ $ts.objectStorageBucketDesc }}</template> <template #caption>{{ $ts.objectStorageBucketDesc }}</template>
</FormInput> </FormInput>
<FormInput v-model="objectStoragePrefix"> <FormInput v-model="objectStoragePrefix" class="_formBlock">
<span>{{ $ts.objectStoragePrefix }}</span> <template #label>{{ $ts.objectStoragePrefix }}</template>
<template #desc>{{ $ts.objectStoragePrefixDesc }}</template> <template #caption>{{ $ts.objectStoragePrefixDesc }}</template>
</FormInput> </FormInput>
<FormInput v-model="objectStorageEndpoint"> <FormInput v-model="objectStorageEndpoint" class="_formBlock">
<span>{{ $ts.objectStorageEndpoint }}</span> <template #label>{{ $ts.objectStorageEndpoint }}</template>
<template #desc>{{ $ts.objectStorageEndpointDesc }}</template> <template #caption>{{ $ts.objectStorageEndpointDesc }}</template>
</FormInput> </FormInput>
<FormInput v-model="objectStorageRegion"> <FormInput v-model="objectStorageRegion" class="_formBlock">
<span>{{ $ts.objectStorageRegion }}</span> <template #label>{{ $ts.objectStorageRegion }}</template>
<template #desc>{{ $ts.objectStorageRegionDesc }}</template> <template #caption>{{ $ts.objectStorageRegionDesc }}</template>
</FormInput> </FormInput>
<FormInput v-model="objectStorageAccessKey"> <FormSplit :min-width="280">
<FormInput v-model="objectStorageAccessKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template> <template #prefix><i class="fas fa-key"></i></template>
<span>Access key</span> <template #label>Access key</template>
</FormInput> </FormInput>
<FormInput v-model="objectStorageSecretKey"> <FormInput v-model="objectStorageSecretKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template> <template #prefix><i class="fas fa-key"></i></template>
<span>Secret key</span> <template #label>Secret key</template>
</FormInput> </FormInput>
</FormSplit>
<FormSwitch v-model="objectStorageUseSSL"> <FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
{{ $ts.objectStorageUseSSL }} <template #label>{{ $ts.objectStorageUseSSL }}</template>
<template #desc>{{ $ts.objectStorageUseSSLDesc }}</template> <template #caption>{{ $ts.objectStorageUseSSLDesc }}</template>
</FormSwitch> </FormSwitch>
<FormSwitch v-model="objectStorageUseProxy"> <FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
{{ $ts.objectStorageUseProxy }} <template #label>{{ $ts.objectStorageUseProxy }}</template>
<template #desc>{{ $ts.objectStorageUseProxyDesc }}</template> <template #caption>{{ $ts.objectStorageUseProxyDesc }}</template>
</FormSwitch> </FormSwitch>
<FormSwitch v-model="objectStorageSetPublicRead"> <FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
{{ $ts.objectStorageSetPublicRead }} <template #label>{{ $ts.objectStorageSetPublicRead }}</template>
</FormSwitch> </FormSwitch>
<FormSwitch v-model="objectStorageS3ForcePathStyle"> <FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
s3ForcePathStyle <template #label>s3ForcePathStyle</template>
</FormSwitch> </FormSwitch>
</template> </template>
</div>
<FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
</FormSuspense> </FormSuspense>
</FormBase> </MkSpacer>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import FormSwitch from '@/components/debobigego/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/debobigego/input.vue'; import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/debobigego/button.vue'; import FormGroup from '@/components/form/group.vue';
import FormBase from '@/components/debobigego/base.vue'; import FormSuspense from '@/components/form/suspense.vue';
import FormGroup from '@/components/debobigego/group.vue'; import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/debobigego/suspense.vue'; import FormSection from '@/components/form/section.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';
@ -79,10 +81,10 @@ export default defineComponent({
components: { components: {
FormSwitch, FormSwitch,
FormInput, FormInput,
FormBase,
FormGroup, FormGroup,
FormButton,
FormSuspense, FormSuspense,
FormSplit,
FormSection,
}, },
emits: ['info'], emits: ['info'],
@ -93,6 +95,12 @@ export default defineComponent({
title: this.$ts.objectStorage, title: this.$ts.objectStorage,
icon: 'fas fa-cloud', icon: 'fas fa-cloud',
bg: 'var(--bg)', bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
}, },
useObjectStorage: false, useObjectStorage: false,
objectStorageBaseUrl: null, objectStorageBaseUrl: null,

View file

@ -81,6 +81,7 @@ import number from '@/filters/number';
import MkInstanceInfo from './instance.vue'; import MkInstanceInfo from './instance.vue';
import XMetrics from './metrics.vue'; import XMetrics from './metrics.vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
export default defineComponent({ export default defineComponent({
@ -113,7 +114,7 @@ export default defineComponent({
notesComparedToThePrevDay: null, notesComparedToThePrevDay: null,
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}), fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
fetchModLogs: () => os.api('admin/show-moderation-logs', {}), fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
queueStatsConnection: markRaw(os.stream.useChannel('queueStats')), queueStatsConnection: markRaw(stream.useChannel('queueStats')),
} }
}, },

View file

@ -17,6 +17,7 @@ import XQueue from './queue.chart.vue';
import FormBase from '@/components/debobigego/base.vue'; import FormBase from '@/components/debobigego/base.vue';
import FormButton from '@/components/debobigego/button.vue'; import FormButton from '@/components/debobigego/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
export default defineComponent({ export default defineComponent({
@ -36,7 +37,7 @@ export default defineComponent({
icon: 'fas fa-clipboard-list', icon: 'fas fa-clipboard-list',
bg: 'var(--bg)', bg: 'var(--bg)',
}, },
connection: markRaw(os.stream.useChannel('queueStats')), connection: markRaw(stream.useChannel('queueStats')),
} }
}, },

View file

@ -1,31 +1,25 @@
<template> <template>
<FormBase> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<FormLink to="/admin/bot-protection"> <div class="_formRoot">
<FormLink to="/admin/bot-protection" class="_formBlock">
<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }} <i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
<template v-if="enableHcaptcha" #suffix>hCaptcha</template> <template v-if="enableHcaptcha" #suffix>hCaptcha</template>
<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
<template v-else #suffix>{{ $ts.none }} ({{ $ts.notRecommended }})</template> <template v-else #suffix>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
</FormLink> </FormLink>
</div>
<FormSwitch v-model="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch>
<FormSwitch v-model="emailRequiredForSignup">{{ $ts.emailRequiredForSignup }}</FormSwitch>
<FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
</FormSuspense> </FormSuspense>
</FormBase> </MkSpacer>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue'; import { defineAsyncComponent, defineComponent } from 'vue';
import FormLink from '@/components/debobigego/link.vue'; import FormLink from '@/components/form/link.vue';
import FormSwitch from '@/components/debobigego/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import FormButton from '@/components/debobigego/button.vue'; import FormInfo from '@/components/ui/info.vue';
import FormBase from '@/components/debobigego/base.vue'; import FormSuspense from '@/components/form/suspense.vue';
import FormGroup from '@/components/debobigego/group.vue'; import FormSection from '@/components/form/section.vue';
import FormInfo from '@/components/debobigego/info.vue';
import FormSuspense from '@/components/debobigego/suspense.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';
@ -34,10 +28,8 @@ export default defineComponent({
components: { components: {
FormLink, FormLink,
FormSwitch, FormSwitch,
FormBase,
FormGroup,
FormButton,
FormInfo, FormInfo,
FormSection,
FormSuspense, FormSuspense,
}, },
@ -52,8 +44,6 @@ export default defineComponent({
}, },
enableHcaptcha: false, enableHcaptcha: false,
enableRecaptcha: false, enableRecaptcha: false,
enableRegistration: false,
emailRequiredForSignup: false,
} }
}, },
@ -66,18 +56,7 @@ export default defineComponent({
const meta = await os.api('meta', { detail: true }); const meta = await os.api('meta', { detail: true });
this.enableHcaptcha = meta.enableHcaptcha; this.enableHcaptcha = meta.enableHcaptcha;
this.enableRecaptcha = meta.enableRecaptcha; this.enableRecaptcha = meta.enableRecaptcha;
this.enableRegistration = !meta.disableRegistration;
this.emailRequiredForSignup = meta.emailRequiredForSignup;
}, },
save() {
os.apiWithDialog('admin/update-meta', {
disableRegistration: !this.enableRegistration,
emailRequiredForSignup: this.emailRequiredForSignup,
}).then(() => {
fetchInstance();
});
}
} }
}); });
</script> </script>

View file

@ -1,72 +1,113 @@
<template> <template>
<FormBase> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<FormInput v-model="name"> <div class="_formRoot">
<span>{{ $ts.instanceName }}</span> <FormInput v-model="name" class="_formBlock">
<template #label>{{ $ts.instanceName }}</template>
</FormInput> </FormInput>
<FormTextarea v-model="description"> <FormTextarea v-model="description" class="_formBlock">
<span>{{ $ts.instanceDescription }}</span> <template #label>{{ $ts.instanceDescription }}</template>
</FormTextarea> </FormTextarea>
<FormInput v-model="iconUrl"> <FormInput v-model="iconUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template> <template #prefix><i class="fas fa-link"></i></template>
<span>{{ $ts.iconUrl }}</span> <template #label>{{ $ts.iconUrl }}</template>
</FormInput> </FormInput>
<FormInput v-model="bannerUrl"> <FormInput v-model="bannerUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template> <template #prefix><i class="fas fa-link"></i></template>
<span>{{ $ts.bannerUrl }}</span> <template #label>{{ $ts.bannerUrl }}</template>
</FormInput> </FormInput>
<FormInput v-model="backgroundImageUrl"> <FormInput v-model="backgroundImageUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template> <template #prefix><i class="fas fa-link"></i></template>
<span>{{ $ts.backgroundImageUrl }}</span> <template #label>{{ $ts.backgroundImageUrl }}</template>
</FormInput> </FormInput>
<FormInput v-model="tosUrl"> <FormInput v-model="tosUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template> <template #prefix><i class="fas fa-link"></i></template>
<span>{{ $ts.tosUrl }}</span> <template #label>{{ $ts.tosUrl }}</template>
</FormInput> </FormInput>
<FormInput v-model="maintainerName"> <FormSplit :min-width="300">
<span>{{ $ts.maintainerName }}</span> <FormInput v-model="maintainerName" class="_formBlock">
<template #label>{{ $ts.maintainerName }}</template>
</FormInput> </FormInput>
<FormInput v-model="maintainerEmail" type="email"> <FormInput v-model="maintainerEmail" type="email" class="_formBlock">
<template #prefix><i class="fas fa-envelope"></i></template> <template #prefix><i class="fas fa-envelope"></i></template>
<span>{{ $ts.maintainerEmail }}</span> <template #label>{{ $ts.maintainerEmail }}</template>
</FormInput> </FormInput>
</FormSplit>
<FormTextarea v-model="pinnedUsers"> <FormTextarea v-model="pinnedUsers" class="_formBlock">
<span>{{ $ts.pinnedUsers }}</span> <template #label>{{ $ts.pinnedUsers }}</template>
<template #desc>{{ $ts.pinnedUsersDescription }}</template> <template #caption>{{ $ts.pinnedUsersDescription }}</template>
</FormTextarea> </FormTextarea>
<FormInput v-model="maxNoteTextLength" type="number"> <FormInput v-model="maxNoteTextLength" type="number" class="_formBlock">
<template #prefix><i class="fas fa-pencil-alt"></i></template> <template #prefix><i class="fas fa-pencil-alt"></i></template>
<span>{{ $ts.maxNoteTextLength }}</span> <template #label>{{ $ts.maxNoteTextLength }}</template>
</FormInput> </FormInput>
<FormSwitch v-model="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch> <FormSection>
<FormSwitch v-model="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch> <FormSwitch v-model="enableRegistration" class="_formBlock">
<FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo> <template #label>{{ $ts.enableRegistration }}</template>
</FormSwitch>
<FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> <FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
<template #label>{{ $ts.emailRequiredForSignup }}</template>
</FormSwitch>
</FormSection>
<FormSection>
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ $ts.enableLocalTimeline }}</FormSwitch>
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ $ts.enableGlobalTimeline }}</FormSwitch>
<FormInfo class="_formBlock">{{ $ts.disablingTimelinesInfo }}</FormInfo>
</FormSection>
<FormSection>
<template #label>{{ $ts.files }}</template>
<FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
<template #label>{{ $ts.cacheRemoteFiles }}</template>
<template #caption>{{ $ts.cacheRemoteFilesDescription }}</template>
</FormSwitch>
<FormSwitch v-model="proxyRemoteFiles" class="_formBlock">
<template #label>{{ $ts.proxyRemoteFiles }}</template>
<template #caption>{{ $ts.proxyRemoteFilesDescription }}</template>
</FormSwitch>
<FormSplit :min-width="280">
<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
<template #label>{{ $ts.driveCapacityPerLocalAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ $ts.inMb }}</template>
</FormInput>
<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
<template #label>{{ $ts.driveCapacityPerRemoteAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ $ts.inMb }}</template>
</FormInput>
</FormSplit>
</FormSection>
</div>
</FormSuspense> </FormSuspense>
</FormBase> </MkSpacer>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import FormSwitch from '@/components/debobigego/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/debobigego/input.vue'; import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/debobigego/button.vue'; import FormTextarea from '@/components/form/textarea.vue';
import FormBase from '@/components/debobigego/base.vue'; import FormInfo from '@/components/ui/info.vue';
import FormGroup from '@/components/debobigego/group.vue'; import FormSection from '@/components/form/section.vue';
import FormTextarea from '@/components/debobigego/textarea.vue'; import FormSplit from '@/components/form/split.vue';
import FormInfo from '@/components/debobigego/info.vue'; import FormSuspense from '@/components/form/suspense.vue';
import FormSuspense from '@/components/debobigego/suspense.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';
@ -75,12 +116,11 @@ export default defineComponent({
components: { components: {
FormSwitch, FormSwitch,
FormInput, FormInput,
FormBase, FormSuspense,
FormGroup,
FormButton,
FormTextarea, FormTextarea,
FormInfo, FormInfo,
FormSuspense, FormSection,
FormSplit,
}, },
emits: ['info'], emits: ['info'],
@ -91,6 +131,12 @@ export default defineComponent({
title: this.$ts.general, title: this.$ts.general,
icon: 'fas fa-cog', icon: 'fas fa-cog',
bg: 'var(--bg)', bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
}, },
name: null, name: null,
description: null, description: null,
@ -104,6 +150,12 @@ export default defineComponent({
enableLocalTimeline: false, enableLocalTimeline: false,
enableGlobalTimeline: false, enableGlobalTimeline: false,
pinnedUsers: '', pinnedUsers: '',
cacheRemoteFiles: false,
proxyRemoteFiles: false,
localDriveCapacityMb: 0,
remoteDriveCapacityMb: 0,
enableRegistration: false,
emailRequiredForSignup: false,
} }
}, },
@ -126,6 +178,12 @@ export default defineComponent({
this.enableLocalTimeline = !meta.disableLocalTimeline; this.enableLocalTimeline = !meta.disableLocalTimeline;
this.enableGlobalTimeline = !meta.disableGlobalTimeline; this.enableGlobalTimeline = !meta.disableGlobalTimeline;
this.pinnedUsers = meta.pinnedUsers.join('\n'); this.pinnedUsers = meta.pinnedUsers.join('\n');
this.cacheRemoteFiles = meta.cacheRemoteFiles;
this.proxyRemoteFiles = meta.proxyRemoteFiles;
this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
this.enableRegistration = !meta.disableRegistration;
this.emailRequiredForSignup = meta.emailRequiredForSignup;
}, },
save() { save() {
@ -142,6 +200,12 @@ export default defineComponent({
disableLocalTimeline: !this.enableLocalTimeline, disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline, disableGlobalTimeline: !this.enableGlobalTimeline,
pinnedUsers: this.pinnedUsers.split('\n'), pinnedUsers: this.pinnedUsers.split('\n'),
cacheRemoteFiles: this.cacheRemoteFiles,
proxyRemoteFiles: this.proxyRemoteFiles,
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
disableRegistration: !this.enableRegistration,
emailRequiredForSignup: this.emailRequiredForSignup,
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance();
}); });

View file

@ -6,7 +6,7 @@
<template #prefix><i class="fas fa-search"></i></template> <template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.host }}</template> <template #label>{{ $ts.host }}</template>
</MkInput> </MkInput>
<div class="_inputSplit" style="margin-top: var(--margin);"> <FormSplit style="margin-top: var(--margin);">
<MkSelect v-model="state"> <MkSelect v-model="state">
<template #label>{{ $ts.state }}</template> <template #label>{{ $ts.state }}</template>
<option value="all">{{ $ts.all }}</option> <option value="all">{{ $ts.all }}</option>
@ -38,7 +38,7 @@
<option value="+driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.descendingOrder }})</option> <option value="+driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.descendingOrder }})</option>
<option value="-driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.ascendingOrder }})</option> <option value="-driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.ascendingOrder }})</option>
</MkSelect> </MkSelect>
</div> </FormSplit>
</div> </div>
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
@ -101,6 +101,7 @@ import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/form/select.vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
@ -110,6 +111,7 @@ export default defineComponent({
MkInput, MkInput,
MkSelect, MkSelect,
MkPagination, MkPagination,
FormSplit,
}, },
emits: ['info'], emits: ['info'],

View file

@ -44,6 +44,7 @@ import * as Acct from 'misskey-js/built/acct';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import { acct } from '@/filters/user'; import { acct } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
export default defineComponent({ export default defineComponent({
@ -66,7 +67,7 @@ export default defineComponent({
}, },
mounted() { mounted() {
this.connection = markRaw(os.stream.useChannel('messagingIndex')); this.connection = markRaw(stream.useChannel('messagingIndex'));
this.connection.on('message', this.onMessage); this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead); this.connection.on('read', this.onRead);

View file

@ -28,6 +28,7 @@ import * as autosize from 'autosize';
import { formatTimeString } from '@/scripts/format-time-string'; import { formatTimeString } from '@/scripts/format-time-string';
import { selectFile } from '@/scripts/select-file'; import { selectFile } from '@/scripts/select-file';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import { Autocomplete } from '@/scripts/autocomplete'; import { Autocomplete } from '@/scripts/autocomplete';
import { throttle } from 'throttle-debounce'; import { throttle } from 'throttle-debounce';
@ -48,7 +49,7 @@ export default defineComponent({
file: null, file: null,
sending: false, sending: false,
typing: throttle(3000, () => { typing: throttle(3000, () => {
os.stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id }); stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id });
}), }),
}; };
}, },

View file

@ -43,6 +43,7 @@ import XForm from './messaging-room.form.vue';
import * as Acct from 'misskey-js/built/acct'; import * as Acct from 'misskey-js/built/acct';
import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll'; import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import { popout } from '@/scripts/popout'; import { popout } from '@/scripts/popout';
import * as sound from '@/scripts/sound'; import * as sound from '@/scripts/sound';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
@ -141,7 +142,7 @@ const Component = defineComponent({
this.group = group; this.group = group;
} }
this.connection = markRaw(os.stream.useChannel('messaging', { this.connection = markRaw(stream.useChannel('messaging', {
otherparty: this.user ? this.user.id : undefined, otherparty: this.user ? this.user.id : undefined,
group: this.group ? this.group.id : undefined, group: this.group ? this.group.id : undefined,
})); }));

View file

@ -9,6 +9,7 @@ import { defineComponent, markRaw } from 'vue';
import GameSetting from './game.setting.vue'; import GameSetting from './game.setting.vue';
import GameBoard from './game.board.vue'; import GameBoard from './game.board.vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
export default defineComponent({ export default defineComponent({
@ -61,7 +62,7 @@ export default defineComponent({
if (this.connection) { if (this.connection) {
this.connection.dispose(); this.connection.dispose();
} }
this.connection = markRaw(os.stream.useChannel('gamesReversiGame', { this.connection = markRaw(stream.useChannel('gamesReversiGame', {
gameId: this.game.id gameId: this.game.id
})); }));
this.connection.on('started', this.onStarted); this.connection.on('started', this.onStarted);

View file

@ -62,6 +62,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, markRaw } from 'vue'; import { defineComponent, markRaw } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkFolder from '@/components/ui/folder.vue'; import MkFolder from '@/components/ui/folder.vue';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
@ -92,7 +93,7 @@ export default defineComponent({
mounted() { mounted() {
if (this.$i) { if (this.$i) {
this.connection = markRaw(os.stream.useChannel('gamesReversi')); this.connection = markRaw(stream.useChannel('gamesReversi'));
this.connection.on('invited', this.onInvited); this.connection.on('invited', this.onInvited);

View file

@ -5,7 +5,7 @@
<div class="_formBlock uawsfosz"> <div class="_formBlock uawsfosz">
<div class="meter"><div :style="meterStyle"></div></div> <div class="meter"><div :style="meterStyle"></div></div>
</div> </div>
<div class="_inputSplit _formBlock"> <FormSplit>
<MkKeyValue class="_formBlock"> <MkKeyValue class="_formBlock">
<template #key>{{ $ts.capacity }}</template> <template #key>{{ $ts.capacity }}</template>
<template #value>{{ bytes(capacity, 1) }}</template> <template #value>{{ bytes(capacity, 1) }}</template>
@ -14,7 +14,7 @@
<template #key>{{ $ts.inUse }}</template> <template #key>{{ $ts.inUse }}</template>
<template #value>{{ bytes(usage, 1) }}</template> <template #value>{{ bytes(usage, 1) }}</template>
</MkKeyValue> </MkKeyValue>
</div> </FormSplit>
</FormSection> </FormSection>
<FormSection> <FormSection>
@ -38,6 +38,7 @@ import * as tinycolor from 'tinycolor2';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/key-value.vue'; import MkKeyValue from '@/components/key-value.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os'; import * as os from '@/os';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
@ -49,6 +50,7 @@ export default defineComponent({
FormLink, FormLink,
FormSection, FormSection,
MkKeyValue, MkKeyValue,
FormSplit,
}, },
emits: ['info'], emits: ['info'],

View file

@ -21,7 +21,6 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import FormTextarea from '@/components/form/textarea.vue'; import FormTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue'; import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/debobigego/base.vue';
import FormButton from '@/components/ui/button.vue'; import FormButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import { menuDef } from '@/menu'; import { menuDef } from '@/menu';
@ -31,7 +30,6 @@ import { unisonReload } from '@/scripts/unison-reload';
export default defineComponent({ export default defineComponent({
components: { components: {
FormBase,
FormButton, FormButton,
FormTextarea, FormTextarea,
FormRadios, FormRadios,

View file

@ -13,7 +13,6 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import FormButton from '@/components/ui/button.vue'; import FormButton from '@/components/ui/button.vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/debobigego/base.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import { notificationTypes } from 'misskey-js'; import { notificationTypes } from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
@ -21,7 +20,6 @@ import * as symbols from '@/symbols';
export default defineComponent({ export default defineComponent({
components: { components: {
FormBase,
FormLink, FormLink,
FormButton, FormButton,
FormSection, FormSection,

View file

@ -1,6 +1,7 @@
import { onUnmounted, Ref, ref, watch } from 'vue'; import { onUnmounted, Ref, ref, watch } from 'vue';
import { $i } from './account'; import { $i } from './account';
import { api } from './os'; import { api } from './os';
import { stream } from './stream';
type StateDef = Record<string, { type StateDef = Record<string, {
where: 'account' | 'device' | 'deviceAccount'; where: 'account' | 'device' | 'deviceAccount';
@ -9,6 +10,8 @@ type StateDef = Record<string, {
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never; type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
const connection = $i && stream.useChannel('main');
export class Storage<T extends StateDef> { export class Storage<T extends StateDef> {
public readonly key: string; public readonly key: string;
public readonly keyForLocalStorage: string; public readonly keyForLocalStorage: string;
@ -69,8 +72,19 @@ export class Storage<T extends StateDef> {
localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache)); localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
}); });
}, 1); }, 1);
// streamingのuser storage updateイベントを監視して更新
connection?.on('registryUpdated', ({ scope, key, value }: { scope: string[], key: keyof T, value: T[typeof key]['default'] }) => {
if (scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return;
// TODO: streamingのuser storage updateイベントを監視して更新 this.state[key] = value;
this.reactiveState[key].value = value;
const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}');
if (cache[key] !== value) {
cache[key] = value;
localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
}
});
} }
} }

View file

@ -1,4 +1,5 @@
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { DriveFile } from 'misskey-js/built/entities'; import { DriveFile } from 'misskey-js/built/entities';
@ -48,7 +49,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
const marker = Math.random().toString(); // TODO: UUIDとか使う const marker = Math.random().toString(); // TODO: UUIDとか使う
const connection = os.stream.useChannel('main'); const connection = stream.useChannel('main');
connection.on('urlUploadFinished', data => { connection.on('urlUploadFinished', data => {
if (data.marker === marker) { if (data.marker === marker) {
res(multiple ? [data.file] : data.file); res(multiple ? [data.file] : data.file);

View file

@ -0,0 +1,6 @@
import * as Misskey from 'misskey-js';
import { markRaw } from 'vue';
import { $i } from '@/account';
import { url } from '@/config';
export const stream = markRaw(new Misskey.Stream(url, $i));

View file

@ -386,16 +386,6 @@ hr {
backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px));
} }
._inputSplit {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
grid-gap: 12px;
> * {
margin: 0 !important;
}
}
._formBlock { ._formBlock {
margin: 1.5em 0; margin: 1.5em 0;
} }

View file

@ -15,10 +15,11 @@
<script lang="ts"> <script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue'; import { defineAsyncComponent, defineComponent } from 'vue';
import { stream, popup, popups, uploads, pendingApiRequestsCount } from '@/os'; import { popup, popups, uploads, pendingApiRequestsCount } from '@/os';
import * as sound from '@/scripts/sound'; import * as sound from '@/scripts/sound';
import { $i } from '@/account'; import { $i } from '@/account';
import { swInject } from './sw-inject'; import { swInject } from './sw-inject';
import { stream } from '@/stream';
export default defineComponent({ export default defineComponent({
components: { components: {

View file

@ -11,6 +11,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
export default defineComponent({ export default defineComponent({
data() { data() {
@ -20,14 +21,14 @@ export default defineComponent({
}, },
computed: { computed: {
stream() { stream() {
return os.stream; return stream;
}, },
}, },
created() { created() {
os.stream.on('_disconnected_', this.onDisconnected); stream.on('_disconnected_', this.onDisconnected);
}, },
beforeUnmount() { beforeUnmount() {
os.stream.off('_disconnected_', this.onDisconnected); stream.off('_disconnected_', this.onDisconnected);
}, },
methods: { methods: {
onDisconnected() { onDisconnected() {

View file

@ -118,6 +118,7 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
import { checkWordMute } from '@/scripts/check-word-mute'; import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import { noteActions, noteViewInterruptors } from '@/store'; import { noteActions, noteViewInterruptors } from '@/store';
import { reactionPicker } from '@/scripts/reaction-picker'; import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
@ -243,7 +244,7 @@ export default defineComponent({
async created() { async created() {
if (this.$i) { if (this.$i) {
this.connection = os.stream; this.connection = stream;
} }
this.collapsed = this.appearNote.cw == null && this.appearNote.text && ( this.collapsed = this.appearNote.cw == null && this.appearNote.text && (

View file

@ -26,6 +26,7 @@ import { computed, defineComponent, markRaw } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XNotes from '../notes.vue'; import XNotes from '../notes.vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import * as sound from '@/scripts/sound'; import * as sound from '@/scripts/sound';
import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll'; import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
import follow from '@/directives/follow-append'; import follow from '@/directives/follow-append';
@ -106,7 +107,7 @@ export default defineComponent({
sound.play(note.userId === this.$i.id ? 'noteMy' : 'note'); sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
}; };
this.connection = markRaw(os.stream.useChannel('channel', { this.connection = markRaw(stream.useChannel('channel', {
channelId: this.channelId channelId: this.channelId
})); }));
this.connection.on('note', prepend); this.connection.on('note', prepend);

View file

@ -17,6 +17,7 @@
import { computed, defineComponent, markRaw } from 'vue'; import { computed, defineComponent, markRaw } from 'vue';
import XNotes from '../notes.vue'; import XNotes from '../notes.vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import * as sound from '@/scripts/sound'; import * as sound from '@/scripts/sound';
import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll'; import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
import follow from '@/directives/follow-append'; import follow from '@/directives/follow-append';
@ -90,23 +91,23 @@ export default defineComponent({
if (this.src == 'home') { if (this.src == 'home') {
endpoint = 'notes/timeline'; endpoint = 'notes/timeline';
this.connection = markRaw(os.stream.useChannel('homeTimeline')); this.connection = markRaw(stream.useChannel('homeTimeline'));
this.connection.on('note', prepend); this.connection.on('note', prepend);
this.connection2 = markRaw(os.stream.useChannel('main')); this.connection2 = markRaw(stream.useChannel('main'));
this.connection2.on('follow', onChangeFollowing); this.connection2.on('follow', onChangeFollowing);
this.connection2.on('unfollow', onChangeFollowing); this.connection2.on('unfollow', onChangeFollowing);
} else if (this.src == 'local') { } else if (this.src == 'local') {
endpoint = 'notes/local-timeline'; endpoint = 'notes/local-timeline';
this.connection = markRaw(os.stream.useChannel('localTimeline')); this.connection = markRaw(stream.useChannel('localTimeline'));
this.connection.on('note', prepend); this.connection.on('note', prepend);
} else if (this.src == 'social') { } else if (this.src == 'social') {
endpoint = 'notes/hybrid-timeline'; endpoint = 'notes/hybrid-timeline';
this.connection = markRaw(os.stream.useChannel('hybridTimeline')); this.connection = markRaw(stream.useChannel('hybridTimeline'));
this.connection.on('note', prepend); this.connection.on('note', prepend);
} else if (this.src == 'global') { } else if (this.src == 'global') {
endpoint = 'notes/global-timeline'; endpoint = 'notes/global-timeline';
this.connection = markRaw(os.stream.useChannel('globalTimeline')); this.connection = markRaw(stream.useChannel('globalTimeline'));
this.connection.on('note', prepend); this.connection.on('note', prepend);
} }

View file

@ -59,6 +59,7 @@ import * as Acct from 'misskey-js/built/acct';
import { formatTimeString } from '@/scripts/format-time-string'; import { formatTimeString } from '@/scripts/format-time-string';
import { Autocomplete } from '@/scripts/autocomplete'; import { Autocomplete } from '@/scripts/autocomplete';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import { selectFiles } from '@/scripts/select-file'; import { selectFiles } from '@/scripts/select-file';
import { notePostInterruptors, postFormActions } from '@/store'; import { notePostInterruptors, postFormActions } from '@/store';
import { throttle } from 'throttle-debounce'; import { throttle } from 'throttle-debounce';
@ -130,7 +131,7 @@ export default defineComponent({
imeText: '', imeText: '',
typing: throttle(3000, () => { typing: throttle(3000, () => {
if (this.channel) { if (this.channel) {
os.stream.send('typingOnChannel', { channel: this.channel }); stream.send('typingOnChannel', { channel: this.channel });
} }
}), }),
postFormActions, postFormActions,

View file

@ -49,6 +49,7 @@
import { defineComponent, markRaw } from 'vue'; import { defineComponent, markRaw } from 'vue';
import define from './define'; import define from './define';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
import number from '@/filters/number'; import number from '@/filters/number';
import * as sound from '@/scripts/sound'; import * as sound from '@/scripts/sound';
@ -70,7 +71,7 @@ export default defineComponent({
extends: widget, extends: widget,
data() { data() {
return { return {
connection: markRaw(os.stream.useChannel('queueStats')), connection: markRaw(stream.useChannel('queueStats')),
inbox: { inbox: {
activeSincePrevTick: 0, activeSincePrevTick: 0,
active: 0, active: 0,

View file

@ -20,6 +20,7 @@ import MkContainer from '@/components/ui/container.vue';
import define from './define'; import define from './define';
import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
const widget = define({ const widget = define({
name: 'photos', name: 'photos',
@ -48,7 +49,7 @@ export default defineComponent({
}; };
}, },
mounted() { mounted() {
this.connection = markRaw(os.stream.useChannel('main')); this.connection = markRaw(stream.useChannel('main'));
this.connection.on('driveFileCreated', this.onDriveFileCreated); this.connection.on('driveFileCreated', this.onDriveFileCreated);

View file

@ -23,6 +23,7 @@ import XCpu from './cpu.vue';
import XMemory from './mem.vue'; import XMemory from './mem.vue';
import XDisk from './disk.vue'; import XDisk from './disk.vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream';
const widget = define({ const widget = define({
name: 'serverMetric', name: 'serverMetric',
@ -63,7 +64,7 @@ export default defineComponent({
os.api('server-info', {}).then(res => { os.api('server-info', {}).then(res => {
this.meta = res; this.meta = res;
}); });
this.connection = markRaw(os.stream.useChannel('serverStats')); this.connection = markRaw(stream.useChannel('serverStats'));
}, },
unmounted() { unmounted() {
this.connection.dispose(); this.connection.dispose();

View file

@ -46,6 +46,7 @@ module.exports = {
loader: 'vue-loader', loader: 'vue-loader',
options: { options: {
cssSourceMap: false, cssSourceMap: false,
reactivityTransform: true,
compilerOptions: { compilerOptions: {
preserveWhitespace: false preserveWhitespace: false
} }

View file

@ -6162,10 +6162,10 @@ vue-eslint-parser@^8.0.1:
lodash "^4.17.21" lodash "^4.17.21"
semver "^7.3.5" semver "^7.3.5"
vue-loader@16.8.3: vue-loader@17.0.0:
version "16.8.3" version "17.0.0"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.8.3.tgz#d43e675def5ba9345d6c7f05914c13d861997087" resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-17.0.0.tgz#2eaa80aab125b19f00faa794b5bd867b17f85acb"
integrity sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA== integrity sha512-OWSXjrzIvbF2LtOUmxT3HYgwwubbfFelN8PAP9R9dwpIkj48TVioHhWWSx7W7fk+iF5cgg3CBJRxwTdtLU4Ecg==
dependencies: dependencies:
chalk "^4.1.0" chalk "^4.1.0"
hash-sum "^2.0.0" hash-sum "^2.0.0"