wip
This commit is contained in:
		
							parent
							
								
									1ba5dfd79c
								
							
						
					
					
						commit
						c7f80182c2
					
				
					 7 changed files with 264 additions and 64 deletions
				
			
		|  | @ -14,6 +14,7 @@ | |||
| 		"vue/no-unused-vars": false, | ||||
| 		"vue/attributes-order": false, | ||||
| 		"vue/require-prop-types": false, | ||||
| 		"vue/require-default-prop": false, | ||||
| 		"no-console": 0, | ||||
| 		"no-unused-vars": 0, | ||||
| 		"no-empty": 0 | ||||
|  |  | |||
							
								
								
									
										75
									
								
								src/client/app/desktop/views/components/list-timeline.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/client/app/desktop/views/components/list-timeline.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | |||
| <template> | ||||
| 	<mk-notes ref="timeline" :more="more"/> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	props: ['list'], | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			connection: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	watch: { | ||||
| 		$route: 'fetch' | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id); | ||||
| 		this.connection.on('note', this.onNote); | ||||
| 		this.connection.on('userAdded', this.onUserAdded); | ||||
| 		this.connection.on('userRemoved', this.onUserRemoved); | ||||
| 
 | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.off('note', this.onNote); | ||||
| 		this.connection.off('userAdded', this.onUserAdded); | ||||
| 		this.connection.off('userRemoved', this.onUserRemoved); | ||||
| 		this.connection.close(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			(this as any).api('notes/list-timeline', { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes | ||||
| 			}).then(notes => { | ||||
| 				if (notes.length == fetchLimit + 1) { | ||||
| 					notes.pop(); | ||||
| 					this.existMore = true; | ||||
| 				} | ||||
| 				(this.$refs.timeline as any).init(notes); | ||||
| 				this.fetching = false; | ||||
| 				this.$emit('loaded'); | ||||
| 			}); | ||||
| 		}, | ||||
| 		more() { | ||||
| 			this.moreFetching = true; | ||||
| 
 | ||||
| 			(this as any).api('notes/list-timeline', { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes | ||||
| 			}).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; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | @ -2,10 +2,8 @@ | |||
| <mk-window ref="window" is-modal width="500px" height="550px" @closed="$destroy"> | ||||
| 	<span slot="header" :class="$style.header">%fa:list% リスト</span> | ||||
| 
 | ||||
| 	<button class="ui">リストを作成</button> | ||||
| 	<a v-for="list in lists" :key="list.id"> | ||||
| 
 | ||||
| 	</a> | ||||
| 	<button class="ui" @click="add">リストを作成</button> | ||||
| 	<router-link v-for="list in lists" :key="list.id" :to="`/i/lists/${list.id}`">{{ list.title }}</router-link> | ||||
| </mk-window> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -25,6 +23,17 @@ export default Vue.extend({ | |||
| 		}); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		add() { | ||||
| 			(this as any).apis.input({ | ||||
| 				title: 'リスト名', | ||||
| 			}).then(async title => { | ||||
| 				const list = await (this as any).api('users/lists/create', { | ||||
| 					title | ||||
| 				}); | ||||
| 
 | ||||
| 				this.$router.push(`i/lists/${ list.id }`); | ||||
| 			}); | ||||
| 		}, | ||||
| 		close() { | ||||
| 			(this as any).$refs.window.close(); | ||||
| 		} | ||||
|  |  | |||
|  | @ -9,8 +9,11 @@ | |||
| 			</p> | ||||
| 		</template> | ||||
| 	</transition-group> | ||||
| 	<footer> | ||||
| 		<slot name="footer"></slot> | ||||
| 	<footer v-if="loadMore"> | ||||
| 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||
| 			<template v-if="!moreFetching">%i18n:@load-more%</template> | ||||
| 			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template> | ||||
| 		</button> | ||||
| 	</footer> | ||||
| </div> | ||||
| </template> | ||||
|  | @ -19,16 +22,29 @@ | |||
| import Vue from 'vue'; | ||||
| import XNote from './notes.note.vue'; | ||||
| 
 | ||||
| const displayLimit = 30; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XNote | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		notes: { | ||||
| 			type: Array, | ||||
| 			default: () => [] | ||||
| 		more: { | ||||
| 			type: Function, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			notes: [], | ||||
| 			queue: [], | ||||
| 			fetching: false, | ||||
| 			moreFetching: false | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		_notes(): any[] { | ||||
| 			return (this.notes as any).map(note => { | ||||
|  | @ -40,12 +56,74 @@ export default Vue.extend({ | |||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		window.addEventListener('scroll', this.onScroll); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		window.removeEventListener('scroll', this.onScroll); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		isScrollTop() { | ||||
| 			return window.scrollY <= 8; | ||||
| 		}, | ||||
| 
 | ||||
| 		focus() { | ||||
| 			(this.$el as any).children[0].focus(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onNoteUpdated(i, note) { | ||||
| 			Vue.set((this as any).notes, i, note); | ||||
| 		}, | ||||
| 
 | ||||
| 		init(notes) { | ||||
| 			this.queue = []; | ||||
| 			this.notes = notes; | ||||
| 		}, | ||||
| 
 | ||||
| 		prepend(note) { | ||||
| 			if (this.isScrollTop()) { | ||||
| 				this.notes.unshift(note); | ||||
| 
 | ||||
| 				// オーバーフローしたら古い投稿は捨てる | ||||
| 				if (this.notes.length >= displayLimit) { | ||||
| 					this.notes = this.notes.slice(0, displayLimit); | ||||
| 				} | ||||
| 			} else { | ||||
| 				this.queue.unshift(note); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		append(note) { | ||||
| 			this.notes.push(note); | ||||
| 		}, | ||||
| 
 | ||||
| 		tail() { | ||||
| 			return this.notes[this.notes.length - 1]; | ||||
| 		}, | ||||
| 
 | ||||
| 		releaseQueue() { | ||||
| 			this.queue.forEach(n => this.prepend(n)); | ||||
| 			this.queue = []; | ||||
| 		}, | ||||
| 
 | ||||
| 		async loadMore() { | ||||
| 			this.moreFetching = true; | ||||
| 			await this.more(); | ||||
| 			this.moreFetching = false; | ||||
| 		}, | ||||
| 
 | ||||
| 		onScroll() { | ||||
| 			if (this.isScrollTop()) { | ||||
| 				this.releaseQueue(); | ||||
| 			} | ||||
| 
 | ||||
| 			if ((this as any).os.i.clientSettings.fetchOnScroll !== false) { | ||||
| 				const current = window.scrollY + window.innerHeight; | ||||
| 				if (current > document.body.offsetHeight - 8) this.loadMore(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <div class="mk-home-timeline"> | ||||
| <div class="mk-timeline-core"> | ||||
| 	<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> | ||||
| 	<mk-friends-maker v-if="src == 'home' && alone"/> | ||||
| 	<div class="fetching" v-if="fetching"> | ||||
|  | @ -8,12 +8,7 @@ | |||
| 	<p class="empty" v-if="notes.length == 0 && !fetching"> | ||||
| 		%fa:R comments%%i18n:@empty% | ||||
| 	</p> | ||||
| 	<mk-notes :notes="notes" ref="timeline"> | ||||
| 		<button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||
| 			<template v-if="!moreFetching">%i18n:@load-more%</template> | ||||
| 			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template> | ||||
| 		</button> | ||||
| 	</mk-notes> | ||||
| 	<mk-notes :notes="notes" ref="timeline" :more="canFetchMore ? more : null"/> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -22,7 +17,6 @@ import Vue from 'vue'; | |||
| import { url } from '../../../config'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| const displayLimit = 30; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
|  | @ -37,8 +31,6 @@ export default Vue.extend({ | |||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			notes: [], | ||||
| 			queue: [], | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			date: null | ||||
|  | @ -67,7 +59,7 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 
 | ||||
| 		canFetchMore(): boolean { | ||||
| 			return !this.moreFetching && !this.fetching && this.notes.length > 0 && this.existMore; | ||||
| 			return !this.moreFetching && !this.fetching && this.existMore; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -82,7 +74,6 @@ export default Vue.extend({ | |||
| 		} | ||||
| 
 | ||||
| 		document.addEventListener('keydown', this.onKeydown); | ||||
| 		window.addEventListener('scroll', this.onScroll); | ||||
| 
 | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|  | @ -96,7 +87,6 @@ export default Vue.extend({ | |||
| 		this.stream.dispose(this.connectionId); | ||||
| 
 | ||||
| 		document.removeEventListener('keydown', this.onKeydown); | ||||
| 		window.removeEventListener('scroll', this.onScroll); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  | @ -105,7 +95,6 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 
 | ||||
| 		fetch(cb?) { | ||||
| 			this.queue = []; | ||||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			(this as any).api(this.endpoint, { | ||||
|  | @ -118,7 +107,7 @@ export default Vue.extend({ | |||
| 					notes.pop(); | ||||
| 					this.existMore = true; | ||||
| 				} | ||||
| 				this.notes = notes; | ||||
| 				(this.$refs.timeline as any).init(notes); | ||||
| 				this.fetching = false; | ||||
| 				this.$emit('loaded'); | ||||
| 				if (cb) cb(); | ||||
|  | @ -132,7 +121,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 			(this as any).api(this.endpoint, { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: this.notes[this.notes.length - 1].id, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes | ||||
| 			}).then(notes => { | ||||
|  | @ -141,33 +130,11 @@ export default Vue.extend({ | |||
| 				} else { | ||||
| 					this.existMore = false; | ||||
| 				} | ||||
| 				this.notes = this.notes.concat(notes); | ||||
| 				notes.forEach(n => (this.$refs.timeline as any).append(n)); | ||||
| 				this.moreFetching = false; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		prependNote(note, silent = false) { | ||||
| 			// サウンドを再生する | ||||
| 			if ((this as any).os.isEnableSounds && !silent) { | ||||
| 				const sound = new Audio(`${url}/assets/post.mp3`); | ||||
| 				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; | ||||
| 				sound.play(); | ||||
| 			} | ||||
| 
 | ||||
| 			// Prepent a note | ||||
| 			this.notes.unshift(note); | ||||
| 
 | ||||
| 			// オーバーフローしたら古い投稿は捨てる | ||||
| 			if (this.notes.length >= displayLimit) { | ||||
| 				this.notes = this.notes.slice(0, displayLimit); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		releaseQueue() { | ||||
| 			this.queue.forEach(n => this.prependNote(n, true)); | ||||
| 			this.queue = []; | ||||
| 		}, | ||||
| 
 | ||||
| 		onNote(note) { | ||||
| 			//#region 弾く | ||||
| 			const isMyNote = note.userId == (this as any).os.i.id; | ||||
|  | @ -186,11 +153,15 @@ export default Vue.extend({ | |||
| 			} | ||||
| 			//#endregion | ||||
| 
 | ||||
| 			if (this.isScrollTop()) { | ||||
| 				this.prependNote(note); | ||||
| 			} else { | ||||
| 				this.queue.unshift(note); | ||||
| 			// サウンドを再生する | ||||
| 			if ((this as any).os.isEnableSounds) { | ||||
| 				const sound = new Audio(`${url}/assets/post.mp3`); | ||||
| 				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; | ||||
| 				sound.play(); | ||||
| 			} | ||||
| 
 | ||||
| 			// Prepend a note | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
| 		}, | ||||
| 
 | ||||
| 		onChangeFollowing() { | ||||
|  | @ -206,17 +177,6 @@ export default Vue.extend({ | |||
| 			this.fetch(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onScroll() { | ||||
| 			if ((this as any).os.i.clientSettings.fetchOnScroll !== false) { | ||||
| 				const current = window.scrollY + window.innerHeight; | ||||
| 				if (current > document.body.offsetHeight - 8) this.more(); | ||||
| 			} | ||||
| 
 | ||||
| 			if (this.isScrollTop()) { | ||||
| 				this.releaseQueue(); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onKeydown(e) { | ||||
| 			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { | ||||
| 				if (e.which == 84) { // t | ||||
|  | @ -231,7 +191,7 @@ export default Vue.extend({ | |||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
| 
 | ||||
| .mk-home-timeline | ||||
| .mk-timeline-core | ||||
| 	> .newer-indicator | ||||
| 		position -webkit-sticky | ||||
| 		position sticky | ||||
|  |  | |||
|  | @ -45,6 +45,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import MkListsWindow from './lists-window.vue'; | ||||
| import MkSettingsWindow from './settings-window.vue'; | ||||
| import MkDriveWindow from './drive-window.vue'; | ||||
| import contains from '../../../common/scripts/contains'; | ||||
|  | @ -83,6 +84,10 @@ export default Vue.extend({ | |||
| 			this.close(); | ||||
| 			(this as any).os.new(MkDriveWindow); | ||||
| 		}, | ||||
| 		list() { | ||||
| 			this.close(); | ||||
| 			(this as any).os.new(MkListsWindow); | ||||
| 		}, | ||||
| 		settings() { | ||||
| 			this.close(); | ||||
| 			(this as any).os.new(MkSettingsWindow); | ||||
|  |  | |||
							
								
								
									
										72
									
								
								src/client/app/desktop/views/pages/list.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/client/app/desktop/views/pages/list.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,72 @@ | |||
| <template> | ||||
| <mk-ui> | ||||
| 	<header :class="$style.header"> | ||||
| 		<h1>{{ list.title }}</h1> | ||||
| 	</header> | ||||
| 	<mk-list-timeline :list="list"/> | ||||
| </mk-ui> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			list: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	watch: { | ||||
| 		$route: 'fetch' | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			(this as any).api('users/lists/show', { | ||||
| 				id: this.$route.params.list | ||||
| 			}).then(list => { | ||||
| 				this.list = list; | ||||
| 				this.fetching = false; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" module> | ||||
| .header | ||||
| 	width 100% | ||||
| 	max-width 600px | ||||
| 	margin 0 auto | ||||
| 	color #555 | ||||
| 
 | ||||
| .notes | ||||
| 	max-width 600px | ||||
| 	margin 0 auto | ||||
| 	border solid 1px rgba(0, 0, 0, 0.075) | ||||
| 	border-radius 6px | ||||
| 	overflow hidden | ||||
| 
 | ||||
| .loading | ||||
| 	padding 64px 0 | ||||
| 
 | ||||
| .empty | ||||
| 	display block | ||||
| 	margin 0 auto | ||||
| 	padding 32px | ||||
| 	max-width 400px | ||||
| 	text-align center | ||||
| 	color #999 | ||||
| 
 | ||||
| 	> [data-fa] | ||||
| 		display block | ||||
| 		margin-bottom 16px | ||||
| 		font-size 3em | ||||
| 		color #ccc | ||||
| 
 | ||||
| </style> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue