Merge remote-tracking branch 'misskey-original/develop' into develop

# Conflicts:
#	packages/backend/src/models/json-schema/user.ts
#	packages/frontend/src/components/MkNote.vue
#	packages/frontend/src/components/MkReactionsViewer.reaction.vue
#	packages/frontend/src/pages/settings/mute-block.word-mute.vue
#	packages/misskey-js/etc/misskey-js.api.md
This commit is contained in:
mattyatea 2023-11-24 22:44:42 +09:00
commit ff79746100
75 changed files with 1234 additions and 537 deletions

View file

@ -65,9 +65,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<img src="https://avatars.githubusercontent.com/u/67428053?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@kakkokari-gtyih</span>
</a>
<a href="https://github.com/taichanNE30" target="_blank" :class="$style.contributor">
<a href="https://github.com/tai-cha" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/40626578?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@taichanNE30</span>
<span :class="$style.contributorUsername">@tai-cha</span>
</a>
</div>
</FormSection>

View file

@ -9,12 +9,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<XHeader :actions="headerActions" :tabs="headerTabs"/>
</template>
<MkSpacer :contentMax="900">
<MkSwitch :modelValue="publishing" @update:modelValue="onChangePublishing">
{{ i18n.ts.publishing }}
</MkSwitch>
<MkSelect v-model="filterType" :class="$style.input" @update:modelValue="filterItems">
<template #label>{{ i18n.ts.state }}</template>
<option value="all">{{ i18n.ts.all }}</option>
<option value="publishing">{{ i18n.ts.publishing }}</option>
<option value="expired">{{ i18n.ts.expired }}</option>
</MkSelect>
<div>
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
<MkAd v-if="ad.url" :specify="ad"/>
<MkAd v-if="ad.url" :key="ad.id" :specify="ad"/>
<MkInput v-model="ad.url" type="url">
<template #label>URL</template>
</MkInput>
@ -82,14 +85,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import { ref } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
@ -101,24 +104,34 @@ let ads: any[] = $ref([]);
const localTime = new Date();
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday];
let publishing = false;
const filterType = ref('all');
let publishing: boolean | null = null;
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
ads = adsResponse.map(r => {
const exdate = new Date(r.expiresAt);
const stdate = new Date(r.startsAt);
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
return {
...r,
expiresAt: exdate.toISOString().slice(0, 16),
startsAt: stdate.toISOString().slice(0, 16),
};
});
if (adsResponse != null) {
ads = adsResponse.map(r => {
const exdate = new Date(r.expiresAt);
const stdate = new Date(r.startsAt);
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
return {
...r,
expiresAt: exdate.toISOString().slice(0, 16),
startsAt: stdate.toISOString().slice(0, 16),
};
});
}
});
const onChangePublishing = (v) => {
publishing = v;
const filterItems = (v) => {
if (v === 'publishing') {
publishing = true;
} else if (v === 'expired') {
publishing = false;
} else {
publishing = null;
}
refresh();
};
@ -197,6 +210,7 @@ function save(ad) {
function more() {
os.api('admin/ad/list', { untilId: ads.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => {
if (adsResponse == null) return;
ads = ads.concat(adsResponse.map(r => {
const exdate = new Date(r.expiresAt);
const stdate = new Date(r.startsAt);
@ -213,6 +227,7 @@ function more() {
function refresh() {
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
if (adsResponse == null) return;
ads = adsResponse.map(r => {
const exdate = new Date(r.expiresAt);
const stdate = new Date(r.startsAt);
@ -252,4 +267,7 @@ definePageMetadata({
margin-bottom: var(--margin);
}
}
.input {
margin-bottom: 32px;
}
</style>

View file

@ -214,13 +214,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
<div class="_gaps_s">
<MkFoldableSection>
<template #header>Manual roles</template>
<template #header>{{ i18n.ts._role.manualRoles }}</template>
<div class="_gaps_s">
<MkRolePreview v-for="role in roles.filter(x => x.target === 'manual')" :key="role.id" :role="role" :forModeration="true"/>
</div>
</MkFoldableSection>
<MkFoldableSection>
<template #header>Conditional roles</template>
<template #header>{{ i18n.ts._role.conditionalRoles }}</template>
<div class="_gaps_s">
<MkRolePreview v-for="role in roles.filter(x => x.target === 'conditional')" :key="role.id" :role="role" :forModeration="true"/>
</div>

View file

@ -72,6 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="medium">{{ i18n.ts.medium }}</option>
<option value="large">{{ i18n.ts.large }}</option>
</MkRadios>
<MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
</div>
<MkSelect v-model="instanceTicker">
@ -254,6 +255,7 @@ const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serve
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction'));
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
@ -358,6 +360,7 @@ watch([
overridedDeviceKind,
mediaListWithOneImageAppearance,
reactionsDisplaySize,
limitWidthOfReaction,
highlightSensitiveMedia,
keepScreenOn,
showMediaTimeline,

View file

@ -9,7 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.wordMute }}</template>
<XWordMute/>
<XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.hardWordMute }}</template>
<XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/>
</MkFolder>
<MkFolder>
@ -129,6 +136,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os.js';
import { infoImageUrl } from '@/instance.js';
import { $i } from '@/account.js';
import MkFolder from '@/components/MkFolder.vue';
const renoteMutingPagination = {
@ -207,6 +215,14 @@ async function toggleBlockItem(item) {
}
}
async function saveMutedWords(mutedWords: (string | string[])[]) {
await os.api('i/update', { mutedWords });
}
async function saveHardMutedWords(hardMutedWords: (string | string[])[]) {
await os.api('i/update', { hardMutedWords });
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);

View file

@ -19,16 +19,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import {computed, ref, watch} from 'vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkTab from '@/components/MkTab.vue';
import * as os from '@/os.js';
import number from '@/filters/number.js';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const props = defineProps<{
muted: (string[] | string)[];
}>();
const emit = defineEmits<{
(ev: 'save', value: (string[] | string)[]): void;
}>();
import MkSwitch from "@/components/MkSwitch.vue";
const render = (mutedWords) => mutedWords.map(x => {
@ -86,9 +87,7 @@ async function save() {
return;
}
await os.api('i/update', {
mutedWords: parsed,
});
emit('save', parsed);
changed.value = false;
}

View file

@ -11,10 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="fetching"/>
<div v-if="!fetching && files.length > 0" :class="$style.stream">
<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 v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.img" @click="showingFiles.push(file.file.id)">
<!-- TODO: 画像以外のファイルに対応 -->
<ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/>
<div :class="$style.sensitive">
<div>
<div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div>
<div>{{ i18n.ts.clickToShow }}</div>
</div>
</div>
</div>
<MkA v-else :class="$style.img" :to="notePage(file.note)">
@ -90,6 +94,7 @@ onMounted(() => {
}
.img {
position: relative;
height: 128px;
border-radius: 6px;
overflow: clip;
@ -101,8 +106,24 @@ onMounted(() => {
text-align: center;
}
.sensitiveImg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
filter: brightness(0.7);
}
.sensitive {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: grid;
place-items: center;
font-size: 0.8em;
color: #fff;
cursor: pointer;
}
</style>

View file

@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XPages v-else-if="tab === 'pages'" :user="user"/>
<XFlashs v-else-if="tab === 'flashs'" :user="user"/>
<XGallery v-else-if="tab === 'gallery'" :user="user"/>
<XRaw v-else-if="tab === 'raw'" :user="user"/>
</div>
<MkError v-else-if="error" @retry="fetchUser()"/>
<MkLoading v-else/>
@ -44,6 +45,7 @@ const XLists = defineAsyncComponent(() => import('./lists.vue'));
const XPages = defineAsyncComponent(() => import('./pages.vue'));
const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
const props = withDefaults(defineProps<{
acct: string;
@ -112,6 +114,10 @@ const headerTabs = $computed(() => user ? [{
key: 'gallery',
title: i18n.ts.gallery,
icon: 'ti ti-icons',
}, {
key: 'raw',
title: 'Raw',
icon: 'ti ti-code',
}] : []);
definePageMetadata(computed(() => user ? {

View file

@ -0,0 +1,130 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkSpacer :contentMax="600" :marginMin="16" :marginMax="32">
<div class="_gaps_m">
<div :class="$style.userMInfoRoot">
<MkAvatar :class="$style.userMInfoAvatar" :user="user" indicator link preview/>
<div :class="$style.userMInfoMetaRoot">
<span :class="$style.userMInfoMetaName"><MkUserName :class="$style.userMInfoMetaName" :user="user"/></span>
<span :class="$style.userMInfoMetaSub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
<span :class="$style.userMInfoMetaState">
<span v-if="suspended" :class="$style.suspended">Suspended</span>
<span v-if="silenced" :class="$style.silenced">Silenced</span>
<span v-if="moderator" :class="$style.moderator">Moderator</span>
</span>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 1em;">
<MkKeyValue :copy="user.id" oneline>
<template #key>ID</template>
<template #value><span class="_monospace">{{ user.id }}</span></template>
</MkKeyValue>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.createdAt }}</template>
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
</MkKeyValue>
</div>
<FormSection>
<template #label>Raw</template>
<MkObjectView tall :value="user"></MkObjectView>
</FormSection>
</div>
</MkSpacer>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import * as Misskey from 'misskey-js';
import { acct } from '@/filters/user.js';
import { i18n } from '@/i18n.js';
import MkKeyValue from '@/components/MkKeyValue.vue';
import FormSection from '@/components/form/section.vue';
import MkObjectView from '@/components/MkObjectView.vue';
const props = defineProps<{
user: Misskey.entities.User;
}>();
const moderator = computed(() => props.user.isModerator ?? false);
const silenced = computed(() => props.user.isSilenced ?? false);
const suspended = computed(() => props.user.isSuspended ?? false);
</script>
<style lang="scss" module>
.userMInfoRoot {
display: flex;
align-items: center;
}
.userMInfoAvatar {
display: block;
width: 64px;
height: 64px;
margin-right: 16px;
}
.userMInfoMetaRoot {
flex: 1;
overflow: hidden;
}
.userMInfoMetaName {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.userMInfoMetaSub {
display: block;
width: 100%;
font-size: 85%;
opacity: 0.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.userMInfoMetaState {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 4px;
&:empty {
display: none;
}
> .suspended,
> .silenced,
> .moderator {
display: inline-block;
border: solid 1px;
border-radius: 6px;
padding: 2px 6px;
font-size: 85%;
}
> .suspended {
color: var(--error);
border-color: var(--error);
}
> .silenced {
color: var(--warn);
border-color: var(--warn);
}
> .moderator {
color: var(--success);
border-color: var(--success);
}
}
</style>