Implement #2736
This commit is contained in:
		
							parent
							
								
									195f676500
								
							
						
					
					
						commit
						d9f0e158a3
					
				
					 25 changed files with 239 additions and 39 deletions
				
			
		| 
						 | 
					@ -50,6 +50,30 @@ export class HomeStream extends Stream {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.on('unreadMention', () => {
 | 
				
			||||||
 | 
								os.store.dispatch('mergeMe', {
 | 
				
			||||||
 | 
									hasUnreadMentions: true
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.on('readAllUnreadMentions', () => {
 | 
				
			||||||
 | 
								os.store.dispatch('mergeMe', {
 | 
				
			||||||
 | 
									hasUnreadMentions: false
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.on('unreadSpecifiedNote', () => {
 | 
				
			||||||
 | 
								os.store.dispatch('mergeMe', {
 | 
				
			||||||
 | 
									hasUnreadSpecifiedNotes: true
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.on('readAllUnreadSpecifiedNotes', () => {
 | 
				
			||||||
 | 
								os.store.dispatch('mergeMe', {
 | 
				
			||||||
 | 
									hasUnreadSpecifiedNotes: false
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.on('clientSettingUpdated', x => {
 | 
							this.on('clientSettingUpdated', x => {
 | 
				
			||||||
			os.store.commit('settings/set', {
 | 
								os.store.commit('settings/set', {
 | 
				
			||||||
				key: x.key,
 | 
									key: x.key,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -213,10 +213,14 @@ export default Vue.extend({
 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		capture(withHandler = false) {
 | 
							capture(withHandler = false) {
 | 
				
			||||||
			if (this.$store.getters.isSignedIn) {
 | 
								if (this.$store.getters.isSignedIn) {
 | 
				
			||||||
				this.connection.send({
 | 
									const data = {
 | 
				
			||||||
					type: 'capture',
 | 
										type: 'capture',
 | 
				
			||||||
					id: this.p.id
 | 
										id: this.p.id
 | 
				
			||||||
				});
 | 
									} as any;
 | 
				
			||||||
 | 
									if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
 | 
				
			||||||
 | 
										data.read = true;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									this.connection.send(data);
 | 
				
			||||||
				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
 | 
									if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,8 +8,8 @@
 | 
				
			||||||
		<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</span>
 | 
							<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</span>
 | 
				
			||||||
		<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
 | 
							<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
 | 
				
			||||||
		<div class="buttons">
 | 
							<div class="buttons">
 | 
				
			||||||
			<button :data-active="src == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%">%fa:at%</button>
 | 
								<button :data-active="src == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%">%fa:at%<i class="badge" v-if="$store.state.i.hasUnreadMentions">%fa:circle%</i></button>
 | 
				
			||||||
			<button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%</button>
 | 
								<button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i></button>
 | 
				
			||||||
			<button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button>
 | 
								<button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button>
 | 
				
			||||||
			<button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button>
 | 
								<button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
| 
						 | 
					@ -202,6 +202,13 @@ root(isDark)
 | 
				
			||||||
				line-height 42px
 | 
									line-height 42px
 | 
				
			||||||
				color isDark ? #9baec8 : #ccc
 | 
									color isDark ? #9baec8 : #ccc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									> .badge
 | 
				
			||||||
 | 
										position absolute
 | 
				
			||||||
 | 
										top -4px
 | 
				
			||||||
 | 
										right 4px
 | 
				
			||||||
 | 
										font-size 10px
 | 
				
			||||||
 | 
										color $theme-color
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				&:hover
 | 
									&:hover
 | 
				
			||||||
					color isDark ? #b2c1d5 : #aaa
 | 
										color isDark ? #b2c1d5 : #aaa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -147,10 +147,14 @@ export default Vue.extend({
 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		capture(withHandler = false) {
 | 
							capture(withHandler = false) {
 | 
				
			||||||
			if (this.$store.getters.isSignedIn) {
 | 
								if (this.$store.getters.isSignedIn) {
 | 
				
			||||||
				this.connection.send({
 | 
									const data = {
 | 
				
			||||||
					type: 'capture',
 | 
										type: 'capture',
 | 
				
			||||||
					id: this.p.id
 | 
										id: this.p.id
 | 
				
			||||||
				});
 | 
									} as any;
 | 
				
			||||||
 | 
									if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
 | 
				
			||||||
 | 
										data.read = true;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									this.connection.send(data);
 | 
				
			||||||
				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
 | 
									if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -160,10 +160,14 @@ export default Vue.extend({
 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		capture(withHandler = false) {
 | 
							capture(withHandler = false) {
 | 
				
			||||||
			if (this.$store.getters.isSignedIn) {
 | 
								if (this.$store.getters.isSignedIn) {
 | 
				
			||||||
				this.connection.send({
 | 
									const data = {
 | 
				
			||||||
					type: 'capture',
 | 
										type: 'capture',
 | 
				
			||||||
					id: this.p.id
 | 
										id: this.p.id
 | 
				
			||||||
				});
 | 
									} as any;
 | 
				
			||||||
 | 
									if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
 | 
				
			||||||
 | 
										data.read = true;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									this.connection.send(data);
 | 
				
			||||||
				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
 | 
									if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -188,9 +188,6 @@ root(isDark)
 | 
				
			||||||
				overflow hidden
 | 
									overflow hidden
 | 
				
			||||||
				text-overflow ellipsis
 | 
									text-overflow ellipsis
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				[data-fa], [data-icon]
 | 
					 | 
				
			||||||
					margin-right 4px
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				> img
 | 
									> img
 | 
				
			||||||
					display inline-block
 | 
										display inline-block
 | 
				
			||||||
					vertical-align bottom
 | 
										vertical-align bottom
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<span slot="header">
 | 
						<span slot="header">
 | 
				
			||||||
		<template v-if="folder">%fa:R folder-open%{{ folder.name }}</template>
 | 
							<template v-if="folder"><span style="margin-right:4px;">%fa:R folder-open%</span>{{ folder.name }}</template>
 | 
				
			||||||
		<template v-if="file"><mk-file-type-icon data-icon :type="file.type"/>{{ file.name }}</template>
 | 
							<template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template>
 | 
				
			||||||
		<template v-if="!folder && !file">%fa:cloud%%i18n:@drive%</template>
 | 
							<template v-if="!folder && !file"><span style="margin-right:4px;">%fa:cloud%</span>%i18n:@drive%</template>
 | 
				
			||||||
	</span>
 | 
						</span>
 | 
				
			||||||
	<template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template>
 | 
						<template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template>
 | 
				
			||||||
	<mk-drive
 | 
						<mk-drive
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<span slot="header">%fa:star%%i18n:@title%</span>
 | 
						<span slot="header"><span style="margin-right:4px;">%fa:star%</span>%i18n:@title%</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<main>
 | 
						<main>
 | 
				
			||||||
		<template v-for="favorite in favorites">
 | 
							<template v-for="favorite in favorites">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<span slot="header">%fa:gamepad%%i18n:@reversi%</span>
 | 
						<span slot="header"><span style="margin-right:4px;">%fa:gamepad%</span>%i18n:@reversi%</span>
 | 
				
			||||||
	<mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/>
 | 
						<mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/>
 | 
				
			||||||
</mk-ui>
 | 
					</mk-ui>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<span slot="header" @click="showNav = true">
 | 
						<span slot="header" @click="showNav = true">
 | 
				
			||||||
		<span>
 | 
							<span :class="$style.title">
 | 
				
			||||||
			<span v-if="src == 'home'">%fa:home%%i18n:@home%</span>
 | 
								<span v-if="src == 'home'">%fa:home%%i18n:@home%</span>
 | 
				
			||||||
			<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
 | 
								<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
 | 
				
			||||||
			<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
 | 
								<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,7 @@
 | 
				
			||||||
			<template v-if="!showNav">%fa:angle-down%</template>
 | 
								<template v-if="!showNav">%fa:angle-down%</template>
 | 
				
			||||||
			<template v-else>%fa:angle-up%</template>
 | 
								<template v-else>%fa:angle-up%</template>
 | 
				
			||||||
		</span>
 | 
							</span>
 | 
				
			||||||
 | 
							<i :class="$style.badge" v-if="$store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i>
 | 
				
			||||||
	</span>
 | 
						</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<template slot="func">
 | 
						<template slot="func">
 | 
				
			||||||
| 
						 | 
					@ -32,8 +33,8 @@
 | 
				
			||||||
					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
 | 
										<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
 | 
				
			||||||
					<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
 | 
										<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
 | 
				
			||||||
					<div class="hr"></div>
 | 
										<div class="hr"></div>
 | 
				
			||||||
					<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span>
 | 
										<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%<i class="badge" v-if="$store.state.i.hasUnreadMentions">%fa:circle%</i></span>
 | 
				
			||||||
					<span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%</span>
 | 
										<span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i></span>
 | 
				
			||||||
					<template v-if="lists">
 | 
										<template v-if="lists">
 | 
				
			||||||
						<div class="hr" v-if="lists.length > 0"></div>
 | 
											<div class="hr" v-if="lists.length > 0"></div>
 | 
				
			||||||
						<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
 | 
											<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
 | 
				
			||||||
| 
						 | 
					@ -220,6 +221,11 @@ root(isDark)
 | 
				
			||||||
					&:not([data-active]):hover
 | 
										&:not([data-active]):hover
 | 
				
			||||||
						background isDark ? #353e4a : #eee
 | 
											background isDark ? #353e4a : #eee
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										> .badge
 | 
				
			||||||
 | 
											margin-left 6px
 | 
				
			||||||
 | 
											font-size 10px
 | 
				
			||||||
 | 
											color $theme-color
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	> .tl
 | 
						> .tl
 | 
				
			||||||
		max-width 680px
 | 
							max-width 680px
 | 
				
			||||||
		margin 0 auto
 | 
							margin 0 auto
 | 
				
			||||||
| 
						 | 
					@ -238,3 +244,18 @@ main:not([data-darkmode])
 | 
				
			||||||
	root(false)
 | 
						root(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" module>
 | 
				
			||||||
 | 
					@import '~const.styl'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.title
 | 
				
			||||||
 | 
						i
 | 
				
			||||||
 | 
							margin-right 4px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.badge
 | 
				
			||||||
 | 
						margin-left 6px
 | 
				
			||||||
 | 
						font-size 10px
 | 
				
			||||||
 | 
						color $theme-color
 | 
				
			||||||
 | 
						vertical-align middle
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<span slot="header">
 | 
						<span slot="header">
 | 
				
			||||||
		<template v-if="user">%fa:R comments%{{ user | userName }}</template>
 | 
							<template v-if="user"><span style="margin-right:4px;">%fa:R comments%</span>{{ user | userName }}</template>
 | 
				
			||||||
		<template v-else><mk-ellipsis/></template>
 | 
							<template v-else><mk-ellipsis/></template>
 | 
				
			||||||
	</span>
 | 
						</span>
 | 
				
			||||||
	<mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>
 | 
						<mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<span slot="header">%fa:R comments%%i18n:@messaging%</span>
 | 
						<span slot="header"><span style="margin-right:4px;">%fa:R comments%</span>%i18n:@messaging%</span>
 | 
				
			||||||
	<mk-messaging @navigate="navigate" :header-top="48"/>
 | 
						<mk-messaging @navigate="navigate" :header-top="48"/>
 | 
				
			||||||
</mk-ui>
 | 
					</mk-ui>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<span slot="header">%fa:R sticky-note%%i18n:@title%</span>
 | 
						<span slot="header"><span style="margin-right:4px;">%fa:R sticky-note%</span>%i18n:@title%</span>
 | 
				
			||||||
	<main v-if="!fetching">
 | 
						<main v-if="!fetching">
 | 
				
			||||||
		<div>
 | 
							<div>
 | 
				
			||||||
			<mk-note-detail :note="note"/>
 | 
								<mk-note-detail :note="note"/>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<span slot="header">%fa:R bell%%i18n:@notifications%</span>
 | 
						<span slot="header"><span style="margin-right:4px;">%fa:R bell%</span>%i18n:@notifications%</span>
 | 
				
			||||||
	<template slot="func"><button @click="fn">%fa:check%</button></template>
 | 
						<template slot="func"><button @click="fn">%fa:check%</button></template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<main>
 | 
						<main>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<span slot="header">%fa:cog%%i18n:@settings%</span>
 | 
						<span slot="header"><span style="margin-right:4px;">%fa:cog%</span>%i18n:@settings%</span>
 | 
				
			||||||
	<main :data-darkmode="$store.state.device.darkmode">
 | 
						<main :data-darkmode="$store.state.device.darkmode">
 | 
				
			||||||
		<div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div>
 | 
							<div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<span slot="header">%fa:hashtag%{{ $route.params.tag }}</span>
 | 
						<span slot="header"><span style="margin-right:4px;">%fa:hashtag%</span>{{ $route.params.tag }}</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<main>
 | 
						<main>
 | 
				
			||||||
		<p v-if="!fetching && empty">%fa:search% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p>
 | 
							<p v-if="!fetching && empty">%fa:search% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<span slot="header">%fa:home%%i18n:@dashboard%</span>
 | 
						<span slot="header"><span style="margin-right:4px;">%fa:home%</span>%i18n:@dashboard%</span>
 | 
				
			||||||
	<template slot="func">
 | 
						<template slot="func">
 | 
				
			||||||
		<button @click="customizing = !customizing">%fa:cog%</button>
 | 
							<button @click="customizing = !customizing">%fa:cog%</button>
 | 
				
			||||||
	</template>
 | 
						</template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								src/models/note-unread.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/models/note-unread.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					import * as mongo from 'mongodb';
 | 
				
			||||||
 | 
					import db from '../db/mongodb';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NoteUnread = db.get<INoteUnread>('noteUnreads');
 | 
				
			||||||
 | 
					NoteUnread.createIndex(['userId', 'noteId'], { unique: true });
 | 
				
			||||||
 | 
					export default NoteUnread;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface INoteUnread {
 | 
				
			||||||
 | 
						_id: mongo.ObjectID;
 | 
				
			||||||
 | 
						noteId: mongo.ObjectID;
 | 
				
			||||||
 | 
						userId: mongo.ObjectID;
 | 
				
			||||||
 | 
						isSpecified: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_note: {
 | 
				
			||||||
 | 
							userId: mongo.ObjectID;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -295,8 +295,8 @@ export const pack = async (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	delete _note._user;
 | 
						delete _note._user;
 | 
				
			||||||
	delete _note._reply;
 | 
						delete _note._reply;
 | 
				
			||||||
	delete _note.repost;
 | 
						delete _note._renote;
 | 
				
			||||||
	delete _note.mentions;
 | 
						delete _note._files;
 | 
				
			||||||
	if (_note.geo) delete _note.geo.type;
 | 
						if (_note.geo) delete _note.geo.type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Populate user
 | 
						// Populate user
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -348,7 +348,8 @@ export const pack = (
 | 
				
			||||||
	me?: string | mongo.ObjectID | IUser,
 | 
						me?: string | mongo.ObjectID | IUser,
 | 
				
			||||||
	options?: {
 | 
						options?: {
 | 
				
			||||||
		detail?: boolean,
 | 
							detail?: boolean,
 | 
				
			||||||
		includeSecrets?: boolean
 | 
							includeSecrets?: boolean,
 | 
				
			||||||
 | 
							includeHasUnreadNotes?: boolean
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
) => new Promise<any>(async (resolve, reject) => {
 | 
					) => new Promise<any>(async (resolve, reject) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -510,6 +511,11 @@ export const pack = (
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!opts.includeHasUnreadNotes) {
 | 
				
			||||||
 | 
							delete _user.hasUnreadSpecifiedNotes;
 | 
				
			||||||
 | 
							delete _user.hasUnreadMentions;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// resolve promises in _user object
 | 
						// resolve promises in _user object
 | 
				
			||||||
	_user = await rap(_user);
 | 
						_user = await rap(_user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,6 +22,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
 | 
				
			||||||
	// Serialize
 | 
						// Serialize
 | 
				
			||||||
	res(await pack(user, user, {
 | 
						res(await pack(user, user, {
 | 
				
			||||||
		detail: true,
 | 
							detail: true,
 | 
				
			||||||
 | 
							includeHasUnreadNotes: true,
 | 
				
			||||||
		includeSecrets: isSecure
 | 
							includeSecrets: isSecure
 | 
				
			||||||
	}));
 | 
						}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ import readNotification from '../common/read-notification';
 | 
				
			||||||
import call from '../call';
 | 
					import call from '../call';
 | 
				
			||||||
import { IApp } from '../../../models/app';
 | 
					import { IApp } from '../../../models/app';
 | 
				
			||||||
import shouldMuteThisNote from '../../../misc/should-mute-this-note';
 | 
					import shouldMuteThisNote from '../../../misc/should-mute-this-note';
 | 
				
			||||||
 | 
					import readNote from '../../../services/note/read';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const log = debug('misskey');
 | 
					const log = debug('misskey');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -94,6 +95,9 @@ export default async function(
 | 
				
			||||||
				if (!msg.id) return;
 | 
									if (!msg.id) return;
 | 
				
			||||||
				log(`CAPTURE: ${msg.id} by @${user.username}`);
 | 
									log(`CAPTURE: ${msg.id} by @${user.username}`);
 | 
				
			||||||
				subscriber.on(`note-stream:${msg.id}`, onNoteStream);
 | 
									subscriber.on(`note-stream:${msg.id}`, onNoteStream);
 | 
				
			||||||
 | 
									if (msg.read) {
 | 
				
			||||||
 | 
										readNote(user._id, msg.id);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
				break;
 | 
									break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'decapture':
 | 
								case 'decapture':
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,7 @@ import { TextElementMention } from '../../mfm/parse/elements/mention';
 | 
				
			||||||
import { TextElementHashtag } from '../../mfm/parse/elements/hashtag';
 | 
					import { TextElementHashtag } from '../../mfm/parse/elements/hashtag';
 | 
				
			||||||
import { updateNoteStats } from '../update-chart';
 | 
					import { updateNoteStats } from '../update-chart';
 | 
				
			||||||
import { erase, unique } from '../../prelude/array';
 | 
					import { erase, unique } from '../../prelude/array';
 | 
				
			||||||
 | 
					import insertNoteUnread from './unread';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
 | 
					type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -170,6 +171,17 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 | 
				
			||||||
	// Increment notes count (user)
 | 
						// Increment notes count (user)
 | 
				
			||||||
	incNotesCountOfUser(user);
 | 
						incNotesCountOfUser(user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 未読通知を作成
 | 
				
			||||||
 | 
						if (data.visibility == 'specified') {
 | 
				
			||||||
 | 
							data.visibleUsers.forEach(u => {
 | 
				
			||||||
 | 
								insertNoteUnread(u, note, true);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							mentionedUsers.forEach(u => {
 | 
				
			||||||
 | 
								insertNoteUnread(u, note, false);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (data.reply) {
 | 
						if (data.reply) {
 | 
				
			||||||
		saveReply(data.reply, note);
 | 
							saveReply(data.reply, note);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -314,16 +326,6 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
 | 
				
			||||||
		publishGlobalTimelineStream(noteObj);
 | 
							publishGlobalTimelineStream(noteObj);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (note.visibility == 'specified') {
 | 
					 | 
				
			||||||
		visibleUsers.forEach(async (u) => {
 | 
					 | 
				
			||||||
			const n = await pack(note, u, {
 | 
					 | 
				
			||||||
				detail: true
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			publishUserStream(u._id, 'note', n);
 | 
					 | 
				
			||||||
			publishHybridTimelineStream(u._id, n);
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (['public', 'home', 'followers'].includes(note.visibility)) {
 | 
						if (['public', 'home', 'followers'].includes(note.visibility)) {
 | 
				
			||||||
		// フォロワーに配信
 | 
							// フォロワーに配信
 | 
				
			||||||
		publishToFollowers(note, user, noteActivity);
 | 
							publishToFollowers(note, user, noteActivity);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										62
									
								
								src/services/note/read.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/services/note/read.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,62 @@
 | 
				
			||||||
 | 
					import * as mongo from 'mongodb';
 | 
				
			||||||
 | 
					import { publishUserStream } from '../../stream';
 | 
				
			||||||
 | 
					import User from '../../models/user';
 | 
				
			||||||
 | 
					import NoteUnread from '../../models/note-unread';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Mark a note as read
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default (
 | 
				
			||||||
 | 
						user: string | mongo.ObjectID,
 | 
				
			||||||
 | 
						note: string | mongo.ObjectID
 | 
				
			||||||
 | 
					) => new Promise<any>(async (resolve, reject) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const userId: mongo.ObjectID = mongo.ObjectID.prototype.isPrototypeOf(user)
 | 
				
			||||||
 | 
							? user as mongo.ObjectID
 | 
				
			||||||
 | 
							: new mongo.ObjectID(user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const noteId: mongo.ObjectID = mongo.ObjectID.prototype.isPrototypeOf(note)
 | 
				
			||||||
 | 
							? note as mongo.ObjectID
 | 
				
			||||||
 | 
							: new mongo.ObjectID(note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Remove document
 | 
				
			||||||
 | 
						await NoteUnread.remove({
 | 
				
			||||||
 | 
							userId: userId,
 | 
				
			||||||
 | 
							noteId: noteId
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const count1 = await NoteUnread
 | 
				
			||||||
 | 
							.count({
 | 
				
			||||||
 | 
								userId: userId,
 | 
				
			||||||
 | 
								isSpecified: false
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								limit: 1
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const count2 = await NoteUnread
 | 
				
			||||||
 | 
							.count({
 | 
				
			||||||
 | 
								userId: userId,
 | 
				
			||||||
 | 
								isSpecified: true
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								limit: 1
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (count1 == 0 || count2 == 0) {
 | 
				
			||||||
 | 
							User.update({ _id: userId }, {
 | 
				
			||||||
 | 
								$set: {
 | 
				
			||||||
 | 
									hasUnreadMentions: count1 != 0 || count2 != 0,
 | 
				
			||||||
 | 
									hasUnreadSpecifiedNotes: count2 != 0
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (count1 == 0) {
 | 
				
			||||||
 | 
							// 全て既読になったイベントを発行
 | 
				
			||||||
 | 
							publishUserStream(userId, 'readAllUnreadMentions');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (count2 == 0) {
 | 
				
			||||||
 | 
							// 全て既読になったイベントを発行
 | 
				
			||||||
 | 
							publishUserStream(userId, 'readAllUnreadSpecifiedNotes');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										47
									
								
								src/services/note/unread.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/services/note/unread.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					import NoteUnread from '../../models/note-unread';
 | 
				
			||||||
 | 
					import User, { IUser } from '../../models/user';
 | 
				
			||||||
 | 
					import { INote } from '../../models/note';
 | 
				
			||||||
 | 
					import Mute from '../../models/mute';
 | 
				
			||||||
 | 
					import { publishUserStream } from '../../stream';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function(user: IUser, note: INote, isSpecified = false) {
 | 
				
			||||||
 | 
						const unread = await NoteUnread.insert({
 | 
				
			||||||
 | 
							noteId: note._id,
 | 
				
			||||||
 | 
							userId: user._id,
 | 
				
			||||||
 | 
							isSpecified,
 | 
				
			||||||
 | 
							_note: {
 | 
				
			||||||
 | 
								userId: note.userId
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 3秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
 | 
				
			||||||
 | 
						setTimeout(async () => {
 | 
				
			||||||
 | 
							const exist = await NoteUnread.findOne({ _id: unread._id });
 | 
				
			||||||
 | 
							if (exist == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//#region ただしミュートされているなら発行しない
 | 
				
			||||||
 | 
							const mute = await Mute.find({
 | 
				
			||||||
 | 
								muterId: user._id
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							const mutedUserIds = mute.map(m => m.muteeId.toString());
 | 
				
			||||||
 | 
							if (mutedUserIds.includes(note.userId.toString())) return;
 | 
				
			||||||
 | 
							//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							User.update({
 | 
				
			||||||
 | 
								_id: user._id
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								$set: isSpecified ? {
 | 
				
			||||||
 | 
									hasUnreadSpecifiedNotes: true,
 | 
				
			||||||
 | 
									hasUnreadMentions: true
 | 
				
			||||||
 | 
								} : {
 | 
				
			||||||
 | 
									hasUnreadMentions: true
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							publishUserStream(user._id, 'unreadMention', note._id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (isSpecified) {
 | 
				
			||||||
 | 
								publishUserStream(user._id, 'unreadSpecifiedNote', note._id);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}, 3000);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue