1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-22 10:34:13 +02:00

Merge branch 'develop' into mahjong

This commit is contained in:
syuilo
2024-07-28 15:18:08 +09:00
252 changed files with 6666 additions and 4778 deletions

View File

@@ -243,6 +243,21 @@ const patronsWithIcon = [{
}, {
name: '越貝鯛丸',
icon: 'https://assets.misskey-hub.net/patrons/86c7374de37849b882d8ebbc833dc968.jpg',
}, {
name: '☔あめ🍬(灬˘╰╯˘灬)',
icon: 'https://assets.misskey-hub.net/patrons/676eea72d4884d3f89aababbb62533fb.jpg',
}, {
name: '貯水よび',
icon: 'https://assets.misskey-hub.net/patrons/2974506d53244bbe94a67707b27099e2.jpg',
}, {
name: 'はるかさ',
icon: 'https://assets.misskey-hub.net/patrons/26ce2432739a400aa3aa0de0ef67a107.jpg',
}, {
name: '天鈴のあ',
icon: 'https://assets.misskey-hub.net/patrons/995cdbb00bd6421184461a883adfe1d9.jpg',
}, {
name: 'えとゔぁす',
icon: 'https://assets.misskey-hub.net/patrons/2578f441b82a44cfaa55ba83a318b26e.jpg',
}];
const patrons = [
@@ -347,6 +362,7 @@ const patrons = [
'SHO SEKIGUCHI',
'塩キャベツ',
'はとぽぷさん',
'100の人 (エスパー・イーシア)',
];
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));

View File

@@ -0,0 +1,205 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps_m">
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
<div style="overflow: clip;">
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
<div :class="$style.bannerName">
<b>{{ instance.name ?? host }}</b>
</div>
</div>
</div>
<MkKeyValue>
<template #key>{{ i18n.ts.description }}</template>
<template #value><div v-html="instance.description"></div></template>
</MkKeyValue>
<FormSection>
<div class="_gaps_m">
<MkKeyValue :copy="version">
<template #key>Misskey</template>
<template #value>{{ version }}</template>
</MkKeyValue>
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
</div>
<FormLink to="/about-misskey">
<template #icon><i class="ti ti-info-circle"></i></template>
{{ i18n.ts.aboutMisskey }}
</FormLink>
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
<template #icon><i class="ti ti-code"></i></template>
{{ i18n.ts.sourceCode }}
</FormLink>
<MkInfo v-else warn>
{{ i18n.ts.sourceCodeIsNotYetProvided }}
</MkInfo>
</div>
</FormSection>
<FormSection>
<div class="_gaps_m">
<FormSplit>
<MkKeyValue :copy="instance.maintainerName">
<template #key>{{ i18n.ts.administrator }}</template>
<template #value>
<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template>
</MkKeyValue>
<MkKeyValue :copy="instance.maintainerEmail">
<template #key>{{ i18n.ts.contact }}</template>
<template #value>
<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.inquiry }}</template>
<template #value>
<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template>
</MkKeyValue>
</FormSplit>
<div class="_gaps_s">
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
<template #icon><i class="ti ti-user-shield"></i></template>
<template #default>{{ i18n.ts.impressum }}</template>
</FormLink>
<MkFolder v-if="instance.serverRules.length > 0">
<template #icon><i class="ti ti-checkup-list"></i></template>
<template #label>{{ i18n.ts.serverRules }}</template>
<ol class="_gaps_s" :class="$style.rules">
<li v-for="item in instance.serverRules" :key="item" :class="$style.rule">
<div :class="$style.ruleText" v-html="item"></div>
</li>
</ol>
</MkFolder>
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
<template #icon><i class="ti ti-license"></i></template>
<template #default>{{ i18n.ts.termsOfService }}</template>
</FormLink>
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
<template #icon><i class="ti ti-shield-lock"></i></template>
<template #default>{{ i18n.ts.privacyPolicy }}</template>
</FormLink>
<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
<template #icon><i class="ti ti-message"></i></template>
<template #default>{{ i18n.ts.feedback }}</template>
</FormLink>
</div>
</div>
</FormSection>
<FormSuspense v-slot="{ result: stats }" :p="initStats">
<FormSection>
<template #label>{{ i18n.ts.statistics }}</template>
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts.users }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.notes }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template>
</MkKeyValue>
</FormSplit>
</FormSection>
</FormSuspense>
<FormSection>
<template #label>Well-known resources</template>
<div class="_gaps_s">
<FormLink to="/.well-known/host-meta" external>host-meta</FormLink>
<FormLink to="/.well-known/host-meta.json" external>host-meta.json</FormLink>
<FormLink to="/.well-known/nodeinfo" external>nodeinfo</FormLink>
<FormLink to="/robots.txt" external>robots.txt</FormLink>
<FormLink to="/manifest.json" external>manifest.json</FormLink>
</div>
</FormSection>
</div>
</template>
<script lang="ts" setup>
import { host, version } from '@/config.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import number from '@/filters/number.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkLink from '@/components/MkLink.vue';
const initStats = () => misskeyApi('stats', {});
</script>
<style lang="scss" module>
.banner {
text-align: center;
border-radius: 10px;
overflow: clip;
background-color: var(--panel);
background-size: cover;
background-position: center center;
}
.bannerIcon {
display: block;
margin: 16px auto 0 auto;
height: 64px;
border-radius: 8px;
}
.bannerName {
display: block;
padding: 16px;
color: #fff;
text-shadow: 0 0 8px #000;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
}
.rules {
counter-reset: item;
list-style: none;
padding: 0;
margin: 0;
}
.rule {
display: flex;
gap: 8px;
word-break: break-word;
&::before {
flex-shrink: 0;
display: flex;
position: sticky;
top: calc(var(--stickyTop, 0px) + 8px);
counter-increment: item;
content: counter(item);
width: 32px;
height: 32px;
line-height: 32px;
background-color: var(--accentedBg);
color: var(--accent);
font-size: 13px;
font-weight: bold;
align-items: center;
justify-content: center;
border-radius: 999px;
}
}
.ruleText {
padding-top: 6px;
}
</style>

View File

@@ -8,113 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
<div class="_gaps_m">
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
<div style="overflow: clip;">
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
<div :class="$style.bannerName">
<b>{{ instance.name ?? host }}</b>
</div>
</div>
</div>
<MkKeyValue>
<template #key>{{ i18n.ts.description }}</template>
<template #value><div v-html="instance.description"></div></template>
</MkKeyValue>
<FormSection>
<div class="_gaps_m">
<MkKeyValue :copy="version">
<template #key>Misskey</template>
<template #value>{{ version }}</template>
</MkKeyValue>
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
</div>
<FormLink to="/about-misskey">
<template #icon><i class="ti ti-info-circle"></i></template>
{{ i18n.ts.aboutMisskey }}
</FormLink>
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
<template #icon><i class="ti ti-code"></i></template>
{{ i18n.ts.sourceCode }}
</FormLink>
<MkInfo v-else warn>
{{ i18n.ts.sourceCodeIsNotYetProvided }}
</MkInfo>
</div>
</FormSection>
<FormSection>
<div class="_gaps_m">
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts.administrator }}</template>
<template #value>{{ instance.maintainerName }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.contact }}</template>
<template #value>{{ instance.maintainerEmail }}</template>
</MkKeyValue>
</FormSplit>
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
<template #icon><i class="ti ti-user-shield"></i></template>
{{ i18n.ts.impressum }}
</FormLink>
<div class="_gaps_s">
<MkFolder v-if="instance.serverRules.length > 0">
<template #label>
<i class="ti ti-checkup-list"></i>
{{ i18n.ts.serverRules }}
</template>
<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>
<template #icon><i class="ti ti-license"></i></template>
{{ i18n.ts.termsOfService }}
</FormLink>
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
<template #icon><i class="ti ti-shield-lock"></i></template>
{{ i18n.ts.privacyPolicy }}
</FormLink>
<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
<template #icon><i class="ti ti-message"></i></template>
{{ i18n.ts.feedback }}
</FormLink>
</div>
</div>
</FormSection>
<FormSuspense :p="initStats">
<FormSection>
<template #label>{{ i18n.ts.statistics }}</template>
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts.users }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.notes }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template>
</MkKeyValue>
</FormSplit>
</FormSection>
</FormSuspense>
<FormSection>
<template #label>Well-known resources</template>
<div class="_gaps_s">
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
</div>
</FormSection>
</div>
<XOverview/>
</MkSpacer>
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
<XEmojis/>
@@ -130,26 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XEmojis from './about.emojis.vue';
import XFederation from './about.federation.vue';
import { version, host } from '@/config.js';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkInstanceStats from '@/components/MkInstanceStats.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import number from '@/filters/number.js';
import { computed, defineAsyncComponent, ref, watch } from 'vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { instance } from '@/instance.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
const XEmojis = defineAsyncComponent(() => import('@/pages/about.emojis.vue'));
const XFederation = defineAsyncComponent(() => import('@/pages/about.federation.vue'));
const MkInstanceStats = defineAsyncComponent(() => import('@/components/MkInstanceStats.vue'));
const props = withDefaults(defineProps<{
initialTab?: string;
@@ -157,7 +41,6 @@ const props = withDefaults(defineProps<{
initialTab: 'overview',
});
const stats = ref<Misskey.entities.StatsResponse | null>(null);
const tab = ref(props.initialTab);
watch(tab, () => {
@@ -166,11 +49,6 @@ watch(tab, () => {
}
});
const initStats = () => misskeyApi('stats', {
}).then((res) => {
stats.value = res;
});
const headerActions = computed(() => []);
const headerTabs = computed(() => [{
@@ -195,64 +73,3 @@ definePageMetadata(() => ({
icon: 'ti ti-info-circle',
}));
</script>
<style lang="scss" module>
.banner {
text-align: center;
border-radius: 10px;
overflow: clip;
background-size: cover;
background-position: center center;
}
.bannerIcon {
display: block;
margin: 16px auto 0 auto;
height: 64px;
border-radius: 8px;
}
.bannerName {
display: block;
padding: 16px;
color: #fff;
text-shadow: 0 0 8px #000;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
}
.rules {
counter-reset: item;
list-style: none;
padding: 0;
margin: 0;
}
.rule {
display: flex;
gap: 8px;
word-break: break-word;
&::before {
flex-shrink: 0;
display: flex;
position: sticky;
top: calc(var(--stickyTop, 0px) + 8px);
counter-increment: item;
content: counter(item);
width: 32px;
height: 32px;
line-height: 32px;
background-color: var(--accentedBg);
color: var(--accent);
font-size: 13px;
font-weight: bold;
align-items: center;
justify-content: center;
border-radius: 999px;
}
}
.ruleText {
padding-top: 6px;
}
</style>

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkModalWindow
ref="dialog"
ref="dialogEl"
:width="400"
:height="490"
:withOkButton="false"
@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, toRefs } from 'vue';
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
import { entities } from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
@@ -88,6 +88,7 @@ type NotificationRecipientMethod = 'email' | 'webhook';
const emit = defineEmits<{
(ev: 'submitted'): void;
(ev: 'canceled'): void;
(ev: 'closed'): void;
}>();
@@ -98,6 +99,8 @@ const props = defineProps<{
const { mode, id } = toRefs(props);
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
const loading = ref<number>(0);
const title = ref<string>('');
@@ -166,18 +169,21 @@ async function onSubmitClicked() {
}
}
dialogEl.value?.close();
emit('submitted');
// eslint-disable-next-line
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (ex: any) {
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
emit('closed');
dialogEl.value?.close();
emit('canceled');
}
});
}
function onCancelClicked() {
emit('closed');
dialogEl.value?.close();
emit('canceled');
}
async function onEditSystemWebhookClicked() {
@@ -262,7 +268,8 @@ onMounted(async () => {
} catch (ex: any) {
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
emit('closed');
dialogEl.value?.close();
emit('canceled');
}
} else {
userId.value = moderators.value[0]?.id ?? null;
@@ -296,11 +303,13 @@ onMounted(async () => {
gap: 8px;
button {
width: 2.5em;
height: 2.5em;
min-width: 2.5em;
min-height: 2.5em;
min-width: 0;
min-height: 0;
width: 34px;
height: 34px;
flex-shrink: 0;
box-sizing: border-box;
margin: 1px 0;
padding: 6px;
}
}

View File

@@ -108,26 +108,27 @@ async function onDeleteButtonClicked(id: string) {
}
async function showEditor(mode: 'create' | 'edit', id?: string) {
const { dispose, needLoad } = await new Promise<{ dispose: () => void, needLoad: boolean }>(async resolve => {
const { dispose: _dispose } = os.popup(
const { needLoad } = await new Promise<{ needLoad: boolean }>(async resolve => {
const { dispose } = os.popup(
defineAsyncComponent(() => import('./notification-recipient.editor.vue')),
{
mode,
id,
},
{
submitted: async () => {
resolve({ dispose: _dispose, needLoad: true });
submitted: () => {
resolve({ needLoad: true });
},
canceled: () => {
resolve({ needLoad: false });
},
closed: () => {
resolve({ dispose: _dispose, needLoad: false });
dispose();
},
},
);
});
dispose();
if (needLoad) {
await fetchRecipients();
}

View File

@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local">
<template #label>{{ i18n.ts.expirationDate }}</template>
</MkInput>
<MkInput v-model="createCount" type="number">
<MkInput v-model="createCount" type="number" min="1">
<template #label>{{ i18n.ts.createCount }}</template>
</MkInput>
<MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton>

View File

@@ -378,6 +378,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])">
<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
<template #suffix>
<span v-if="role.policies.canUpdateBioMedia.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canUpdateBioMedia.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUpdateBioMedia)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canUpdateBioMedia.value" :disabled="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canUpdateBioMedia.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.pinMax, 'pinLimit'])">
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
<template #suffix>

View File

@@ -134,6 +134,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])">
<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
<template #suffix>{{ policies.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canUpdateBioMedia">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
<template #suffix>{{ policies.pinLimit }}</template>

View File

@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
<div ref="rootEl" v-hotkey.global="keymap">
<div ref="rootEl">
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
<div :class="$style.tl">
<MkTimeline
@@ -44,9 +44,6 @@ const antenna = ref<Misskey.entities.Antenna | null>(null);
const queue = ref(0);
const rootEl = shallowRef<HTMLElement>();
const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
const keymap = computed(() => ({
't': focus,
}));
function queueUpdated(q) {
queue.value = q;

View File

@@ -93,7 +93,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { PageHeaderItem } from '@/types/page-header.js';
import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { miLocalStorage } from '@/local-storage.js';
import { useRouter } from '@/router/supplier.js';

View File

@@ -43,7 +43,7 @@ import { url } from '@/config.js';
import MkButton from '@/components/MkButton.vue';
import { clipsCache } from '@/cache.js';
import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{
clipId: string,

View File

@@ -7,18 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader/></template>
<MkSpacer :contentMax="600" :marginMin="20">
<div class="_gaps">
<MkKeyValue>
<template #key>{{ i18n.ts.inquiry }}</template>
<div class="_gaps_m">
<MkKeyValue :copy="instance.maintainerName">
<template #key>{{ i18n.ts.administrator }}</template>
<template #value>
<MkLink :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.email }}</template>
<MkKeyValue :copy="instance.maintainerEmail">
<template #key>{{ i18n.ts.contact }}</template>
<template #value>
<div>{{ instance.maintainerEmail }}</div>
<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template>
</MkKeyValue>
<MkKeyValue :copy="instance.inquiryUrl">
<template #key>{{ i18n.ts.inquiry }}</template>
<template #value>
<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template>
</MkKeyValue>
</div>
@@ -28,8 +36,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { instance } from '@/instance.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkLink from '@/components/MkLink.vue';

View File

@@ -234,6 +234,7 @@ onMounted(async () => {
background-color: var(--accentedBg);
color: var(--accent);
text-decoration: none;
outline: none;
}
&.danger {

View File

@@ -210,7 +210,7 @@ import { apiUrl } from '@/config.js';
import { $i } from '@/account.js';
import * as sound from '@/scripts/sound.js';
import MkRange from '@/components/MkRange.vue';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
type FrontendMonoDefinition = {
id: string;

View File

@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { misskeyApiGet } from '@/scripts/misskey-api.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';

View File

@@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { AISCRIPT_VERSION } from '@syuilo/aiscript';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -48,7 +49,7 @@ import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import { useRouter } from '@/router/supplier.js';
const PRESET_DEFAULT = `/// @ 0.18.0
const PRESET_DEFAULT = `/// @ ${AISCRIPT_VERSION}
var name = ""
@@ -66,7 +67,7 @@ Ui:render([
])
`;
const PRESET_OMIKUJI = `/// @ 0.18.0
const PRESET_OMIKUJI = `/// @ ${AISCRIPT_VERSION}
// ユーザーごとに日替わりのおみくじのプリセット
// 選択肢
@@ -109,7 +110,7 @@ Ui:render([
])
`;
const PRESET_SHUFFLE = `/// @ 0.18.0
const PRESET_SHUFFLE = `/// @ ${AISCRIPT_VERSION}
// 巻き戻し可能な文字シャッフルのプリセット
let string = "ペペロンチーノ"
@@ -188,7 +189,7 @@ var cursor = 0
do()
`;
const PRESET_QUIZ = `/// @ 0.18.0
const PRESET_QUIZ = `/// @ ${AISCRIPT_VERSION}
let title = '地理クイズ'
let qas = [{
@@ -301,7 +302,7 @@ qaEls.push(Ui:C:container({
Ui:render(qaEls)
`;
const PRESET_TIMELINE = `/// @ 0.18.0
const PRESET_TIMELINE = `/// @ ${AISCRIPT_VERSION}
// APIリクエストを行いローカルタイムラインを表示するプリセット
@fetch() {

View File

@@ -78,7 +78,8 @@ import MkCode from '@/components/MkCode.vue';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { pleaseLogin } from '@/scripts/please-login.js';
const props = defineProps<{
id: string;
@@ -143,6 +144,7 @@ function shareWithNote() {
function like() {
if (!flash.value) return;
pleaseLogin();
os.apiWithDialog('flash/like', {
flashId: flash.value.id,
@@ -154,6 +156,7 @@ function like() {
async function unlike() {
if (!flash.value) return;
pleaseLogin();
const confirm = await os.confirm({
type: 'warning',

View File

@@ -1,71 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
</div>
</template>
<script lang="ts" setup>
import { } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { mainRouter } from '@/router/main.js';
async function follow(user): Promise<void> {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.followConfirm({ name: user.name || user.username }),
});
if (canceled) {
window.close();
return;
}
os.apiWithDialog('following/create', {
userId: user.id,
withReplies: defaultStore.state.defaultWithReplies,
});
user.withReplies = defaultStore.state.defaultWithReplies;
}
const acct = new URL(location.href).searchParams.get('acct');
if (acct == null) {
throw new Error('acct required');
}
let promise;
if (acct.startsWith('https://')) {
promise = misskeyApi('ap/show', {
uri: acct,
});
promise.then(res => {
if (res.type === 'User') {
follow(res.object);
} else if (res.type === 'Note') {
mainRouter.push(`/notes/${res.object.id}`);
} else {
os.alert({
type: 'error',
text: 'Not a user',
}).then(() => {
window.close();
});
}
});
} else {
promise = misskeyApi('users/show', Misskey.acct.parse(acct));
promise.then(user => {
follow(user);
});
}
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
</script>

View File

@@ -77,7 +77,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js';
const router = useRouter();

View File

@@ -8,12 +8,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader/></template>
<MkSpacer :contentMax="800">
<div class="_gaps">
<div class="_panel">
<div class="_panel" :class="$style.link">
<MkA to="/bubble-game">
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
</MkA>
</div>
<div class="_panel">
<div class="_panel" :class="$style.link">
<MkA to="/reversi">
<img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
</MkA>
@@ -37,3 +37,10 @@ definePageMetadata(() => ({
icon: 'ti ti-device-gamepad',
}));
</script>
<style module>
.link:focus-within {
outline: 2px solid var(--focus);
outline-offset: -2px;
}
</style>

View File

@@ -0,0 +1,97 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
<div v-if="state === 'done'" class="_buttonsCenter">
<MkButton @click="close">{{ i18n.ts.close }}</MkButton>
<MkButton @click="goToMisskey">{{ i18n.ts.goToMisskey }}</MkButton>
</div>
<div v-else class="_fullInfo">
<MkLoading/>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { mainRouter } from '@/router/main.js';
import MkButton from '@/components/MkButton.vue';
const state = ref<'fetching' | 'done'>('fetching');
function fetch() {
const params = new URL(location.href).searchParams;
// acctのほうはdeprecated
let uri = params.get('uri') ?? params.get('acct');
if (uri == null) {
state.value = 'done';
return;
}
let promise: Promise<any>;
if (uri.startsWith('https://')) {
promise = misskeyApi('ap/show', {
uri,
});
promise.then(res => {
if (res.type === 'User') {
mainRouter.replace(res.object.host ? `/@${res.object.username}@${res.object.host}` : `/@${res.object.username}`);
} else if (res.type === 'Note') {
mainRouter.replace(`/notes/${res.object.id}`);
} else {
os.alert({
type: 'error',
text: 'Not a user',
});
}
});
} else {
if (uri.startsWith('acct:')) {
uri = uri.slice(5);
}
promise = misskeyApi('users/show', Misskey.acct.parse(uri));
promise.then(user => {
mainRouter.replace(user.host ? `/@${user.username}@${user.host}` : `/@${user.username}`);
});
}
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
}
function close(): void {
window.close();
// 閉じなければ100ms後タイムラインに
window.setTimeout(() => {
location.href = '/';
}, 100);
}
function goToMisskey(): void {
location.href = '/';
}
fetch();
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.lookup,
icon: 'ti ti-world-search',
});
</script>

View File

@@ -125,7 +125,7 @@ import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { instance } from '@/instance.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{
pageName: string;
@@ -286,6 +286,7 @@ definePageMetadata(() => ({
background-color: var(--accentedBg);
color: var(--accent);
text-decoration: none;
outline: none;
}
}

View File

@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { computed, ref, watch, type StyleValue } from 'vue';
import tinycolor from 'tinycolor2';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -102,10 +102,10 @@ function fetchDriveInfo(): void {
});
}
function genUsageBar(fsize: number): object {
function genUsageBar(fsize: number): StyleValue {
return {
width: `${fsize / usage.value * 100}%`,
background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }),
background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }).toHslString(),
};
}

View File

@@ -95,7 +95,7 @@ const meterStyle = computed(() => {
h: 180 - (usage.value / capacity.value * 180),
s: 0.7,
l: 0.5,
}),
}).toHslString(),
};
});

View File

@@ -169,6 +169,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
<MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</MkSwitch>
<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</MkSwitch>
</div>
<MkSelect v-model="serverDisconnectedBehavior">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@@ -315,6 +316,7 @@ const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enabl
const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer'));
const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia'));
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
@@ -357,6 +359,7 @@ watch([
disableStreamingTimeline,
enableSeasonalScreenEffect,
alwaysConfirmFollow,
confirmWhenRevealingSensitiveMedia,
], async () => {
await reloadAsk();
});

View File

@@ -82,7 +82,7 @@ import MkCode from '@/components/MkCode.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { ColdDeviceStorage } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';

View File

@@ -113,8 +113,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'sound_note',
'sound_noteMy',
'sound_notification',
'sound_antenna',
'sound_channel',
];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
'lightTheme',

View File

@@ -342,6 +342,7 @@ definePageMetadata(() => ({
&:hover, &:focus {
opacity: .7;
}
&:active {
cursor: pointer;
}

View File

@@ -54,8 +54,6 @@ const sounds = ref<Record<OperationType, Ref<SoundStore>>>({
note: defaultStore.reactiveState.sound_note,
noteMy: defaultStore.reactiveState.sound_noteMy,
notification: defaultStore.reactiveState.sound_notification,
antenna: defaultStore.reactiveState.sound_antenna,
channel: defaultStore.reactiveState.sound_channel,
reaction: defaultStore.reactiveState.sound_reaction,
});

View File

@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="statusbar.props.shuffle">
<template #label>{{ i18n.ts.shuffle }}</template>
</MkSwitch>
<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number">
<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" min="1">
<template #label>{{ i18n.ts.refreshInterval }}</template>
</MkInput>
<MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</template>
<template v-else-if="statusbar.type === 'federation'">
<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number">
<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" min="1">
<template #label>{{ i18n.ts.refreshInterval }}</template>
</MkInput>
<MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">

View File

@@ -38,7 +38,7 @@ import MkSelect from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import { Theme, getBuiltinThemesRef } from '@/scripts/theme.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import * as os from '@/os.js';
import { getThemes, removeTheme } from '@/theme-store.js';
import { i18n } from '@/i18n.js';

View File

@@ -213,12 +213,18 @@ definePageMetadata(() => ({
}
}
.dn:focus-visible ~ .toggle {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
.toggle {
cursor: pointer;
display: inline-block;
position: relative;
width: 90px;
height: 50px;
margin: 4px; // focus用のアウトライン
background-color: #83D8FF;
border-radius: 90px - 6;
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;

View File

@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
<MkSpacer :contentMax="800">
<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
<div :key="src" ref="rootEl" v-hotkey.global="keymap">
<div :key="src" ref="rootEl">
<MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
{{ i18n.ts._timelineDescription[src] }}
</MkInfo>
@@ -58,9 +58,6 @@ provide('shouldOmitHeaderTitle', true);
const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
const keymap = {
't': focus,
};
const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
const rootEl = shallowRef<HTMLElement>();

View File

@@ -32,9 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
<div v-if="$i" class="actions">
<div class="actions">
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
<MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
<MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div>
</div>
<MkAvatar class="avatar" :user="user" indicator/>
@@ -167,12 +167,14 @@ import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { $i, iAmModerator } from '@/account.js';
import { dateString } from '@/filters/date.js';
import { confetti } from '@/scripts/confetti.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
import { useRouter } from '@/router/supplier.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
function calcAge(birthdate: string): number {
const date = new Date(birthdate);
@@ -220,8 +222,14 @@ watch(moderationNote, async () => {
const style = computed(() => {
if (props.user.bannerUrl == null) return {};
return {
backgroundImage: `url(${ props.user.bannerUrl })`,
if (defaultStore.state.disableShowingAnimatedImages) {
return {
backgroundImage: `url(${ getStaticImageUrl(props.user.bannerUrl) })`,
};
} else {
return {
backgroundImage: `url(${ props.user.bannerUrl })`,
};
};
});

View File

@@ -0,0 +1,109 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :key="note.id" :class="$style.note">
<div class="_panel _gaps_s" :class="$style.content">
<div v-if="note.cw != null" :class="$style.richcontent">
<div><Mfm :text="note.cw" :author="note.user"/></div>
<MkCwButton v-model="showContent" :text="note.text" :renote="note.renote" :files="note.files" :poll="note.poll" style="margin: 4px 0;"/>
<div v-if="showContent">
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
</div>
<div v-else ref="noteTextEl" :class="[$style.text, { [$style.collapsed]: shouldCollapse }]">
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
<div v-if="note.files && note.files.length > 0" :class="$style.richcontent">
<MkMediaList :mediaList="note.files.slice(0, 4)"/>
</div>
<div v-if="note.poll">
<MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
</div>
<div v-if="note.reactionCount > 0" :class="$style.reactions">
<MkReactionsViewer :note="note" :maxNumber="16"/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, shallowRef, onUpdated, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue';
import MkCwButton from '@/components/MkCwButton.vue';
defineProps<{
note: Misskey.entities.Note;
}>();
const noteTextEl = shallowRef<HTMLDivElement>();
const shouldCollapse = ref(false);
const showContent = ref(false);
function calcCollapse() {
if (noteTextEl.value) {
const height = noteTextEl.value.scrollHeight;
if (height > 200) {
shouldCollapse.value = true;
}
}
}
onMounted(() => {
calcCollapse();
});
onUpdated(() => {
calcCollapse();
});
</script>
<style lang="scss" module>
.note {
margin-left: auto;
}
.text {
position: relative;
max-height: 200px;
overflow: hidden;
&.collapsed::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
}
}
.content {
padding: 16px;
margin: 0 0 0 auto;
max-width: max-content;
border-radius: 16px;
}
.reactions {
box-sizing: border-box;
margin: 8px -16px -8px;
padding: 8px 16px 0;
width: calc(100% + 32px);
border-top: 1px solid var(--divider);
}
.richcontent {
min-width: 250px;
}
</style>

View File

@@ -4,24 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<div ref="scrollEl" :class="[$style.scrollbox, { [$style.scroll]: isScrolling }]">
<div v-for="note in notes" :key="note.id" :class="$style.note">
<div class="_panel" :class="$style.content">
<div>
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
<div v-if="note.files.length > 0" :class="$style.richcontent">
<MkMediaList :mediaList="note.files"/>
</div>
<div v-if="note.poll">
<MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
</div>
</div>
<MkReactionsViewer ref="reactionsViewer" :note="note"/>
</div>
<div :class="$style.root" class="_gaps">
<div
ref="notesMainContainerEl"
class="_gaps"
:class="[$style.scrollBoxMain, { [$style.scrollIntro]: (scrollState === 'intro'), [$style.scrollLoop]: (scrollState === 'loop') }]"
@animationend="changeScrollState"
>
<XNote v-for="note in notes" :key="`${note.id}_1`" :class="$style.note" :note="note"/>
</div>
<div v-if="isScrolling" class="_gaps" :class="[$style.scrollBoxSub, { [$style.scrollIntro]: (scrollState === 'intro'), [$style.scrollLoop]: (scrollState === 'loop') }]">
<XNote v-for="note in notes" :key="`${note.id}_2`" :class="$style.note" :note="note"/>
</div>
</div>
</template>
@@ -29,43 +22,54 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { onUpdated, ref, shallowRef } from 'vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue';
import XNote from '@/pages/welcome.timeline.note.vue';
import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { getScrollContainer } from '@/scripts/scroll.js';
const notes = ref<Misskey.entities.Note[]>([]);
const isScrolling = ref(false);
const scrollEl = shallowRef<HTMLElement>();
const scrollState = ref<null | 'intro' | 'loop'>(null);
const notesMainContainerEl = shallowRef<HTMLElement>();
misskeyApiGet('notes/featured').then(_notes => {
notes.value = _notes;
});
function changeScrollState() {
if (scrollState.value !== 'loop') {
scrollState.value = 'loop';
}
}
onUpdated(() => {
if (!scrollEl.value) return;
const container = getScrollContainer(scrollEl.value);
if (!notesMainContainerEl.value) return;
const container = getScrollContainer(notesMainContainerEl.value);
const containerHeight = container ? container.clientHeight : window.innerHeight;
if (scrollEl.value.offsetHeight > containerHeight) {
if (notesMainContainerEl.value.offsetHeight > containerHeight) {
if (scrollState.value === null) {
scrollState.value = 'intro';
}
isScrolling.value = true;
}
});
</script>
<style lang="scss" module>
@keyframes scroll {
@keyframes scrollIntro {
0% {
transform: translate3d(0, 0, 0);
}
5% {
transform: translate3d(0, 0, 0);
100% {
transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0);
}
75% {
transform: translate3d(0, calc(-100% + 90vh), 0);
}
@keyframes scrollConstant {
0% {
transform: translate3d(0, -128px, 0);
}
90% {
transform: translate3d(0, calc(-100% + 90vh), 0);
100% {
transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0);
}
}
@@ -73,24 +77,26 @@ onUpdated(() => {
text-align: right;
}
.scrollbox {
&.scroll {
animation: scroll 45s linear infinite;
.scrollBoxMain {
&.scrollIntro {
animation: scrollIntro 30s linear forwards;
}
&.scrollLoop {
animation: scrollConstant 30s linear infinite;
}
}
.note {
margin: 16px 0 16px auto;
.scrollBoxSub {
&.scrollIntro {
animation: scrollIntro 30s linear forwards;
}
&.scrollLoop {
animation: scrollConstant 30s linear infinite;
}
}
.content {
padding: 16px;
margin: 0 0 0 auto;
max-width: max-content;
border-radius: 16px;
}
.richcontent {
min-width: 250px;
.root:has(.note:hover) .scrollBoxMain,
.root:has(.note:hover) .scrollBoxSub {
animation-play-state: paused;
}
</style>