From 9ce0f96de3ba32e25893f6d248f35badaa522479 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Tue, 5 Jun 2018 21:36:21 +0900 Subject: [PATCH 1/4] wip --- locales/ja.yml | 1 + src/client/app/desktop/script.ts | 2 + .../views/components/ui.header.nav.vue | 6 + .../app/desktop/views/components/ui.vue | 9 + .../desktop/views/pages/deck/deck.column.vue | 59 ++ .../views/pages/deck/deck.note.sub.vue | 153 +++++ .../desktop/views/pages/deck/deck.note.vue | 539 ++++++++++++++++++ .../desktop/views/pages/deck/deck.notes.vue | 248 ++++++++ .../app/desktop/views/pages/deck/deck.tl.vue | 143 +++++ .../app/desktop/views/pages/deck/deck.vue | 42 ++ 10 files changed, 1202 insertions(+) create mode 100644 src/client/app/desktop/views/pages/deck/deck.column.vue create mode 100644 src/client/app/desktop/views/pages/deck/deck.note.sub.vue create mode 100644 src/client/app/desktop/views/pages/deck/deck.note.vue create mode 100644 src/client/app/desktop/views/pages/deck/deck.notes.vue create mode 100644 src/client/app/desktop/views/pages/deck/deck.tl.vue create mode 100644 src/client/app/desktop/views/pages/deck/deck.vue diff --git a/locales/ja.yml b/locales/ja.yml index a62b341f69..026c2308c3 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -606,6 +606,7 @@ desktop/views/components/ui.header.account.vue: desktop/views/components/ui.header.nav.vue: home: "ホーム" + deck: "デッキ" messaging: "メッセージ" game: "ゲーム" diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index 8fb6096afa..61f1f5b870 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -23,6 +23,7 @@ import updateAvatar from './api/update-avatar'; import updateBanner from './api/update-banner'; import MkIndex from './views/pages/index.vue'; +import MkDeck from './views/pages/deck/deck.vue'; import MkUser from './views/pages/user/user.vue'; import MkFavorites from './views/pages/favorites.vue'; import MkSelectDrive from './views/pages/selectdrive.vue'; @@ -50,6 +51,7 @@ init(async (launch) => { mode: 'history', routes: [ { path: '/', name: 'index', component: MkIndex }, + { path: '/deck', name: 'deck', component: MkDeck }, { path: '/i/customize-home', component: MkHomeCustomize }, { path: '/i/favorites', component: MkFavorites }, { path: '/i/messaging/:user', component: MkMessagingRoom }, diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue index 4780c57cb4..8e792b3df5 100644 --- a/src/client/app/desktop/views/components/ui.header.nav.vue +++ b/src/client/app/desktop/views/components/ui.header.nav.vue @@ -8,6 +8,12 @@ <p>%i18n:@home%</p> </router-link> </li> + <li class="deck" :class="{ active: $route.name == 'deck' }"> + <router-link to="/deck"> + %fa:columns% + <p>%i18n:@deck%</p> + </router-link> + </li> <li class="messaging"> <a @click="messaging"> %fa:comments% diff --git a/src/client/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue index 32cc71e4b0..ad6fc69dfa 100644 --- a/src/client/app/desktop/views/components/ui.vue +++ b/src/client/app/desktop/views/components/ui.vue @@ -37,7 +37,16 @@ export default Vue.extend({ <style lang="stylus" scoped> .mk-ui + display flex + flex-direction column + flex 1 + > .header @media (max-width 1000px) display none + + > .content + display flex + flex-direction column + flex 1 </style> diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue new file mode 100644 index 0000000000..4e06798293 --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.column.vue @@ -0,0 +1,59 @@ +<template> +<div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs"> + <header> + <slot name="header">Timeline</slot> + </header> + <div ref="body"> + <x-tl ref="tl"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XTl from './deck.tl.vue'; + +export default Vue.extend({ + components: { + XTl + }, + mounted() { + this.$nextTick(() => { + this.$refs.tl.mount(this.$refs.body); + }); + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark) + flex 1 + max-width 330px + height 100% + margin-right 16px + background isDark ? #282C37 : #fff + border-radius 6px + box-shadow 0 2px 16px rgba(#000, 0.1) + overflow hidden + + > header + z-index 1 + line-height 48px + padding 0 16px + color isDark ? #e3e5e8 : #888 + background isDark ? #313543 : #fff + box-shadow 0 1px rgba(#000, 0.15) + + > div + height calc(100% - 48px) + overflow auto + +.dnpfarvgbnfmyzbdquhhzyxcmstpdqzs[data-darkmode] + root(true) + +.dnpfarvgbnfmyzbdquhhzyxcmstpdqzs:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/pages/deck/deck.note.sub.vue b/src/client/app/desktop/views/pages/deck/deck.note.sub.vue new file mode 100644 index 0000000000..b458b74186 --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.note.sub.vue @@ -0,0 +1,153 @@ +<template> +<div class="fnlfosztlhtptnongximhlbykxblytcq"> + <mk-avatar class="avatar" :user="note.user"/> + <div class="main"> + <header> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span> + <span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span> + <span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span> + <span class="username"><mk-acct :user="note.user"/></span> + <div class="info"> + <span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> + <router-link class="created-at" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + <span class="visibility" v-if="note.visibility != 'public'"> + <template v-if="note.visibility == 'home'">%fa:home%</template> + <template v-if="note.visibility == 'followers'">%fa:unlock%</template> + <template v-if="note.visibility == 'specified'">%fa:envelope%</template> + <template v-if="note.visibility == 'private'">%fa:lock%</template> + </span> + </div> + </header> + <div class="body"> + <mk-sub-note-content class="text" :note="note"/> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + note: { + type: Object, + required: true + }, + // TODO + truncate: { + type: Boolean, + default: true + } + } +}); +</script> + +<style lang="stylus" scoped> +root(isDark) + display flex + padding 16px + font-size 10px + background isDark ? #21242d : #fcfcfc + + &.smart + > .main + width 100% + + > header + align-items center + + > .avatar + flex-shrink 0 + display block + margin 0 8px 0 0 + width 38px + height 38px + border-radius 8px + + > .main + flex 1 + min-width 0 + + > header + display flex + align-items baseline + margin-bottom 2px + white-space nowrap + + > .avatar + flex-shrink 0 + margin-right 8px + width 18px + height 18px + border-radius 100% + + > .name + display block + margin 0 0.5em 0 0 + padding 0 + overflow hidden + color isDark ? #fff : #607073 + font-size 1em + font-weight 700 + text-align left + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .is-admin + > .is-bot + > .is-cat + align-self center + margin 0 0.5em 0 0 + padding 1px 5px + font-size 0.8em + color isDark ? #758188 : #aaa + border solid 1px isDark ? #57616f : #ddd + border-radius 3px + + &.is-admin + border-color isDark ? #d42c41 : #f56a7b + color isDark ? #d42c41 : #f56a7b + + > .username + text-align left + margin 0 + color isDark ? #606984 : #d1d8da + + > .info + margin-left auto + font-size 0.9em + + > * + color isDark ? #606984 : #b2b8bb + + > .mobile + margin-right 6px + + > .visibility + margin-left 6px + + > .body + + > .text + margin 0 + padding 0 + color isDark ? #959ba7 : #717171 + + pre + max-height 120px + font-size 80% + +.fnlfosztlhtptnongximhlbykxblytcq[data-darkmode] + root(true) + +.fnlfosztlhtptnongximhlbykxblytcq:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue new file mode 100644 index 0000000000..8582a37b91 --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.note.vue @@ -0,0 +1,539 @@ +<template> +<div class="zyjjkidcqjnlegkqebitfviomuqmseqk" :class="{ renote: isRenote }"> + <div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> + <x-sub :note="p.reply"/> + </div> + <div class="renote" v-if="isRenote"> + <mk-avatar class="avatar" :user="note.user"/> + %fa:retweet% + <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span> + <mk-time :time="note.createdAt"/> + </div> + <article> + <mk-avatar class="avatar" :user="p.user"/> + <div class="main"> + <header> + <router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> + <span class="is-admin" v-if="p.user.isAdmin">admin</span> + <span class="is-bot" v-if="p.user.isBot">bot</span> + <span class="is-cat" v-if="p.user.isCat">cat</span> + <span class="username"><mk-acct :user="p.user"/></span> + <div class="info"> + <span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span> + <router-link class="created-at" :to="p | notePage"> + <mk-time :time="p.createdAt"/> + </router-link> + <span class="visibility" v-if="p.visibility != 'public'"> + <template v-if="p.visibility == 'home'">%fa:home%</template> + <template v-if="p.visibility == 'followers'">%fa:unlock%</template> + <template v-if="p.visibility == 'specified'">%fa:envelope%</template> + <template v-if="p.visibility == 'private'">%fa:lock%</template> + </span> + </div> + </header> + <div class="body"> + <p v-if="p.cw != null" class="cw"> + <span class="text" v-if="p.cw != ''">{{ p.cw }}</span> + <span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@less%' : '%i18n:@more%' }}</span> + </p> + <div class="content" v-show="p.cw == null || showContent"> + <div class="text"> + <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> + <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span> + <a class="reply" v-if="p.reply">%fa:reply%</a> + <mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="$store.state.i"/> + <a class="rp" v-if="p.renote != null">RP:</a> + </div> + <div class="media" v-if="p.media.length > 0"> + <mk-media-list :media-list="p.media"/> + </div> + <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> + </div> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> + <div class="renote" v-if="p.renote"> + <mk-note-preview :note="p.renote"/> + </div> + </div> + <span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> + </div> + <footer> + <mk-reactions-viewer :note="p" ref="reactionsViewer"/> + <button @click="reply"> + <template v-if="p.reply">%fa:reply-all%</template> + <template v-else>%fa:reply%</template> + </button> + <button @click="renote" title="Renote">%fa:retweet%</button> + <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton">%fa:plus%</button> + <button class="menu" @click="menu" ref="menuButton">%fa:ellipsis-h%</button> + </footer> + </div> + </article> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import parse from '../../../../../../text/parse'; +import canHideText from '../../../../common/scripts/can-hide-text'; + +import MkNoteMenu from '../../../../common/views/components/note-menu.vue'; +import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue'; +import XSub from './deck.note.sub.vue'; + +export default Vue.extend({ + components: { + XSub + }, + + props: ['note'], + + data() { + return { + showContent: false, + connection: null, + connectionId: null + }; + }, + + computed: { + isRenote(): boolean { + return (this.note.renote && + this.note.text == null && + this.note.mediaIds.length == 0 && + this.note.poll == null); + }, + + p(): any { + return this.isRenote ? this.note.renote : this.note; + }, + + reactionsCount(): number { + return this.p.reactionCounts + ? Object.keys(this.p.reactionCounts) + .map(key => this.p.reactionCounts[key]) + .reduce((a, b) => a + b) + : 0; + }, + + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } + } + }, + + created() { + if (this.$store.getters.isSignedIn) { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + } + }, + + mounted() { + this.capture(true); + + if (this.$store.getters.isSignedIn) { + this.connection.on('_connected_', this.onStreamConnected); + } + + // Draw map + if (this.p.geo) { + const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true; + if (shouldShowMap) { + (this as any).os.getGoogleMaps().then(maps => { + const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); + const map = new maps.Map(this.$refs.map, { + center: uluru, + zoom: 15 + }); + new maps.Marker({ + position: uluru, + map: map + }); + }); + } + } + }, + + beforeDestroy() { + this.decapture(true); + + if (this.$store.getters.isSignedIn) { + this.connection.off('_connected_', this.onStreamConnected); + (this as any).os.stream.dispose(this.connectionId); + } + }, + + methods: { + canHideText, + + capture(withHandler = false) { + if (this.$store.getters.isSignedIn) { + this.connection.send({ + type: 'capture', + id: this.p.id + }); + if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); + } + }, + + decapture(withHandler = false) { + if (this.$store.getters.isSignedIn) { + this.connection.send({ + type: 'decapture', + id: this.p.id + }); + if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated); + } + }, + + onStreamConnected() { + this.capture(); + }, + + onStreamNoteUpdated(data) { + const note = data.note; + if (note.id == this.note.id) { + this.$emit('update:note', note); + } else if (note.id == this.note.renoteId) { + this.note.renote = note; + } + }, + + reply() { + (this as any).apis.post({ + reply: this.p + }); + }, + + renote() { + (this as any).apis.post({ + renote: this.p + }); + }, + + react() { + (this as any).os.new(MkReactionPicker, { + source: this.$refs.reactButton, + note: this.p, + compact: true + }); + }, + + menu() { + (this as any).os.new(MkNoteMenu, { + source: this.$refs.menuButton, + note: this.p, + compact: true + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark) + font-size 12px + border-bottom solid 1px isDark ? #1c2023 : #eaeaea + + &:last-of-type + border-bottom none + + &.smart + > article + > .main + > header + align-items center + margin-bottom 4px + + > .renote + display flex + align-items center + padding 8px 16px + line-height 28px + white-space pre + color #9dbb00 + background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%) + + .avatar + flex-shrink 0 + display inline-block + width 20px + height 20px + margin 0 8px 0 0 + border-radius 6px + + [data-fa] + margin-right 4px + + > span + flex-shrink 0 + + &:last-of-type + margin-right 8px + + .name + overflow hidden + flex-shrink 1 + text-overflow ellipsis + white-space nowrap + font-weight bold + + > .mk-time + display block + margin-left auto + flex-shrink 0 + font-size 0.9em + + & + article + padding-top 8px + + > article + display flex + padding 16px 16px 9px + + > .avatar + flex-shrink 0 + display block + margin 0 10px 8px 0 + width 42px + height 42px + border-radius 6px + //position -webkit-sticky + //position sticky + //top 62px + + > .main + flex 1 + min-width 0 + + > header + display flex + align-items baseline + white-space nowrap + + > .avatar + flex-shrink 0 + margin-right 8px + width 20px + height 20px + border-radius 100% + + > .name + display block + margin 0 0.5em 0 0 + padding 0 + overflow hidden + color isDark ? #fff : #627079 + font-weight bold + text-decoration none + text-overflow ellipsis + + > .is-admin + > .is-bot + > .is-cat + align-self center + margin 0 0.5em 0 0 + padding 1px 6px + font-size 0.8em + color isDark ? #758188 : #aaa + border solid 1px isDark ? #57616f : #ddd + border-radius 3px + + &.is-admin + border-color isDark ? #d42c41 : #f56a7b + color isDark ? #d42c41 : #f56a7b + + > .username + margin 0 0.5em 0 0 + overflow hidden + text-overflow ellipsis + color isDark ? #606984 : #ccc + + > .info + margin-left auto + font-size 0.9em + + > * + color isDark ? #606984 : #c0c0c0 + + > .mobile + margin-right 6px + + > .visibility + margin-left 6px + + > .body + + > .cw + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + color isDark ? #fff : #717171 + + > .text + margin-right 8px + + > .toggle + 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 + + > .content + + > .text + display block + margin 0 + padding 0 + overflow-wrap break-word + color isDark ? #fff : #717171 + + >>> .title + display block + margin-bottom 4px + padding 4px + font-size 90% + text-align center + background isDark ? #2f3944 : #eef1f3 + border-radius 4px + + >>> .code + margin 8px 0 + + >>> .quote + margin 8px + padding 6px 12px + color isDark ? #6f808e : #aaa + border-left solid 3px isDark ? #637182 : #eee + + > .reply + margin-right 8px + color isDark ? #99abbf : #717171 + + > .rp + margin-left 4px + font-style oblique + color #a0bf46 + + [data-is-me]:after + content "you" + padding 0 4px + margin-left 4px + font-size 80% + color $theme-color-foreground + background $theme-color + border-radius 4px + + .mk-url-preview + margin-top 8px + + > .tags + margin 4px 0 0 0 + + > * + display inline-block + margin 0 8px 0 0 + padding 2px 8px 2px 16px + font-size 90% + color #8d969e + background isDark ? #313543 : #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background isDark ? #282c37 : #fff + border-radius 100% + + > .media + > img + display block + max-width 100% + + > .location + margin 4px 0 + font-size 12px + color #ccc + + > .map + width 100% + height 200px + + &:empty + display none + + > .mk-poll + font-size 80% + + > .renote + margin 8px 0 + + > .mk-note-preview + padding 16px + border dashed 1px isDark ? #4e945e : #c0dac6 + border-radius 8px + + > .app + font-size 12px + color #ccc + + > footer + > button + margin 0 + padding 8px + background transparent + border none + box-shadow none + font-size 1em + color isDark ? #606984 : #ddd + cursor pointer + + &:not(:last-child) + margin-right 28px + + &:hover + color isDark ? #9198af : #666 + + > .count + display inline + margin 0 0 0 8px + color #999 + + &.reacted + color $theme-color + +.zyjjkidcqjnlegkqebitfviomuqmseqk[data-darkmode] + root(true) + +.zyjjkidcqjnlegkqebitfviomuqmseqk:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue new file mode 100644 index 0000000000..ff871b049d --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue @@ -0,0 +1,248 @@ +<template> +<div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu"> + <div class="newer-indicator" v-show="queue.length > 0"></div> + + <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> + + <div v-if="!fetching && requestInitPromise != null"> + <p>%i18n:@error%</p> + <button @click="resolveInitPromise">%i18n:@retry%</button> + </div> + + <transition-group name="mk-notes" class="transition"> + <template v-for="(note, i) in _notes"> + <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> + <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> + <span>%fa:angle-up%{{ note._datetext }}</span> + <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> + </p> + </template> + </transition-group> + + <footer v-if="more"> + <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <template v-if="!moreFetching">%i18n:@load-more%</template> + <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> + </button> + </footer> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { url } from '../../../config'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; + +import XNote from './deck.note.vue'; + +const displayLimit = 30; + +export default Vue.extend({ + components: { + XNote + }, + + props: { + more: { + type: Function, + required: false + } + }, + + data() { + return { + rootEl: null, + requestInitPromise: null as () => Promise<any[]>, + notes: [], + queue: [], + unreadCount: 0, + fetching: true, + moreFetching: false + }; + }, + + computed: { + _notes(): any[] { + return (this.notes as any).map(note => { + const date = new Date(note.createdAt).getDate(); + const month = new Date(note.createdAt).getMonth() + 1; + note._date = date; + note._datetext = `${month}月 ${date}日`; + return note; + }); + } + }, + + beforeDestroy() { + this.root.removeEventListener('scroll', this.onScroll); + }, + + methods: { + mount(root) { + this.rootEl = root; + this.rootEl.addEventListener('scroll', this.onScroll); + }, + + isScrollTop() { + if (this.rootEl == null) return true; + return this.rootEl.scrollTop <= 8; + }, + + focus() { + (this.$el as any).children[0].focus(); + }, + + onNoteUpdated(i, note) { + Vue.set((this as any).notes, i, note); + }, + + init(promiseGenerator: () => Promise<any[]>) { + this.requestInitPromise = promiseGenerator; + this.resolveInitPromise(); + }, + + resolveInitPromise() { + this.queue = []; + this.notes = []; + this.fetching = true; + + const promise = this.requestInitPromise(); + + promise.then(notes => { + this.notes = notes; + this.requestInitPromise = null; + this.fetching = false; + }, e => { + this.fetching = false; + }); + }, + + prepend(note, silent = false) { + //#region 弾く + const isMyNote = note.userId == this.$store.state.i.id; + const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; + + if (this.$store.state.settings.showMyRenotes === false) { + if (isMyNote && isPureRenote) { + return; + } + } + + if (this.$store.state.settings.showRenotedMyNotes === false) { + if (isPureRenote && (note.renote.userId == this.$store.state.i.id)) { + return; + } + } + //#endregion + + if (this.isScrollTop()) { + // Prepend the note + this.notes.unshift(note); + + // オーバーフローしたら古い投稿は捨てる + if (this.notes.length >= displayLimit) { + this.notes = this.notes.slice(0, displayLimit); + } + } else { + this.queue.push(note); + } + }, + + append(note) { + this.notes.push(note); + }, + + tail() { + return this.notes[this.notes.length - 1]; + }, + + releaseQueue() { + this.queue.forEach(n => this.prepend(n, true)); + this.queue = []; + }, + + async loadMore() { + if (this.more == null) return; + if (this.moreFetching) return; + + this.moreFetching = true; + await this.more(); + this.moreFetching = false; + }, + + onScroll() { + if (this.isScrollTop()) { + this.releaseQueue(); + } + + if (this.rootEl && this.$store.state.settings.fetchOnScroll !== false) { + const current = this.rootEl.scrollTop + this.rootEl.clientHeight; + if (current > this.rootEl.scrollHeight - 8) this.loadMore(); + } + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark) + .transition + .mk-notes-enter + .mk-notes-leave-to + opacity 0 + transform translateY(-30px) + + > * + transition transform .3s ease, opacity .3s ease + + > .date + display block + margin 0 + line-height 32px + font-size 14px + text-align center + color isDark ? #666b79 : #aaa + background isDark ? #242731 : #fdfdfd + border-bottom solid 1px isDark ? #1c2023 : #eaeaea + + span + margin 0 16px + + [data-fa] + margin-right 8px + + > .newer-indicator + position -webkit-sticky + position sticky + z-index 100 + height 3px + background $theme-color + + > footer + > button + display block + margin 0 + padding 16px + width 100% + text-align center + color #ccc + background isDark ? #282C37 : #fff + border-top solid 1px isDark ? #1c2023 : #eaeaea + border-bottom-left-radius 6px + border-bottom-right-radius 6px + + &:hover + background isDark ? #2e3440 : #f5f5f5 + + &:active + background isDark ? #21242b : #eee + +.eamppglmnmimdhrlzhplwpvyeaqmmhxu[data-darkmode] + root(true) + +.eamppglmnmimdhrlzhplwpvyeaqmmhxu:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/pages/deck/deck.tl.vue b/src/client/app/desktop/views/pages/deck/deck.tl.vue new file mode 100644 index 0000000000..ce9a77703f --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.tl.vue @@ -0,0 +1,143 @@ +<template> + <x-notes ref="timeline" :more="existMore ? more : null"/> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XNotes from './deck.notes.vue'; + +const fetchLimit = 10; + +export default Vue.extend({ + components: { + XNotes + }, + + props: { + root: { + type: Object, + required: false + }, + src: { + type: String, + required: false, + default: 'home' + } + }, + + data() { + return { + fetching: true, + moreFetching: false, + existMore: false, + connection: null, + connectionId: null, + unreadCount: 0, + date: null + }; + }, + + computed: { + stream(): any { + return this.src == 'home' + ? (this as any).os.stream + : this.src == 'local' + ? (this as any).os.streams.localTimelineStream + : (this as any).os.streams.globalTimelineStream; + }, + + endpoint(): string { + return this.src == 'home' + ? 'notes/timeline' + : this.src == 'local' + ? 'notes/local-timeline' + : 'notes/global-timeline'; + } + }, + + mounted() { + this.connection = this.stream.getConnection(); + this.connectionId = this.stream.use(); + + this.connection.on('note', this.onNote); + if (this.src == 'home') { + this.connection.on('follow', this.onChangeFollowing); + this.connection.on('unfollow', this.onChangeFollowing); + } + + this.fetch(); + }, + + beforeDestroy() { + this.connection.off('note', this.onNote); + if (this.src == 'home') { + this.connection.off('follow', this.onChangeFollowing); + this.connection.off('unfollow', this.onChangeFollowing); + } + this.stream.dispose(this.connectionId); + }, + + methods: { + mount(root) { + this.$refs.timeline.mount(root); + }, + + fetch() { + this.fetching = true; + + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api(this.endpoint, { + limit: fetchLimit + 1, + untilDate: this.date ? this.date.getTime() : undefined, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + this.$emit('loaded'); + }, rej); + })); + }, + + more() { + this.moreFetching = true; + + const promise = (this as any).api(this.endpoint, { + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes + }); + + promise.then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + } else { + this.existMore = false; + } + notes.forEach(n => (this.$refs.timeline as any).append(n)); + this.moreFetching = false; + }); + + return promise; + }, + + onNote(note) { + // Prepend a note + (this.$refs.timeline as any).prepend(note); + }, + + onChangeFollowing() { + this.fetch(); + }, + + focus() { + (this.$refs.timeline as any).focus(); + } + } +}); +</script> diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue new file mode 100644 index 0000000000..afb65d2335 --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.vue @@ -0,0 +1,42 @@ +<template> +<mk-ui :class="$style.root"> + <div class="qlvquzbjribqcaozciifydkngcwtyzje"> + <x-column src="home"/> + <x-column src="home"/> + <x-column src="home"/> + <x-column src="home"/> + </div> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XColumn from './deck.column.vue'; + +export default Vue.extend({ + components: { + XColumn + } +}); +</script> + +<style lang="stylus" module> +.root + height 100vh +</style> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark) + display flex + flex 1 + padding 16px + +.qlvquzbjribqcaozciifydkngcwtyzje[data-darkmode] + root(true) + +.qlvquzbjribqcaozciifydkngcwtyzje:not([data-darkmode]) + root(false) + +</style> From e28d1c756971cdfda5364c80f6af7a69dbf5ffc1 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Tue, 5 Jun 2018 21:44:02 +0900 Subject: [PATCH 2/4] wip --- src/client/app/desktop/views/pages/deck/deck.column.vue | 3 ++- src/client/app/desktop/views/pages/deck/deck.vue | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue index 4e06798293..e0fc394f33 100644 --- a/src/client/app/desktop/views/pages/deck/deck.column.vue +++ b/src/client/app/desktop/views/pages/deck/deck.column.vue @@ -30,7 +30,7 @@ export default Vue.extend({ root(isDark) flex 1 - max-width 330px + min-width 330px height 100% margin-right 16px background isDark ? #282C37 : #fff @@ -49,6 +49,7 @@ root(isDark) > div height calc(100% - 48px) overflow auto + overflow-x hidden .dnpfarvgbnfmyzbdquhhzyxcmstpdqzs[data-darkmode] root(true) diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue index afb65d2335..0c32b7d665 100644 --- a/src/client/app/desktop/views/pages/deck/deck.vue +++ b/src/client/app/desktop/views/pages/deck/deck.vue @@ -31,7 +31,8 @@ export default Vue.extend({ root(isDark) display flex flex 1 - padding 16px + padding 16px 0 16px 16px + overflow auto .qlvquzbjribqcaozciifydkngcwtyzje[data-darkmode] root(true) From dfa2c951d67a64f4e72a0ebca7ff81e40fc25976 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Tue, 5 Jun 2018 22:54:03 +0900 Subject: [PATCH 3/4] wip --- .../views/components/ui.header.nav.vue | 2 +- .../desktop/views/pages/deck/deck.column.vue | 25 +++++++++++--- .../desktop/views/pages/deck/deck.notes.vue | 16 +++++---- .../views/pages/deck/deck.tl-column.vue | 33 +++++++++++++++++++ .../app/desktop/views/pages/deck/deck.tl.vue | 4 --- .../app/desktop/views/pages/deck/deck.vue | 11 +++---- src/services/note/create.ts | 4 ++- 7 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 src/client/app/desktop/views/pages/deck/deck.tl-column.vue diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue index 8e792b3df5..fe2637cec3 100644 --- a/src/client/app/desktop/views/components/ui.header.nav.vue +++ b/src/client/app/desktop/views/components/ui.header.nav.vue @@ -11,7 +11,7 @@ <li class="deck" :class="{ active: $route.name == 'deck' }"> <router-link to="/deck"> %fa:columns% - <p>%i18n:@deck%</p> + <p>%i18n:@deck% <small>(beta)</small></p> </router-link> </li> <li class="messaging"> diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue index e0fc394f33..8d0b3c0fdb 100644 --- a/src/client/app/desktop/views/pages/deck/deck.column.vue +++ b/src/client/app/desktop/views/pages/deck/deck.column.vue @@ -1,10 +1,10 @@ <template> <div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs"> <header> - <slot name="header">Timeline</slot> + <slot name="header"></slot> </header> <div ref="body"> - <x-tl ref="tl"/> + <slot></slot> </div> </div> </template> @@ -17,9 +17,23 @@ export default Vue.extend({ components: { XTl }, + provide() { + return { + getColumn() { + return this; + }, + getScrollContainer() { + return this.$refs.body; + } + }; + }, mounted() { this.$nextTick(() => { - this.$refs.tl.mount(this.$refs.body); + this.$emit('mounted'); + + setInterval(() => { + this.$emit('mounted'); + }, 100); }); } }); @@ -31,6 +45,7 @@ export default Vue.extend({ root(isDark) flex 1 min-width 330px + max-width 330px height 100% margin-right 16px background isDark ? #282C37 : #fff @@ -40,14 +55,14 @@ root(isDark) > header z-index 1 - line-height 48px + line-height 42px padding 0 16px color isDark ? #e3e5e8 : #888 background isDark ? #313543 : #fff box-shadow 0 1px rgba(#000, 0.15) > div - height calc(100% - 48px) + height calc(100% - 42px) overflow auto overflow-x hidden diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue index ff871b049d..48be4e585c 100644 --- a/src/client/app/desktop/views/pages/deck/deck.notes.vue +++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue @@ -73,16 +73,20 @@ export default Vue.extend({ } }, + inject: ['getColumn', 'getScrollContainer'], + + created() { + this.getColumn().$once('mounted', () => { + this.rootEl = this.getScrollContainer(); + this.rootEl.addEventListener('scroll', this.onScroll); + }) + }, + beforeDestroy() { - this.root.removeEventListener('scroll', this.onScroll); + this.rootEl.removeEventListener('scroll', this.onScroll); }, methods: { - mount(root) { - this.rootEl = root; - this.rootEl.addEventListener('scroll', this.onScroll); - }, - isScrollTop() { if (this.rootEl == null) return true; return this.rootEl.scrollTop <= 8; diff --git a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue new file mode 100644 index 0000000000..674f04077f --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue @@ -0,0 +1,33 @@ +<template> +<div> + <x-column> + <span slot="header"> + <template v-if="src == 'home'">%fa:home% %i18n:@home%</template> + <template v-if="src == 'local'">%fa:R comments% %i18n:@local%</template> + <template v-if="src == 'global'">%fa:globe% %i18n:@global%</template> + <template v-if="src == 'list'">%fa:list% {{ list.title }}</template> + </span> + <x-tl :src="src"/> + </x-column> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XColumn from './deck.column.vue'; +import XTl from './deck.tl.vue'; + +export default Vue.extend({ + components: { + XColumn, + XTl + }, + + props: { + src: { + type: String, + required: false + } + }, +}); +</script> diff --git a/src/client/app/desktop/views/pages/deck/deck.tl.vue b/src/client/app/desktop/views/pages/deck/deck.tl.vue index ce9a77703f..0a788b32ed 100644 --- a/src/client/app/desktop/views/pages/deck/deck.tl.vue +++ b/src/client/app/desktop/views/pages/deck/deck.tl.vue @@ -14,10 +14,6 @@ export default Vue.extend({ }, props: { - root: { - type: Object, - required: false - }, src: { type: String, required: false, diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue index 0c32b7d665..dfd480029c 100644 --- a/src/client/app/desktop/views/pages/deck/deck.vue +++ b/src/client/app/desktop/views/pages/deck/deck.vue @@ -1,21 +1,20 @@ <template> <mk-ui :class="$style.root"> <div class="qlvquzbjribqcaozciifydkngcwtyzje"> - <x-column src="home"/> - <x-column src="home"/> - <x-column src="home"/> - <x-column src="home"/> + <x-tl-column src="home"/> + <x-tl-column src="local"/> + <x-tl-column src="global"/> </div> </mk-ui> </template> <script lang="ts"> import Vue from 'vue'; -import XColumn from './deck.column.vue'; +import XTlColumn from './deck.tl-column.vue'; export default Vue.extend({ components: { - XColumn + XTlColumn } }); </script> diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 37d21fecad..f820182a42 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -221,7 +221,9 @@ export default async (user: IUser, data: { } // Publish note to global timeline stream - publishGlobalTimelineStream(noteObj); + if (note.visibility == 'public' && note.replyId == null) { + publishGlobalTimelineStream(noteObj); + } if (note.visibility == 'specified') { data.visibleUsers.forEach(async u => { From 2e919b788f449b34872bd3e530b2f4f637365c00 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Tue, 5 Jun 2018 23:19:04 +0900 Subject: [PATCH 4/4] wip --- .../pages/deck/deck.notifications-column.vue | 22 ++ .../views/pages/deck/deck.notifications.vue | 335 ++++++++++++++++++ .../app/desktop/views/pages/deck/deck.vue | 5 +- 3 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 src/client/app/desktop/views/pages/deck/deck.notifications-column.vue create mode 100644 src/client/app/desktop/views/pages/deck/deck.notifications.vue diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue b/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue new file mode 100644 index 0000000000..0566989642 --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue @@ -0,0 +1,22 @@ +<template> +<div> + <x-column> + <span slot="header">%fa:bell R% %i18n:@notifications%</span> + + <x-notifications/> + </x-column> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XColumn from './deck.column.vue'; +import XNotifications from './deck.notifications.vue'; + +export default Vue.extend({ + components: { + XColumn, + XNotifications + } +}); +</script> diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications.vue b/src/client/app/desktop/views/pages/deck/deck.notifications.vue new file mode 100644 index 0000000000..7a9646b587 --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.notifications.vue @@ -0,0 +1,335 @@ +<template> +<div class="oxynyeqmfvracxnglgulyqfgqxnxmehl"> + <div class="notifications" v-if="notifications.length != 0"> + <transition-group name="mk-notifications" class="transition"> + <template v-for="(notification, i) in _notifications"> + <div class="notification" :class="notification.type" :key="notification.id"> + <mk-time :time="notification.createdAt"/> + + <template v-if="notification.type == 'reaction'"> + <mk-avatar class="avatar" :user="notification.user"/> + <div class="text"> + <p> + <mk-reaction-icon :reaction="notification.reaction"/> + <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> + </p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% + </router-link> + </div> + </template> + + <template v-if="notification.type == 'renote'"> + <mk-avatar class="avatar" :user="notification.note.user"/> + <div class="text"> + <p>%fa:retweet% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right% + </router-link> + </div> + </template> + + <template v-if="notification.type == 'quote'"> + <mk-avatar class="avatar" :user="notification.note.user"/> + <div class="text"> + <p>%fa:quote-left% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link> + </div> + </template> + + <template v-if="notification.type == 'follow'"> + <mk-avatar class="avatar" :user="notification.user"/> + <div class="text"> + <p>%fa:user-plus% + <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> + </p> + </div> + </template> + + <template v-if="notification.type == 'receiveFollowRequest'"> + <mk-avatar class="avatar" :user="notification.user"/> + <div class="text"> + <p>%fa:user-clock% + <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> + </p> + </div> + </template> + + <template v-if="notification.type == 'reply'"> + <mk-avatar class="avatar" :user="notification.note.user"/> + <div class="text"> + <p>%fa:reply% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link> + </div> + </template> + + <template v-if="notification.type == 'mention'"> + <mk-avatar class="avatar" :user="notification.note.user"/> + <div class="text"> + <p>%fa:at% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a> + </div> + </template> + + <template v-if="notification.type == 'poll_vote'"> + <mk-avatar class="avatar" :user="notification.user"/> + <div class="text"> + <p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% + </router-link> + </div> + </template> + </div> + + <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> + <span>%fa:angle-up%{{ notification._datetext }}</span> + <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> + </p> + </template> + </transition-group> + </div> + <button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> + <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }} + </button> + <p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p> + <p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import getNoteSummary from '../../../../../../renderers/get-note-summary'; + +export default Vue.extend({ + data() { + return { + fetching: true, + fetchingMoreNotifications: false, + notifications: [], + moreNotifications: false, + connection: null, + connectionId: null, + getNoteSummary + }; + }, + computed: { + _notifications(): any[] { + return (this.notifications as any).map(notification => { + const date = new Date(notification.createdAt).getDate(); + const month = new Date(notification.createdAt).getMonth() + 1; + notification._date = date; + notification._datetext = `${month}月 ${date}日`; + return notification; + }); + } + }, + mounted() { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + + this.connection.on('notification', this.onNotification); + + const max = 10; + + (this as any).api('i/notifications', { + limit: max + 1 + }).then(notifications => { + if (notifications.length == max + 1) { + this.moreNotifications = true; + notifications.pop(); + } + + this.notifications = notifications; + this.fetching = false; + }); + }, + beforeDestroy() { + this.connection.off('notification', this.onNotification); + (this as any).os.stream.dispose(this.connectionId); + }, + methods: { + fetchMoreNotifications() { + this.fetchingMoreNotifications = true; + + const max = 30; + + (this as any).api('i/notifications', { + limit: max + 1, + untilId: this.notifications[this.notifications.length - 1].id + }).then(notifications => { + if (notifications.length == max + 1) { + this.moreNotifications = true; + notifications.pop(); + } else { + this.moreNotifications = false; + } + this.notifications = this.notifications.concat(notifications); + this.fetchingMoreNotifications = false; + }); + }, + onNotification(notification) { + // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない + this.connection.send({ + type: 'read_notification', + id: notification.id + }); + + this.notifications.unshift(notification); + } + } +}); +</script> + +<style lang="stylus" scoped> +root(isDark) + .transition + .mk-notifications-enter + .mk-notifications-leave-to + opacity 0 + transform translateY(-30px) + + > * + transition transform .3s ease, opacity .3s ease + + > .notifications + > * + > .notification + margin 0 + padding 16px + overflow-wrap break-word + font-size 0.9em + border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05) + + &:last-child + border-bottom none + + > .mk-time + display inline + position absolute + top 16px + right 12px + vertical-align top + color isDark ? #606984 : rgba(#000, 0.6) + font-size small + + &:after + content "" + display block + clear both + + > .avatar + display block + float left + position -webkit-sticky + position sticky + top 16px + width 36px + height 36px + border-radius 6px + + > .text + float right + width calc(100% - 36px) + padding-left 8px + + p + margin 0 + + i, .mk-reaction-icon + margin-right 4px + + .note-preview + color isDark ? #c2cad4 : rgba(#000, 0.7) + + .note-ref + color isDark ? #c2cad4 : rgba(#000, 0.7) + + [data-fa] + font-size 1em + font-weight normal + font-style normal + display inline-block + margin-right 3px + + &.renote, &.quote + .text p i + color #77B255 + + &.follow + .text p i + color #53c7ce + + &.receiveFollowRequest + .text p i + color #888 + + &.reply, &.mention + .text p i + color #555 + + > .date + display block + margin 0 + line-height 32px + text-align center + font-size 0.8em + color isDark ? #666b79 : #aaa + background isDark ? #242731 : #fdfdfd + border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05) + + span + margin 0 16px + + [data-fa] + margin-right 8px + + > .more + display block + width 100% + padding 16px + color #555 + border-top solid 1px rgba(#000, 0.05) + + &:hover + background rgba(#000, 0.025) + + &:active + background rgba(#000, 0.05) + + &.fetching + cursor wait + + > [data-fa] + margin-right 4px + + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .loading + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + +.oxynyeqmfvracxnglgulyqfgqxnxmehl[data-darkmode] + root(true) + +.oxynyeqmfvracxnglgulyqfgqxnxmehl:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue index dfd480029c..fb5e55086c 100644 --- a/src/client/app/desktop/views/pages/deck/deck.vue +++ b/src/client/app/desktop/views/pages/deck/deck.vue @@ -2,6 +2,7 @@ <mk-ui :class="$style.root"> <div class="qlvquzbjribqcaozciifydkngcwtyzje"> <x-tl-column src="home"/> + <x-notifications-column/> <x-tl-column src="local"/> <x-tl-column src="global"/> </div> @@ -11,10 +12,12 @@ <script lang="ts"> import Vue from 'vue'; import XTlColumn from './deck.tl-column.vue'; +import XNotificationsColumn from './deck.notifications-column.vue'; export default Vue.extend({ components: { - XTlColumn + XTlColumn, + XNotificationsColumn } }); </script>