merge: all upstream changes

This commit is contained in:
Mar0xy 2023-10-13 19:01:17 +02:00
commit f8f128b347
No known key found for this signature in database
GPG key ID: 56569BBE47D2C828
170 changed files with 4490 additions and 2218 deletions

View file

@ -46,14 +46,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #value>{{ instance.maintainerEmail }}</template>
</MkKeyValue>
</FormSplit>
<MkFolder v-if="instance.serverRules.length > 0">
<template #label>{{ i18n.ts.serverRules }}</template>
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink>
<div class="_formLinks">
<MkFolder v-if="instance.serverRules.length > 0">
<template #label>{{ i18n.ts.serverRules }}</template>
<ol class="_gaps_s" :class="$style.rules">
<li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
</ol>
</MkFolder>
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink>
<ol class="_gaps_s" :class="$style.rules">
<li v-for="item, index in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
</ol>
</MkFolder>
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink>
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>{{ i18n.ts.privacyPolicy }}</FormLink>
</div>
</div>
</FormSection>

View 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="ph-key ph-bold ph-lg"></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="ph-check ph-bold ph-lg"></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>

View file

@ -198,6 +198,11 @@ const menuDef = $computed(() => [{
text: i18n.ts.proxyAccount,
to: '/admin/proxy-account',
active: currentPage?.route.name === 'proxy-account',
}, {
icon: 'ph-square-arrow-out ph-bold pg-lg',
text: i18n.ts.externalServices,
to: '/admin/external-services',
active: currentPage?.route.name === 'external-services',
}, {
icon: 'ph-faders ph-bold ph-lg',
text: i18n.ts.other,

View file

@ -25,6 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.tosUrl }}</template>
</MkInput>
<MkInput v-model="privacyPolicyUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
</MkInput>
<MkTextarea v-model="preservedUsernames">
<template #label>{{ i18n.ts.preservedUsernames }}</template>
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
@ -69,6 +74,7 @@ let emailRequiredForSignup: boolean = $ref(false);
let sensitiveWords: string = $ref('');
let preservedUsernames: string = $ref('');
let tosUrl: string | null = $ref(null);
let privacyPolicyUrl: string | null = $ref(null);
async function init() {
const meta = await os.api('admin/meta');
@ -77,6 +83,7 @@ async function init() {
sensitiveWords = meta.sensitiveWords.join('\n');
preservedUsernames = meta.preservedUsernames.join('\n');
tosUrl = meta.tosUrl;
privacyPolicyUrl = meta.privacyPolicyUrl;
}
function save() {
@ -84,6 +91,7 @@ function save() {
disableRegistration: !enableRegistration,
emailRequiredForSignup,
tosUrl,
privacyPolicyUrl,
sensitiveWords: sensitiveWords.split('\n'),
preservedUsernames: preservedUsernames.split('\n'),
}).then(() => {

View file

@ -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>

View file

@ -34,6 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
</FormSplit>
<MkInput v-model="impressumUrl">
<template #label>{{ i18n.ts.impressumUrl }}</template>
<template #prefix><i class="ti ti-link"></i></template>
<template #caption>{{ i18n.ts.impressumDescription }}</template>
</MkInput>
<MkTextarea v-model="pinnedUsers">
<template #label>{{ i18n.ts.pinnedUsers }}</template>
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
@ -81,16 +87,40 @@ 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="ph-key ph-bold ph-lg"></i></template>
<template #label>DeepL Auth Key</template>
<MkInput v-model="perLocalUserUserTimelineCacheMax" type="number">
<template #label>perLocalUserUserTimelineCacheMax</template>
</MkInput>
<MkSwitch v-model="deeplIsPro">
<template #label>Pro account</template>
</MkSwitch>
<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>
</div>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts._ad.adsSettings }}</template>
<div class="_gaps_m">
<div class="_gaps_s">
<MkInput v-model="notesPerOneAd" :min="0" type="number">
<template #label>{{ i18n.ts._ad.notesPerOneAd }}</template>
<template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
</MkInput>
<MkInfo v-if="notesPerOneAd > 0 && notesPerOneAd < 20" :warn="true">
{{ i18n.ts._ad.adsTooClose }}
</MkInfo>
</div>
</div>
</FormSection>
</div>
@ -113,6 +143,7 @@ import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkInfo from '@/components/MkInfo.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
@ -127,14 +158,18 @@ let shortName: string | null = $ref(null);
let description: string | null = $ref(null);
let maintainerName: string | null = $ref(null);
let maintainerEmail: string | null = $ref(null);
let impressumUrl: string | null = $ref(null);
let pinnedUsers: string = $ref('');
let cacheRemoteFiles: boolean = $ref(false);
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);
let notesPerOneAd: number = $ref(0);
async function init(): Promise<void> {
const meta = await os.api('admin/meta');
@ -143,34 +178,42 @@ async function init(): Promise<void> {
description = meta.description;
maintainerName = meta.maintainerName;
maintainerEmail = meta.maintainerEmail;
impressumUrl = meta.impressumUrl;
pinnedUsers = meta.pinnedUsers.join('\n');
cacheRemoteFiles = meta.cacheRemoteFiles;
cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles;
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;
notesPerOneAd = meta.notesPerOneAd;
}
function save(): void {
os.apiWithDialog('admin/update-meta', {
async function save(): void {
await os.apiWithDialog('admin/update-meta', {
name,
shortName: shortName === '' ? null : shortName,
description,
maintainerName,
maintainerEmail,
impressumUrl,
pinnedUsers: pinnedUsers.split('\n'),
cacheRemoteFiles,
cacheRemoteSensitiveFiles,
enableServiceWorker,
swPublicKey,
swPrivateKey,
deeplAuthKey,
deeplIsPro,
}).then(() => {
fetchInstance();
perLocalUserUserTimelineCacheMax,
perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax,
notesPerOneAd,
});
fetchInstance();
}
const headerTabs = $computed(() => []);

View file

@ -102,7 +102,6 @@ let searchKey = $ref('');
const featuredPagination = $computed(() => ({
endpoint: 'notes/featured' as const,
limit: 10,
offsetMode: true,
params: {
channelId: props.channelId,
},

View file

@ -0,0 +1,302 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps">
<MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo>
<MkLoading v-if="fetching"/>
<div v-else-if="file" class="_gaps">
<div :class="$style.filePreviewRoot">
<MkMediaList :mediaList="[file]"></MkMediaList>
</div>
<div :class="$style.fileQuickActionsRoot">
<button class="_button" :class="$style.fileNameEditBtn" @click="rename()">
<h2 class="_nowrap" :class="$style.fileName">{{ file.name }}</h2>
<i class="ti ti-pencil" :class="$style.fileNameEditIcon"></i>
</button>
<div :class="$style.fileQuickActionsOthers">
<button v-tooltip="i18n.ts.createNoteFromTheFile" class="_button" :class="$style.fileQuickActionsOthersButton" @click="postThis()">
<i class="ti ti-pencil"></i>
</button>
<button v-if="isImage" v-tooltip="i18n.ts.cropImage" class="_button" :class="$style.fileQuickActionsOthersButton" @click="crop()">
<i class="ti ti-crop"></i>
</button>
<button v-if="file.isSensitive" v-tooltip="i18n.ts.unmarkAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()">
<i class="ti ti-eye"></i>
</button>
<button v-else v-tooltip="i18n.ts.markAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()">
<i class="ti ti-eye-exclamation"></i>
</button>
<a v-tooltip="i18n.ts.download" :href="file.url" :download="file.name" class="_button" :class="$style.fileQuickActionsOthersButton">
<i class="ti ti-download"></i>
</a>
<button v-tooltip="i18n.ts.delete" class="_button" :class="[$style.fileQuickActionsOthersButton, $style.danger]" @click="deleteFile()">
<i class="ti ti-trash"></i>
</button>
</div>
</div>
<div>
<button class="_button" :class="$style.fileAltEditBtn" @click="describe()">
<MkKeyValue>
<template #key>{{ i18n.ts.description }}</template>
<template #value>{{ file.comment ? file.comment : `(${i18n.ts.none})` }}<i class="ti ti-pencil" :class="$style.fileAltEditIcon"></i></template>
</MkKeyValue>
</button>
<MkKeyValue :class="$style.fileMetaDataChildren">
<template #key>{{ i18n.ts._fileViewer.uploadedAt }}</template>
<template #value><MkTime :time="file.createdAt" mode="detail"/></template>
</MkKeyValue>
<MkKeyValue :class="$style.fileMetaDataChildren">
<template #key>{{ i18n.ts._fileViewer.type }}</template>
<template #value>{{ file.type }}</template>
</MkKeyValue>
<MkKeyValue :class="$style.fileMetaDataChildren">
<template #key>{{ i18n.ts._fileViewer.size }}</template>
<template #value>{{ bytes(file.size) }}</template>
</MkKeyValue>
</div>
</div>
<div v-else class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineAsyncComponent, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import MkInfo from '@/components/MkInfo.vue';
import MkMediaList from '@/components/MkMediaList.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import bytes from '@/filters/bytes.js';
import { infoImageUrl } from '@/instance.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { useRouter } from '@/router.js';
const router = useRouter();
const props = defineProps<{
fileId: string;
}>();
const fetching = ref(true);
const file = ref<Misskey.entities.DriveFile>();
const isImage = computed(() => file.value?.type.startsWith('image/'));
async function fetch() {
fetching.value = true;
file.value = await os.api('drive/files/show', {
fileId: props.fileId,
}).catch((err) => {
console.error(err);
return undefined;
});
fetching.value = false;
}
function postThis() {
if (!file.value) return;
os.post({
initialFiles: [file.value],
});
}
function crop() {
if (!file.value) return;
os.cropImage(file.value, {
aspectRatio: NaN,
uploadFolder: file.value.folderId ?? null,
});
}
function toggleSensitive() {
if (!file.value) return;
os.apiWithDialog('drive/files/update', {
fileId: file.value.id,
isSensitive: !file.value.isSensitive,
}).then(async () => {
await fetch();
}).catch(err => {
os.alert({
type: 'error',
title: i18n.ts.error,
text: err.message,
});
});
}
function rename() {
if (!file.value) return;
os.inputText({
title: i18n.ts.renameFile,
placeholder: i18n.ts.inputNewFileName,
default: file.value.name,
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.apiWithDialog('drive/files/update', {
fileId: file.value.id,
name: name,
}).then(async () => {
await fetch();
});
});
}
function describe() {
if (!file.value) return;
os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
default: file.value.comment ?? '',
file: file.value,
}, {
done: caption => {
os.apiWithDialog('drive/files/update', {
fileId: file.value.id,
comment: caption.length === 0 ? null : caption,
}).then(async () => {
await fetch();
});
},
}, 'closed');
}
async function deleteFile() {
if (!file.value) return;
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('driveFileDeleteConfirm', { name: file.value.name }),
});
if (canceled) return;
await os.apiWithDialog('drive/files/delete', {
fileId: file.value.id,
});
router.push('/my/drive');
}
onMounted(async () => {
await fetch();
});
</script>
<style lang="scss" module>
.filePreviewRoot {
background: var(--panel);
border-radius: var(--radius);
// MkMediaList 4px
padding: calc(1rem - 4px) 1rem 1rem;
}
.fileQuickActionsRoot {
display: flex;
flex-direction: column;
gap: 8px;
}
@container (min-width: 500px) {
.fileQuickActionsRoot {
flex-direction: row;
align-items: center;
}
}
.fileQuickActionsOthers {
margin-left: auto;
margin-right: 1rem;
display: flex;
gap: 8px;
.fileQuickActionsOthersButton {
padding: .5rem;
border-radius: 99rem;
&:hover,
&:focus-visible {
background-color: var(--accentedBg);
color: var(--accent);
text-decoration: none;
}
&.danger {
color: #ff2a2a;
}
&.danger:hover,
&.danger:focus-visible {
background-color: rgba(255, 42, 42, .15);
}
}
}
.fileNameEditBtn {
padding: .5rem 1rem;
display: flex;
align-items: center;
min-width: 0;
font-weight: 700;
border-radius: var(--radius);
font-size: .8rem;
>.fileNameEditIcon {
color: transparent;
visibility: hidden;
padding-left: .5rem;
}
>.fileName {
margin: 0;
}
&:hover {
background-color: var(--accentedBg);
>.fileName,
>.fileNameEditIcon {
visibility: visible;
color: var(--accent);
}
}
}
.fileMetaDataChildren {
padding: .5rem 1rem;
}
.fileAltEditBtn {
text-align: start;
display: block;
width: 100%;
padding: .5rem 1rem;
border-radius: var(--radius);
.fileAltEditIcon {
display: inline-block;
color: transparent;
visibility: hidden;
padding-left: .5rem;
}
&:hover {
color: var(--accent);
background-color: var(--accentedBg);
.fileAltEditIcon {
color: var(--accent);
visibility: visible;
}
}
}
</style>

View file

@ -0,0 +1,33 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps">
<MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo>
<MkNotes ref="tlComponent" :pagination="pagination"/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
import { Paging } from '@/components/MkPagination.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkNotes from '@/components/MkNotes.vue';
const props = defineProps<{
fileId: string;
}>();
const realFileId = computed(() => props.fileId);
const pagination = ref<Paging>({
endpoint: 'drive/files/attached-notes',
limit: 10,
params: {
fileId: realFileId.value,
},
});
</script>

View file

@ -0,0 +1,52 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header>
<MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/>
</template>
<MkSpacer v-if="tab === 'info'" :contentMax="800">
<XFileInfo :fileId="fileId"/>
</MkSpacer>
<MkSpacer v-else-if="tab === 'notes'" :contentMax="800">
<XNotes :fileId="fileId"/>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, ref, defineAsyncComponent } from 'vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const props = defineProps<{
fileId: string;
}>();
const XFileInfo = defineAsyncComponent(() => import('./drive.file.info.vue'));
const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue'));
const tab = ref('info');
const headerActions = computed(() => []);
const headerTabs = computed(() => [{
key: 'info',
title: i18n.ts.info,
icon: 'ti ti-info-circle',
}, {
key: 'notes',
title: i18n.ts._fileViewer.attachedNotes,
icon: 'ti ti-pencil',
}]);
definePageMetadata(computed(() => ({
title: i18n.ts._fileViewer.title,
icon: 'ti ti-file',
})));
</script>

View file

@ -22,7 +22,6 @@ import { i18n } from '@/i18n.js';
const paginationForNotes = {
endpoint: 'notes/featured' as const,
limit: 10,
offsetMode: true,
};
const paginationForPolls = {

View file

@ -286,8 +286,7 @@ definePageMetadata(computed(() => {
let title = i18n.ts._pages.newPage;
if (props.initPageId) {
title = i18n.ts._pages.editPage;
}
else if (props.initPageName && props.initUser) {
} else if (props.initPageName && props.initUser) {
title = i18n.ts._pages.readPage;
}
return {

View file

@ -139,21 +139,11 @@ const menuDef = computed(() => [{
text: i18n.ts.roles,
to: '/settings/roles',
active: currentPage?.route.name === 'roles',
}, {
icon: 'ph-speaker-none ph-bold pg-lg',
text: i18n.ts.instanceMute,
to: '/settings/instance-mute',
active: currentPage?.route.name === 'instance-mute',
}, {
icon: 'ph-prohibit ph-bold ph-lg',
text: i18n.ts.muteAndBlock,
to: '/settings/mute-block',
active: currentPage?.route.name === 'mute-block',
}, {
icon: 'ph-speaker-x ph-bold ph-lg',
text: i18n.ts.wordMute,
to: '/settings/word-mute',
active: currentPage?.route.name === 'word-mute',
}, {
icon: 'ph-key ph-bold pg-lg',
text: 'API',

View file

@ -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: 'ph-prohibit ph-bold pg-lg',
});
</script>

View file

@ -5,6 +5,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<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>
@ -106,6 +120,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<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 { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js';

View file

@ -91,13 +91,4 @@ async function save() {
changed.value = false;
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.wordMute,
icon: 'ph-bell-slash ph-bold ph-lg',
});
</script>

View file

@ -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,
});

View file

@ -15,10 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tl">
<MkTimeline
ref="tlComponent"
:key="src + withRenotes + onlyFiles"
:key="src + withRenotes + withReplies + onlyFiles"
:src="src.split(':')[0]"
:list="src.split(':')[1]"
:withRenotes="withRenotes"
:withReplies="withReplies"
:onlyFiles="onlyFiles"
:sound="true"
@queue="queueUpdated"
@ -42,7 +43,7 @@ import { instance } from '@/instance.js';
import { $i } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
import { antennasCache, userListsCache } from '@/cache';
import { antennasCache, userListsCache } from '@/cache.js';
provide('shouldOmitHeaderTitle', true);
@ -61,10 +62,15 @@ let queue = $ref(0);
let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global');
const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) });
const withRenotes = $ref(true);
const withReplies = $ref($i ? defaultStore.state.tlWithReplies : false);
const onlyFiles = $ref(false);
watch($$(src), () => queue = 0);
watch($$(withReplies), (x) => {
if ($i) defaultStore.set('tlWithReplies', x);
});
function queueUpdated(q: number): void {
queue = q;
}
@ -142,7 +148,11 @@ const headerActions = $computed(() => [{
text: i18n.ts.showRenotes,
icon: 'ph-repeat ph-bold ph-lg',
ref: $$(withRenotes),
}, {
}, src === 'local' || src === 'social' ? {
type: 'switch',
text: i18n.ts.showRepliesToOthersInTimeline,
ref: $$(withReplies),
} : undefined, {
type: 'switch',
text: i18n.ts.fileAttachedOnly,
icon: 'ph-image ph-bold pg-lg',

View file

@ -61,20 +61,7 @@ function settings() {
router.push(`/my/lists/${props.listId}`);
}
async function timetravel() {
const { canceled, result: date } = await os.inputDate({
title: i18n.ts.date,
});
if (canceled) return;
tlEl.timetravel(date);
}
const headerActions = $computed(() => list ? [{
icon: 'ph-calendar ph-bold ph-lg',
text: i18n.ts.jumpToSpecifiedDate,
handler: timetravel,
}, {
icon: 'ph-gear ph-bold pg-lg',
text: i18n.ts.settings,
handler: settings,

View file

@ -131,15 +131,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<XFiles :key="user.id" :user="user"/>
<XActivity :key="user.id" :user="user"/>
</template>
<!-- <div v-if="!disableNotes">
<div style="margin-bottom: 8px; z-index: 1;">{{ i18n.ts.featured }}</div>
<MkNotes :class="$style.tl" :noGap="true" :pagination="pagination"/>
</div> -->
<MkStickyContainer>
<template #header>
<MkTab v-model="noteview" :class="$style.tab">
<option :value="null">{{ i18n.ts.notes }}</option>
<option value="replies">{{ i18n.ts.notesAndReplies }}</option>
<option value="replies">{{ i18n.ts.all }}</option>
<option value="files">{{ i18n.ts.withFiles }}</option>
</MkTab>
</template>
<MkNotes v-if="!disableNotes" :class="$style.tl" :noGap="true" :pagination="pagination"/>
<MkNotes :class="$style.tl" :noGap="true" :pagination="AllPagination"/>
</MkStickyContainer>
</div>
</div>
@ -248,11 +252,21 @@ watch($$(moderationNote), async () => {
});
const pagination = {
endpoint: 'users/featured-notes' as const,
limit: 10,
params: computed(() => ({
userId: props.user.id
})),
};
const AllPagination = {
endpoint: 'users/notes' as const,
limit: 10,
params: computed(() => ({
userId: props.user.id,
withReplies: noteview === 'replies' || noteview === 'files',
withRenotes: noteview === 'all',
withReplies: noteview === 'all' || noteview === 'files',
withChannelNotes: noteview === 'all',
withFiles: noteview === 'files',
})),
};

View file

@ -10,15 +10,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<MkLoading v-if="fetching"/>
<div v-if="!fetching && files.length > 0" :class="$style.stream">
<MkA
v-for="file in files"
:key="file.note.id + file.file.id"
:class="$style.img"
:to="notePage(file.note)"
>
<!-- TODO: 画像以外のファイルに対応 -->
<ImgWithBlurhash :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name"/>
</MkA>
<template v-for="file in files" :key="file.note.id + file.file.id">
<div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.sensitive" @click="showingFiles.push(file.file.id)">
<div>
<div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div>
<div>{{ i18n.ts.clickToShow }}</div>
</div>
</div>
<MkA v-else :class="$style.img" :to="notePage(file.note)">
<!-- TODO: 画像以外のファイルに対応 -->
<ImgWithBlurhash :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name"/>
</MkA>
</template>
</div>
<p v-if="!fetching && files.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p>
</div>
@ -45,6 +48,7 @@ let files = $ref<{
note: Misskey.entities.Note;
file: Misskey.entities.DriveFile;
}[]>([]);
let showingFiles = $ref<string[]>([]);
function thumbnail(image: Misskey.entities.DriveFile): string {
return defaultStore.state.disableShowingAnimatedImages
@ -94,4 +98,9 @@ onMounted(() => {
padding: 16px;
text-align: center;
}
.sensitive {
display: grid;
place-items: center;
}
</style>

View 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',
})),
};