メッセージタイムラインを追加
This commit is contained in:
		
							parent
							
								
									2fad6e6d5f
								
							
						
					
					
						commit
						ab83e08bc7
					
				|  | @ -169,6 +169,7 @@ common: | |||
|     hashtag: "ハッシュタグ" | ||||
|     global: "グローバル" | ||||
|     mentions: "あなた宛て" | ||||
|     direct: "ダイレクト投稿" | ||||
|     notifications: "通知" | ||||
|     list: "リスト" | ||||
|     swap-left: "左に移動" | ||||
|  | @ -916,6 +917,7 @@ desktop/views/components/timeline.vue: | |||
|   hybrid: "ソーシャル" | ||||
|   global: "グローバル" | ||||
|   mentions: "あなた宛て" | ||||
|   messages: "メッセージ" | ||||
|   list: "リスト" | ||||
|   hashtag: "ハッシュタグ" | ||||
|   add-tag-timeline: "ハッシュタグを追加" | ||||
|  | @ -1322,6 +1324,7 @@ mobile/views/pages/home.vue: | |||
|   hybrid: "ソーシャル" | ||||
|   global: "グローバル" | ||||
|   mentions: "あなた宛て" | ||||
|   messages: "メッセージ" | ||||
| 
 | ||||
| mobile/views/pages/tag.vue: | ||||
|   no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" | ||||
|  |  | |||
|  | @ -38,7 +38,14 @@ export default Vue.extend({ | |||
| 			streamManager: null, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			date: null | ||||
| 			date: null, | ||||
| 			baseQuery: { | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes | ||||
| 			}, | ||||
| 			query: {}, | ||||
| 			endpoint: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -47,53 +54,102 @@ export default Vue.extend({ | |||
| 			return this.$store.state.i.followingCount == 0; | ||||
| 		}, | ||||
| 
 | ||||
| 		endpoint(): string { | ||||
| 			switch (this.src) { | ||||
| 				case 'home': return 'notes/timeline'; | ||||
| 				case 'local': return 'notes/local-timeline'; | ||||
| 				case 'hybrid': return 'notes/hybrid-timeline'; | ||||
| 				case 'global': return 'notes/global-timeline'; | ||||
| 				case 'mentions': return 'notes/mentions'; | ||||
| 				case 'tag': return 'notes/search_by_tag'; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		canFetchMore(): boolean { | ||||
| 			return !this.moreFetching && !this.fetching && this.existMore; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		const prepend = note => { | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
| 		}; | ||||
| 
 | ||||
| 		if (this.src == 'tag') { | ||||
| 			this.endpoint = 'notes/search_by_tag'; | ||||
| 			this.query = { | ||||
| 				query: this.tagTl.query | ||||
| 			}; | ||||
| 			this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.connection.close(); | ||||
| 			}); | ||||
| 		} else if (this.src == 'home') { | ||||
| 			this.endpoint = 'notes/timeline'; | ||||
| 			const onChangeFollowing = () => { | ||||
| 				this.fetch(); | ||||
| 			}; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 			this.connection.on('follow', this.onChangeFollowing); | ||||
| 			this.connection.on('unfollow', this.onChangeFollowing); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.connection.on('follow', onChangeFollowing); | ||||
| 			this.connection.on('unfollow', onChangeFollowing); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.connection.off('follow', onChangeFollowing); | ||||
| 				this.connection.off('unfollow', onChangeFollowing); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'local') { | ||||
| 			this.endpoint = 'notes/local-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.localTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'hybrid') { | ||||
| 			this.endpoint = 'notes/hybrid-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.hybridTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'global') { | ||||
| 			this.endpoint = 'notes/global-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.globalTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'mentions') { | ||||
| 			this.endpoint = 'notes/mentions'; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('mention', this.onNote); | ||||
| 			this.connection.on('mention', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('mention', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'messages') { | ||||
| 			this.endpoint = 'notes/mentions'; | ||||
| 			this.query = { | ||||
| 				visibility: 'specified' | ||||
| 			}; | ||||
| 			const onNote = note => { | ||||
| 				if (note.visibility == 'specified') { | ||||
| 					prepend(note); | ||||
| 				} | ||||
| 			}; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('mention', onNote); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('mention', onNote); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		document.addEventListener('keydown', this.onKeydown); | ||||
|  | @ -102,28 +158,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		if (this.src == 'tag') { | ||||
| 			this.connection.off('note', this.onNote); | ||||
| 			this.connection.close(); | ||||
| 		} else if (this.src == 'home') { | ||||
| 			this.connection.off('note', this.onNote); | ||||
| 			this.connection.off('follow', this.onChangeFollowing); | ||||
| 			this.connection.off('unfollow', this.onChangeFollowing); | ||||
| 			this.streamManager.dispose(this.connectionId); | ||||
| 		} else if (this.src == 'local') { | ||||
| 			this.connection.off('note', this.onNote); | ||||
| 			this.streamManager.dispose(this.connectionId); | ||||
| 		} else if (this.src == 'hybrid') { | ||||
| 			this.connection.off('note', this.onNote); | ||||
| 			this.streamManager.dispose(this.connectionId); | ||||
| 		} else if (this.src == 'global') { | ||||
| 			this.connection.off('note', this.onNote); | ||||
| 			this.streamManager.dispose(this.connectionId); | ||||
| 		} else if (this.src == 'mentions') { | ||||
| 			this.connection.off('mention', this.onNote); | ||||
| 			this.streamManager.dispose(this.connectionId); | ||||
| 		} | ||||
| 
 | ||||
| 		this.$emit('beforeDestroy'); | ||||
| 		document.removeEventListener('keydown', this.onKeydown); | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -132,14 +167,10 @@ export default Vue.extend({ | |||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				(this as any).api(this.endpoint, { | ||||
| 				(this as any).api(this.endpoint, Object.assign({ | ||||
| 					limit: fetchLimit + 1, | ||||
| 					untilDate: this.date ? this.date.getTime() : undefined, | ||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 					query: this.tagTl ? this.tagTl.query : undefined | ||||
| 				}).then(notes => { | ||||
| 					untilDate: this.date ? this.date.getTime() : undefined | ||||
| 				}, this.baseQuery, this.query)).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
| 						this.existMore = true; | ||||
|  | @ -156,14 +187,10 @@ export default Vue.extend({ | |||
| 
 | ||||
| 			this.moreFetching = true; | ||||
| 
 | ||||
| 			const promise = (this as any).api(this.endpoint, { | ||||
| 			const promise = (this as any).api(this.endpoint, Object.assign({ | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 				query: this.tagTl ? this.tagTl.query : undefined | ||||
| 			}); | ||||
| 				untilId: (this.$refs.timeline as any).tail().id | ||||
| 			}, this.baseQuery, this.query)); | ||||
| 
 | ||||
| 			promise.then(notes => { | ||||
| 				if (notes.length == fetchLimit + 1) { | ||||
|  | @ -178,15 +205,6 @@ export default Vue.extend({ | |||
| 			return promise; | ||||
| 		}, | ||||
| 
 | ||||
| 		onNote(note) { | ||||
| 			// Prepend a note | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
| 		}, | ||||
| 
 | ||||
| 		onChangeFollowing() { | ||||
| 			this.fetch(); | ||||
| 		}, | ||||
| 
 | ||||
| 		focus() { | ||||
| 			(this.$refs.timeline as any).focus(); | ||||
| 		}, | ||||
|  |  | |||
|  | @ -5,10 +5,11 @@ | |||
| 		<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</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 == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</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> | ||||
| 		<div class="buttons"> | ||||
| 			<button :data-active="src == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%">%fa:at%</button> | ||||
| 			<button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%</button> | ||||
| 			<button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button> | ||||
| 			<button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button> | ||||
| 		</div> | ||||
|  | @ -18,6 +19,7 @@ | |||
| 	<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> | ||||
| 	<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> | ||||
| 	<x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> | ||||
| 	<x-core v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> | ||||
| 	<x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> | ||||
| 	<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> | ||||
| </div> | ||||
|  | @ -202,6 +204,20 @@ root(isDark) | |||
| 				&:active | ||||
| 					color isDark ? #b2c1d5 : #999 | ||||
| 
 | ||||
| 				&[data-active] | ||||
| 					color $theme-color | ||||
| 					cursor default | ||||
| 
 | ||||
| 					&:before | ||||
| 						content "" | ||||
| 						display block | ||||
| 						position absolute | ||||
| 						bottom 0 | ||||
| 						left 0 | ||||
| 						width 100% | ||||
| 						height 2px | ||||
| 						background $theme-color | ||||
| 
 | ||||
| 		> span | ||||
| 			display inline-block | ||||
| 			padding 0 10px | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ | |||
| <x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/> | ||||
| <x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked"/> | ||||
| <x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked"/> | ||||
| <x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked"/> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -16,13 +17,15 @@ import XTlColumn from './deck.tl-column.vue'; | |||
| import XNotificationsColumn from './deck.notifications-column.vue'; | ||||
| import XWidgetsColumn from './deck.widgets-column.vue'; | ||||
| import XMentionsColumn from './deck.mentions-column.vue'; | ||||
| import XDirectColumn from './deck.direct-column.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XTlColumn, | ||||
| 		XNotificationsColumn, | ||||
| 		XWidgetsColumn, | ||||
| 		XMentionsColumn | ||||
| 		XMentionsColumn, | ||||
| 		XDirectColumn | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
|  |  | |||
|  | @ -0,0 +1,38 @@ | |||
| <template> | ||||
| <x-column :name="name" :column="column" :is-stacked="isStacked"> | ||||
| 	<span slot="header">%fa:envelope R%{{ name }}</span> | ||||
| 
 | ||||
| 	<x-direct/> | ||||
| </x-column> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XDirect from './deck.direct.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XDirect | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		name(): string { | ||||
| 			if (this.column.name) return this.column.name; | ||||
| 			return '%i18n:common.deck.direct%'; | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
|  | @ -0,0 +1,97 @@ | |||
| <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: { | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = (this as any).os.stream.getConnection(); | ||||
| 		this.connectionId = (this as any).os.stream.use(); | ||||
| 
 | ||||
| 		this.connection.on('mention', this.onNote); | ||||
| 
 | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('mention', this.onNote); | ||||
| 		(this as any).os.stream.dispose(this.connectionId); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				(this as any).api('notes/mentions', { | ||||
| 					limit: fetchLimit + 1, | ||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 					visibility: 'specified' | ||||
| 				}).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('notes/mentions', { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 				visibility: 'specified' | ||||
| 			}); | ||||
| 
 | ||||
| 			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 | ||||
| 			if (note.visibility == 'specified') { | ||||
| 				(this.$refs.timeline as any).prepend(note); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | @ -147,6 +147,15 @@ export default Vue.extend({ | |||
| 							type: 'mentions' | ||||
| 						}); | ||||
| 					} | ||||
| 				}, { | ||||
| 					icon: '%fa:envelope R%', | ||||
| 					text: '%i18n:common.deck.direct%', | ||||
| 					action: () => { | ||||
| 						this.$store.dispatch('settings/addDeckColumn', { | ||||
| 							id: uuid(), | ||||
| 							type: 'direct' | ||||
| 						}); | ||||
| 					} | ||||
| 				}, { | ||||
| 					icon: '%fa:list%', | ||||
| 					text: '%i18n:common.deck.list%', | ||||
|  |  | |||
|  | @ -37,7 +37,14 @@ export default Vue.extend({ | |||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			unreadCount: 0, | ||||
| 			date: null | ||||
| 			date: null, | ||||
| 			baseQuery: { | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes | ||||
| 			}, | ||||
| 			query: {}, | ||||
| 			endpoint: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -46,80 +53,109 @@ export default Vue.extend({ | |||
| 			return this.$store.state.i.followingCount == 0; | ||||
| 		}, | ||||
| 
 | ||||
| 		endpoint(): string { | ||||
| 			switch (this.src) { | ||||
| 				case 'home': return 'notes/timeline'; | ||||
| 				case 'local': return 'notes/local-timeline'; | ||||
| 				case 'hybrid': return 'notes/hybrid-timeline'; | ||||
| 				case 'global': return 'notes/global-timeline'; | ||||
| 				case 'mentions': return 'notes/mentions'; | ||||
| 				case 'tag': return 'notes/search_by_tag'; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		canFetchMore(): boolean { | ||||
| 			return !this.moreFetching && !this.fetching && this.existMore; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		const prepend = note => { | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
| 		}; | ||||
| 
 | ||||
| 		if (this.src == 'tag') { | ||||
| 			this.endpoint = 'notes/search_by_tag'; | ||||
| 			this.query = { | ||||
| 				query: this.tagTl.query | ||||
| 			}; | ||||
| 			this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.connection.close(); | ||||
| 			}); | ||||
| 		} else if (this.src == 'home') { | ||||
| 			this.endpoint = 'notes/timeline'; | ||||
| 			const onChangeFollowing = () => { | ||||
| 				this.fetch(); | ||||
| 			}; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 			this.connection.on('follow', this.onChangeFollowing); | ||||
| 			this.connection.on('unfollow', this.onChangeFollowing); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.connection.on('follow', onChangeFollowing); | ||||
| 			this.connection.on('unfollow', onChangeFollowing); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.connection.off('follow', onChangeFollowing); | ||||
| 				this.connection.off('unfollow', onChangeFollowing); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'local') { | ||||
| 			this.endpoint = 'notes/local-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.localTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'hybrid') { | ||||
| 			this.endpoint = 'notes/hybrid-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.hybridTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'global') { | ||||
| 			this.endpoint = 'notes/global-timeline'; | ||||
| 			this.streamManager = (this as any).os.streams.globalTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('note', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'mentions') { | ||||
| 			this.endpoint = 'notes/mentions'; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('mention', this.onNote); | ||||
| 			this.connection.on('mention', prepend); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('mention', prepend); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} else if (this.src == 'messages') { | ||||
| 			this.endpoint = 'notes/mentions'; | ||||
| 			this.query = { | ||||
| 				visibility: 'specified' | ||||
| 			}; | ||||
| 			const onNote = note => { | ||||
| 				if (note.visibility == 'specified') { | ||||
| 					prepend(note); | ||||
| 				} | ||||
| 			}; | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('mention', onNote); | ||||
| 			this.$once('beforeDestroy', () => { | ||||
| 				this.connection.off('mention', onNote); | ||||
| 				this.streamManager.dispose(this.connectionId); | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		if (this.src == 'tag') { | ||||
| 			this.connection.off('note', this.onNote); | ||||
| 			this.connection.close(); | ||||
| 		} else if (this.src == 'home') { | ||||
| 			this.connection.off('note', this.onNote); | ||||
| 			this.connection.off('follow', this.onChangeFollowing); | ||||
| 			this.connection.off('unfollow', this.onChangeFollowing); | ||||
| 			this.streamManager.dispose(this.connectionId); | ||||
| 		} else if (this.src == 'local') { | ||||
| 			this.connection.off('note', this.onNote); | ||||
| 			this.streamManager.dispose(this.connectionId); | ||||
| 		} else if (this.src == 'hybrid') { | ||||
| 			this.connection.off('note', this.onNote); | ||||
| 			this.streamManager.dispose(this.connectionId); | ||||
| 		} else if (this.src == 'global') { | ||||
| 			this.connection.off('note', this.onNote); | ||||
| 			this.streamManager.dispose(this.connectionId); | ||||
| 		} else if (this.src == 'mentions') { | ||||
| 			this.connection.off('mention', this.onNote); | ||||
| 			this.streamManager.dispose(this.connectionId); | ||||
| 		} | ||||
| 		this.$emit('beforeDestroy'); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  | @ -127,14 +163,10 @@ export default Vue.extend({ | |||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				(this as any).api(this.endpoint, { | ||||
| 				(this as any).api(this.endpoint, Object.assign({ | ||||
| 					limit: fetchLimit + 1, | ||||
| 					untilDate: this.date ? this.date.getTime() : undefined, | ||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 					query: this.tagTl ? this.tagTl.query : undefined | ||||
| 				}).then(notes => { | ||||
| 					untilDate: this.date ? this.date.getTime() : undefined | ||||
| 				}, this.baseQuery, this.query)).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
| 						this.existMore = true; | ||||
|  | @ -151,14 +183,10 @@ export default Vue.extend({ | |||
| 
 | ||||
| 			this.moreFetching = true; | ||||
| 
 | ||||
| 			const promise = (this as any).api(this.endpoint, { | ||||
| 			const promise = (this as any).api(this.endpoint, Object.assign({ | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 				query: this.tagTl ? this.tagTl.query : undefined | ||||
| 			}); | ||||
| 				untilId: (this.$refs.timeline as any).tail().id | ||||
| 			}, this.baseQuery, this.query)); | ||||
| 
 | ||||
| 			promise.then(notes => { | ||||
| 				if (notes.length == fetchLimit + 1) { | ||||
|  | @ -173,15 +201,6 @@ export default Vue.extend({ | |||
| 			return promise; | ||||
| 		}, | ||||
| 
 | ||||
| 		onNote(note) { | ||||
| 			// Prepend a note | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
| 		}, | ||||
| 
 | ||||
| 		onChangeFollowing() { | ||||
| 			this.fetch(); | ||||
| 		}, | ||||
| 
 | ||||
| 		focus() { | ||||
| 			(this.$refs.timeline as any).focus(); | ||||
| 		}, | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ | |||
| 			<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span> | ||||
| 			<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span> | ||||
| 			<span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span> | ||||
| 			<span v-if="src == 'messages'">%fa:envelope R%%i18n:@messages%</span> | ||||
| 			<span v-if="src == 'list'">%fa:list%{{ list.title }}</span> | ||||
| 			<span v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span> | ||||
| 		</span> | ||||
|  | @ -23,16 +24,21 @@ | |||
| 	<main :data-darkmode="$store.state.device.darkmode"> | ||||
| 		<div class="nav" v-if="showNav"> | ||||
| 			<div class="bg" @click="showNav = false"></div> | ||||
| 			<div class="pointer"></div> | ||||
| 			<div class="body"> | ||||
| 				<div> | ||||
| 					<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> | ||||
| 					<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</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> | ||||
| 					<div class="hr"></div> | ||||
| 					<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span> | ||||
| 					<span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%</span> | ||||
| 					<template v-if="lists"> | ||||
| 						<div class="hr"></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> | ||||
| 					</template> | ||||
| 					<div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div> | ||||
| 					<span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id">%fa:hashtag% {{ tl.title }}</span> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | @ -44,6 +50,7 @@ | |||
| 			<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> | ||||
| 			<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> | ||||
| 			<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> | ||||
| 			<x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> | ||||
| 			<x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> | ||||
| 			<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> | ||||
| 		</div> | ||||
|  | @ -150,6 +157,26 @@ export default Vue.extend({ | |||
| 
 | ||||
| root(isDark) | ||||
| 	> .nav | ||||
| 		> .pointer | ||||
| 			position fixed | ||||
| 			z-index 10002 | ||||
| 			top 56px | ||||
| 			left 0 | ||||
| 			right 0 | ||||
| 
 | ||||
| 			$size = 16px | ||||
| 
 | ||||
| 			&:after | ||||
| 				content "" | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				top -($size * 2) | ||||
| 				left s('calc(50% - %s)', $size) | ||||
| 				border-top solid $size transparent | ||||
| 				border-left solid $size transparent | ||||
| 				border-right solid $size transparent | ||||
| 				border-bottom solid $size isDark ? #272f3a : #fff | ||||
| 
 | ||||
| 		> .bg | ||||
| 			position fixed | ||||
| 			z-index 10000 | ||||
|  | @ -166,28 +193,22 @@ root(isDark) | |||
| 			left 0 | ||||
| 			right 0 | ||||
| 			width 300px | ||||
| 			max-height calc(100% - 70px) | ||||
| 			margin 0 auto | ||||
| 			overflow auto | ||||
| 			-webkit-overflow-scrolling touch | ||||
| 			background isDark ? #272f3a : #fff | ||||
| 			border-radius 8px | ||||
| 			box-shadow 0 0 16px rgba(#000, 0.1) | ||||
| 
 | ||||
| 			$balloon-size = 16px | ||||
| 
 | ||||
| 			&:after | ||||
| 				content "" | ||||
| 				display block | ||||
| 				position absolute | ||||
| 				top -($balloon-size * 2) + 1.5px | ||||
| 				left s('calc(50% - %s)', $balloon-size) | ||||
| 				border-top solid $balloon-size transparent | ||||
| 				border-left solid $balloon-size transparent | ||||
| 				border-right solid $balloon-size transparent | ||||
| 				border-bottom solid $balloon-size isDark ? #272f3a : #fff | ||||
| 
 | ||||
| 			> div | ||||
| 				padding 8px 0 | ||||
| 
 | ||||
| 				> * | ||||
| 				> .hr | ||||
| 					margin 8px 0 | ||||
| 					border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1) | ||||
| 
 | ||||
| 				> *:not(.hr) | ||||
| 					display block | ||||
| 					padding 8px 16px | ||||
| 					color isDark ? #cdd0d8 : #666 | ||||
|  |  | |||
|  | @ -27,6 +27,9 @@ export const meta = { | |||
| 
 | ||||
| 		untilId: $.type(ID).optional.note({ | ||||
| 		}), | ||||
| 
 | ||||
| 		visibility: $.str.optional.note({ | ||||
| 		}), | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
|  | @ -52,6 +55,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = | |||
| 		_id: -1 | ||||
| 	}; | ||||
| 
 | ||||
| 	if (ps.visibility) { | ||||
| 		query.visibility = ps.visibility; | ||||
| 	} | ||||
| 
 | ||||
| 	if (ps.following) { | ||||
| 		const followingIds = await getFriendIds(user._id); | ||||
| 
 | ||||
|  |  | |||
|  | @ -142,6 +142,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< | |||
| 		mentionedUsers.push(await User.findOne({ _id: data.reply.userId })); | ||||
| 	} | ||||
| 
 | ||||
| 	if (data.visibility == 'specified') { | ||||
| 		data.visibleUsers.forEach(u => { | ||||
| 			if (!mentionedUsers.some(x => x._id.equals(u._id))) { | ||||
| 				mentionedUsers.push(u); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	const note = await insertNote(user, data, tags, mentionedUsers); | ||||
| 
 | ||||
| 	res(note); | ||||
|  | @ -188,7 +196,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< | |||
| 	const nm = new NotificationManager(user, note); | ||||
| 	const nmRelatedPromises = []; | ||||
| 
 | ||||
| 	createMentionedEvents(mentionedUsers, noteObj, nm); | ||||
| 	createMentionedEvents(mentionedUsers, note, nm); | ||||
| 
 | ||||
| 	const noteActivity = await renderActivity(data, note); | ||||
| 
 | ||||
|  | @ -318,7 +326,7 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren | |||
| 
 | ||||
| 	if (['public', 'home', 'followers'].includes(note.visibility)) { | ||||
| 		// フォロワーに配信
 | ||||
| 		publishToFollowers(note, noteObj, user, noteActivity); | ||||
| 		publishToFollowers(note, user, noteActivity); | ||||
| 	} | ||||
| 
 | ||||
| 	// リストに配信
 | ||||
|  | @ -456,7 +464,7 @@ async function publishToUserLists(note: INote, noteObj: any) { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function publishToFollowers(note: INote, noteObj: any, user: IUser, noteActivity: any) { | ||||
| async function publishToFollowers(note: INote, user: IUser, noteActivity: any) { | ||||
| 	const detailPackedNote = await pack(note, null, { | ||||
| 		detail: true, | ||||
| 		skipHide: true | ||||
|  | @ -505,9 +513,13 @@ function deliverNoteToMentionedRemoteUsers(mentionedUsers: IUser[], user: ILocal | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function createMentionedEvents(mentionedUsers: IUser[], noteObj: any, nm: NotificationManager) { | ||||
| function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: NotificationManager) { | ||||
| 	mentionedUsers.filter(u => isLocalUser(u)).forEach(async (u) => { | ||||
| 		publishUserStream(u._id, 'mention', noteObj); | ||||
| 		const detailPackedNote = await pack(note, u, { | ||||
| 			detail: true | ||||
| 		}); | ||||
| 
 | ||||
| 		publishUserStream(u._id, 'mention', detailPackedNote); | ||||
| 
 | ||||
| 		// Create notification
 | ||||
| 		nm.push(u._id, 'mention'); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue