commit
d21da0211c
14 changed files with 1614 additions and 1 deletions
|
@ -606,6 +606,7 @@ desktop/views/components/ui.header.account.vue:
|
||||||
|
|
||||||
desktop/views/components/ui.header.nav.vue:
|
desktop/views/components/ui.header.nav.vue:
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
|
deck: "デッキ"
|
||||||
messaging: "メッセージ"
|
messaging: "メッセージ"
|
||||||
game: "ゲーム"
|
game: "ゲーム"
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import updateAvatar from './api/update-avatar';
|
||||||
import updateBanner from './api/update-banner';
|
import updateBanner from './api/update-banner';
|
||||||
|
|
||||||
import MkIndex from './views/pages/index.vue';
|
import MkIndex from './views/pages/index.vue';
|
||||||
|
import MkDeck from './views/pages/deck/deck.vue';
|
||||||
import MkUser from './views/pages/user/user.vue';
|
import MkUser from './views/pages/user/user.vue';
|
||||||
import MkFavorites from './views/pages/favorites.vue';
|
import MkFavorites from './views/pages/favorites.vue';
|
||||||
import MkSelectDrive from './views/pages/selectdrive.vue';
|
import MkSelectDrive from './views/pages/selectdrive.vue';
|
||||||
|
@ -50,6 +51,7 @@ init(async (launch) => {
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', name: 'index', component: MkIndex },
|
{ path: '/', name: 'index', component: MkIndex },
|
||||||
|
{ path: '/deck', name: 'deck', component: MkDeck },
|
||||||
{ path: '/i/customize-home', component: MkHomeCustomize },
|
{ path: '/i/customize-home', component: MkHomeCustomize },
|
||||||
{ path: '/i/favorites', component: MkFavorites },
|
{ path: '/i/favorites', component: MkFavorites },
|
||||||
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
||||||
|
|
|
@ -8,6 +8,12 @@
|
||||||
<p>%i18n:@home%</p>
|
<p>%i18n:@home%</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="deck" :class="{ active: $route.name == 'deck' }">
|
||||||
|
<router-link to="/deck">
|
||||||
|
%fa:columns%
|
||||||
|
<p>%i18n:@deck% <small>(beta)</small></p>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
<li class="messaging">
|
<li class="messaging">
|
||||||
<a @click="messaging">
|
<a @click="messaging">
|
||||||
%fa:comments%
|
%fa:comments%
|
||||||
|
|
|
@ -37,7 +37,16 @@ export default Vue.extend({
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.mk-ui
|
.mk-ui
|
||||||
|
display flex
|
||||||
|
flex-direction column
|
||||||
|
flex 1
|
||||||
|
|
||||||
> .header
|
> .header
|
||||||
@media (max-width 1000px)
|
@media (max-width 1000px)
|
||||||
display none
|
display none
|
||||||
|
|
||||||
|
> .content
|
||||||
|
display flex
|
||||||
|
flex-direction column
|
||||||
|
flex 1
|
||||||
</style>
|
</style>
|
||||||
|
|
75
src/client/app/desktop/views/pages/deck/deck.column.vue
Normal file
75
src/client/app/desktop/views/pages/deck/deck.column.vue
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs">
|
||||||
|
<header>
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</header>
|
||||||
|
<div ref="body">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import XTl from './deck.tl.vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
XTl
|
||||||
|
},
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
getColumn() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
getScrollContainer() {
|
||||||
|
return this.$refs.body;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$emit('mounted');
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
this.$emit('mounted');
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark)
|
||||||
|
flex 1
|
||||||
|
min-width 330px
|
||||||
|
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 42px
|
||||||
|
padding 0 16px
|
||||||
|
color isDark ? #e3e5e8 : #888
|
||||||
|
background isDark ? #313543 : #fff
|
||||||
|
box-shadow 0 1px rgba(#000, 0.15)
|
||||||
|
|
||||||
|
> div
|
||||||
|
height calc(100% - 42px)
|
||||||
|
overflow auto
|
||||||
|
overflow-x hidden
|
||||||
|
|
||||||
|
.dnpfarvgbnfmyzbdquhhzyxcmstpdqzs[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.dnpfarvgbnfmyzbdquhhzyxcmstpdqzs:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
|
</style>
|
153
src/client/app/desktop/views/pages/deck/deck.note.sub.vue
Normal file
153
src/client/app/desktop/views/pages/deck/deck.note.sub.vue
Normal file
|
@ -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>
|
539
src/client/app/desktop/views/pages/deck/deck.note.vue
Normal file
539
src/client/app/desktop/views/pages/deck/deck.note.vue
Normal file
|
@ -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>
|
252
src/client/app/desktop/views/pages/deck/deck.notes.vue
Normal file
252
src/client/app/desktop/views/pages/deck/deck.notes.vue
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
<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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
inject: ['getColumn', 'getScrollContainer'],
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.getColumn().$once('mounted', () => {
|
||||||
|
this.rootEl = this.getScrollContainer();
|
||||||
|
this.rootEl.addEventListener('scroll', this.onScroll);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.rootEl.removeEventListener('scroll', this.onScroll);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
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>
|
|
@ -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>
|
335
src/client/app/desktop/views/pages/deck/deck.notifications.vue
Normal file
335
src/client/app/desktop/views/pages/deck/deck.notifications.vue
Normal file
|
@ -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>
|
33
src/client/app/desktop/views/pages/deck/deck.tl-column.vue
Normal file
33
src/client/app/desktop/views/pages/deck/deck.tl-column.vue
Normal file
|
@ -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>
|
139
src/client/app/desktop/views/pages/deck/deck.tl.vue
Normal file
139
src/client/app/desktop/views/pages/deck/deck.tl.vue
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
<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: {
|
||||||
|
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>
|
45
src/client/app/desktop/views/pages/deck/deck.vue
Normal file
45
src/client/app/desktop/views/pages/deck/deck.vue
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
</mk-ui>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<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,
|
||||||
|
XNotificationsColumn
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" module>
|
||||||
|
.root
|
||||||
|
height 100vh
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark)
|
||||||
|
display flex
|
||||||
|
flex 1
|
||||||
|
padding 16px 0 16px 16px
|
||||||
|
overflow auto
|
||||||
|
|
||||||
|
.qlvquzbjribqcaozciifydkngcwtyzje[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.qlvquzbjribqcaozciifydkngcwtyzje:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
|
</style>
|
|
@ -221,7 +221,9 @@ export default async (user: IUser, data: {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish note to global timeline stream
|
// Publish note to global timeline stream
|
||||||
publishGlobalTimelineStream(noteObj);
|
if (note.visibility == 'public' && note.replyId == null) {
|
||||||
|
publishGlobalTimelineStream(noteObj);
|
||||||
|
}
|
||||||
|
|
||||||
if (note.visibility == 'specified') {
|
if (note.visibility == 'specified') {
|
||||||
data.visibleUsers.forEach(async u => {
|
data.visibleUsers.forEach(async u => {
|
||||||
|
|
Loading…
Reference in a new issue