Merge branch 'develop' into sw-notification-action

This commit is contained in:
tamaina
2021-05-06 20:52:53 +09:00
129 changed files with 4778 additions and 906 deletions

View File

@@ -6,7 +6,7 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
type Captcha = {
render(container: string | Node, options: {
@@ -32,7 +32,7 @@ declare global {
export default defineComponent({
props: {
provider: {
type: String,
type: String as PropType<CaptchaProvider>,
required: true,
},
sitekey: {
@@ -51,19 +51,25 @@ export default defineComponent({
},
computed: {
loaded() {
return !!window[this.provider as CaptchaProvider];
variable(): string {
switch (this.provider) {
case 'hcaptcha': return 'hcaptcha';
case 'recaptcha': return 'grecaptcha';
}
},
src() {
loaded(): boolean {
return !!window[this.variable];
},
src(): string {
const endpoint = ({
hcaptcha: 'https://hcaptcha.com/1',
recaptcha: 'https://www.recaptcha.net/recaptcha',
} as Record<PropertyKey, unknown>)[this.provider];
} as Record<CaptchaProvider, string>)[this.provider];
return `${typeof endpoint == 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
return `${typeof endpoint === 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
},
captcha() {
return window[this.provider as CaptchaProvider] || {} as unknown as Captcha;
captcha(): Captcha {
return window[this.variable] || {} as unknown as Captcha;
},
},
@@ -94,7 +100,7 @@ export default defineComponent({
methods: {
reset() {
this.captcha?.reset();
if (this.captcha?.reset) this.captcha.reset();
},
requestRender() {
if (this.captcha.render && this.$refs.captcha instanceof Element) {

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { defineComponent, h, TransitionGroup } from 'vue';
import MkAd from '@client/components/global/ad.vue';
export default defineComponent({
props: {
@@ -22,6 +23,11 @@ export default defineComponent({
required: false,
default: false
},
ad: {
type: Boolean,
required: false,
default: false
},
},
methods: {
@@ -58,11 +64,7 @@ export default defineComponent({
if (
i != this.items.length - 1 &&
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
!item._prId_ &&
!this.items[i + 1]._prId_ &&
!item._featuredId_ &&
!this.items[i + 1]._featuredId_
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
) {
const separator = h('div', {
class: 'separator',
@@ -86,7 +88,15 @@ export default defineComponent({
return [el, separator];
} else {
return el;
if (this.ad && item._shouldInsertAd_) {
return [h(MkAd, {
class: 'a', // advertiseの意(ブロッカー対策)
key: item.id + ':ad',
prefer: ['horizontal', 'horizontal-big'],
}), el];
} else {
return el;
}
}
}));
},
@@ -95,6 +105,10 @@ export default defineComponent({
<style lang="scss">
.sqadhkmv {
> *:empty {
display: none;
}
> *:not(:last-child) {
margin-bottom: var(--margin);
}

View File

@@ -81,7 +81,7 @@ export default defineComponent({
getMenu() {
return [{
text: this.$ts.rename,
icon: faICursor,
icon: 'fas fa-i-cursor',
action: this.rename
}, {
text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,

View File

@@ -247,7 +247,7 @@ export default defineComponent({
}
}, null, {
text: this.$ts.rename,
icon: faICursor,
icon: 'fas fa-i-cursor',
action: this.rename
}, null, {
text: this.$ts.delete,

View File

@@ -614,7 +614,7 @@ export default defineComponent({
type: 'label'
}, this.folder ? {
text: this.$ts.renameFolder,
icon: faICursor,
icon: 'fas fa-i-cursor',
action: () => { this.renameFolder(this.folder); }
} : undefined, this.folder ? {
text: this.$ts.deleteFolder,

View File

@@ -35,6 +35,7 @@
class="_button"
@click="chosen(emoji, $event)"
tabindex="0"
:key="emoji"
>
<MkEmoji :emoji="emoji" :normal="true"/>
</button>
@@ -104,7 +105,7 @@ export default defineComponent({
return {
emojilist: markRaw(emojilist),
getStaticImageUrl,
pinned: this.$store.state.reactions,
pinned: this.$store.reactiveState.reactions,
width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
big: this.asReactionPicker ? isDeviceTouch : false,

View File

@@ -0,0 +1,71 @@
<template>
<XModalWindow ref="dialog"
:width="370"
:height="400"
@close="$refs.dialog.close()"
@closed="$emit('closed')"
>
<template #header>{{ $ts.forgotPassword }}</template>
<form class="_monolithic_" @submit.prevent="onSubmit" v-if="$instance.enableEmail">
<div class="_section">
<MkInput v-model:value="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
<span>{{ $ts.username }}</span>
<template #prefix>@</template>
</MkInput>
<MkInput v-model:value="email" type="email" spellcheck="false" required>
<span>{{ $ts.emailAddress }}</span>
<template #desc>{{ $ts._forgotPassword.enterEmail }}</template>
</MkInput>
<MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
</div>
<div class="_section">
<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
</div>
</form>
<div v-else>
{{ $ts._forgotPassword.contactAdmin }}
</div>
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XModalWindow from '@client/components/ui/modal-window.vue';
import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue';
import * as os from '@client/os';
export default defineComponent({
components: {
XModalWindow,
MkButton,
MkInput,
},
emits: ['done', 'closed'],
data() {
return {
username: '',
email: '',
processing: false,
};
},
methods: {
async onSubmit() {
this.processing = true;
await os.apiWithDialog('request-reset-password', {
username: this.username,
email: this.email,
});
this.$emit('done');
this.$refs.dialog.close();
}
}
});
</script>

View File

@@ -0,0 +1,126 @@
<template>
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel" tabindex="-1">
<div class="thumbnail">
<ImgWithBlurhash class="img" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash"/>
</div>
<article>
<header>
<MkAvatar :user="post.user" class="avatar"/>
</header>
<footer>
<span class="title">{{ post.title }}</span>
</footer>
</article>
</MkA>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { userName } from '@client/filters/user';
import ImgWithBlurhash from '@client/components/img-with-blurhash.vue';
import * as os from '@client/os';
export default defineComponent({
components: {
ImgWithBlurhash
},
props: {
post: {
type: Object,
required: true
},
},
methods: {
userName
}
});
</script>
<style lang="scss" scoped>
.ttasepnz {
display: block;
position: relative;
height: 200px;
&:hover {
text-decoration: none;
color: var(--accent);
> .thumbnail {
transform: scale(1.1);
}
> article {
> footer {
&:before {
opacity: 1;
}
}
}
}
> .thumbnail {
width: 100%;
height: 100%;
position: absolute;
transition: all 0.5s ease;
> .img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
> article {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
> header {
position: absolute;
top: 0;
width: 100%;
padding: 12px;
box-sizing: border-box;
display: flex;
> .avatar {
margin-left: auto;
width: 32px;
height: 32px;
}
}
> footer {
position: absolute;
bottom: 0;
width: 100%;
padding: 16px;
box-sizing: border-box;
color: #fff;
text-shadow: 0 0 8px #000;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
&:before {
content: "";
display: block;
position: absolute;
z-index: -1;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(rgba(0, 0, 0, 0.4), transparent);
opacity: 0;
transition: opacity 0.5s ease;
}
> .title {
font-weight: bold;
}
}
}
}
</style>

View File

@@ -0,0 +1,163 @@
<template>
<div class="qiivuoyo" v-if="ad">
<div class="main" :class="ad.place" v-if="!showMenu">
<a :href="ad.url" target="_blank">
<img :src="ad.imageUrl">
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle"></span></button>
</a>
</div>
<div class="menu" v-else>
<div class="body">
<div>Ads by {{ host }}</div>
<!--<MkButton>{{ $ts.stopThisAd }}</MkButton>-->
<button class="_textButton" @click="toggleMenu">{{ $ts.close }}</button>
</div>
</div>
</div>
<div v-else></div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { instance } from '@client/instance';
import { host } from '@client/config';
import MkButton from '@client/components/ui/button.vue';
export default defineComponent({
components: {
MkButton
},
props: {
prefer: {
type: Array,
required: true
},
specify: {
type: Object,
required: false
},
},
setup(props) {
const showMenu = ref(false);
const toggleMenu = () => {
showMenu.value = !showMenu.value;
};
let ad = null;
if (props.specify) {
ad = props.specify;
} else {
let ads = instance.ads.filter(ad => props.prefer.includes(ad.place));
if (ads.length === 0) {
ads = instance.ads.filter(ad => ad.place === 'square');
}
const high = ads.filter(ad => ad.priority === 'high');
const middle = ads.filter(ad => ad.priority === 'middle');
const low = ads.filter(ad => ad.priority === 'low');
if (high.length > 0) {
ad = high[Math.floor(Math.random() * high.length)];
} else if (middle.length > 0) {
ad = middle[Math.floor(Math.random() * middle.length)];
} else if (low.length > 0) {
ad = low[Math.floor(Math.random() * low.length)];
}
}
return {
ad,
showMenu,
toggleMenu,
host,
};
}
});
</script>
<style lang="scss" scoped>
.qiivuoyo {
background-size: auto auto;
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
> .main {
text-align: center;
> a {
display: inline-block;
position: relative;
vertical-align: bottom;
&:hover {
> img {
filter: contrast(120%);
}
}
> img {
display: block;
object-fit: contain;
margin: auto;
}
> .menu {
position: absolute;
top: 0;
right: 0;
background: var(--panel);
}
}
&.square {
> a ,
> a > img {
max-width: min(300px, 100%);
max-height: 300px;
}
}
&.horizontal {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 80px;
}
}
&.horizontal-big {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 250px;
}
}
&.vertical {
> a ,
> a > img {
max-width: min(100px, 100%);
}
}
}
> .menu {
padding: 8px;
text-align: center;
> .body {
padding: 8px;
margin: 0 auto;
max-width: 400px;
border: solid 1px var(--divider);
}
}
}
</style>

View File

@@ -113,8 +113,6 @@ export default defineComponent({
> .icon {
padding-left: 2px;
font-size: .9em;
font-weight: 400;
font-style: normal;
}
> .self {

View File

@@ -12,8 +12,10 @@ import url from './global/url.vue';
import i18n from './global/i18n';
import loading from './global/loading.vue';
import error from './global/error.vue';
import ad from './global/ad.vue';
export default function(app: App) {
app.component('I18n', i18n);
app.component('Mfm', mfm);
app.component('MkA', a);
app.component('MkAcct', acct);
@@ -25,5 +27,5 @@ export default function(app: App) {
app.component('MkUrl', url);
app.component('MkLoading', loading);
app.component('MkError', error);
app.component('I18n', i18n);
app.component('MkAd', ad);
}

View File

@@ -3,12 +3,12 @@
<div class="szkkfdyq _popup">
<div class="main">
<template v-for="item in items">
<button v-if="item.action" class="_button" @click="$event => { item.action($event); close(); }">
<button v-if="item.action" class="_button" @click="$event => { item.action($event); close(); }" v-click-anime>
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button>
<MkA v-else :to="item.to" @click.passive="close()">
<MkA v-else :to="item.to" @click.passive="close()" v-click-anime>
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
@@ -16,16 +16,16 @@
</template>
</div>
<div class="sub">
<MkA to="/docs" @click.passive="close()">
<MkA to="/docs" @click.passive="close()" v-click-anime>
<i class="fas fa-question-circle icon"></i>
<div class="text">{{ $ts.help }}</div>
</MkA>
<MkA to="/about" @click.passive="close()">
<MkA to="/about" @click.passive="close()" v-click-anime>
<i class="fas fa-info-circle icon"></i>
<div class="text">{{ $t('aboutX', { x: instanceName }) }}</div>
</MkA>
<MkA to="/about-misskey" @click.passive="close()">
<i class="fas fa-info-circle icon"></i>
<MkA to="/about-misskey" @click.passive="close()" v-click-anime>
<img src="/static-assets/favicon.png" class="icon"/>
<div class="text">{{ $ts.aboutMisskey }}</div>
</MkA>
</div>
@@ -101,6 +101,7 @@ export default defineComponent({
flex-direction: column;
align-items: center;
justify-content: center;
vertical-align: bottom;
width: 128px;
height: 128px;
border-radius: var(--radius);
@@ -117,6 +118,7 @@ export default defineComponent({
> .icon {
font-size: 26px;
height: 32px;
}
> .text {

View File

@@ -1,5 +1,5 @@
<template>
<div class="yohlumlk">
<div class="yohlumlk" v-size="{ min: [350, 500] }">
<MkAvatar class="avatar" :user="note.user"/>
<div class="main">
<XNoteHeader class="header" :note="note" :mini="true"/>
@@ -50,18 +50,19 @@ export default defineComponent({
display: flex;
margin: 0;
padding: 0;
overflow: hidden;
overflow: clip;
font-size: 0.95em;
> .avatar {
@media (min-width: 350px) {
&.min-width_350px {
> .avatar {
margin: 0 10px 0 0;
width: 44px;
height: 44px;
}
}
@media (min-width: 500px) {
&.min-width_500px {
> .avatar {
margin: 0 12px 0 0;
width: 48px;
height: 48px;

View File

@@ -17,7 +17,7 @@
</MkButton>
</div>
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap">
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap" :ad="true">
<XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/>
</XList>

View File

@@ -93,7 +93,7 @@ export default defineComponent({
if (this.menu) return;
this.menu = os.modalMenu([{
text: this.$ts.renameFile,
icon: faICursor,
icon: 'fas fa-i-cursor',
action: () => { this.rename(file) }
}, {
text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,

View File

@@ -11,6 +11,7 @@
<MkInput v-model:value="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required>
<span>{{ $ts.password }}</span>
<template #prefix><i class="fas fa-lock"></i></template>
<template #desc><button class="_textButton" @click="resetPassword">{{ $ts.forgotPassword }}</button></template>
</MkInput>
<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
</div>
@@ -49,8 +50,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { toUnicode } from 'punycode/';
import MkButton from './ui/button.vue';
import MkInput from './ui/input.vue';
import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue';
import { apiUrl, host } from '@client/config';
import { byteify, hexify } from '@client/scripts/2fa';
import * as os from '@client/os';
@@ -197,6 +198,11 @@ export default defineComponent({
this.signing = false;
});
}
},
resetPassword() {
os.popup(import('@client/components/forgot-password.vue'), {}, {
}, 'closed');
}
}
});

View File

@@ -12,14 +12,16 @@ export default defineComponent({
return withDirectives(h('div', {
class: 'pxhvhrfw',
}, options.map(option => h('button', {
}, options.map(option => withDirectives(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))), [
}, option.children), [
[resolveDirective('click-anime')]
]))), [
[resolveDirective('size'), { max: [500] }]
]);
}

View File

@@ -139,7 +139,8 @@ export default defineComponent({
}
&.primary {
color: #fff;
font-weight: bold;
color: #fff !important;
background: var(--accent);
&:not(:disabled):hover {
@@ -200,10 +201,6 @@ export default defineComponent({
min-width: 100px;
}
&.primary {
font-weight: bold;
}
> .ripples {
position: absolute;
z-index: 0;

View File

@@ -199,6 +199,7 @@ export default defineComponent({
> .fade {
display: block;
position: absolute;
z-index: 10;
bottom: 0;
left: 0;
width: 100%;

View File

@@ -10,8 +10,8 @@
<div v-else class="cxiknjgy">
<slot :items="items"></slot>
<div class="more" v-show="more" key="_more_">
<MkButton class="button" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
<div class="more _gap" v-show="more" key="_more_">
<MkButton class="button" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</MkButton>
@@ -38,6 +38,12 @@ export default defineComponent({
pagination: {
required: true
},
disableAutoLoad: {
type: Boolean,
required: false,
default: false,
}
},
});
</script>

View File

@@ -8,33 +8,35 @@
@closed="$emit('closed')"
>
<template #header>{{ $ts.selectUser }}</template>
<div class="tbhwbxda _section">
<div class="inputs">
<MkInput v-model:value="username" class="input" @update:value="search" ref="username"><span>{{ $ts.username }}</span><template #prefix>@</template></MkInput>
<MkInput v-model:value="host" class="input" @update:value="search"><span>{{ $ts.host }}</span><template #prefix>@</template></MkInput>
</div>
</div>
<div class="tbhwbxda _section result" v-if="username != '' || host != ''" :class="{ hit: users.length > 0 }">
<div class="users" v-if="users.length > 0">
<div class="user" v-for="user in users" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
<div class="body">
<MkUserName :user="user" class="name"/>
<MkAcct :user="user" class="acct"/>
</div>
<div class="tbhwbxda _monolithic_">
<div class="_section">
<div class="inputs">
<MkInput v-model:value="username" class="input" @update:value="search" ref="username"><span>{{ $ts.username }}</span><template #prefix>@</template></MkInput>
<MkInput v-model:value="host" class="input" @update:value="search"><span>{{ $ts.host }}</span><template #prefix>@</template></MkInput>
</div>
</div>
<div v-else class="empty">
<span>{{ $ts.noUsers }}</span>
<div class="_section result" v-if="username != '' || host != ''" :class="{ hit: users.length > 0 }">
<div class="users" v-if="users.length > 0">
<div class="user" v-for="user in users" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
<div class="body">
<MkUserName :user="user" class="name"/>
<MkAcct :user="user" class="acct"/>
</div>
</div>
</div>
<div v-else class="empty">
<span>{{ $ts.noUsers }}</span>
</div>
</div>
</div>
<div class="tbhwbxda _section recent" v-if="username == '' && host == ''">
<div class="users">
<div class="user" v-for="user in recentUsers" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
<div class="body">
<MkUserName :user="user" class="name"/>
<MkAcct :user="user" class="acct"/>
<div class="_section recent" v-if="username == '' && host == ''">
<div class="users">
<div class="user" v-for="user in recentUsers" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
<div class="body">
<MkUserName :user="user" class="name"/>
<MkAcct :user="user" class="acct"/>
</div>
</div>
</div>
</div>
@@ -122,76 +124,78 @@ export default defineComponent({
<style lang="scss" scoped>
.tbhwbxda {
display: flex;
flex-direction: column;
overflow: auto;
height: 100%;
&.result.hit {
padding: 0;
}
&.recent {
padding: 0;
}
> .inputs {
> .input {
display: inline-block;
width: 50%;
margin: 0;
}
}
> .users {
flex: 1;
> ._section {
display: flex;
flex-direction: column;
overflow: auto;
padding: 8px 0;
height: 100%;
> .user {
display: flex;
align-items: center;
padding: 8px var(--root-margin);
font-size: 14px;
&.result.hit {
padding: 0;
}
&:hover {
background: var(--X7);
&.recent {
padding: 0;
}
> .inputs {
> .input {
display: inline-block;
width: 50%;
margin: 0;
}
}
&.selected {
background: var(--accent);
color: #fff;
}
> .users {
flex: 1;
overflow: auto;
padding: 8px 0;
> * {
pointer-events: none;
user-select: none;
}
> .user {
display: flex;
align-items: center;
padding: 8px var(--root-margin);
font-size: 14px;
> .avatar {
width: 45px;
height: 45px;
}
> .body {
padding: 0 8px;
min-width: 0;
> .name {
display: block;
font-weight: bold;
&:hover {
background: var(--X7);
}
> .acct {
opacity: 0.5;
&.selected {
background: var(--accent);
color: #fff;
}
> * {
pointer-events: none;
user-select: none;
}
> .avatar {
width: 45px;
height: 45px;
}
> .body {
padding: 0 8px;
min-width: 0;
> .name {
display: block;
font-weight: bold;
}
> .acct {
opacity: 0.5;
}
}
}
}
}
> .empty {
opacity: 0.7;
text-align: center;
> .empty {
opacity: 0.7;
text-align: center;
}
}
}
</style>