ハッシュタグタイムラインを実装
This commit is contained in:
		| @@ -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(); | ||||
|   | ||||
							
								
								
									
										65
									
								
								src/client/app/desktop/views/components/settings.tags.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/client/app/desktop/views/components/settings.tags.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo