ハッシュタグタイムラインを実装
This commit is contained in:
		
							parent
							
								
									433dbe179d
								
							
						
					
					
						commit
						109738ccb9
					
				|  | @ -166,6 +166,7 @@ common: | |||
|     home: "ホーム" | ||||
|     local: "ローカル" | ||||
|     hybrid: "ソーシャル" | ||||
|     hashtag: "ハッシュタグ" | ||||
|     global: "グローバル" | ||||
|     mentions: "あなた宛て" | ||||
|     notifications: "通知" | ||||
|  | @ -916,6 +917,10 @@ desktop/views/components/timeline.vue: | |||
|   global: "グローバル" | ||||
|   mentions: "あなた宛て" | ||||
|   list: "リスト" | ||||
|   hashtag: "ハッシュタグ" | ||||
|   add-tag-timeline: "ハッシュタグを追加" | ||||
|   add-list: "リストを追加" | ||||
|   list-name: "リスト名" | ||||
| 
 | ||||
| desktop/views/components/ui.header.vue: | ||||
|   welcome-back: "おかえりなさい、" | ||||
|  |  | |||
|  | @ -0,0 +1,13 @@ | |||
| import Stream from './stream'; | ||||
| import MiOS from '../../../mios'; | ||||
| 
 | ||||
