Merge remote-tracking branch 'misskey-original/develop' into timeline-toka
# Conflicts: # locales/index.d.ts # locales/ja-JP.yml # package.json # packages/backend/src/server/api/endpoints/admin/emoji/add.ts # packages/backend/src/server/api/stream/channels/global-timeline.ts # packages/backend/src/server/api/stream/channels/home-timeline.ts # packages/backend/src/server/api/stream/channels/hybrid-timeline.ts # packages/backend/src/server/api/stream/channels/local-timeline.ts # packages/frontend/src/components/MkNote.vue # packages/frontend/src/components/MkPostForm.vue # packages/frontend/src/pages/timeline.vue # pnpm-lock.yaml
This commit is contained in:
commit
560482315b
177 changed files with 3915 additions and 2343 deletions
|
|
@ -292,6 +292,7 @@ const patrons = [
|
|||
'kino3277',
|
||||
'美少女JKぐーちゃん',
|
||||
'てば',
|
||||
'たっくん',
|
||||
];
|
||||
|
||||
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
|
|
|||
81
packages/frontend/src/pages/admin/external-services.vue
Normal file
81
packages/frontend/src/pages/admin/external-services.vue
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormSection>
|
||||
<template #label>DeepL Translation</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="deeplAuthKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>DeepL Auth Key</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="deeplIsPro">
|
||||
<template #label>Pro account</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</FormSection>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
|
||||
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { fetchInstance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
let deeplAuthKey: string = $ref('');
|
||||
let deeplIsPro: boolean = $ref(false);
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
deeplAuthKey = meta.deeplAuthKey;
|
||||
deeplIsPro = meta.deeplIsPro;
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
deeplAuthKey,
|
||||
deeplIsPro,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.externalServices,
|
||||
icon: 'ti ti-link',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.footer {
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -212,6 +212,11 @@ const menuDef = $computed(() => [{
|
|||
text: i18n.ts.proxyAccount,
|
||||
to: '/admin/proxy-account',
|
||||
active: currentPage?.route.name === 'proxy-account',
|
||||
}, {
|
||||
icon: 'ti ti-link',
|
||||
text: i18n.ts.externalServices,
|
||||
to: '/admin/external-services',
|
||||
active: currentPage?.route.name === 'external-services',
|
||||
}, {
|
||||
icon: 'ti ti-adjustments',
|
||||
text: i18n.ts.other,
|
||||
|
|
|
|||
|
|
@ -29,8 +29,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
|
||||
<span v-else-if="log.type === 'unsuspendRemoteInstance'">: {{ log.info.host }}</span>
|
||||
<span v-else-if="log.type === 'createGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>
|
||||
<span v-else-if="log.type === 'updateGlobalAnnouncement'">: {{ log.info.before.title }}</span>
|
||||
<span v-else-if="log.type === 'deleteGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>
|
||||
<span v-else-if="log.type === 'createUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'updateUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'deleteUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'deleteNote'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'deleteDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
||||
</template>
|
||||
|
|
@ -88,6 +92,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateGlobalAnnouncement'">
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateUserAnnouncement'">
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<details>
|
||||
<summary>raw</summary>
|
||||
|
|
|
|||
|
|
@ -160,26 +160,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])">
|
||||
<template #label>{{ i18n.ts._role._options.canEditNote }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.canEditNote.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canEditNote.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canEditNote)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.canEditNote.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="role.policies.canEditNote.value" :disabled="role.policies.canEditNote.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="role.policies.canEditNote.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.canInvite, 'canInvite'])">
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #suffix>
|
||||
|
|
|
|||
|
|
@ -48,14 +48,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])">
|
||||
<template #label>{{ i18n.ts._role._options.canEditNote }}</template>
|
||||
<template #suffix>{{ policies.canEditNote ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canEditNote">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
|
|
|
|||
|
|
@ -81,16 +81,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>DeepL Translation</template>
|
||||
<template #label>Timeline caching</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="deeplAuthKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>DeepL Auth Key</template>
|
||||
<MkInput v-model="perLocalUserUserTimelineCacheMax" type="number">
|
||||
<template #label>perLocalUserUserTimelineCacheMax</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="perRemoteUserUserTimelineCacheMax" type="number">
|
||||
<template #label>perRemoteUserUserTimelineCacheMax</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="perUserHomeTimelineCacheMax" type="number">
|
||||
<template #label>perUserHomeTimelineCacheMax</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="perUserListTimelineCacheMax" type="number">
|
||||
<template #label>perUserListTimelineCacheMax</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="deeplIsPro">
|
||||
<template #label>Pro account</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
|
@ -133,8 +141,10 @@ let cacheRemoteSensitiveFiles: boolean = $ref(false);
|
|||
let enableServiceWorker: boolean = $ref(false);
|
||||
let swPublicKey: any = $ref(null);
|
||||
let swPrivateKey: any = $ref(null);
|
||||
let deeplAuthKey: string = $ref('');
|
||||
let deeplIsPro: boolean = $ref(false);
|
||||
let perLocalUserUserTimelineCacheMax: number = $ref(0);
|
||||
let perRemoteUserUserTimelineCacheMax: number = $ref(0);
|
||||
let perUserHomeTimelineCacheMax: number = $ref(0);
|
||||
let perUserListTimelineCacheMax: number = $ref(0);
|
||||
|
||||
async function init(): Promise<void> {
|
||||
const meta = await os.api('admin/meta');
|
||||
|
|
@ -149,8 +159,10 @@ async function init(): Promise<void> {
|
|||
enableServiceWorker = meta.enableServiceWorker;
|
||||
swPublicKey = meta.swPublickey;
|
||||
swPrivateKey = meta.swPrivateKey;
|
||||
deeplAuthKey = meta.deeplAuthKey;
|
||||
deeplIsPro = meta.deeplIsPro;
|
||||
perLocalUserUserTimelineCacheMax = meta.perLocalUserUserTimelineCacheMax;
|
||||
perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax;
|
||||
perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax;
|
||||
perUserListTimelineCacheMax = meta.perUserListTimelineCacheMax;
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
|
|
@ -166,8 +178,10 @@ function save(): void {
|
|||
enableServiceWorker,
|
||||
swPublicKey,
|
||||
swPrivateKey,
|
||||
deeplAuthKey,
|
||||
deeplIsPro,
|
||||
perLocalUserUserTimelineCacheMax,
|
||||
perRemoteUserUserTimelineCacheMax,
|
||||
perUserHomeTimelineCacheMax,
|
||||
perUserListTimelineCacheMax,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -102,7 +102,6 @@ let searchKey = $ref('');
|
|||
const featuredPagination = $computed(() => ({
|
||||
endpoint: 'notes/featured' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
params: {
|
||||
channelId: props.channelId,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import { i18n } from '@/i18n.js';
|
|||
const paginationForNotes = {
|
||||
endpoint: 'notes/featured' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
const paginationForPolls = {
|
||||
|
|
|
|||
|
|
@ -29,16 +29,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div class="_gaps_s">
|
||||
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
||||
<div v-for="user in users" :key="user.id" :class="$style.userItem">
|
||||
<MkA :class="$style.userItemBody" :to="`${userPage(user)}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<MkButton v-if="!fetching && queueUserIds.length !== 0" v-appear="enableInfiniteScroll ? fetchMoreUsers : null" :class="$style.more" :style="{ cursor: 'pointer' }" primary rounded @click="fetchMoreUsers">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-if="fetching" class="loading"/>
|
||||
|
||||
<MkPagination ref="paginationEl" :pagination="membershipsPagination">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<div v-for="item in items" :key="item.id">
|
||||
<div :class="$style.userItem">
|
||||
<MkA :class="$style.userItemBody" :to="`${userPage(item.user)}`">
|
||||
<MkUserCardMini :user="item.user"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.menu" @click="showMembershipMenu(item, $event)"><i class="ti ti-dots"></i></button>
|
||||
<button class="_button" :class="$style.remove" @click="removeUser(item, $event)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
|
|
@ -59,9 +65,11 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { userListsCache } from '@/cache';
|
||||
import { userListsCache } from '@/cache.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
|
||||
const {
|
||||
enableInfiniteScroll,
|
||||
} = defaultStore.reactiveState;
|
||||
|
|
@ -70,40 +78,25 @@ const props = defineProps<{
|
|||
listId: string;
|
||||
}>();
|
||||
|
||||
const FETCH_USERS_LIMIT = 20;
|
||||
|
||||
const paginationEl = ref<InstanceType<typeof MkPagination>>();
|
||||
let list = $ref<Misskey.entities.UserList | null>(null);
|
||||
let users = $ref<Misskey.entities.UserLite[]>([]);
|
||||
let queueUserIds = $ref<string[]>([]);
|
||||
let fetching = $ref(true);
|
||||
const isPublic = ref(false);
|
||||
const name = ref('');
|
||||
const membershipsPagination = {
|
||||
endpoint: 'users/lists/get-memberships' as const,
|
||||
limit: 30,
|
||||
params: computed(() => ({
|
||||
listId: props.listId,
|
||||
})),
|
||||
};
|
||||
|
||||
function fetchList() {
|
||||
fetching = true;
|
||||
os.api('users/lists/show', {
|
||||
listId: props.listId,
|
||||
}).then(_list => {
|
||||
list = _list;
|
||||
name.value = list.name;
|
||||
isPublic.value = list.isPublic;
|
||||
queueUserIds = list.userIds;
|
||||
|
||||
return fetchMoreUsers();
|
||||
});
|
||||
}
|
||||
|
||||
function fetchMoreUsers() {
|
||||
if (!list) return;
|
||||
if (fetching && users.length !== 0) return; // fetchingがtrueならやめるが、usersが空なら続行
|
||||
fetching = true;
|
||||
os.api('users/show', {
|
||||
userIds: queueUserIds.slice(0, FETCH_USERS_LIMIT),
|
||||
}).then(_users => {
|
||||
users = users.concat(_users);
|
||||
queueUserIds = queueUserIds.slice(FETCH_USERS_LIMIT);
|
||||
}).finally(() => {
|
||||
fetching = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -114,12 +107,12 @@ function addUser() {
|
|||
listId: list.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
users.push(user);
|
||||
paginationEl.value.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function removeUser(user, ev) {
|
||||
async function removeUser(item, ev) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.remove,
|
||||
icon: 'ti ti-x',
|
||||
|
|
@ -128,9 +121,28 @@ async function removeUser(user, ev) {
|
|||
if (!list) return;
|
||||
os.api('users/lists/pull', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
userId: item.userId,
|
||||
}).then(() => {
|
||||
users = users.filter(x => x.id !== user.id);
|
||||
paginationEl.value.removeItem(item.id);
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function showMembershipMenu(item, ev) {
|
||||
os.popupMenu([{
|
||||
text: item.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline,
|
||||
icon: item.withReplies ? 'ti ti-messages-off' : 'ti ti-messages',
|
||||
action: async () => {
|
||||
os.api('users/lists/update-membership', {
|
||||
listId: list.id,
|
||||
userId: item.userId,
|
||||
withReplies: !item.withReplies,
|
||||
}).then(() => {
|
||||
paginationEl.value.updateItem(item.id, (old) => ({
|
||||
...old,
|
||||
withReplies: !item.withReplies,
|
||||
}));
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
|
|
@ -202,6 +214,12 @@ definePageMetadata(computed(() => list ? {
|
|||
align-self: center;
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.more {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #value><code class="_monospace">{{ code }}</code></template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
|
||||
<MkButton primary rounded gradate @click="downloadBackupCodes"><i class="ti ti-download"></i> {{ i18n.ts.download }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
|
|
@ -108,6 +110,7 @@ import * as os from '@/os.js';
|
|||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { confetti } from '@/scripts/confetti.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
defineProps<{
|
||||
twoFactorData: {
|
||||
|
|
@ -143,6 +146,16 @@ async function tokenDone() {
|
|||
});
|
||||
}
|
||||
|
||||
function downloadBackupCodes() {
|
||||
if (backupCodes.value !== undefined) {
|
||||
const txtBlob = new Blob([backupCodes.value.join('\n')], { type: 'text/plain' });
|
||||
const dummya = document.createElement('a');
|
||||
dummya.href = URL.createObjectURL(txtBlob);
|
||||
dummya.download = `${$i?.username}-2fa-backup-codes.txt`;
|
||||
dummya.click();
|
||||
}
|
||||
}
|
||||
|
||||
function allDone() {
|
||||
dialog.value.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,21 +139,11 @@ const menuDef = computed(() => [{
|
|||
text: i18n.ts.roles,
|
||||
to: '/settings/roles',
|
||||
active: currentPage?.route.name === 'roles',
|
||||
}, {
|
||||
icon: 'ti ti-planet-off',
|
||||
text: i18n.ts.instanceMute,
|
||||
to: '/settings/instance-mute',
|
||||
active: currentPage?.route.name === 'instance-mute',
|
||||
}, {
|
||||
icon: 'ti ti-ban',
|
||||
text: i18n.ts.muteAndBlock,
|
||||
to: '/settings/mute-block',
|
||||
active: currentPage?.route.name === 'mute-block',
|
||||
}, {
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.wordMute,
|
||||
to: '/settings/word-mute',
|
||||
active: currentPage?.route.name === 'word-mute',
|
||||
}, {
|
||||
icon: 'ti ti-api',
|
||||
text: 'API',
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import * as os from '@/os.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const instanceMutes = ref($i!.mutedInstances.join('\n'));
|
||||
const changed = ref(false);
|
||||
|
|
@ -46,13 +45,4 @@ async function save() {
|
|||
watch(instanceMutes, () => {
|
||||
changed.value = true;
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.instanceMute,
|
||||
icon: 'ti ti-planet-off',
|
||||
});
|
||||
</script>
|
||||
|
|
@ -5,13 +5,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<MkTab v-model="tab">
|
||||
<option value="renoteMute">{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</option>
|
||||
<option value="mute">{{ i18n.ts.mutedUsers }}</option>
|
||||
<option value="block">{{ i18n.ts.blockedUsers }}</option>
|
||||
</MkTab>
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-message-off"></i></template>
|
||||
<template #label>{{ i18n.ts.wordMute }}</template>
|
||||
|
||||
<XWordMute/>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-planet-off"></i></template>
|
||||
<template #label>{{ i18n.ts.instanceMute }}</template>
|
||||
|
||||
<XInstanceMute/>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-repeat-off"></i></template>
|
||||
<template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template>
|
||||
|
||||
<div v-if="tab === 'renoteMute'">
|
||||
<MkPagination :pagination="renoteMutingPagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
|
|
@ -37,9 +48,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-eye-off"></i></template>
|
||||
<template #label>{{ i18n.ts.mutedUsers }}</template>
|
||||
|
||||
<div v-else-if="tab === 'mute'">
|
||||
<MkPagination :pagination="mutingPagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
|
|
@ -67,9 +81,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-ban"></i></template>
|
||||
<template #label>{{ i18n.ts.blockedUsers }}</template>
|
||||
|
||||
<div v-else-if="tab === 'block'">
|
||||
<MkPagination :pagination="blockingPagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
|
|
@ -97,24 +114,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XInstanceMute from './mute-block.instance-mute.vue';
|
||||
import XWordMute from './mute-block.word-mute.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
let tab = $ref('renoteMute');
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const renoteMutingPagination = {
|
||||
endpoint: 'renote-mute/list' as const,
|
||||
|
|
|
|||
|
|
@ -5,29 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<MkTab v-model="tab">
|
||||
<option value="soft">{{ i18n.ts._wordMute.soft }}</option>
|
||||
<option value="hard">{{ i18n.ts._wordMute.hard }}</option>
|
||||
</MkTab>
|
||||
<div>
|
||||
<div v-show="tab === 'soft'" class="_gaps_m">
|
||||
<MkInfo>{{ i18n.ts._wordMute.softDescription }}</MkInfo>
|
||||
<MkTextarea v-model="softMutedWords">
|
||||
<span>{{ i18n.ts._wordMute.muteWords }}</span>
|
||||
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
<div v-show="tab === 'hard'" class="_gaps_m">
|
||||
<MkInfo>{{ i18n.ts._wordMute.hardDescription }} {{ i18n.ts.reflectMayTakeTime }}</MkInfo>
|
||||
<MkTextarea v-model="hardMutedWords">
|
||||
<span>{{ i18n.ts._wordMute.muteWords }}</span>
|
||||
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
<MkKeyValue v-if="hardWordMutedNotesCount != null">
|
||||
<template #key>{{ i18n.ts._wordMute.mutedNotes }}</template>
|
||||
<template #value>{{ number(hardWordMutedNotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
<MkTextarea v-model="mutedWords">
|
||||
<span>{{ i18n.ts._wordMute.muteWords }}</span>
|
||||
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
<MkButton primary inline :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
|
|
@ -56,25 +38,15 @@ const render = (mutedWords) => mutedWords.map(x => {
|
|||
}).join('\n');
|
||||
|
||||
const tab = ref('soft');
|
||||
const softMutedWords = ref(render(defaultStore.state.mutedWords));
|
||||
const hardMutedWords = ref(render($i!.mutedWords));
|
||||
const hardWordMutedNotesCount = ref(null);
|
||||
const mutedWords = ref(render($i!.mutedWords));
|
||||
const changed = ref(false);
|
||||
|
||||
os.api('i/get-word-muted-notes-count', {}).then(response => {
|
||||
hardWordMutedNotesCount.value = response?.count;
|
||||
});
|
||||
|
||||
watch(softMutedWords, () => {
|
||||
changed.value = true;
|
||||
});
|
||||
|
||||
watch(hardMutedWords, () => {
|
||||
watch(mutedWords, () => {
|
||||
changed.value = true;
|
||||
});
|
||||
|
||||
async function save() {
|
||||
const parseMutes = (mutes, tab) => {
|
||||
const parseMutes = (mutes) => {
|
||||
// split into lines, remove empty lines and unnecessary whitespace
|
||||
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
|
||||
|
||||
|
|
@ -92,7 +64,7 @@ async function save() {
|
|||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.regexpError,
|
||||
text: i18n.t('regexpErrorDescription', { tab, line: i + 1 }) + '\n' + err.toString(),
|
||||
text: i18n.t('regexpErrorDescription', { tab: 'word mute', line: i + 1 }) + '\n' + err.toString(),
|
||||
});
|
||||
// re-throw error so these invalid settings are not saved
|
||||
throw err;
|
||||
|
|
@ -105,29 +77,18 @@ async function save() {
|
|||
return lines;
|
||||
};
|
||||
|
||||
let softMutes, hardMutes;
|
||||
let parsed;
|
||||
try {
|
||||
softMutes = parseMutes(softMutedWords.value, i18n.ts._wordMute.soft);
|
||||
hardMutes = parseMutes(hardMutedWords.value, i18n.ts._wordMute.hard);
|
||||
parsed = parseMutes(mutedWords.value);
|
||||
} catch (err) {
|
||||
// already displayed error message in parseMutes
|
||||
return;
|
||||
}
|
||||
|
||||
defaultStore.set('mutedWords', softMutes);
|
||||
await os.api('i/update', {
|
||||
mutedWords: hardMutes,
|
||||
mutedWords: parsed,
|
||||
});
|
||||
|
||||
changed.value = false;
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.wordMute,
|
||||
icon: 'ti ti-message-off',
|
||||
});
|
||||
</script>
|
||||
|
|
@ -38,14 +38,12 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
|
||||
const masterVolume = computed(soundConfigStore.makeGetterSetter('sound_masterVolume'));
|
||||
|
||||
const soundsKeys = ['note', 'noteMy', 'notification', 'chat', 'chatBg', 'antenna', 'channel'] as const;
|
||||
const soundsKeys = ['note', 'noteMy', 'notification', 'antenna', 'channel'] as const;
|
||||
|
||||
const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
|
||||
note: soundConfigStore.reactiveState.sound_note,
|
||||
noteMy: soundConfigStore.reactiveState.sound_noteMy,
|
||||
notification: soundConfigStore.reactiveState.sound_notification,
|
||||
chat: soundConfigStore.reactiveState.sound_chat,
|
||||
chatBg: soundConfigStore.reactiveState.sound_chatBg,
|
||||
antenna: soundConfigStore.reactiveState.sound_antenna,
|
||||
channel: soundConfigStore.reactiveState.sound_channel,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.tl">
|
||||
<MkTimeline
|
||||
ref="tlComponent"
|
||||
:key="src + withRenotes + withReplies + onlyFiles"
|
||||
:key="src + withRenotes + onlyFiles"
|
||||
:src="src.split(':')[0]"
|
||||
:list="src.split(':')[1]"
|
||||
:withRenotes="withRenotes"
|
||||
:withReplies="withReplies"
|
||||
|
||||
:onlyFiles="onlyFiles"
|
||||
:sound="true"
|
||||
@queue="queueUpdated"
|
||||
|
|
|
|||
|
|
@ -129,14 +129,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
|
||||
<template v-if="narrow">
|
||||
<XPhotos :key="user.id" :user="user"/>
|
||||
<XFiles :key="user.id" :user="user"/>
|
||||
<XActivity :key="user.id" :user="user"/>
|
||||
</template>
|
||||
<MkNotes v-if="!disableNotes" :class="$style.tl" :noGap="true" :pagination="pagination"/>
|
||||
<div v-if="!disableNotes">
|
||||
<div style="margin-bottom: 8px;">{{ i18n.ts.featured }}</div>
|
||||
<MkNotes :class="$style.tl" :noGap="true" :pagination="pagination"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
|
||||
<XPhotos :key="user.id" :user="user"/>
|
||||
<XFiles :key="user.id" :user="user"/>
|
||||
<XActivity :key="user.id" :user="user"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -185,7 +188,7 @@ function calcAge(birthdate: string): number {
|
|||
return yearDiff;
|
||||
}
|
||||
|
||||
const XPhotos = defineAsyncComponent(() => import('./index.photos.vue'));
|
||||
const XFiles = defineAsyncComponent(() => import('./index.files.vue'));
|
||||
const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
@ -213,7 +216,7 @@ watch($$(moderationNote), async () => {
|
|||
});
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'users/notes' as const,
|
||||
endpoint: 'users/featured-notes' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
|
|
|
|||
|
|
@ -6,20 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<MkContainer :max-height="300" :foldable="true">
|
||||
<template #icon><i class="ti ti-photo"></i></template>
|
||||
<template #header>{{ i18n.ts.images }}</template>
|
||||
<template #header>{{ i18n.ts.files }}</template>
|
||||
<div :class="$style.root">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-if="!fetching && images.length > 0" :class="$style.stream">
|
||||
<div v-if="!fetching && files.length > 0" :class="$style.stream">
|
||||
<MkA
|
||||
v-for="image in images"
|
||||
:key="image.note.id + image.file.id"
|
||||
v-for="file in files"
|
||||
:key="file.note.id + file.file.id"
|
||||
:class="$style.img"
|
||||
:to="notePage(image.note)"
|
||||
:to="notePage(file.note)"
|
||||
>
|
||||
<ImgWithBlurhash :hash="image.file.blurhash" :src="thumbnail(image.file)" :title="image.file.name"/>
|
||||
<!-- TODO: 画像以外のファイルに対応 -->
|
||||
<ImgWithBlurhash :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name"/>
|
||||
</MkA>
|
||||
</div>
|
||||
<p v-if="!fetching && images.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p>
|
||||
<p v-if="!fetching && files.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p>
|
||||
</div>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
|
@ -40,7 +41,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
let fetching = $ref(true);
|
||||
let images = $ref<{
|
||||
let files = $ref<{
|
||||
note: Misskey.entities.Note;
|
||||
file: Misskey.entities.DriveFile;
|
||||
}[]>([]);
|
||||
|
|
@ -52,24 +53,15 @@ function thumbnail(image: Misskey.entities.DriveFile): string {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
const image = [
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/avif',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/apng',
|
||||
'image/vnd.mozilla.apng',
|
||||
];
|
||||
os.api('users/notes', {
|
||||
userId: props.user.id,
|
||||
fileType: image,
|
||||
withFiles: true,
|
||||
excludeNsfw: defaultStore.state.nsfw !== 'ignore',
|
||||
limit: 10,
|
||||
limit: 15,
|
||||
}).then(notes => {
|
||||
for (const note of notes) {
|
||||
for (const file of note.files) {
|
||||
images.push({
|
||||
files.push({
|
||||
note,
|
||||
file,
|
||||
});
|
||||
|
|
@ -29,7 +29,7 @@ const props = defineProps<{
|
|||
user: Misskey.entities.UserDetailed;
|
||||
}>();
|
||||
|
||||
const include = ref<string | null>(null);
|
||||
const include = ref<string | null>('all');
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'users/notes' as const,
|
||||
|
|
@ -38,6 +38,7 @@ const pagination = {
|
|||
userId: props.user.id,
|
||||
withRenotes: include.value === 'all',
|
||||
withReplies: include.value === 'all' || include.value === 'files',
|
||||
withChannelNotes: include.value === 'all',
|
||||
withFiles: include.value === 'files',
|
||||
})),
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue