1
0
mirror of https://github.com/hotomoe/hotomoe synced 2024-11-28 14:58:15 +09:00

nanka iroiro (#6847)

* wip

* wip

* wip

* wip

* Update ja-JP.yml

* wip

* wip

* wip
This commit is contained in:
syuilo 2020-11-17 14:59:15 +09:00 committed by GitHub
parent 50e917d232
commit 0044d83801
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 558 additions and 183 deletions

View File

@ -316,6 +316,8 @@ bannerUrl: "バナー画像のURL"
basicInfo: "基本情報"
pinnedUsers: "ピン留めユーザー"
pinnedUsersDescription: "「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。"
pinnedPages: "ピン留めページ"
pinnedPagesDescription: "インスタンスのトップページにピン留めしたいページのパスを改行で区切って記述します。"
hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptchaを有効にする"
hcaptchaSiteKey: "サイトキー"
@ -1117,6 +1119,7 @@ _pages:
unlike: "いいね解除"
my: "自分のページ"
liked: "いいねしたページ"
featured: "人気"
inspector: "インスペクター"
contents: "コンテンツ"
content: "ページブロック"

View File

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class instancePinnedPages1605585339718 implements MigrationInterface {
name = 'instancePinnedPages1605585339718'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{"/announcements", "/featured", "/channels", "/pages", "/explore", "/games/reversi", "/about-misskey"}'::varchar[]`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedPages"`);
}
}

View File

@ -8,7 +8,7 @@
<MkError v-if="error" @retry="init()"/>
<div v-show="more && reversed" style="margin-bottom: var(--margin);">
<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<button class="_loadMore" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</button>

View File

@ -1,26 +1,32 @@
<template>
<div class="pxhvhrfw" v-size="{ max: [500] }">
<button v-for="item in items" class="_button" @click="$emit('update:value', item.value)" :class="{ active: value === item.value }" :disabled="value === item.value" :key="item.value"><Fa v-if="item.icon" :icon="item.icon" class="icon"/>{{ item.label }}</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, h, resolveDirective, withDirectives } from 'vue';
export default defineComponent({
props: {
items: {
type: Array,
required: true,
},
value: {
required: true,
},
},
render() {
const options = this.$slots.default();
return withDirectives(h('div', {
class: 'pxhvhrfw',
}, options.map(option => h('button', {
class: ['_button', { active: this.value === option.props.value }],
key: option.props.value,
disabled: this.value === option.props.value,
onClick: () => {
this.$emit('update:value', option.props.value);
}
}, option.children))), [
[resolveDirective('size'), { max: [500] }]
]);
}
});
</script>
<style lang="scss" scoped>
<style lang="scss">
.pxhvhrfw {
display: flex;

View File

@ -9,7 +9,10 @@
<template #header>Req Viewer</template>
<div class="rlkneywz">
<MkTab v-model:value="tab" :items="[{ label: 'Request', value: 'req', }, { label: 'Response', value: 'res', }]" style="border-bottom: solid 1px var(--divider);"/>
<MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);">
<option value="req">Request</option>
<option value="res">Response</option>
</MkTab>
<code v-if="tab === 'req'">{{ reqStr }}</code>
<code v-if="tab === 'res'">{{ resStr }}</code>

View File

@ -4,7 +4,12 @@
<Fa :icon="faTerminal" style="margin-right: 0.5em;"/>Task Manager
</template>
<div class="qljqmnzj">
<MkTab v-model:value="tab" :items="[{ label: 'Windows', value: 'windows', }, { label: 'Stream', value: 'stream', }, { label: 'Stream (Pool)', value: 'streamPool', }, { label: 'API', value: 'api', }]" style="border-bottom: solid 1px var(--divider);"/>
<MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);">
<option value="windows">Windows</option>
<option value="stream">Stream</option>
<option value="streamPool">Stream (Pool)</option>
<option value="api">API</option>
</MkTab>
<div class="content">
<div v-if="tab === 'windows'" class="windows" v-follow>

View File

@ -20,7 +20,7 @@
</div>
</div>
<XPostForm :channel="channel" class="post-form _content _panel _vMargin" fixed/>
<XPostForm :channel="channel" class="post-form _content _panel _vMargin" fixed v-if="this.$store.getters.isSignedIn"/>
<XTimeline class="_content _vMargin" src="channel" :channel="channelId" @before="before" @after="after"/>
</div>

View File

@ -1,26 +1,30 @@
<template>
<div>
<div class="_section" style="padding: 0;">
<MkTab class="_content" v-model:value="tab" :items="[{ label: $t('_channel.featured'), value: 'featured', icon: faFireAlt }, { label: $t('_channel.following'), value: 'following', icon: faHeart }, { label: $t('_channel.owned'), value: 'owned', icon: faEdit }]"/>
<div class="_section" style="padding: 0;" v-if="this.$store.getters.isSignedIn">
<MkTab class="_content" v-model:value="tab">
<option value="featured"><Fa :icon="faFireAlt"/> {{ $t('_channel.featured') }}</option>
<option value="following"><Fa :icon="faHeart"/> {{ $t('_channel.following') }}</option>
<option value="owned"><Fa :icon="faEdit"/> {{ $t('_channel.owned') }}</option>
</MkTab>
</div>
<div class="_section">
<div class="_content grwlizim featured" v-if="tab === 'featured'">
<MkPagination :pagination="featuredPagination" #default="{items}">
<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
<MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/>
</MkPagination>
</div>
<div class="_content grwlizim following" v-if="tab === 'following'">
<MkPagination :pagination="followingPagination" #default="{items}">
<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
<MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/>
</MkPagination>
</div>
<div class="_content grwlizim owned" v-if="tab === 'owned'">
<MkButton class="new" @click="create()"><Fa :icon="faPlus"/></MkButton>
<MkPagination :pagination="ownedPagination" #default="{items}">
<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
<MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/>
</MkPagination>
</div>
</div>
@ -44,7 +48,11 @@ export default defineComponent({
return {
INFO: {
title: this.$t('channel'),
icon: faSatelliteDish
icon: faSatelliteDish,
action: {
icon: faPlus,
handler: this.create
}
},
tab: 'featured',
featuredPagination: {
@ -69,23 +77,3 @@ export default defineComponent({
}
});
</script>
<style lang="scss" scoped>
.grwlizim {
padding: 16px 0;
&.my .uveselbe:first-child {
margin-top: 16px;
}
.uveselbe:not(:last-child) {
margin-bottom: 8px;
}
@media (min-width: 500px) {
.uveselbe:not(:last-child) {
margin-bottom: 16px;
}
}
}
</style>

View File

@ -1,7 +1,10 @@
<template>
<div class="mk-instance-emojis">
<div class="_section" style="padding: 0;">
<MkTab v-model:value="tab" :items="[{ label: $t('local'), value: 'local' }, { label: $t('remote'), value: 'remote' }]"/>
<MkTab v-model:value="tab">
<option value="local">{{ $t('local') }}</option>
<option value="remote">{{ $t('remote') }}</option>
</MkTab>
</div>
<div class="_section">

View File

@ -1,6 +1,6 @@
<template>
<div v-if="meta">
<section class="_section info">
<div v-if="meta" class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
<div class="_content">
<MkInput v-model:value="name">{{ $t('instanceName') }}</MkInput>
@ -16,7 +16,7 @@
</div>
</section>
<section class="_section info">
<section class="_card _vMargin">
<div class="_content">
<MkInput v-model:value="maxNoteTextLength" type="number" :save="() => save()"><template #icon><Fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</MkInput>
</div>
@ -30,7 +30,7 @@
</div>
</section>
<section class="_section info">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faUser"/> {{ $t('registration') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableRegistration" @update:value="save()">{{ $t('enableRegistration') }}</MkSwitch>
@ -38,7 +38,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableHcaptcha">{{ $t('enableHcaptcha') }}</MkSwitch>
@ -56,7 +56,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableRecaptcha" ref="enableRecaptcha">{{ $t('enableRecaptcha') }}</MkSwitch>
@ -74,7 +74,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableEmail" @update:value="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></MkSwitch>
@ -97,7 +97,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></MkSwitch>
@ -113,7 +113,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
<div class="_content">
<MkTextarea v-model:value="pinnedUsers">
@ -125,7 +125,19 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faThumbtack"/> {{ $t('pinnedPages') }}</div>
<div class="_content">
<MkTextarea v-model:value="pinnedPages">
<template #desc>{{ $t('pinnedPagesDescription') }}</template>
</MkTextarea>
</div>
<div class="_footer">
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faCloud"/> {{ $t('files') }}</div>
<div class="_content">
<MkSwitch v-model:value="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></MkSwitch>
@ -138,7 +150,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faCloud"/> {{ $t('objectStorage') }}</div>
<div class="_content">
<MkSwitch v-model:value="useObjectStorage">{{ $t('useObjectStorage') }}</MkSwitch>
@ -166,7 +178,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
<div class="_content">
<MkInput :value="proxyAccount ? proxyAccount.username : null" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></MkInput>
@ -174,7 +186,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
<div class="_content">
<MkTextarea v-model:value="blockedHosts">
@ -186,7 +198,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
<div class="_content">
<header><Fa :icon="faTwitter"/> Twitter</header>
@ -220,7 +232,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faArchway" /> Summaly Proxy</div>
<div class="_content">
<MkInput v-model:value="summalyProxy">URL</MkInput>
@ -260,6 +272,7 @@ export default defineComponent({
title: this.$t('instance'),
icon: faCog,
},
meta: null,
url,
proxyAccount: null,
proxyAccountId: null,
@ -269,6 +282,7 @@ export default defineComponent({
remoteDriveCapacityMb: 0,
blockedHosts: '',
pinnedUsers: '',
pinnedPages: '',
maintainerName: null,
maintainerEmail: null,
name: null,
@ -323,13 +337,9 @@ export default defineComponent({
}
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
async created() {
this.meta = await os.api('meta', { detail: true });
created() {
this.name = this.meta.name;
this.description = this.meta.description;
this.tosUrl = this.meta.tosUrl;
@ -356,6 +366,7 @@ export default defineComponent({
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
this.blockedHosts = this.meta.blockedHosts.join('\n');
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
this.pinnedPages = this.meta.pinnedPages.join('\n');
this.enableServiceWorker = this.meta.enableServiceWorker;
this.swPublicKey = this.meta.swPublickey;
this.swPrivateKey = this.meta.swPrivateKey;
@ -506,6 +517,7 @@ export default defineComponent({
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
blockedHosts: this.blockedHosts.split('\n') || [],
pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
pinnedPages: this.pinnedPages ? this.pinnedPages.split('\n') : [],
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey,

View File

@ -1,7 +1,11 @@
<template>
<div class="">
<div class="_section" style="padding: 0;">
<MkTab v-model:value="tab" :items="[{ label: $t('ownedGroups'), value: 'owned' }, { label: $t('joinedGroups'), value: 'joined' }, { label: $t('invites'), icon: faEnvelopeOpenText, value: 'invites' }]"/>
<MkTab v-model:value="tab">
<option value="owned">{{ $t('ownedGroups') }}</option>
<option value="joined">{{ $t('joinedGroups') }}</option>
<option value="invites"><Fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</option>
</MkTab>
</div>
<div class="_section">

View File

@ -1,21 +1,31 @@
<template>
<div class="fcuexfpr">
<div v-if="note" class="note">
<div class="_section">
<XNotes v-if="showNext" class="_content" :pagination="next"/>
<MkButton v-else-if="hasNext" class="load _content" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton>
<div class="_section" v-if="showNext">
<XNotes class="_content" :pagination="next"/>
</div>
<div class="_section">
<div class="_content">
<div class="_section main">
<MkButton v-if="!showNext && hasNext" class="load next _content" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton>
<div class="_content _vMargin">
<MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_vMargin"/>
<XNote v-model:note="note" :key="note.id" :detail="true" class="_vMargin"/>
</div>
<div class="_content clips _vMargin" v-if="clips && clips.length > 0">
<div class="title">{{ $t('clip') }}</div>
<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _vMargin">
<b>{{ item.name }}</b>
<div v-if="item.description" class="description">{{ item.description }}</div>
<div class="user">
<MkAvatar :user="item.user" class="avatar"/> <MkUserName :user="item.user" :nowrap="false"/>
</div>
</MkA>
</div>
<MkButton v-if="!showPrev && hasPrev" class="load prev _content" @click="showPrev = true"><Fa :icon="faChevronDown"/></MkButton>
</div>
<div class="_section">
<XNotes v-if="showPrev" class="_content" :pagination="prev"/>
<MkButton v-else-if="hasPrev" class="load _content" @click="showPrev = true"><Fa :icon="faChevronDown"/></MkButton>
<div class="_section" v-if="showPrev">
<XNotes class="_content" :pagination="prev"/>
</div>
</div>
@ -28,7 +38,6 @@
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { faChevronUp, faChevronDown } from '@fortawesome/free-solid-svg-icons';
import Progress from '@/scripts/loading';
import XNote from '@/components/note.vue';
import XNotes from '@/components/notes.vue';
import MkRemoteCaution from '@/components/remote-caution.vue';
@ -55,6 +64,7 @@ export default defineComponent({
avatar: this.note.user,
} : null),
note: null,
clips: null,
hasPrev: false,
hasNext: false,
showPrev: false,
@ -88,11 +98,13 @@ export default defineComponent({
},
methods: {
fetch() {
Progress.start();
os.api('notes/show', {
noteId: this.noteId
}).then(note => {
Promise.all([
os.api('notes/clips', {
noteId: note.id,
}),
os.api('users/notes', {
userId: note.userId,
untilId: note.id,
@ -103,15 +115,14 @@ export default defineComponent({
sinceId: note.id,
limit: 1,
}),
]).then(([prev, next]) => {
]).then(([clips, prev, next]) => {
this.clips = clips;
this.hasPrev = prev.length !== 0;
this.hasNext = next.length !== 0;
this.note = note;
});
}).catch(e => {
this.error = e;
}).finally(() => {
Progress.done();
});
}
}
@ -121,10 +132,46 @@ export default defineComponent({
<style lang="scss" scoped>
.fcuexfpr {
> .note {
> ._section {
> .main {
> .load {
min-width: 0;
border-radius: 999px;
&.next {
margin-bottom: var(--margin);
}
&.prev {
margin-top: var(--margin);
}
}
> .clips {
> .title {
font-weight: bold;
padding: 12px;
}
> .item {
display: block;
padding: 16px;
> .description {
padding: 8px 0;
}
> .user {
$height: 32px;
padding-top: 16px;
border-top: solid 1px var(--divider);
line-height: $height;
> .avatar {
width: $height;
height: $height;
}
}
}
}
}
}

View File

@ -277,7 +277,7 @@ export default defineComponent({
type: 'success',
text: this.$t('_pages.created')
});
this.$router.push(`/my/pages/edit/${this.pageId}`);
this.$router.push(`/pages/edit/${this.pageId}`);
}).catch(onError);
}
},
@ -296,7 +296,7 @@ export default defineComponent({
type: 'success',
text: this.$t('_pages.deleted')
});
this.$router.push(`/my/pages`);
this.$router.push(`/pages`);
});
});
},

View File

@ -22,7 +22,7 @@
<div class="_content">
<MkA :to="`./${page.name}/view-source`" class="link">{{ $t('_pages.viewSource') }}</MkA>
<template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId">
<MkA :to="`/my/pages/edit/${page.id}`" class="link">{{ $t('_pages.editThisPage') }}</MkA>
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ $t('_pages.editThisPage') }}</MkA>
<button v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $t('unpin') }}</button>
<button v-else @click="pin(true)" class="link _textButton">{{ $t('pin') }}</button>
</template>

View File

@ -1,8 +1,18 @@
<template>
<div>
<MkTab v-model:value="tab" :items="[{ label: $t('_pages.my'), value: 'my', icon: faEdit }, { label: $t('_pages.liked'), value: 'liked', icon: faHeart }]"/>
<MkTab v-model:value="tab" v-if="this.$store.getters.isSignedIn">
<option value="featured"><Fa :icon="faFireAlt"/> {{ $t('_pages.featured') }}</option>
<option value="my"><Fa :icon="faEdit"/> {{ $t('_pages.my') }}</option>
<option value="liked"><Fa :icon="faHeart"/> {{ $t('_pages.liked') }}</option>
</MkTab>
<div class="_section">
<div class="rknalgpo _content" v-if="tab === 'featured'">
<MkPagination :pagination="featuredPagesPagination" #default="{items}">
<MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
</MkPagination>
</div>
<div class="rknalgpo _content my" v-if="tab === 'my'">
<MkButton class="new" @click="create()"><Fa :icon="faPlus"/></MkButton>
<MkPagination :pagination="myPagesPagination" #default="{items}">
@ -21,7 +31,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons';
import { faPlus, faEdit, faFireAlt } from '@fortawesome/free-solid-svg-icons';
import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
import MkPagePreview from '@/components/page-preview.vue';
import MkPagination from '@/components/ui/pagination.vue';
@ -42,7 +52,11 @@ export default defineComponent({
handler: this.create
}
},
tab: 'my',
tab: 'featured',
featuredPagesPagination: {
endpoint: 'pages/featured',
noPaging: true,
},
myPagesPagination: {
endpoint: 'i/pages',
limit: 5,
@ -51,12 +65,12 @@ export default defineComponent({
endpoint: 'i/page-likes',
limit: 5,
},
faStickyNote, faPlus, faEdit, faHeart
faStickyNote, faPlus, faEdit, faHeart, faFireAlt
};
},
methods: {
create() {
this.$router.push(`/my/pages/new`);
this.$router.push(`/pages/new`);
}
}
});

View File

@ -1,6 +1,9 @@
<template>
<section class="rrfwjxfl _section">
<MkTab v-model:value="tab" :items="[{ label: $t('mutedUsers'), value: 'mute' }, { label: $t('blockedUsers'), value: 'block' }]" style="margin-bottom: var(--margin);"/>
<MkTab v-model:value="tab" style="margin-bottom: var(--margin);">
<option value="mute">{{ $t('mutedUsers') }}</option>
<option value="block">{{ $t('blockedUsers') }}</option>
</MkTab>
<div class="_content" v-if="tab === 'mute'">
<MkPagination :pagination="mutingPagination" class="muting">
<template #empty><MkInfo>{{ $t('noUsers') }}</MkInfo></template>

View File

@ -1,7 +1,10 @@
<template>
<div class="_section">
<div class="_card">
<MkTab v-model:value="tab" :items="[{ label: $t('_wordMute.soft'), value: 'soft' }, { label: $t('_wordMute.hard'), value: 'hard' }]"/>
<MkTab v-model:value="tab">
<option value="soft">{{ $t('_wordMute.soft') }}</option>
<option value="hard">{{ $t('_wordMute.hard') }}</option>
</MkTab>
<div class="_content">
<div v-show="tab === 'soft'">
<MkInfo>{{ $t('_wordMute.softDescription') }}</MkInfo>

View File

@ -0,0 +1,141 @@
<template>
<div class="xyeqzsjl _panel">
<header>
<button class="_button" @click="back()" v-if="history.length > 0"><Fa :icon="faChevronLeft"/></button>
<XHeader class="title" :info="pageInfo" :with-back="false"/>
</header>
<div>
<component :is="component" v-bind="props" :ref="changePage"/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
import XWindow from '@/components/ui/window.vue';
import XHeader from '@/ui/_common_/header.vue';
import { popout } from '@/scripts/popout';
import { resolve } from '@/router';
import { url } from '@/config';
export default defineComponent({
components: {
XWindow,
XHeader,
},
provide() {
return {
navHook: (path) => {
this.navigate(path);
}
};
},
props: {
initialPath: {
type: String,
required: true,
},
},
data() {
return {
pageInfo: null,
path: this.initialPath,
component: null,
props: null,
history: [],
faChevronLeft,
};
},
computed: {
url(): string {
return url + this.path;
},
},
created() {
const { component, props } = resolve(this.initialPath);
this.component = component;
this.props = props;
},
methods: {
changePage(page) {
if (page == null) return;
if (page.INFO) {
this.pageInfo = page.INFO;
}
},
navigate(path, record = true) {
if (record) this.history.push(this.path);
this.path = path;
const { component, props } = resolve(path);
this.component = component;
this.props = props;
},
back() {
this.navigate(this.history.pop(), false);
},
expand() {
this.$router.push(this.path);
this.$refs.window.close();
},
popout() {
popout(this.path, this.$el);
this.$refs.window.close();
},
},
});
</script>
<style lang="scss" scoped>
.xyeqzsjl {
--section-padding: 16px;
display: flex;
flex-direction: column;
contain: content;
> header {
$height: 50px;
display: flex;
position: relative;
z-index: 1;
height: $height;
line-height: $height;
box-shadow: 0px 1px var(--divider);
> button {
height: $height;
width: $height;
&:hover {
color: var(--fgHighlighted);
}
}
> .title {
flex: 1;
position: relative;
line-height: $height;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
}
> div {
flex: 1;
overflow: auto;
}
}
</style>

View File

@ -1,18 +1,13 @@
<template>
<div class="rsqzvsbo">
<div class="_section">
<div class="_content _panel about" v-if="meta">
<div class="body">
<div class="desc" v-html="meta.description || $t('introMisskey')"></div>
<MkButton @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</MkButton>
<MkButton @click="signin()" style="display: inline-block;">{{ $t('login') }}</MkButton>
</div>
</div>
<div class="rsqzvsbo _section" v-if="meta">
<div class="about">
<h1>{{ instanceName }}</h1>
<div class="desc" v-html="meta.description || $t('introMisskey')"></div>
<MkButton @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</MkButton>
<MkButton @click="signin()" style="display: inline-block;">{{ $t('login') }}</MkButton>
</div>
<div class="_section">
<div class="_content">
<XNotes :pagination="featuredPagination"/>
</div>
<div class="blocks">
<XBlock class="block" v-for="path in meta.pinnedPages" :initial-path="path" :key="path"/>
</div>
</div>
</template>
@ -24,33 +19,30 @@ import XSigninDialog from '@/components/signin-dialog.vue';
import XSignupDialog from '@/components/signup-dialog.vue';
import MkButton from '@/components/ui/button.vue';
import XNotes from '@/components/notes.vue';
import { host } from '@/config';
import XBlock from './welcome.entrance.block.vue';
import { host, instanceName } from '@/config';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
XNotes,
XBlock,
},
data() {
return {
featuredPagination: {
endpoint: 'notes/featured',
limit: 10,
noPaging: true,
},
host: toUnicode(host),
instanceName,
meta: null,
};
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
created() {
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
os.api('stats').then(stats => {
this.stats = stats;
});
@ -74,15 +66,42 @@ export default defineComponent({
<style lang="scss" scoped>
.rsqzvsbo {
> ._section {
> .about {
> .body {
padding: 32px;
text-align: center;
@media (max-width: 500px) {
padding: 16px;
}
}
> .about {
display: inline-block;
padding: 24px;
margin-bottom: var(--margin);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.5);
border-radius: var(--radius);
text-align: center;
box-sizing: border-box;
min-width: 300px;
max-width: 800px;
&, * {
color: #fff !important;
}
> h1 {
margin: 0 0 16px 0;
}
}
> .blocks {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
grid-gap: var(--margin);
text-align: left;
> .block {
height: 600px;
}
@media (max-width: 800px) {
grid-template-columns: 1fr;
}
}
}

View File

@ -10,6 +10,7 @@ import { defineComponent } from 'vue';
import XSetup from './welcome.setup.vue';
import XEntrance from './welcome.entrance.vue';
import { instanceName } from '@/config';
import * as os from '@/os';
export default defineComponent({
components: {
@ -20,16 +21,17 @@ export default defineComponent({
data() {
return {
INFO: {
title: instanceName || 'Misskey',
title: instanceName,
icon: null
},
meta: null
}
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
created() {
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
}
});
</script>

View File

@ -33,6 +33,9 @@ export const router = createRouter({
{ path: '/explore', component: page('explore') },
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
{ path: '/search', component: page('search') },
{ path: '/pages', name: 'pages', component: page('pages') },
{ path: '/pages/new', component: page('page-editor/page-editor') },
{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
{ path: '/channels', component: page('channels') },
{ path: '/channels/new', component: page('channel-editor') },
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
@ -47,9 +50,6 @@ export const router = createRouter({
{ path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) },
{ path: '/my/drive', name: 'drive', component: page('drive') },
{ path: '/my/drive/folder/:folder', component: page('drive') },
{ path: '/my/pages', name: 'pages', component: page('pages') },
{ path: '/my/pages/new', component: page('page-editor/page-editor') },
{ path: '/my/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
{ path: '/my/follow-requests', component: page('follow-requests') },
{ path: '/my/lists', component: page('my-lists/index') },
{ path: '/my/lists/:list', component: page('my-lists/list') },

View File

@ -96,8 +96,7 @@ export const sidebarDef = {
pages: {
title: 'pages',
icon: faFileAlt,
show: computed(() => store.getters.isSignedIn),
to: '/my/pages',
to: '/pages',
},
clips: {
title: 'clip',

View File

@ -7,12 +7,12 @@
<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
</header>
<div class="banner" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
<h1>{{ instanceName }}</h1>
<div class="banner" :class="{ asBg: $route.path === '/' }" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
<h1 v-if="$route.path !== '/'">{{ instanceName }}</h1>
</div>
<div class="contents" ref="contents" :class="{ wallpaper }">
<header class="header" ref="header">
<header class="header" ref="header" v-show="$route.path !== '/'">
<XHeader :info="pageInfo"/>
</header>
<main ref="main">
@ -116,11 +116,10 @@ export default defineComponent({
<style lang="scss" scoped>
.mk-app {
min-height: 100vh;
max-width: 1300px;
margin: 0 auto;
box-shadow: 1px 0 var(--divider), -1px 0 var(--divider);
> header {
position: relative;
z-index: 1;
background: var(--panel);
padding: 0 16px;
text-align: center;
@ -145,6 +144,12 @@ export default defineComponent({
background-size: cover;
background-position: center;
&.asBg {
position: absolute;
left: 0;
height: 320px;
}
&:after {
content: "";
display: block;
@ -166,6 +171,9 @@ export default defineComponent({
}
> .contents {
position: relative;
z-index: 1;
> .header {
position: sticky;
top: 0;

View File

@ -76,6 +76,11 @@ export class Meta {
})
public blockedHosts: string[];
@Column('varchar', {
length: 512, array: true, default: '{"/announcements", "/featured", "/channels", "/explore", "/games/reversi", "/about-misskey"}'
})
public pinnedPages: string[];
@Column('varchar', {
length: 512,
nullable: true,

View File

@ -85,8 +85,9 @@ export class PageRepository extends Repository<Page> {
public packMany(
pages: Page[],
me?: User['id'] | User | null | undefined,
) {
return Promise.all(pages.map(x => this.pack(x)));
return Promise.all(pages.map(x => this.pack(x, me)));
}
}

View File

@ -208,6 +208,10 @@ export const meta = {
}
},
pinnedPages: {
validator: $.optional.arr($.str),
},
langs: {
validator: $.optional.arr($.str),
desc: {
@ -537,6 +541,10 @@ export default define(meta, async (ps, me) => {
set.langs = ps.langs.filter(Boolean);
}
if (Array.isArray(ps.pinnedPages)) {
set.pinnedPages = ps.pinnedPages.filter(Boolean);
}
if (ps.summalyProxy !== undefined) {
set.summalyProxy = ps.summalyProxy;
}

View File

@ -99,8 +99,6 @@ export default define(meta, async (ps, me) => {
}
});
const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null;
const response: any = {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
@ -122,8 +120,6 @@ export default define(meta, async (ps, me) => {
disableGlobalTimeline: instance.disableGlobalTimeline,
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
cacheRemoteFiles: instance.cacheRemoteFiles,
proxyRemoteFiles: instance.proxyRemoteFiles,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableRecaptcha: instance.enableRecaptcha,
@ -135,9 +131,6 @@ export default define(meta, async (ps, me) => {
iconUrl: instance.iconUrl,
maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH),
emojis: await Emojis.packMany(emojis),
requireSetup: (await Users.count({
host: null,
})) === 0,
enableEmail: instance.enableEmail,
enableTwitterIntegration: instance.enableTwitterIntegration,
@ -146,10 +139,20 @@ export default define(meta, async (ps, me) => {
enableServiceWorker: instance.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null,
...(ps.detail ? {
pinnedPages: instance.pinnedPages,
cacheRemoteFiles: instance.cacheRemoteFiles,
proxyRemoteFiles: instance.proxyRemoteFiles,
requireSetup: (await Users.count({
host: null,
})) === 0,
} : {})
};
if (ps.detail) {
const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null;
response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
response.features = {
registration: !instance.disableRegistration,
localTimeLine: !instance.disableLocalTimeline,
@ -164,42 +167,42 @@ export default define(meta, async (ps, me) => {
serviceWorker: instance.enableServiceWorker,
miauth: true,
};
}
if (me && me.isAdmin) {
response.useStarForReactionFallback = instance.useStarForReactionFallback;
response.pinnedUsers = instance.pinnedUsers;
response.hiddenTags = instance.hiddenTags;
response.blockedHosts = instance.blockedHosts;
response.hcaptchaSecretKey = instance.hcaptchaSecretKey;
response.recaptchaSecretKey = instance.recaptchaSecretKey;
response.proxyAccountId = instance.proxyAccountId;
response.twitterConsumerKey = instance.twitterConsumerKey;
response.twitterConsumerSecret = instance.twitterConsumerSecret;
response.githubClientId = instance.githubClientId;
response.githubClientSecret = instance.githubClientSecret;
response.discordClientId = instance.discordClientId;
response.discordClientSecret = instance.discordClientSecret;
response.summalyProxy = instance.summalyProxy;
response.email = instance.email;
response.smtpSecure = instance.smtpSecure;
response.smtpHost = instance.smtpHost;
response.smtpPort = instance.smtpPort;
response.smtpUser = instance.smtpUser;
response.smtpPass = instance.smtpPass;
response.swPrivateKey = instance.swPrivateKey;
response.useObjectStorage = instance.useObjectStorage;
response.objectStorageBaseUrl = instance.objectStorageBaseUrl;
response.objectStorageBucket = instance.objectStorageBucket;
response.objectStoragePrefix = instance.objectStoragePrefix;
response.objectStorageEndpoint = instance.objectStorageEndpoint;
response.objectStorageRegion = instance.objectStorageRegion;
response.objectStoragePort = instance.objectStoragePort;
response.objectStorageAccessKey = instance.objectStorageAccessKey;
response.objectStorageSecretKey = instance.objectStorageSecretKey;
response.objectStorageUseSSL = instance.objectStorageUseSSL;
response.objectStorageUseProxy = instance.objectStorageUseProxy;
response.objectStorageSetPublicRead = instance.objectStorageSetPublicRead;
if (me && me.isAdmin) {
response.useStarForReactionFallback = instance.useStarForReactionFallback;
response.pinnedUsers = instance.pinnedUsers;
response.hiddenTags = instance.hiddenTags;
response.blockedHosts = instance.blockedHosts;
response.hcaptchaSecretKey = instance.hcaptchaSecretKey;
response.recaptchaSecretKey = instance.recaptchaSecretKey;
response.proxyAccountId = instance.proxyAccountId;
response.twitterConsumerKey = instance.twitterConsumerKey;
response.twitterConsumerSecret = instance.twitterConsumerSecret;
response.githubClientId = instance.githubClientId;
response.githubClientSecret = instance.githubClientSecret;
response.discordClientId = instance.discordClientId;
response.discordClientSecret = instance.discordClientSecret;
response.summalyProxy = instance.summalyProxy;
response.email = instance.email;
response.smtpSecure = instance.smtpSecure;
response.smtpHost = instance.smtpHost;
response.smtpPort = instance.smtpPort;
response.smtpUser = instance.smtpUser;
response.smtpPass = instance.smtpPass;
response.swPrivateKey = instance.swPrivateKey;
response.useObjectStorage = instance.useObjectStorage;
response.objectStorageBaseUrl = instance.objectStorageBaseUrl;
response.objectStorageBucket = instance.objectStorageBucket;
response.objectStoragePrefix = instance.objectStoragePrefix;
response.objectStorageEndpoint = instance.objectStorageEndpoint;
response.objectStorageRegion = instance.objectStorageRegion;
response.objectStoragePort = instance.objectStoragePort;
response.objectStorageAccessKey = instance.objectStorageAccessKey;
response.objectStorageSecretKey = instance.objectStorageSecretKey;
response.objectStorageUseSSL = instance.objectStorageUseSSL;
response.objectStorageUseProxy = instance.objectStorageUseProxy;
response.objectStorageSetPublicRead = instance.objectStorageSetPublicRead;
}
}
return response;

View File

@ -0,0 +1,54 @@
import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ClipNotes, Clips } from '../../../../models';
import { getNote } from '../../common/getters';
import { ApiError } from '../../error';
import { In } from 'typeorm';
export const meta = {
tags: ['clips', 'notes'],
requireCredential: false as const,
params: {
noteId: {
validator: $.type(ID),
},
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'Note',
}
},
errors: {
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: '47db1a1c-b0af-458d-8fb4-986e4efafe1e'
}
}
};
export default define(meta, async (ps, me) => {
const note = await getNote(ps.noteId).catch(e => {
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw e;
});
const clipNotes = await ClipNotes.find({
noteId: note.id,
});
const clips = await Clips.find({
id: In(clipNotes.map(x => x.clipId)),
});
return await Promise.all(clips.map(x => Clips.pack(x)));
});

View File

@ -0,0 +1,29 @@
import define from '../../define';
import { Pages } from '../../../../models';
export const meta = {
tags: ['pages'],
requireCredential: false as const,
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'Page',
}
},
};
export default define(meta, async (ps, me) => {
const query = Pages.createQueryBuilder('page')
.where('page.visibility = \'public\'')
.andWhere('page.likedCount > 0')
.orderBy('page.likedCount', 'DESC');
const pages = await query.take(10).getMany();
return await Pages.packMany(pages, me);
});

View File

@ -299,6 +299,7 @@ router.get('/@:user/pages/:page', async ctx => {
});
// Clip
// TODO: 非publicなclipのハンドリング
router.get('/clips/:clip', async ctx => {
const clip = await Clips.findOne({
id: ctx.params.clip,