| export class HashtagStream extends Stream { | ||||
| 	constructor(os: MiOS, me, q) { | ||||
| 		super(os, 'hashtag', me ? { | ||||
| 			i: me.token, | ||||
| 			q: JSON.stringify(q) | ||||
| 		} : { | ||||
| 			q: JSON.stringify(q) | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | @ -1,13 +1,19 @@ | |||
| <template> | ||||
| <mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy"> | ||||
| 	<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span> | ||||
| 	<mk-settings @done="close"/> | ||||
| 	<mk-settings :initial-page="initialPage" @done="close"/> | ||||
| </mk-window> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		initialPage: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		close() { | ||||
| 			(this as any).$refs.window.close(); | ||||
|  |  | |||
|  | @ -0,0 +1,65 @@ | |||
| <template> | ||||
| <div class="vfcitkilproprqtbnpoertpsziierwzi"> | ||||
| 	<div v-for="timeline in timelines" class="timeline"> | ||||
| 		<ui-input v-model="timeline.title" @change="save"> | ||||
| 			<span>%i18n:@title%</span> | ||||
| 		</ui-input> | ||||
| 		<ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" @input="onQueryChange(timeline, $event)"> | ||||
| 			<span>%i18n:@query%</span> | ||||
| 		</ui-textarea> | ||||
| 		<ui-button class="save" @click="save">%i18n:@save%</ui-button> | ||||
| 	</div> | ||||
| 	<ui-button class="add" @click="add">%i18n:@add%</ui-button> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import * as uuid from 'uuid'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			timelines: this.$store.state.settings.tagTimelines | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		add() { | ||||
| 			this.timelines.push({ | ||||
| 				id: uuid(), | ||||
| 				title: '', | ||||
| 				query: '' | ||||
| 			}); | ||||
| 
 | ||||
| 			this.save(); | ||||
| 		}, | ||||
| 
 | ||||
| 		save() { | ||||
| 			this.$store.dispatch('settings/set', { key: 'tagTimelines', value: this.timelines }); | ||||
| 		}, | ||||
| 
 | ||||
| 		onQueryChange(timeline, value) { | ||||
| 			timeline.query = value.split('\n').map(tags => tags.split(' ')); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| 
 | ||||
| root(isDark) | ||||
| 	> .timeline | ||||
| 		padding-bottom 16px | ||||
| 		border-bottom solid 1px rgba(#000, 0.1) | ||||
| 
 | ||||
| 	> .add | ||||
| 		margin-top 16px | ||||
| 
 | ||||
| .vfcitkilproprqtbnpoertpsziierwzi[data-darkmode] | ||||
| 	root(true) | ||||
| 
 | ||||
| .vfcitkilproprqtbnpoertpsziierwzi:not([data-darkmode]) | ||||
| 	root(false) | ||||
| 
 | ||||
| </style> | ||||
|  | @ -5,6 +5,7 @@ | |||
| 		<p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p> | ||||
| 		<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p> | ||||
| 		<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p> | ||||
| 		<p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p> | ||||
| 		<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p> | ||||
| 		<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p> | ||||
| 		<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p> | ||||
|  | @ -138,6 +139,11 @@ | |||
| 			<x-drive/> | ||||
| 		</section> | ||||
| 
 | ||||
| 		<section class="hashtags" v-show="page == 'hashtags'"> | ||||
| 			<h1>%i18n:@tags%</h1> | ||||
| 			<x-tags/> | ||||
| 		</section> | ||||
| 
 | ||||
| 		<section class="mute" v-show="page == 'mute'"> | ||||
| 			<h1>%i18n:@mute%</h1> | ||||
| 			<x-mute/> | ||||
|  | @ -222,6 +228,7 @@ import XApi from './settings.api.vue'; | |||
| import XApps from './settings.apps.vue'; | ||||
| import XSignins from './settings.signins.vue'; | ||||
| import XDrive from './settings.drive.vue'; | ||||
| import XTags from './settings.tags.vue'; | ||||
| import { url, langs, version } from '../../../config'; | ||||
| import checkForUpdate from '../../../common/scripts/check-for-update'; | ||||
| 
 | ||||
|  | @ -234,11 +241,18 @@ export default Vue.extend({ | |||
| 		XApi, | ||||
| 		XApps, | ||||
| 		XSignins, | ||||
| 		XDrive | ||||
| 		XDrive, | ||||
| 		XTags | ||||
| 	}, | ||||
| 	props: { | ||||
| 		initialPage: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			page: 'profile', | ||||
| 			page: this.initialPage || 'profile', | ||||
| 			meta: null, | ||||
| 			version, | ||||
| 			langs, | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { HashtagStream } from '../../../common/scripts/streaming/hashtag'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
|  | @ -23,6 +24,9 @@ export default Vue.extend({ | |||
| 		src: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		tagTl: { | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -31,6 +35,7 @@ export default Vue.extend({ | |||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			streamManager: null, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			date: null | ||||
|  | @ -42,16 +47,6 @@ export default Vue.extend({ | |||
| 			return this.$store.state.i.followingCount == 0; | ||||
| 		}, | ||||
| 
 | ||||
| 		stream(): any { | ||||
| 			switch (this.src) { | ||||
| 				case 'home': return (this as any).os.stream; | ||||
| 				case 'local': return (this as any).os.streams.localTimelineStream; | ||||
| 				case 'hybrid': return (this as any).os.streams.hybridTimelineStream; | ||||
| 				case 'global': return (this as any).os.streams.globalTimelineStream; | ||||
| 				case 'mentions': return (this as any).os.stream; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		endpoint(): string { | ||||
| 			switch (this.src) { | ||||
| 				case 'home': return 'notes/timeline'; | ||||
|  | @ -59,6 +54,7 @@ export default Vue.extend({ | |||
| 				case 'hybrid': return 'notes/hybrid-timeline'; | ||||
| 				case 'global': return 'notes/global-timeline'; | ||||
| 				case 'mentions': return 'notes/mentions'; | ||||
| 				case 'tag': return 'notes/search_by_tag'; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
|  | @ -68,13 +64,36 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = this.stream.getConnection(); | ||||
| 		this.connectionId = this.stream.use(); | ||||
| 
 | ||||
| 		this.connection.on(this.src == 'mentions' ? 'mention' : 'note', this.onNote); | ||||
| 		if (this.src == 'home') { | ||||
| 		if (this.src == 'tag') { | ||||
| 			this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 		} else if (this.src == 'home') { | ||||
| 			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); | ||||
| 		} else if (this.src == 'local') { | ||||
| 			this.streamManager = (this as any).os.streams.localTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 		} else if (this.src == 'hybrid') { | ||||
| 			this.streamManager = (this as any).os.streams.hybridTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 		} else if (this.src == 'global') { | ||||
| 			this.streamManager = (this as any).os.streams.globalTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 		} else if (this.src == 'mentions') { | ||||
| 			this.streamManager = (this as any).os.stream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('mention', this.onNote); | ||||
| 		} | ||||
| 
 | ||||
| 		document.addEventListener('keydown', this.onKeydown); | ||||
|  | @ -83,12 +102,27 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off(this.src == 'mentions' ? 'mention' : 'note', this.onNote); | ||||
| 		if (this.src == 'home') { | ||||
| 		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.stream.dispose(this.connectionId); | ||||
| 
 | ||||
| 		document.removeEventListener('keydown', this.onKeydown); | ||||
| 	}, | ||||
|  | @ -103,7 +137,8 @@ export default Vue.extend({ | |||
| 					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 | ||||
| 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 					query: this.tagTl ? this.tagTl.query : undefined | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
|  | @ -126,7 +161,8 @@ export default Vue.extend({ | |||
| 				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 | ||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 				query: this.tagTl ? this.tagTl.query : undefined | ||||
| 			}); | ||||
| 
 | ||||
| 			promise.then(notes => { | ||||
|  |  | |||
|  | @ -6,14 +6,19 @@ | |||
| 		<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> | ||||
| 		<button @click="chooseList" title="%i18n:@list%">%fa:list%</button> | ||||
| 		<div class="buttons"> | ||||
| 			<button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button> | ||||
| 			<button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button> | ||||
| 		</div> | ||||
| 	</header> | ||||
| 	<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> | ||||
| 	<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> | ||||
| 	<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 == '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> | ||||
| </template> | ||||
|  | @ -21,7 +26,8 @@ | |||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XCore from './timeline.core.vue'; | ||||
| import MkUserListsWindow from './user-lists-window.vue'; | ||||
| import Menu from '../../../common/views/components/menu.vue'; | ||||
| import MkSettingsWindow from './settings-window.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
|  | @ -32,6 +38,7 @@ export default Vue.extend({ | |||
| 		return { | ||||
| 			src: 'home', | ||||
| 			list: null, | ||||
| 			tagTl: null, | ||||
| 			enableLocalTimeline: false | ||||
| 		}; | ||||
| 	}, | ||||
|  | @ -41,8 +48,14 @@ export default Vue.extend({ | |||
| 			this.saveSrc(); | ||||
| 		}, | ||||
| 
 | ||||
| 		list() { | ||||
| 		list(x) { | ||||
| 			this.saveSrc(); | ||||
| 			if (x != null) this.tagTl = null; | ||||
| 		}, | ||||
| 
 | ||||
| 		tagTl(x) { | ||||
| 			this.saveSrc(); | ||||
| 			if (x != null) this.list = null; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -55,6 +68,8 @@ export default Vue.extend({ | |||
| 			this.src = this.$store.state.device.tl.src; | ||||
| 			if (this.src == 'list') { | ||||
| 				this.list = this.$store.state.device.tl.arg; | ||||
| 			} else if (this.src == 'tag') { | ||||
| 				this.tagTl = this.$store.state.device.tl.arg; | ||||
| 			} | ||||
| 		} else if (this.$store.state.i.followingCount == 0) { | ||||
| 			this.src = 'hybrid'; | ||||
|  | @ -71,7 +86,7 @@ export default Vue.extend({ | |||
| 		saveSrc() { | ||||
| 			this.$store.commit('device/setTl', { | ||||
| 				src: this.src, | ||||
| 				arg: this.list | ||||
| 				arg: this.src == 'list' ? this.list : this.tagTl | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
|  | @ -79,12 +94,74 @@ export default Vue.extend({ | |||
| 			(this.$refs.tl as any).warp(date); | ||||
| 		}, | ||||
| 
 | ||||
| 		chooseList() { | ||||
| 			const w = (this as any).os.new(MkUserListsWindow); | ||||
| 			w.$once('choosen', list => { | ||||
| 				this.list = list; | ||||
| 				this.src = 'list'; | ||||
| 				w.close(); | ||||
| 		async chooseList() { | ||||
| 			const lists = await (this as any).api('users/lists/list'); | ||||
| 
 | ||||
| 			let menu = [{ | ||||
| 				icon: '%fa:plus%', | ||||
| 				text: '%i18n:@add-list%', | ||||
| 				action: () => { | ||||
| 					(this as any).apis.input({ | ||||
| 						title: '%i18n:@list-name%', | ||||
| 					}).then(async title => { | ||||
| 						const list = await (this as any).api('users/lists/create', { | ||||
| 							title | ||||
| 						}); | ||||
| 
 | ||||
| 						this.list = list; | ||||
| 						this.src = 'list'; | ||||
| 					}); | ||||
| 				} | ||||
| 			}]; | ||||
| 
 | ||||
| 			if (lists.length > 0) { | ||||
| 				menu.push(null); | ||||
| 			} | ||||
| 
 | ||||
| 			menu = menu.concat(lists.map(list => ({ | ||||
| 				icon: '%fa:list%', | ||||
| 				text: list.title, | ||||
| 				action: () => { | ||||
| 					this.list = list; | ||||
| 					this.src = 'list'; | ||||
| 				} | ||||
| 			}))); | ||||
| 
 | ||||
| 			this.os.new(Menu, { | ||||
| 				source: this.$refs.listButton, | ||||
| 				compact: false, | ||||
| 				items: menu | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		chooseTag() { | ||||
| 			let menu = [{ | ||||
| 				icon: '%fa:plus%', | ||||
| 				text: '%i18n:@add-tag-timeline%', | ||||
| 				action: () => { | ||||
| 					(this as any).os.new(MkSettingsWindow, { | ||||
| 						initialPage: 'hashtags' | ||||
| 					}); | ||||
| 				} | ||||
| 			}]; | ||||
| 
 | ||||
| 			if (this.$store.state.settings.tagTimelines.length > 0) { | ||||
| 				menu.push(null); | ||||
| 			} | ||||
| 
 | ||||
| 			menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({ | ||||
| 				icon: '%fa:hashtag%', | ||||
| 				text: t.title, | ||||
| 				action: () => { | ||||
| 					this.tagTl = t; | ||||
| 					this.src = 'tag'; | ||||
| 				} | ||||
| 			}))); | ||||
| 
 | ||||
| 			this.os.new(Menu, { | ||||
| 				source: this.$refs.tagButton, | ||||
| 				compact: false, | ||||
| 				items: menu | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | @ -106,22 +183,24 @@ root(isDark) | |||
| 		border-radius 6px 6px 0 0 | ||||
| 		box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08) | ||||
| 
 | ||||
| 		> button | ||||
| 		> .buttons | ||||
| 			position absolute | ||||
| 			z-index 2 | ||||
| 			top 0 | ||||
| 			right 0 | ||||
| 			padding 0 | ||||
| 			width 42px | ||||
| 			font-size 0.9em | ||||
| 			line-height 42px | ||||
| 			color isDark ? #9baec8 : #ccc | ||||
| 
 | ||||
| 			&:hover | ||||
| 				color isDark ? #b2c1d5 : #aaa | ||||
| 			> button | ||||
| 				padding 0 | ||||
| 				width 42px | ||||
| 				font-size 0.9em | ||||
| 				line-height 42px | ||||
| 				color isDark ? #9baec8 : #ccc | ||||
| 
 | ||||
| 			&:active | ||||
| 				color isDark ? #b2c1d5 : #999 | ||||
| 				&:hover | ||||
| 					color isDark ? #b2c1d5 : #aaa | ||||
| 
 | ||||
| 				&:active | ||||
| 					color isDark ? #b2c1d5 : #999 | ||||
| 
 | ||||
| 		> span | ||||
| 			display inline-block | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
| <x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/> | ||||
| <x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/> | ||||
| <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"/> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,117 @@ | |||
| <template> | ||||
| 	<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
| import { HashtagStream } from '../../../../common/scripts/streaming/hashtag'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XNotes | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		tagTl: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		mediaOnly: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		mediaView: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		mediaOnly() { | ||||
| 			this.fetch(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		if (this.connection) this.connection.close(); | ||||
| 		this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); | ||||
| 		this.connection.on('note', this.onNote); | ||||
| 
 | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.close(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				(this as any).api('notes/search_by_tag', { | ||||
| 					limit: fetchLimit + 1, | ||||
| 					withFiles: this.mediaOnly, | ||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 					query: this.tagTl.query | ||||
| 				}).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/search_by_tag', { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				withFiles: this.mediaOnly, | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 				query: this.tagTl.query | ||||
| 			}); | ||||
| 
 | ||||
| 			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) { | ||||
| 			if (this.mediaOnly && note.files.length == 0) return; | ||||
| 
 | ||||
| 			// Prepend a note | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | @ -6,6 +6,7 @@ | |||
| 		<template v-if="column.type == 'hybrid'">%fa:share-alt%</template> | ||||
| 		<template v-if="column.type == 'global'">%fa:globe%</template> | ||||
| 		<template v-if="column.type == 'list'">%fa:list%</template> | ||||
| 		<template v-if="column.type == 'hashtag'">%fa:hashtag%</template> | ||||
| 		<span>{{ name }}</span> | ||||
| 	</span> | ||||
| 
 | ||||
|  | @ -14,6 +15,7 @@ | |||
| 		<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/> | ||||
| 	</div> | ||||
| 	<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/> | ||||
| 	<x-hashtag-tl v-if="column.type == 'hashtag'" :tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/> | ||||
| 	<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/> | ||||
| </x-column> | ||||
| </template> | ||||
|  | @ -23,12 +25,14 @@ import Vue from 'vue'; | |||
| import XColumn from './deck.column.vue'; | ||||
| import XTl from './deck.tl.vue'; | ||||
| import XListTl from './deck.list-tl.vue'; | ||||
| import XHashtagTl from './deck.hashtag-tl.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XTl, | ||||
| 		XListTl | ||||
| 		XListTl, | ||||
| 		XHashtagTl | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
|  | @ -65,6 +69,7 @@ export default Vue.extend({ | |||
| 				case 'hybrid': return '%i18n:common.deck.hybrid%'; | ||||
| 				case 'global': return '%i18n:common.deck.global%'; | ||||
| 				case 'list': return this.column.list.title; | ||||
| 				case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title; | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|  |  | |||
|  | @ -161,6 +161,20 @@ export default Vue.extend({ | |||
| 							w.close(); | ||||
| 						}); | ||||
| 					} | ||||
| 				}, { | ||||
| 					icon: '%fa:hashtag%', | ||||
| 					text: '%i18n:common.deck.hashtag%', | ||||
| 					action: () => { | ||||
| 						(this as any).apis.input({ | ||||
| 							title: '%i18n:@enter-hashtag-tl-title%' | ||||
| 						}).then(title => { | ||||
| 							this.$store.dispatch('settings/addDeckColumn', { | ||||
| 								id: uuid(), | ||||
| 								type: 'hashtag', | ||||
| 								tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id | ||||
| 							}); | ||||
| 						}); | ||||
| 					} | ||||
| 				}, { | ||||
| 					icon: '%fa:bell R%', | ||||
| 					text: '%i18n:common.deck.notifications%', | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { HashtagStream } from '../../../common/scripts/streaming/hashtag'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
|  | @ -21,6 +22,9 @@ export default Vue.extend({ | |||
| 		src: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		tagTl: { | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -29,6 +33,7 @@ export default Vue.extend({ | |||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			streamManager: null, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			unreadCount: 0, | ||||
|  | @ -41,16 +46,6 @@ export default Vue.extend({ | |||
| 			return this.$store.state.i.followingCount == 0; | ||||
| 		}, | ||||
| 
 | ||||
| 		stream(): any { | ||||
| 			switch (this.src) { | ||||
| 				case 'home': return (this as any).os.stream; | ||||
| 				case 'local': return (this as any).os.streams.localTimelineStream; | ||||
| 				case 'hybrid': return (this as any).os.streams.hybridTimelineStream; | ||||
| 				case 'global': return (this as any).os.streams.globalTimelineStream; | ||||
| 				case 'mentions': return (this as any).os.stream; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		endpoint(): string { | ||||
| 			switch (this.src) { | ||||
| 				case 'home': return 'notes/timeline'; | ||||
|  | @ -58,6 +53,7 @@ export default Vue.extend({ | |||
| 				case 'hybrid': return 'notes/hybrid-timeline'; | ||||
| 				case 'global': return 'notes/global-timeline'; | ||||
| 				case 'mentions': return 'notes/mentions'; | ||||
| 				case 'tag': return 'notes/search_by_tag'; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
|  | @ -67,25 +63,63 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = this.stream.getConnection(); | ||||
| 		this.connectionId = this.stream.use(); | ||||
| 
 | ||||
| 		this.connection.on(this.src == 'mentions' ? 'mention' : 'note', this.onNote); | ||||
| 		if (this.src == 'home') { | ||||
| 		if (this.src == 'tag') { | ||||
| 			this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 		} else if (this.src == 'home') { | ||||
| 			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); | ||||
| 		} else if (this.src == 'local') { | ||||
| 			this.streamManager = (this as any).os.streams.localTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 		} else if (this.src == 'hybrid') { | ||||
| 			this.streamManager = (this as any).os.streams.hybridTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 		} else if (this.src == 'global') { | ||||
| 			this.streamManager = (this as any).os.streams.globalTimelineStream; | ||||
| 			this.connection = this.streamManager.getConnection(); | ||||
| 			this.connectionId = this.streamManager.use(); | ||||
| 			this.connection.on('note', this.onNote); | ||||
| 		} else if (this.src == '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.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off(this.src == 'mentions' ? 'mention' : 'note', this.onNote); | ||||
| 		if (this.src == 'home') { | ||||
| 		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.stream.dispose(this.connectionId); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  | @ -98,7 +132,8 @@ export default Vue.extend({ | |||
| 					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 | ||||
| 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 					query: this.tagTl ? this.tagTl.query : undefined | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
|  | @ -121,7 +156,8 @@ export default Vue.extend({ | |||
| 				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 | ||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes, | ||||
| 				query: this.tagTl ? this.tagTl.query : undefined | ||||
| 			}); | ||||
| 
 | ||||
| 			promise.then(notes => { | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ | |||
| 			<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span> | ||||
| 			<span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span> | ||||
| 			<span v-if="src == 'list'">%fa:list%{{ list.title }}</span> | ||||
| 			<span v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span> | ||||
| 		</span> | ||||
| 		<span style="margin-left:8px"> | ||||
| 			<template v-if="!showNav">%fa:angle-down%</template> | ||||
|  | @ -32,6 +33,7 @@ | |||
| 					<template v-if="lists"> | ||||
| 						<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> | ||||
| 					<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> | ||||
| 		</div> | ||||
|  | @ -42,6 +44,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 == '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> | ||||
| 	</main> | ||||
|  | @ -63,6 +66,7 @@ export default Vue.extend({ | |||
| 			src: 'home', | ||||
| 			list: null, | ||||
| 			lists: null, | ||||
| 			tagTl: null, | ||||
| 			showNav: false, | ||||
| 			enableLocalTimeline: false | ||||
| 		}; | ||||
|  | @ -74,9 +78,16 @@ export default Vue.extend({ | |||
| 			this.saveSrc(); | ||||
| 		}, | ||||
| 
 | ||||
| 		list() { | ||||
| 		list(x) { | ||||
| 			this.showNav = false; | ||||
| 			this.saveSrc(); | ||||
| 			if (x != null) this.tagTl = null; | ||||
| 		}, | ||||
| 
 | ||||
| 		tagTl(x) { | ||||
| 			this.showNav = false; | ||||
| 			this.saveSrc(); | ||||
| 			if (x != null) this.list = null; | ||||
| 		}, | ||||
| 
 | ||||
| 		showNav(v) { | ||||
|  | @ -97,6 +108,8 @@ export default Vue.extend({ | |||
| 			this.src = this.$store.state.device.tl.src; | ||||
| 			if (this.src == 'list') { | ||||
| 				this.list = this.$store.state.device.tl.arg; | ||||
| 			} else if (this.src == 'tag') { | ||||
| 				this.tagTl = this.$store.state.device.tl.arg; | ||||
| 			} | ||||
| 		} else if (this.$store.state.i.followingCount == 0) { | ||||
| 			this.src = 'hybrid'; | ||||
|  | @ -121,7 +134,7 @@ export default Vue.extend({ | |||
| 		saveSrc() { | ||||
| 			this.$store.commit('device/setTl', { | ||||
| 				src: this.src, | ||||
| 				arg: this.list | ||||
| 				arg: this.src == 'list' ? this.list : this.tagTl | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ const defaultSettings = { | |||
| 	home: null, | ||||
| 	mobileHome: [], | ||||
| 	deck: null, | ||||
| 	tagTimelines: [], | ||||
| 	fetchOnScroll: true, | ||||
| 	showMaps: true, | ||||
| 	showPostFormOnTopOfTl: false, | ||||
|  |  | |||
|  | @ -13,12 +13,18 @@ export const meta = { | |||
| 	}, | ||||
| 
 | ||||
| 	params: { | ||||
| 		tag: $.str.note({ | ||||
| 		tag: $.str.optional.note({ | ||||
| 			desc: { | ||||
| 				'ja-JP': 'タグ' | ||||
| 			} | ||||
| 		}), | ||||
| 
 | ||||
| 		query: $.arr($.arr($.str)).optional.note({ | ||||
| 			desc: { | ||||
| 				'ja-JP': 'クエリ' | ||||
| 			} | ||||
| 		}), | ||||
| 
 | ||||
| 		includeUserIds: $.arr($.type(ID)).optional.note({ | ||||
| 			default: [] | ||||
| 		}), | ||||
|  | @ -59,11 +65,9 @@ export const meta = { | |||
| 			} | ||||
| 		}), | ||||
| 
 | ||||
| 		withFiles: $.bool.optional.nullable.note({ | ||||
| 			default: null, | ||||
| 
 | ||||
| 		withFiles: $.bool.optional.note({ | ||||
| 			desc: { | ||||
| 				'ja-JP': 'ファイルが添付された投稿に限定するか否か' | ||||
| 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' | ||||
| 			} | ||||
| 		}), | ||||
| 
 | ||||
|  | @ -126,8 +130,14 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => | |||
| 	} | ||||
| 
 | ||||
| 	const q: any = { | ||||
| 		$and: [{ | ||||
| 		$and: [ps.tag ? { | ||||
| 			tagsLower: ps.tag.toLowerCase() | ||||
| 		} : { | ||||
| 			$or: ps.query.map(tags => ({ | ||||
| 				$and: tags.map(t => ({ | ||||
| 					tagsLower: t.toLowerCase() | ||||
| 				})) | ||||
| 			})) | ||||
| 		}], | ||||
| 		deletedAt: { $exists: false } | ||||
| 	}; | ||||
|  | @ -281,25 +291,10 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => | |||
| 
 | ||||
| 	const withFiles = ps.withFiles != null ? ps.withFiles : ps.media; | ||||
| 
 | ||||
| 	if (withFiles != null) { | ||||
| 		if (withFiles) { | ||||
| 			push({ | ||||
| 				fileIds: { | ||||
| 					$exists: true, | ||||
| 					$ne: null | ||||
| 				} | ||||
| 			}); | ||||
| 		} else { | ||||
| 			push({ | ||||
| 				$or: [{ | ||||
| 					fileIds: { | ||||
| 						$exists: false | ||||
| 					} | ||||
| 				}, { | ||||
| 					fileIds: null | ||||
| 				}] | ||||
| 			}); | ||||
| 		} | ||||
| 	if (withFiles) { | ||||
| 		push({ | ||||
| 			fileIds: { $exists: true, $ne: [] } | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	if (ps.poll != null) { | ||||
|  |  | |||
|  | @ -0,0 +1,48 @@ | |||
| import * as websocket from 'websocket'; | ||||
| import Xev from 'xev'; | ||||
| 
 | ||||
| import { IUser } from '../../../models/user'; | ||||
| import Mute from '../../../models/mute'; | ||||
| import { pack } from '../../../models/note'; | ||||
| 
 | ||||
| export default async function( | ||||
| 	request: websocket.request, | ||||
| 	connection: websocket.connection, | ||||
| 	subscriber: Xev, | ||||
| 	user?: IUser | ||||
| ) { | ||||
| 	const mute = user ? await Mute.find({ muterId: user._id }) : null; | ||||
| 	const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : []; | ||||
| 
 | ||||
| 	const q: Array<string[]> = JSON.parse((request.resourceURL.query as any).q); | ||||
| 
 | ||||
| 	// Subscribe stream
 | ||||
| 	subscriber.on('hashtag', async note => { | ||||
| 		const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase()))); | ||||
| 		if (!matched) return; | ||||
| 
 | ||||
| 		// Renoteなら再pack
 | ||||
| 		if (note.renoteId != null) { | ||||
| 			note.renote = await pack(note.renoteId, user, { | ||||
| 				detail: true | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||
| 		if (mutedUserIds.indexOf(note.userId) != -1) { | ||||
| 			return; | ||||
| 		} | ||||
| 		if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) { | ||||
| 			return; | ||||
| 		} | ||||
| 		if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) { | ||||
| 			return; | ||||
| 		} | ||||
| 		//#endregion
 | ||||
| 
 | ||||
| 		connection.send(JSON.stringify({ | ||||
| 			type: 'note', | ||||
| 			body: note | ||||
| 		})); | ||||
| 	}); | ||||
| } | ||||
|  | @ -14,6 +14,7 @@ import reversiGameStream from './stream/games/reversi-game'; | |||
| import reversiStream from './stream/games/reversi'; | ||||
| import serverStatsStream from './stream/server-stats'; | ||||
| import notesStatsStream from './stream/notes-stats'; | ||||
| import hashtagStream from './stream/hashtag'; | ||||
| import { ParsedUrlQuery } from 'querystring'; | ||||
| import authenticate from './authenticate'; | ||||
| 
 | ||||
|  | @ -57,6 +58,11 @@ module.exports = (server: http.Server) => { | |||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		if (request.resourceURL.pathname === '/hashtag') { | ||||
| 			hashtagStream(request, connection, ev, user); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		if (user == null) { | ||||
| 			connection.send('authentication-failed'); | ||||
| 			connection.close(); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import es from '../../db/elasticsearch'; | ||||
| import Note, { pack, INote } from '../../models/note'; | ||||
| import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user'; | ||||
| import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../stream'; | ||||
| import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../../stream'; | ||||
| import Following from '../../models/following'; | ||||
| import { deliver } from '../../queue'; | ||||
| import renderNote from '../../remote/activitypub/renderer/note'; | ||||
|  | @ -181,6 +181,10 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< | |||
| 		noteObj.isFirstNote = true; | ||||
| 	} | ||||
| 
 | ||||
| 	if (tags.length > 0) { | ||||
| 		publishHashtagStream(noteObj); | ||||
| 	} | ||||
| 
 | ||||
| 	const nm = new NotificationManager(user, note); | ||||
| 	const nmRelatedPromises = []; | ||||
| 
 | ||||
|  |  | |||
|  | @ -78,6 +78,10 @@ class Publisher { | |||
| 	public publishGlobalTimelineStream = (note: any): void => { | ||||
| 		this.publish('global-timeline', null, note); | ||||
| 	} | ||||
| 
 | ||||
| 	public publishHashtagStream = (note: any): void => { | ||||
| 		this.publish('hashtag', null, note); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| const publisher = new Publisher(); | ||||
|  | @ -95,3 +99,4 @@ export const publishReversiGameStream = publisher.publishReversiGameStream; | |||
| export const publishLocalTimelineStream = publisher.publishLocalTimelineStream; | ||||
| export const publishHybridTimelineStream = publisher.publishHybridTimelineStream; | ||||
| export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream; | ||||
| export const publishHashtagStream = publisher.publishHashtagStream; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue