diff options
Diffstat (limited to 'src/client/app/common/views')
37 files changed, 782 insertions, 372 deletions
diff --git a/src/client/app/common/views/components/acct.vue b/src/client/app/common/views/components/acct.vue index 1ad222afdd..542fbb4296 100644 --- a/src/client/app/common/views/components/acct.vue +++ b/src/client/app/common/views/components/acct.vue @@ -1,19 +1,25 @@ <template> <span class="mk-acct"> <span class="name">@{{ user.username }}</span> - <span class="host" v-if="user.host">@{{ user.host }}</span> + <span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span> </span> </template> <script lang="ts"> import Vue from 'vue'; +import { host } from '../../../config'; export default Vue.extend({ - props: ['user'] + props: ['user', 'detail'], + data() { + return { + host + }; + } }); </script> <style lang="stylus" scoped> .mk-acct - > .host + > .host.fade opacity 0.5 </style> diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue index b274eaa0a0..ea05afd6dc 100644 --- a/src/client/app/common/views/components/autocomplete.vue +++ b/src/client/app/common/views/components/autocomplete.vue @@ -125,7 +125,7 @@ export default Vue.extend({ } if (this.type == 'user') { - const cacheKey = 'autocomplete:user:' + this.q; + const cacheKey = `autocomplete:user:${this.q}`; const cache = sessionStorage.getItem(cacheKey); if (cache) { const users = JSON.parse(cache); @@ -148,7 +148,7 @@ export default Vue.extend({ this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]'); this.fetching = false; } else { - const cacheKey = 'autocomplete:hashtag:' + this.q; + const cacheKey = `autocomplete:hashtag:${this.q}`; const cache = sessionStorage.getItem(cacheKey); if (cache) { const hashtags = JSON.parse(cache); diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue index c5ac74e537..a2b0fc6bd3 100644 --- a/src/client/app/common/views/components/avatar.vue +++ b/src/client/app/common/views/components/avatar.vue @@ -1,15 +1,15 @@ <template> - <span class="mk-avatar" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick"> - <span class="inner" :style="style"></span> + <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick"> + <span class="inner" :style="icon"></span> </span> - <span class="mk-avatar" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick"> - <span class="inner" :style="style"></span> + <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick"> + <span class="inner" :style="icon"></span> </span> - <router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id"> - <span class="inner" :style="style"></span> + <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id"> + <span class="inner" :style="icon"></span> </router-link> - <router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview"> - <span class="inner" :style="style"></span> + <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview"> + <span class="inner" :style="icon"></span> </router-link> </template> @@ -43,6 +43,11 @@ export default Vue.extend({ }, style(): any { return { + borderRadius: this.$store.state.settings.circleIcons ? '100%' : null + }; + }, + icon(): any { + return { backgroundColor: this.lightmode ? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})` : this.user.avatarColor && this.user.avatarColor.length == 3 diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue index 6c23cc7969..f64cae6b4b 100644 --- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue +++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue @@ -57,7 +57,7 @@ export default Vue.extend({ } // Check internet connection - fetch('https://google.com?rand=' + Math.random(), { + fetch(`https://google.com?rand=${Math.random()}`, { mode: 'no-cors' }).then(() => { this.internet = true; diff --git a/src/client/app/common/views/components/cw-button.vue b/src/client/app/common/views/components/cw-button.vue new file mode 100644 index 0000000000..06087edc93 --- /dev/null +++ b/src/client/app/common/views/components/cw-button.vue @@ -0,0 +1,44 @@ +<template> +<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle">{{ value ? '%i18n:@hide%' : '%i18n:@show%' }}</button> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + value: { + type: Boolean, + required: true + } + }, + + methods: { + toggle() { + this.$emit('input', !this.value); + } + } +}); +</script> + +<style lang="stylus" scoped> +root(isDark) + display inline-block + padding 4px 8px + font-size 0.7em + color isDark ? #393f4f : #fff + background isDark ? #687390 : #b1b9c1 + border-radius 2px + cursor pointer + user-select none + + &:hover + background isDark ? #707b97 : #bbc4ce + +.nrvgflfuaxwgkxoynpnumyookecqrrvh[data-darkmode] + root(true) + +.nrvgflfuaxwgkxoynpnumyookecqrrvh:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue index b432a2308d..fea19d917e 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.game.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue @@ -50,15 +50,15 @@ </div> <div class="player" v-if="game.isEnded"> - <el-button-group> - <el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button> - <el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</el-button> - </el-button-group> + <div> + <button @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</button> + <button @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</button> + </div> <span>{{ logPos }} / {{ logs.length }}</span> - <el-button-group> - <el-button type="primary" @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</el-button> - <el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</el-button> - </el-button-group> + <div> + <button @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</button> + <button @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</button> + </div> </div> <div class="info"> @@ -159,11 +159,9 @@ export default Vue.extend({ canPutEverywhere: this.game.settings.canPutEverywhere, loopedBoard: this.game.settings.loopedBoard }); - this.logs.forEach((log, i) => { - if (i < v) { - this.o.put(log.color, log.pos); - } - }); + for (const log of this.logs.slice(0, v)) { + this.o.put(log.color, log.pos); + } this.$forceUpdate(); } }, diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue index fa88aeaaf4..d23902aae7 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.index.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue @@ -3,7 +3,6 @@ <h1>%i18n:@title%</h1> <p>%i18n:@sub-title%</p> <div class="play"> - <!--<el-button round>フリーマッチ(準備中)</el-button>--> <form-button primary round @click="match">%i18n:@invite%</form-button> <details> <summary>%i18n:@rule%</summary> diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue index aed8718dd0..fef833d63e 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.room.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue @@ -59,11 +59,6 @@ </header> <div> - <el-alert v-for="message in messages" - :title="message.text" - :type="message.type" - :key="message.id"/> - <template v-for="item in form"> <mk-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm(item)">{{ item.desc || '' }}</mk-switch> @@ -93,7 +88,7 @@ </header> <div> - <el-input v-model="item.value" @change="onChangeForm(item)"/> + <input v-model="item.value" @change="onChangeForm(item)"/> </div> </div> </template> diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index 422a3da050..6f8152cea2 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -1,5 +1,8 @@ import Vue from 'vue'; +import cwButton from './cw-button.vue'; +import tagCloud from './tag-cloud.vue'; +import trends from './trends.vue'; import analogClock from './analog-clock.vue'; import menu from './menu.vue'; import noteHeader from './note-header.vue'; @@ -40,6 +43,9 @@ import uiSelect from './ui/select.vue'; import formButton from './ui/form/button.vue'; import formRadio from './ui/form/radio.vue'; +Vue.component('mk-cw-button', cwButton); +Vue.component('mk-tag-cloud', tagCloud); +Vue.component('mk-trends', trends); Vue.component('mk-analog-clock', analogClock); Vue.component('mk-menu', menu); Vue.component('mk-note-header', noteHeader); diff --git a/src/client/app/common/views/components/media-banner.vue b/src/client/app/common/views/components/media-banner.vue new file mode 100644 index 0000000000..211dbf0208 --- /dev/null +++ b/src/client/app/common/views/components/media-banner.vue @@ -0,0 +1,90 @@ +<template> +<div class="mk-media-banner"> + <div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false"> + <span class="icon">%fa:exclamation-triangle%</span> + <b>%i18n:@sensitive%</b> + <span>%i18n:@click-to-show%</span> + </div> + <div class="audio" v-else-if="media.type.startsWith('audio')"> + <audio class="audio" + :src="media.url" + :title="media.name" + controls + ref="audio" + preload="metadata" /> + </div> + <a class="download" v-else + :href="media.url" + :title="media.name" + :download="media.name" + > + <span class="icon">%fa:download%</span> + <b>{{ media.name }}</b> + </a> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + media: { + type: Object, + required: true + } + }, + data() { + return { + hide: true + }; + } +}) +</script> + +<style lang="stylus" scoped> +root(isDark) + width 100% + border-radius 4px + margin-top 4px + overflow hidden + + > .download, + > .sensitive + display flex + align-items center + font-size 12px + padding 8px 12px + white-space nowrap + + > * + display block + + > b + overflow hidden + text-overflow ellipsis + + > *:not(:last-child) + margin-right .2em + + > .icon + font-size 1.6em + + > .download + background isDark ? #21242d : #f7f7f7 + + > .sensitive + background #111 + color #fff + + > .audio + .audio + display block + width 100% + +.mk-media-banner[data-darkmode] + root(true) + +.mk-media-banner:not([data-darkmode]) + root(false) +</style> diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue index cdfc2c8d3c..d83d6f85cd 100644 --- a/src/client/app/common/views/components/media-list.vue +++ b/src/client/app/common/views/components/media-list.vue @@ -1,18 +1,27 @@ <template> <div class="mk-media-list"> - <div :data-count="mediaList.length" ref="grid"> - <template v-for="media in mediaList"> - <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/> - <mk-media-image :image="media" :key="media.id" v-else :raw="raw"/> - </template> + <template v-for="media in mediaList.filter(media => !previewable(media))"> + <x-banner :media="media" :key="media.id"/> + </template> + <div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container"> + <div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid"> + <template v-for="media in mediaList"> + <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/> + <mk-media-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/> + </template> + </div> </div> </div> </template> <script lang="ts"> import Vue from 'vue'; +import XBanner from './media-banner.vue'; export default Vue.extend({ + components: { + XBanner + }, props: { mediaList: { required: true @@ -22,70 +31,80 @@ export default Vue.extend({ } }, mounted() { - // for Safari bug - this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px'; + //#region for Safari bug + if (this.$refs.grid) { + this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px'; + } + //#endregion + }, + methods: { + previewable(file) { + return file.type.startsWith('video') || file.type.startsWith('image'); + } } }); </script> <style lang="stylus" scoped> .mk-media-list - width 100% + > .gird-container + width 100% + margin-top 4px - &:before - content '' - display block - padding-top 56.25% // 16:9 + &:before + content '' + display block + padding-top 56.25% // 16:9 - > div - position absolute - top 0 - right 0 - bottom 0 - left 0 - display grid - grid-gap 4px + > div + position absolute + top 0 + right 0 + bottom 0 + left 0 + display grid + grid-gap 4px - > * - overflow hidden - border-radius 4px + > * + overflow hidden + border-radius 4px - &[data-count="1"] - grid-template-rows 1fr + &[data-count="1"] + grid-template-rows 1fr - &[data-count="2"] - grid-template-columns 1fr 1fr - grid-template-rows 1fr + &[data-count="2"] + grid-template-columns 1fr 1fr + grid-template-rows 1fr - &[data-count="3"] - grid-template-columns 1fr 0.5fr - grid-template-rows 1fr 1fr + &[data-count="3"] + grid-template-columns 1fr 0.5fr + grid-template-rows 1fr 1fr - > *:nth-child(1) - grid-row 1 / 3 + > *:nth-child(1) + grid-row 1 / 3 - > *:nth-child(3) - grid-column 2 / 3 - grid-row 2 / 3 + > *:nth-child(3) + grid-column 2 / 3 + grid-row 2 / 3 - &[data-count="4"] - grid-template-columns 1fr 1fr - grid-template-rows 1fr 1fr + &[data-count="4"] + grid-template-columns 1fr 1fr + grid-template-rows 1fr 1fr - > *:nth-child(1) - grid-column 1 / 2 - grid-row 1 / 2 + > *:nth-child(1) + grid-column 1 / 2 + grid-row 1 / 2 - > *:nth-child(2) - grid-column 2 / 3 - grid-row 1 / 2 + > *:nth-child(2) + grid-column 2 / 3 + grid-row 1 / 2 - > *:nth-child(3) - grid-column 1 / 2 - grid-row 2 / 3 + > *:nth-child(3) + grid-column 1 / 2 + grid-row 2 / 3 - > *:nth-child(4) - grid-column 2 / 3 - grid-row 2 / 3 + > *:nth-child(4) + grid-column 2 / 3 + grid-row 2 / 3 </style> diff --git a/src/client/app/common/views/components/menu.vue b/src/client/app/common/views/components/menu.vue index 9b16732b9a..fba7e235e0 100644 --- a/src/client/app/common/views/components/menu.vue +++ b/src/client/app/common/views/components/menu.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-menu"> +<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv"> <div class="backdrop" ref="backdrop" @click="close"></div> <div class="popover" :class="{ hukidasi }" ref="popover"> <template v-for="item in items"> @@ -108,7 +108,7 @@ export default Vue.extend({ easing: 'easeInBack', complete: () => { this.$emit('closed'); - this.$destroy(); + this.destroyDom(); } }); } @@ -119,9 +119,10 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -$border-color = rgba(27, 31, 35, 0.15) +root(isDark) + $bg-color = isDark ? #2c303c : #fff + $border-color = rgba(27, 31, 35, 0.15) -.mk-menu position initial > .backdrop @@ -131,14 +132,14 @@ $border-color = rgba(27, 31, 35, 0.15) z-index 10000 width 100% height 100% - background rgba(#000, 0.1) + background rgba(#000, isDark ? 0.5 : 0.1) opacity 0 > .popover position absolute z-index 10001 padding 8px 0 - background #fff + background $bg-color border 1px solid $border-color border-radius 4px box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) @@ -172,12 +173,13 @@ $border-color = rgba(27, 31, 35, 0.15) border-top solid $balloon-size transparent border-left solid $balloon-size transparent border-right solid $balloon-size transparent - border-bottom solid $balloon-size #fff + border-bottom solid $balloon-size $bg-color > button display block padding 8px 16px width 100% + color isDark ? #d6dce2 : #111 &:hover color $theme-color-foreground @@ -191,6 +193,12 @@ $border-color = rgba(27, 31, 35, 0.15) > div margin 8px 0 height 1px - background #eee + background isDark ? #1c2023 : #eee + +.onchrpzrvnoruiaenfcqvccjfuupzzwv[data-darkmode] + root(true) + +.onchrpzrvnoruiaenfcqvccjfuupzzwv:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue index 30143b4f1d..1de41855df 100644 --- a/src/client/app/common/views/components/messaging-room.vue +++ b/src/client/app/common/views/components/messaging-room.vue @@ -3,7 +3,7 @@ @dragover.prevent.stop="onDragover" @drop.prevent.stop="onDrop" > - <div class="stream"> + <div class="body"> <p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p> <p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:@empty%</p> <p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:@no-history%</p> @@ -77,6 +77,12 @@ export default Vue.extend({ this.connection.on('message', this.onMessage); this.connection.on('read', this.onRead); + if (this.isNaked) { + window.addEventListener('scroll', this.onScroll, { passive: true }); + } else { + this.$el.addEventListener('scroll', this.onScroll, { passive: true }); + } + document.addEventListener('visibilitychange', this.onVisibilitychange); this.fetchMessages().then(() => { @@ -90,6 +96,12 @@ export default Vue.extend({ this.connection.off('read', this.onRead); this.connection.close(); + if (this.isNaked) { + window.removeEventListener('scroll', this.onScroll); + } else { + this.$el.removeEventListener('scroll', this.onScroll); + } + document.removeEventListener('visibilitychange', this.onVisibilitychange); }, @@ -226,6 +238,14 @@ export default Vue.extend({ }, 4000); }, + onScroll() { + const el = this.isNaked ? window.document.documentElement : this.$el; + const current = el.scrollTop + el.clientHeight; + if (current > el.scrollHeight - 1) { + this.showIndicator = false; + } + }, + onVisibilitychange() { if (document.hidden) return; this.messages.forEach(message => { @@ -251,7 +271,7 @@ root(isDark) height 100% background isDark ? #191b22 : #fff - > .stream + > .body width 100% max-width 600px margin 0 auto diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts index e97da4302c..224bd6f5de 100644 --- a/src/client/app/common/views/components/misskey-flavored-markdown.ts +++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { VNode } from 'vue'; import * as emojilib from 'emojilib'; import { length } from 'stringz'; import parse from '../../../../../mfm/parse'; @@ -6,10 +6,7 @@ import getAcct from '../../../../../misc/acct/render'; import { url } from '../../../config'; import MkUrl from './url.vue'; import MkGoogle from './google.vue'; - -const flatten = list => list.reduce( - (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] -); +import { concat } from '../../../../../prelude/array'; export default Vue.component('misskey-flavored-markdown', { props: { @@ -32,20 +29,20 @@ export default Vue.component('misskey-flavored-markdown', { }, render(createElement) { - let ast; + let ast: any[]; if (this.ast == null) { // Parse text to ast ast = parse(this.text); } else { - ast = this.ast; + ast = this.ast as any[]; } let bigCount = 0; let motionCount = 0; // Parse ast to DOM - const els = flatten(ast.map(token => { + const els = concat(ast.map((token): VNode[] => { switch (token.type) { case 'text': { const text = token.content.replace(/(\r\n|\n|\r)/g, '\n'); @@ -56,12 +53,12 @@ export default Vue.component('misskey-flavored-markdown', { x[x.length - 1].pop(); return x; } else { - return createElement('span', text.replace(/\n/g, ' ')); + return [createElement('span', text.replace(/\n/g, ' '))]; } } case 'bold': { - return createElement('b', token.bold); + return [createElement('b', token.bold)]; } case 'big': { @@ -95,23 +92,23 @@ export default Vue.component('misskey-flavored-markdown', { } case 'url': { - return createElement(MkUrl, { + return [createElement(MkUrl, { props: { url: token.content, target: '_blank' } - }); + })]; } case 'link': { - return createElement('a', { + return [createElement('a', { attrs: { class: 'link', href: token.url, target: '_blank', title: token.url } - }, token.title); + }, token.title)]; } case 'mention': { @@ -129,16 +126,16 @@ export default Vue.component('misskey-flavored-markdown', { } case 'hashtag': { - return createElement('a', { + return [createElement('a', { attrs: { href: `${url}/tags/${encodeURIComponent(token.hashtag)}`, target: '_blank' } - }, token.content); + }, token.content)]; } case 'code': { - return createElement('pre', { + return [createElement('pre', { class: 'code' }, [ createElement('code', { @@ -146,15 +143,15 @@ export default Vue.component('misskey-flavored-markdown', { innerHTML: token.html } }) - ]); + ])]; } case 'inline-code': { - return createElement('code', { + return [createElement('code', { domProps: { innerHTML: token.html } - }); + })]; } case 'quote': { @@ -164,58 +161,51 @@ export default Vue.component('misskey-flavored-markdown', { const x = text2.split('\n') .map(t => [createElement('span', t), createElement('br')]); x[x.length - 1].pop(); - return createElement('div', { + return [createElement('div', { attrs: { class: 'quote' } - }, x); + }, x)]; } else { - return createElement('span', { + return [createElement('span', { attrs: { class: 'quote' } - }, text2.replace(/\n/g, ' ')); + }, text2.replace(/\n/g, ' '))]; } } case 'title': { - return createElement('div', { + return [createElement('div', { attrs: { class: 'title' } - }, token.title); + }, token.title)]; } case 'emoji': { const emoji = emojilib.lib[token.emoji]; - return createElement('span', emoji ? emoji.char : token.content); + return [createElement('span', emoji ? emoji.char : token.content)]; } case 'search': { - return createElement(MkGoogle, { + return [createElement(MkGoogle, { props: { q: token.query } - }); + })]; } default: { console.log('unknown ast type:', token.type); - } - } - })); - const _els = []; - els.forEach((el, i) => { - if (el.tag == 'br') { - if (!['div', 'pre'].includes(els[i - 1].tag)) { - _els.push(el); + return []; } - } else { - _els.push(el); } - }); + })); + // el.tag === 'br' のとき i !== 0 が保証されるため、短絡評価により els[i - 1] は配列外参照しない + const _els = els.filter((el, i) => !(el.tag === 'br' && ['div', 'pre'].includes(els[i - 1].tag))); return createElement('span', _els); } }); diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue index 27a49a6536..c9912fb1e2 100644 --- a/src/client/app/common/views/components/note-menu.vue +++ b/src/client/app/common/views/components/note-menu.vue @@ -6,17 +6,27 @@ <script lang="ts"> import Vue from 'vue'; +import { url } from '../../../config'; +import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; export default Vue.extend({ props: ['note', 'source', 'compact'], computed: { items() { - const items = []; - items.push({ + const items = [{ + icon: '%fa:info-circle%', + text: '%i18n:@detail%', + action: this.detail + }, { + icon: '%fa:link%', + text: '%i18n:@copy-link%', + action: this.copyLink + }, null, { icon: '%fa:star%', text: '%i18n:@favorite%', action: this.favorite - }); + }]; + if (this.note.userId == this.$store.state.i.id) { items.push({ icon: '%fa:thumbtack%', @@ -42,11 +52,19 @@ export default Vue.extend({ } }, methods: { + detail() { + this.$router.push(`/notes/${ this.note.id }`); + }, + + copyLink() { + copyToClipboard(`${url}/notes/${ this.note.id }`); + }, + pin() { (this as any).api('i/pin', { noteId: this.note.id }).then(() => { - this.$destroy(); + this.destroyDom(); }); }, @@ -55,7 +73,7 @@ export default Vue.extend({ (this as any).api('notes/delete', { noteId: this.note.id }).then(() => { - this.$destroy(); + this.destroyDom(); }); }, @@ -63,13 +81,13 @@ export default Vue.extend({ (this as any).api('notes/favorites/create', { noteId: this.note.id }).then(() => { - this.$destroy(); + this.destroyDom(); }); }, closed() { this.$nextTick(() => { - this.$destroy(); + this.destroyDom(); }); } } diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue index 115c934c8b..30d9799fec 100644 --- a/src/client/app/common/views/components/poll-editor.vue +++ b/src/client/app/common/views/components/poll-editor.vue @@ -20,6 +20,7 @@ <script lang="ts"> import Vue from 'vue'; +import { erase } from '../../../../../prelude/array'; export default Vue.extend({ data() { return { @@ -53,7 +54,7 @@ export default Vue.extend({ get() { return { - choices: this.choices.filter(choice => choice != '') + choices: erase('', this.choices) } }, diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue index 660247edbc..4fe51d219b 100644 --- a/src/client/app/common/views/components/poll.vue +++ b/src/client/app/common/views/components/poll.vue @@ -21,6 +21,7 @@ <script lang="ts"> import Vue from 'vue'; +import { sum } from '../../../../../prelude/array'; export default Vue.extend({ props: ['note'], data() { @@ -33,7 +34,7 @@ export default Vue.extend({ return this.note.poll; }, total(): number { - return this.poll.choices.reduce((a, b) => a + b.votes, 0); + return sum(this.poll.choices.map(x => x.votes)); }, isVoted(): boolean { return this.poll.choices.some(c => c.isVoted); diff --git a/src/client/app/common/views/components/reaction-icon.vue b/src/client/app/common/views/components/reaction-icon.vue index 46886b8ab2..c668efac6b 100644 --- a/src/client/app/common/views/components/reaction-icon.vue +++ b/src/client/app/common/views/components/reaction-icon.vue @@ -1,17 +1,17 @@ <template> <span class="mk-reaction-icon"> - <img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%"> - <img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%"> - <img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%"> - <img v-if="reaction == 'hmm'" src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%"> - <img v-if="reaction == 'surprise'" src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%"> - <img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%"> - <img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%"> - <img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%"> - <img v-if="reaction == 'rip'" src="/assets/reactions/rip.png" alt="%i18n:common.reactions.rip%"> + <img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" alt="%i18n:common.reactions.like%"> + <img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" alt="%i18n:common.reactions.love%"> + <img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" alt="%i18n:common.reactions.laugh%"> + <img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" alt="%i18n:common.reactions.hmm%"> + <img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" alt="%i18n:common.reactions.surprise%"> + <img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" alt="%i18n:common.reactions.congrats%"> + <img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" alt="%i18n:common.reactions.angry%"> + <img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" alt="%i18n:common.reactions.confused%"> + <img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" alt="%i18n:common.reactions.rip%"> <template v-if="reaction == 'pudding'"> - <img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="/assets/reactions/sushi.png" alt="%i18n:common.reactions.pudding%"> - <img v-else src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%"> + <img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" alt="%i18n:common.reactions.pudding%"> + <img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" alt="%i18n:common.reactions.pudding%"> </template> </span> </template> diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue index a455afbf7d..58985658c6 100644 --- a/src/client/app/common/views/components/reaction-picker.vue +++ b/src/client/app/common/views/components/reaction-picker.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-reaction-picker"> +<div class="mk-reaction-picker" v-hotkey.global="keymap"> <div class="backdrop" ref="backdrop" @click="close"></div> <div class="popover" :class="{ compact, big }" ref="popover"> <p v-if="!compact">{{ title }}</p> @@ -31,28 +31,51 @@ export default Vue.extend({ type: Object, required: true }, + source: { required: true }, + compact: { type: Boolean, required: false, default: false }, + cb: { required: false }, + big: { type: Boolean, required: false, default: false } }, + data() { return { title: placeholder }; }, + + computed: { + keymap(): any { + return { + '1': () => this.react('like'), + '2': () => this.react('love'), + '3': () => this.react('laugh'), + '4': () => this.react('hmm'), + '5': () => this.react('surprise'), + '6': () => this.react('congrats'), + '7': () => this.react('angry'), + '8': () => this.react('confused'), + '9': () => this.react('rip'), + '0': () => this.react('pudding'), + }; + } + }, + mounted() { this.$nextTick(() => { const popover = this.$refs.popover as any; @@ -88,6 +111,7 @@ export default Vue.extend({ }); }); }, + methods: { react(reaction) { (this as any).api('notes/reactions/create', { @@ -95,15 +119,19 @@ export default Vue.extend({ reaction: reaction }).then(() => { if (this.cb) this.cb(); - this.$destroy(); + this.$emit('closed'); + this.destroyDom(); }); }, + onMouseover(e) { this.title = e.target.title; }, + onMouseout(e) { this.title = placeholder; }, + close() { (this.$refs.backdrop as any).style.pointerEvents = 'none'; anime({ @@ -120,7 +148,10 @@ export default Vue.extend({ scale: 0.5, duration: 200, easing: 'easeInBack', - complete: () => this.$destroy() + complete: () => { + this.$emit('closed'); + this.destroyDom(); + } }); } } diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue index 5230ac371a..b1c6782e93 100644 --- a/src/client/app/common/views/components/signin.vue +++ b/src/client/app/common/views/components/signin.vue @@ -78,7 +78,7 @@ export default Vue.extend({ cursor wait !important > .avatar - margin 16px auto 0 auto + margin 0 auto 0 auto width 64px height 64px background #ddd diff --git a/src/client/app/common/views/components/tag-cloud.vue b/src/client/app/common/views/components/tag-cloud.vue new file mode 100644 index 0000000000..5f2cc5276a --- /dev/null +++ b/src/client/app/common/views/components/tag-cloud.vue @@ -0,0 +1,90 @@ +<template> +<div class="jtivnzhfwquxpsfidertopbmwmchmnmo"> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <p class="empty" v-else-if="tags.length == 0">%fa:exclamation-circle%%i18n:@empty%</p> + <div v-else> + <vue-word-cloud + :words="tags.slice(0, 20).map(x => [x.name, x.count])" + :color="color" + :spacing="1"> + <template slot-scope="{word, text, weight}"> + <div style="cursor: pointer;" :title="weight"> + {{ text }} + </div> + </template> + </vue-word-cloud> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as VueWordCloud from 'vuewordcloud'; + +export default Vue.extend({ + components: { + [VueWordCloud.name]: VueWordCloud + }, + data() { + return { + tags: [], + fetching: true, + clock: null + }; + }, + mounted() { + this.fetch(); + this.clock = setInterval(this.fetch, 1000 * 60); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + fetch() { + (this as any).api('aggregation/hashtags').then(tags => { + this.tags = tags; + this.fetching = false; + }); + }, + color([, weight]) { + const peak = Math.max.apply(null, this.tags.map(x => x.count)); + const w = weight / peak; + + if (w > 0.9) { + return this.$store.state.device.darkmode ? '#ff4e69' : '#ff4e69'; + } else if (w > 0.5) { + return this.$store.state.device.darkmode ? '#3bc4c7' : '#3bc4c7'; + } else { + return this.$store.state.device.darkmode ? '#fff' : '#555'; + } + } + } +}); +</script> + +<style lang="stylus" scoped> +root(isDark) + height 100% + width 100% + + > .fetching + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + + > div + height 100% + width 100% + +.jtivnzhfwquxpsfidertopbmwmchmnmo[data-darkmode] + root(true) + +.jtivnzhfwquxpsfidertopbmwmchmnmo:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/common/views/widgets/hashtags.chart.vue b/src/client/app/common/views/components/trends.chart.vue index 723a3947f8..723a3947f8 100644 --- a/src/client/app/common/views/widgets/hashtags.chart.vue +++ b/src/client/app/common/views/components/trends.chart.vue diff --git a/src/client/app/common/views/components/trends.vue b/src/client/app/common/views/components/trends.vue new file mode 100644 index 0000000000..0042dbe853 --- /dev/null +++ b/src/client/app/common/views/components/trends.vue @@ -0,0 +1,103 @@ +<template> +<div class="csqvmxybqbycalfhkxvyfrgbrdalkaoc"> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p> + <!-- トランジションを有効にするとなぜかメモリリークする --> + <transition-group v-else tag="div" name="chart"> + <div v-for="stat in stats" :key="stat.tag"> + <div class="tag"> + <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> + <p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p> + </div> + <x-chart class="chart" :src="stat.chart"/> + </div> + </transition-group> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XChart from './trends.chart.vue'; + +export default Vue.extend({ + components: { + XChart + }, + data() { + return { + stats: [], + fetching: true, + clock: null + }; + }, + mounted() { + this.fetch(); + this.clock = setInterval(this.fetch, 1000 * 60); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + fetch() { + (this as any).api('hashtags/trend').then(stats => { + this.stats = stats; + this.fetching = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +root(isDark) + > .fetching + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + + > div + .chart-move + transition transform 1s ease + + > div + display flex + align-items center + padding 14px 16px + + &:not(:last-child) + border-bottom solid 1px isDark ? #393f4f : #eee + + > .tag + flex 1 + overflow hidden + font-size 14px + color isDark ? #9baec8 : #65727b + + > a + display block + width 100% + white-space nowrap + overflow hidden + text-overflow ellipsis + color inherit + + > p + margin 0 + font-size 75% + opacity 0.7 + + > .chart + height 30px + +.csqvmxybqbycalfhkxvyfrgbrdalkaoc[data-darkmode] + root(true) + +.csqvmxybqbycalfhkxvyfrgbrdalkaoc:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/common/views/components/ui/card.vue b/src/client/app/common/views/components/ui/card.vue index 05c51bca6b..aa16b557e1 100644 --- a/src/client/app/common/views/components/ui/card.vue +++ b/src/client/app/common/views/components/ui/card.vue @@ -24,19 +24,34 @@ export default Vue.extend({ root(isDark) margin 16px - padding 16px color isDark ? #fff : #000 background isDark ? #282C37 : #fff box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) - @media (min-width 500px) - padding 32px - > header - font-weight normal - font-size 24px + padding 16px + font-weight bold + font-size 20px color isDark ? #fff : #444 + @media (min-width 500px) + padding 24px 32px + + > section + padding 20px 16px + border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1) + + @media (min-width 500px) + padding 32px + + &.fit-top + padding-top 0 + + > header + margin-bottom 16px + font-weight bold + color isDark ? #fff : #444 + .ui-card[data-darkmode] root(true) diff --git a/src/client/app/common/views/components/ui/radio.vue b/src/client/app/common/views/components/ui/radio.vue index 04a46c5a96..dcdda1cf0e 100644 --- a/src/client/app/common/views/components/ui/radio.vue +++ b/src/client/app/common/views/components/ui/radio.vue @@ -55,7 +55,7 @@ export default Vue.extend({ root(isDark) display inline-block - margin 32px 32px 32px 0 + margin 0 32px 0 0 cursor pointer transition all 0.3s diff --git a/src/client/app/common/views/components/ui/switch.vue b/src/client/app/common/views/components/ui/switch.vue index a9e00d73d2..e88b867801 100644 --- a/src/client/app/common/views/components/ui/switch.vue +++ b/src/client/app/common/views/components/ui/switch.vue @@ -64,6 +64,12 @@ root(isDark) cursor pointer transition all 0.3s + &:first-child + margin-top 0 + + &:last-child + margin-bottom 0 + > * user-select none @@ -89,6 +95,7 @@ root(isDark) > .button display inline-block + flex-shrink 0 margin 3px 0 0 0 width 34px height 14px diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue index 242d9ba5c6..f9b8415b5b 100644 --- a/src/client/app/common/views/components/url-preview.vue +++ b/src/client/app/common/views/components/url-preview.vue @@ -8,13 +8,13 @@ </blockquote> </div> <div v-else class="mk-url-preview"> - <a :href="url" target="_blank" :title="url" v-if="!fetching"> + <a :class="{ mini }" :href="url" target="_blank" :title="url" v-if="!fetching"> <div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div> <article> <header> <h1>{{ title }}</h1> </header> - <p>{{ description }}</p> + <p>{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p> <footer> <img class="icon" v-if="icon" :src="icon"/> <p>{{ sitename }}</p> @@ -118,6 +118,12 @@ export default Vue.extend({ type: Boolean, required: false, default: false + }, + + mini: { + type: Boolean, + required: false, + default: false } }, @@ -164,7 +170,7 @@ export default Vue.extend({ return; } - fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { + fetch(`/url?url=${encodeURIComponent(this.url)}`).then(res => { res.json().then(info => { if (info.url == null) return; this.title = info.title; @@ -293,6 +299,29 @@ root(isDark) width 12px height 12px + &.mini + font-size 10px + + > .thumbnail + position relative + width 100% + height 60px + + > article + left 0 + width 100% + padding 8px + + > header + margin-bottom 4px + + > footer + margin-top 4px + + > img + width 12px + height 12px + .mk-url-preview[data-darkmode] root(true) diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue index e6ffe4466d..04a1f30135 100644 --- a/src/client/app/common/views/components/url.vue +++ b/src/client/app/common/views/components/url.vue @@ -12,6 +12,7 @@ <script lang="ts"> import Vue from 'vue'; +import { toUnicode as decodePunycode } from 'punycode'; export default Vue.extend({ props: ['url', 'target'], data() { @@ -27,11 +28,11 @@ export default Vue.extend({ created() { const url = new URL(this.url); this.schema = url.protocol; - this.hostname = url.hostname; + this.hostname = decodePunycode(url.hostname); this.port = url.port; - this.pathname = url.pathname; - this.query = url.search; - this.hash = url.hash; + this.pathname = decodeURIComponent(url.pathname); + this.query = decodeURIComponent(url.search); + this.hash = decodeURIComponent(url.hash); } }); </script> diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue index 4691604e57..1830b1832e 100644 --- a/src/client/app/common/views/components/visibility-chooser.vue +++ b/src/client/app/common/views/components/visibility-chooser.vue @@ -47,7 +47,7 @@ export default Vue.extend({ props: ['source', 'compact'], data() { return { - v: this.$store.state.device.visibility || 'public' + v: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility } }, mounted() { @@ -97,9 +97,11 @@ export default Vue.extend({ }, methods: { choose(visibility) { - this.$store.commit('device/setVisibility', visibility); + if (this.$store.state.settings.rememberNoteVisibility) { + this.$store.commit('device/setVisibility', visibility); + } this.$emit('chosen', visibility); - this.$destroy(); + this.destroyDom(); }, close() { (this.$refs.backdrop as any).style.pointerEvents = 'none'; @@ -117,7 +119,7 @@ export default Vue.extend({ scale: 0.5, duration: 200, easing: 'easeInBack', - complete: () => this.$destroy() + complete: () => this.destroyDom() }); } } diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue index 5a8b9df476..965ec78559 100644 --- a/src/client/app/common/views/components/welcome-timeline.vue +++ b/src/client/app/common/views/components/welcome-timeline.vue @@ -1,22 +1,24 @@ <template> <div class="mk-welcome-timeline"> - <div v-for="note in notes"> - <mk-avatar class="avatar" :user="note.user" target="_blank"/> - <div class="body"> - <header> - <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link> - <span class="username">@{{ note.user | acct }}</span> - <div class="info"> - <router-link class="created-at" :to="note | notePage"> - <mk-time :time="note.createdAt"/> - </router-link> + <transition-group name="ldzpakcixzickvggyixyrhqwjaefknon" tag="div"> + <div v-for="note in notes" :key="note.id"> + <mk-avatar class="avatar" :user="note.user" target="_blank"/> + <div class="body"> + <header> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <div class="info"> + <router-link class="created-at" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </div> + </header> + <div class="text"> + <misskey-flavored-markdown v-if="note.text" :text="note.text"/> </div> - </header> - <div class="text"> - <misskey-flavored-markdown v-if="note.text" :text="note.text"/> </div> </div> - </div> + </transition-group> </div> </template> @@ -31,15 +33,30 @@ export default Vue.extend({ default: undefined } }, + data() { return { fetching: true, - notes: [] + notes: [], + connection: null, + connectionId: null }; }, + mounted() { this.fetch(); + + this.connection = (this as any).os.streams.localTimelineStream.getConnection(); + this.connectionId = (this as any).os.streams.localTimelineStream.use(); + + this.connection.on('note', this.onNote); + }, + + beforeDestroy() { + this.connection.off('note', this.onNote); + (this as any).os.streams.localTimelineStream.dispose(this.connectionId); }, + methods: { fetch(cb?) { this.fetching = true; @@ -48,77 +65,93 @@ export default Vue.extend({ local: true, reply: false, renote: false, - media: false, - poll: false, - bot: false + file: false, + poll: false }).then(notes => { this.notes = notes; this.fetching = false; }); - } + }, + + onNote(note) { + if (note.replyId != null) return; + if (note.renoteId != null) return; + if (note.poll != null) return; + + this.notes.unshift(note); + }, } }); </script> <style lang="stylus" scoped> +.ldzpakcixzickvggyixyrhqwjaefknon-enter +.ldzpakcixzickvggyixyrhqwjaefknon-leave-to + opacity 0 + transform translateY(-30px) + root(isDark) background isDark ? #282C37 : #fff > div - padding 16px - overflow-wrap break-word - font-size .9em - color isDark ? #fff : #4C4C4C - border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05) + > * + transition transform .3s ease, opacity .3s ease + + > div + padding 16px + overflow-wrap break-word + font-size .9em + color isDark ? #fff : #4C4C4C + border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05) - &:after - content "" - display block - clear both + &:after + content "" + display block + clear both - > .avatar - display block - float left - position -webkit-sticky - position sticky - top 16px - width 42px - height 42px - border-radius 6px + > .avatar + display block + float left + position -webkit-sticky + position sticky + top 16px + width 42px + height 42px + border-radius 6px - > .body - float right - width calc(100% - 42px) - padding-left 12px + > .body + float right + width calc(100% - 42px) + padding-left 12px - > header - display flex - align-items center - margin-bottom 4px - white-space nowrap + > header + display flex + align-items center + margin-bottom 4px + white-space nowrap - > .name - display block - margin 0 .5em 0 0 - padding 0 - overflow hidden - font-weight bold - text-overflow ellipsis - color isDark ? #fff : #627079 + > .name + display block + margin 0 .5em 0 0 + padding 0 + overflow hidden + font-weight bold + text-overflow ellipsis + color isDark ? #fff : #627079 - > .username - margin 0 .5em 0 0 - color isDark ? #606984 : #ccc + > .username + margin 0 .5em 0 0 + color isDark ? #606984 : #ccc - > .info - margin-left auto - font-size 0.9em + > .info + margin-left auto + font-size 0.9em - > .created-at - color isDark ? #606984 : #c0c0c0 + > .created-at + color isDark ? #606984 : #c0c0c0 - > .text - text-align left + > .text + text-align left .mk-welcome-timeline[data-darkmode] root(true) diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts index b252cf5c1f..f7f8e9bf16 100644 --- a/src/client/app/common/views/directives/autocomplete.ts +++ b/src/client/app/common/views/directives/autocomplete.ts @@ -167,7 +167,7 @@ class Autocomplete { private close() { if (this.suggestion == null) return; - this.suggestion.$destroy(); + this.suggestion.destroyDom(); this.suggestion = null; this.textarea.focus(); @@ -191,7 +191,7 @@ class Autocomplete { const acct = renderAcct(value); // 挿入 - this.text = trimmedBefore + '@' + acct + ' ' + after; + this.text = `${trimmedBefore}@${acct} ${after}`; // キャレットを戻す this.vm.$nextTick(() => { @@ -207,7 +207,7 @@ class Autocomplete { const after = source.substr(caret); // 挿入 - this.text = trimmedBefore + '#' + value + ' ' + after; + this.text = `${trimmedBefore}#${value} ${after}`; // キャレットを戻す this.vm.$nextTick(() => { diff --git a/src/client/app/common/views/filters/note.ts b/src/client/app/common/views/filters/note.ts index a611dc8685..3c9c8b7485 100644 --- a/src/client/app/common/views/filters/note.ts +++ b/src/client/app/common/views/filters/note.ts @@ -1,5 +1,5 @@ import Vue from 'vue'; Vue.filter('notePage', note => { - return '/notes/' + note.id; + return `/notes/${note.id}`; }); diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts index ca0910fc53..e5220229b7 100644 --- a/src/client/app/common/views/filters/user.ts +++ b/src/client/app/common/views/filters/user.ts @@ -11,5 +11,5 @@ Vue.filter('userName', user => { }); Vue.filter('userPage', (user, path?) => { - return '/@' + Vue.filter('acct')(user) + (path ? '/' + path : ''); + return `/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`; }); diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue index 13d855d20a..80a870a257 100644 --- a/src/client/app/common/views/pages/follow.vue +++ b/src/client/app/common/views/pages/follow.vue @@ -1,6 +1,6 @@ <template> <div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching" :data-darkmode="$store.state.device.darkmode"> - <div class="signed-in-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + myName + '</b>')"></div> + <div class="signed-in-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${myName}`)"></div> <main> <div class="banner" :style="bannerStyle"></div> @@ -32,7 +32,6 @@ <script lang="ts"> import Vue from 'vue'; import parseAcct from '../../../../../misc/acct/parse'; -import getUserName from '../../../../../misc/get-user-name'; import Progress from '../../../common/scripts/loading'; export default Vue.extend({ @@ -83,7 +82,7 @@ export default Vue.extend({ userId: this.user.id }); } else { - if (this.user.isLocked && this.user.hasPendingFollowRequestFromYou) { + if (this.user.hasPendingFollowRequestFromYou) { this.user = await (this as any).api('following/requests/cancel', { userId: this.user.id }); diff --git a/src/client/app/common/views/widgets/analog-clock.vue b/src/client/app/common/views/widgets/analog-clock.vue index 0de30228b3..04223f0d21 100644 --- a/src/client/app/common/views/widgets/analog-clock.vue +++ b/src/client/app/common/views/widgets/analog-clock.vue @@ -1,8 +1,8 @@ <template> <div class="mkw-analog-clock"> - <mk-widget-container :naked="!(props.design % 2)" :show-header="false"> + <mk-widget-container :naked="props.style % 2 === 0" :show-header="false"> <div class="mkw-analog-clock--body"> - <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="!(props.design && ~props.design)"/> + <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/> </div> </mk-widget-container> </div> @@ -13,13 +13,12 @@ import define from '../../../common/define-widget'; export default define({ name: 'analog-clock', props: () => ({ - design: -1 + style: 0 }) }).extend({ methods: { func() { - if (++this.props.design > 2) - this.props.design = -1; + this.props.style = (this.props.style + 1) % 4; this.save(); } } diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue index 69b2a54fe9..f2fa720f52 100644 --- a/src/client/app/common/views/widgets/broadcast.vue +++ b/src/client/app/common/views/widgets/broadcast.vue @@ -1,6 +1,6 @@ <template> -<div class="mkw-broadcast" - :data-found="broadcasts.length != 0" +<div class="anltbovirfeutcigvwgmgxipejaeozxi" + :data-found="announcements && announcements.length != 0" :data-melt="props.design == 1" :data-mobile="platform == 'mobile'" > @@ -14,18 +14,17 @@ </svg> </div> <p class="fetching" v-if="fetching">%i18n:@fetching%<mk-ellipsis/></p> - <h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:@no-broadcasts%' : broadcasts[i].title }}</h1> + <h1 v-if="!fetching">{{ announcements.length == 0 ? '%i18n:@no-broadcasts%' : announcements[i].title }}</h1> <p v-if="!fetching"> - <span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span> - <template v-if="broadcasts.length == 0">%i18n:@have-a-nice-day%</template> + <span v-if="announcements.length != 0" v-html="announcements[i].text"></span> + <template v-if="announcements.length == 0">%i18n:@have-a-nice-day%</template> </p> - <a v-if="broadcasts.length > 1" @click="next">%i18n:@next% >></a> + <a v-if="announcements.length > 1" @click="next">%i18n:@next% >></a> </div> </template> <script lang="ts"> import define from '../../../common/define-widget'; -import { lang } from '../../../config'; export default define({ name: 'broadcast', @@ -37,26 +36,18 @@ export default define({ return { i: 0, fetching: true, - broadcasts: [] + announcements: [] }; }, mounted() { (this as any).os.getMeta().then(meta => { - let broadcasts = []; - if (meta.broadcasts) { - meta.broadcasts.forEach(broadcast => { - if (broadcast[lang]) { - broadcasts.push(broadcast[lang]); - } - }); - } - this.broadcasts = broadcasts; + this.announcements = meta.broadcasts; this.fetching = false; }); }, methods: { next() { - if (this.i == this.broadcasts.length - 1) { + if (this.i == this.announcements.length - 1) { this.i = 0; } else { this.i++; @@ -75,7 +66,7 @@ export default define({ </script> <style lang="stylus" scoped> -.mkw-broadcast +root(isDark) padding 10px border solid 1px #4078c0 border-radius 6px @@ -135,22 +126,18 @@ export default define({ margin 0 font-size 0.95em font-weight normal - color #4078c0 + color isDark ? #539eff : #4078c0 > p display block z-index 1 margin 0 font-size 0.7em - color #555 + color isDark ? #fff : #555 &.fetching text-align center - a - color #555 - text-decoration underline - > a display block font-size 0.7em @@ -159,4 +146,10 @@ export default define({ > p color #fff +.anltbovirfeutcigvwgmgxipejaeozxi[data-darkmode] + root(true) + +.anltbovirfeutcigvwgmgxipejaeozxi:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue index 56520400b6..0cb6b2df10 100644 --- a/src/client/app/common/views/widgets/hashtags.vue +++ b/src/client/app/common/views/widgets/hashtags.vue @@ -4,20 +4,7 @@ <template slot="header">%fa:hashtag%%i18n:@title%</template> <div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'"> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> - <p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p> - <!-- トランジションを有効にするとなぜかメモリリークする --> - <!-- <transition-group v-else tag="div" name="chart"> --> - <div> - <div v-for="stat in stats" :key="stat.tag"> - <div class="tag"> - <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> - <p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p> - </div> - <x-chart class="chart" :src="stat.chart"/> - </div> - </div> - <!-- </transition-group> --> + <mk-trends/> </div> </mk-widget-container> </div> @@ -25,7 +12,6 @@ <script lang="ts"> import define from '../../../common/define-widget'; -import XChart from './hashtags.chart.vue'; export default define({ name: 'hashtags', @@ -33,89 +19,11 @@ export default define({ compact: false }) }).extend({ - components: { - XChart - }, - data() { - return { - stats: [], - fetching: true, - clock: null - }; - }, - mounted() { - this.fetch(); - this.clock = setInterval(this.fetch, 1000 * 60); - }, - beforeDestroy() { - clearInterval(this.clock); - }, methods: { func() { this.props.compact = !this.props.compact; this.save(); - }, - fetch() { - (this as any).api('hashtags/trend').then(stats => { - this.stats = stats; - this.fetching = false; - }); } } }); </script> - -<style lang="stylus" scoped> -root(isDark) - .mkw-hashtags--body - > .fetching - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - - > div - .chart-move - transition transform 1s ease - - > div - display flex - align-items center - padding 14px 16px - - &:not(:last-child) - border-bottom solid 1px isDark ? #393f4f : #eee - - > .tag - flex 1 - overflow hidden - font-size 14px - color isDark ? #9baec8 : #65727b - - > a - display block - width 100% - white-space nowrap - overflow hidden - text-overflow ellipsis - color inherit - - > p - margin 0 - font-size 75% - opacity 0.7 - - > .chart - height 30px - -.mkw-hashtags[data-darkmode] - root(true) - -.mkw-hashtags:not([data-darkmode]) - root(false) - -</style> |