summaryrefslogtreecommitdiff
path: root/src/client/app/common/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/app/common/views')
-rw-r--r--src/client/app/common/views/components/acct.vue12
-rw-r--r--src/client/app/common/views/components/autocomplete.vue4
-rw-r--r--src/client/app/common/views/components/avatar.vue21
-rw-r--r--src/client/app/common/views/components/connect-failed.troubleshooter.vue2
-rw-r--r--src/client/app/common/views/components/cw-button.vue44
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.game.vue24
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.index.vue1
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.room.vue7
-rw-r--r--src/client/app/common/views/components/index.ts6
-rw-r--r--src/client/app/common/views/components/media-banner.vue90
-rw-r--r--src/client/app/common/views/components/media-list.vue121
-rw-r--r--src/client/app/common/views/components/menu.vue24
-rw-r--r--src/client/app/common/views/components/messaging-room.vue24
-rw-r--r--src/client/app/common/views/components/misskey-flavored-markdown.ts70
-rw-r--r--src/client/app/common/views/components/note-menu.vue32
-rw-r--r--src/client/app/common/views/components/poll-editor.vue3
-rw-r--r--src/client/app/common/views/components/poll.vue3
-rw-r--r--src/client/app/common/views/components/reaction-icon.vue22
-rw-r--r--src/client/app/common/views/components/reaction-picker.vue37
-rw-r--r--src/client/app/common/views/components/signin.vue2
-rw-r--r--src/client/app/common/views/components/tag-cloud.vue90
-rw-r--r--src/client/app/common/views/components/trends.chart.vue (renamed from src/client/app/common/views/widgets/hashtags.chart.vue)0
-rw-r--r--src/client/app/common/views/components/trends.vue103
-rw-r--r--src/client/app/common/views/components/ui/card.vue27
-rw-r--r--src/client/app/common/views/components/ui/radio.vue2
-rw-r--r--src/client/app/common/views/components/ui/switch.vue7
-rw-r--r--src/client/app/common/views/components/url-preview.vue35
-rw-r--r--src/client/app/common/views/components/url.vue9
-rw-r--r--src/client/app/common/views/components/visibility-chooser.vue10
-rw-r--r--src/client/app/common/views/components/welcome-timeline.vue161
-rw-r--r--src/client/app/common/views/directives/autocomplete.ts6
-rw-r--r--src/client/app/common/views/filters/note.ts2
-rw-r--r--src/client/app/common/views/filters/user.ts2
-rw-r--r--src/client/app/common/views/pages/follow.vue5
-rw-r--r--src/client/app/common/views/widgets/analog-clock.vue9
-rw-r--r--src/client/app/common/views/widgets/broadcast.vue43
-rw-r--r--src/client/app/common/views/widgets/hashtags.vue94
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% &gt;&gt;</a>
+ <a v-if="announcements.length > 1" @click="next">%i18n:@next% &gt;&gt;</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>