チャンネルで入力中ユーザーを表示するように、Chat UIでタイムラインでは投稿フォームを上に表示するように
This commit is contained in:
		
							parent
							
								
									5f1a6b6f64
								
							
						
					
					
						commit
						25d37302a8
					
				
					 11 changed files with 143 additions and 41 deletions
				
			
		|  | @ -706,6 +706,7 @@ receiveAnnouncementFromInstance: "インスタンスからのお知らせを受 | ||||||
| emailNotification: "メール通知" | emailNotification: "メール通知" | ||||||
| inChannelSearch: "チャンネル内検索" | inChannelSearch: "チャンネル内検索" | ||||||
| useReactionPickerForContextMenu: "右クリックでリアクションピッカーを開く" | useReactionPickerForContextMenu: "右クリックでリアクションピッカーを開く" | ||||||
|  | typingUsers: "{users}が入力中" | ||||||
| 
 | 
 | ||||||
| _email: | _email: | ||||||
|   _follow: |   _follow: | ||||||
|  |  | ||||||
|  | @ -70,6 +70,7 @@ import * as os from '@/os'; | ||||||
| import { selectFile } from '@/scripts/select-file'; | import { selectFile } from '@/scripts/select-file'; | ||||||
| import { notePostInterruptors, postFormActions } from '@/store'; | import { notePostInterruptors, postFormActions } from '@/store'; | ||||||
| import { isMobile } from '@/scripts/is-mobile'; | import { isMobile } from '@/scripts/is-mobile'; | ||||||
|  | import { throttle } from 'throttle-debounce'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
|  | @ -144,6 +145,11 @@ export default defineComponent({ | ||||||
| 			quoteId: null, | 			quoteId: null, | ||||||
| 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), | 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), | ||||||
| 			imeText: '', | 			imeText: '', | ||||||
|  | 			typing: throttle(3000, () => { | ||||||
|  | 				if (this.channel) { | ||||||
|  | 					os.stream.send('typingOnChannel', { channel: this.channel.id }); | ||||||
|  | 				} | ||||||
|  | 			}), | ||||||
| 			postFormActions, | 			postFormActions, | ||||||
| 			faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug | 			faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug | ||||||
| 		}; | 		}; | ||||||
|  | @ -434,10 +440,12 @@ export default defineComponent({ | ||||||
| 		onKeydown(e: KeyboardEvent) { | 		onKeydown(e: KeyboardEvent) { | ||||||
| 			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); | 			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); | ||||||
| 			if (e.which === 27) this.$emit('esc'); | 			if (e.which === 27) this.$emit('esc'); | ||||||
|  | 			this.typing(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onCompositionUpdate(e: CompositionEvent) { | 		onCompositionUpdate(e: CompositionEvent) { | ||||||
| 			this.imeText = e.data; | 			this.imeText = e.data; | ||||||
|  | 			this.typing(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onCompositionEnd(e: CompositionEvent) { | 		onCompositionEnd(e: CompositionEvent) { | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ export default defineComponent({ | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return h(TransitionGroup, { | 		return h(this.reversed ? 'div' : TransitionGroup, { | ||||||
| 			class: 'hmjzthxl', | 			class: 'hmjzthxl', | ||||||
| 			name: this.reversed ? 'list-reversed' : 'list', | 			name: this.reversed ? 'list-reversed' : 'list', | ||||||
| 			tag: 'div', | 			tag: 'div', | ||||||
|  |  | ||||||
|  | @ -114,14 +114,9 @@ | ||||||
| 				</button> | 				</button> | ||||||
| 			</div> | 			</div> | ||||||
| 		</header> | 		</header> | ||||||
| 		<div class="body"> | 
 | ||||||
| 			<XTimeline v-if="tl.startsWith('channel:')" src="channel" :key="tl" :channel="tl.replace('channel:', '')"/> | 		<XTimeline class="body" v-if="tl.startsWith('channel:')" src="channel" :key="tl" :channel="tl.replace('channel:', '')"/> | ||||||
| 			<XTimeline v-else :src="tl" :key="tl"/> | 		<XTimeline class="body" v-else :src="tl" :key="tl"/> | ||||||
| 		</div> |  | ||||||
| 		<footer class="footer"> |  | ||||||
| 			<XPostForm v-if="tl.startsWith('channel:')" :key="tl" :channel="tl.replace('channel:', '')"/> |  | ||||||
| 			<XPostForm v-else/> |  | ||||||
| 		</footer> |  | ||||||
| 	</main> | 	</main> | ||||||
| 
 | 
 | ||||||
| 	<XSide class="side" ref="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/> | 	<XSide class="side" ref="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/> | ||||||
|  | @ -143,7 +138,6 @@ import XWidgets from './widgets.vue'; | ||||||
| import XCommon from '../_common_/common.vue'; | import XCommon from '../_common_/common.vue'; | ||||||
| import XSide from './side.vue'; | import XSide from './side.vue'; | ||||||
| import XTimeline from './timeline.vue'; | import XTimeline from './timeline.vue'; | ||||||
| import XPostForm from './post-form.vue'; |  | ||||||
| import XHeaderClock from './header-clock.vue'; | import XHeaderClock from './header-clock.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { router } from '@/router'; | import { router } from '@/router'; | ||||||
|  | @ -159,7 +153,6 @@ export default defineComponent({ | ||||||
| 		XWidgets, | 		XWidgets, | ||||||
| 		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる | 		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる | ||||||
| 		XTimeline, | 		XTimeline, | ||||||
| 		XPostForm, |  | ||||||
| 		XHeaderClock, | 		XHeaderClock, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -584,16 +577,6 @@ export default defineComponent({ | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 |  | ||||||
| 		> .footer { |  | ||||||
| 			padding: 0 16px 16px 16px; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .body { |  | ||||||
| 			flex: 1; |  | ||||||
| 			min-width: 0; |  | ||||||
| 			overflow: auto; |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> .side { | 	> .side { | ||||||
|  |  | ||||||
|  | @ -1010,7 +1010,7 @@ export default defineComponent({ | ||||||
| 			flex-shrink: 0; | 			flex-shrink: 0; | ||||||
| 			display: block; | 			display: block; | ||||||
| 			position: sticky; | 			position: sticky; | ||||||
| 			top: 12px; | 			top: 0; | ||||||
| 			margin: 0 14px 0 0; | 			margin: 0 14px 0 0; | ||||||
| 			width: 46px; | 			width: 46px; | ||||||
| 			height: 46px; | 			height: 46px; | ||||||
|  |  | ||||||
|  | @ -65,6 +65,7 @@ import * as os from '@/os'; | ||||||
| import { selectFile } from '@/scripts/select-file'; | import { selectFile } from '@/scripts/select-file'; | ||||||
| import { notePostInterruptors, postFormActions } from '@/store'; | import { notePostInterruptors, postFormActions } from '@/store'; | ||||||
| import { isMobile } from '@/scripts/is-mobile'; | import { isMobile } from '@/scripts/is-mobile'; | ||||||
|  | import { throttle } from 'throttle-debounce'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
|  | @ -131,6 +132,11 @@ export default defineComponent({ | ||||||
| 			quoteId: null, | 			quoteId: null, | ||||||
| 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), | 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), | ||||||
| 			imeText: '', | 			imeText: '', | ||||||
|  | 			typing: throttle(3000, () => { | ||||||
|  | 				if (this.channel) { | ||||||
|  | 					os.stream.send('typingOnChannel', { channel: this.channel }); | ||||||
|  | 				} | ||||||
|  | 			}), | ||||||
| 			postFormActions, | 			postFormActions, | ||||||
| 			faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug | 			faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug | ||||||
| 		}; | 		}; | ||||||
|  | @ -421,10 +427,12 @@ export default defineComponent({ | ||||||
| 		onKeydown(e: KeyboardEvent) { | 		onKeydown(e: KeyboardEvent) { | ||||||
| 			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); | 			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); | ||||||
| 			if (e.which === 27) this.$emit('esc'); | 			if (e.which === 27) this.$emit('esc'); | ||||||
|  | 			this.typing(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onCompositionUpdate(e: CompositionEvent) { | 		onCompositionUpdate(e: CompositionEvent) { | ||||||
| 			this.imeText = e.data; | 			this.imeText = e.data; | ||||||
|  | 			this.typing(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onCompositionEnd(e: CompositionEvent) { | 		onCompositionEnd(e: CompositionEvent) { | ||||||
|  |  | ||||||
|  | @ -1,8 +1,22 @@ | ||||||
| <template> | <template> | ||||||
| <div class="dbiokgaf"> | <div class="dbiokgaf top" v-if="['home', 'local', 'social', 'global'].includes(src)"> | ||||||
|  | 	<XPostForm/> | ||||||
|  | </div> | ||||||
|  | <div class="dbiokgaf tl" ref="body"> | ||||||
| 	<div class="new" v-if="queue > 0" :style="{ width: width + 'px', [pagination.reversed ? 'bottom' : 'top']: pagination.reversed ? bottom + 'px' : top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div> | 	<div class="new" v-if="queue > 0" :style="{ width: width + 'px', [pagination.reversed ? 'bottom' : 'top']: pagination.reversed ? bottom + 'px' : top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div> | ||||||
| 	<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated" v-follow="pagination.reversed"/> | 	<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated" v-follow="pagination.reversed"/> | ||||||
| </div> | </div> | ||||||
|  | <div class="dbiokgaf bottom" v-if="src === 'channel'"> | ||||||
|  | 	<div class="typers" v-if="typers.length > 0"> | ||||||
|  | 		<I18n :src="$ts.typingUsers" text-tag="span" class="users"> | ||||||
|  | 			<template #users> | ||||||
|  | 				<b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b> | ||||||
|  | 			</template> | ||||||
|  | 		</I18n> | ||||||
|  | 		<MkEllipsis/> | ||||||
|  | 	</div> | ||||||
|  | 	<XPostForm :channel="channel"/> | ||||||
|  | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -12,10 +26,12 @@ import * as os from '@/os'; | ||||||
| import * as sound from '@/scripts/sound'; | import * as sound from '@/scripts/sound'; | ||||||
| import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll'; | import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll'; | ||||||
| import follow from '@/directives/follow-append'; | import follow from '@/directives/follow-append'; | ||||||
|  | import XPostForm from './post-form.vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XNotes | 		XNotes, | ||||||
|  | 		XPostForm, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	directives: { | 	directives: { | ||||||
|  | @ -69,6 +85,7 @@ export default defineComponent({ | ||||||
| 			width: 0, | 			width: 0, | ||||||
| 			top: 0, | 			top: 0, | ||||||
| 			bottom: 0, | 			bottom: 0, | ||||||
|  | 			typers: [], | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -166,6 +183,9 @@ export default defineComponent({ | ||||||
| 				channelId: this.channel | 				channelId: this.channel | ||||||
| 			}); | 			}); | ||||||
| 			this.connection.on('note', prepend); | 			this.connection.on('note', prepend); | ||||||
|  | 			this.connection.on('typers', typers => { | ||||||
|  | 				this.typers = this.$i ? typers.filter(u => u.id !== this.$i.id) : typers; | ||||||
|  | 			}); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		this.pagination = { | 		this.pagination = { | ||||||
|  | @ -190,21 +210,21 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		focus() { | 		focus() { | ||||||
| 			this.$refs.tl.focus(); | 			this.$refs.body.focus(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		goTop() { | 		goTop() { | ||||||
| 			const container = getScrollContainer(this.$el); | 			const container = getScrollContainer(this.$refs.body); | ||||||
| 			container.scrollTop = 0; | 			container.scrollTop = 0; | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		queueUpdated(q) { | 		queueUpdated(q) { | ||||||
| 			if (this.$el.offsetWidth !== 0) { | 			if (this.$refs.body.offsetWidth !== 0) { | ||||||
| 				const rect = this.$el.getBoundingClientRect(); | 				const rect = this.$refs.body.getBoundingClientRect(); | ||||||
| 				const scrollTop = getScrollPosition(this.$el); | 				const scrollTop = getScrollPosition(this.$refs.body); | ||||||
| 				this.width = this.$el.offsetWidth; | 				this.width = this.$refs.body.offsetWidth; | ||||||
| 				this.top = rect.top + scrollTop; | 				this.top = rect.top + scrollTop; | ||||||
| 				this.bottom = this.$el.offsetHeight; | 				this.bottom = this.$refs.body.offsetHeight; | ||||||
| 			} | 			} | ||||||
| 			this.queue = q; | 			this.queue = q; | ||||||
| 		}, | 		}, | ||||||
|  | @ -213,11 +233,41 @@ export default defineComponent({ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .dbiokgaf { | .dbiokgaf.top { | ||||||
| 	padding: 16px 0; | 	padding: 16px 16px 0 16px; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 	// TODO: これはノート追加アニメーションによるスクロール発生を抑えるために必要だが、position stickyが効かなくなるので、両者を両立させる良い方法を考える | .dbiokgaf.bottom { | ||||||
| 	overflow: hidden; | 	padding: 0 16px 16px 16px; | ||||||
|  | 	position: relative; | ||||||
|  | 
 | ||||||
|  | 	> .typers { | ||||||
|  | 		position: absolute; | ||||||
|  | 		bottom: 100%; | ||||||
|  | 		padding: 0 8px 0 8px; | ||||||
|  | 		font-size: 0.9em; | ||||||
|  | 		background: var(--panel); | ||||||
|  | 		border-radius: 0 8px 0 0; | ||||||
|  | 		color: var(--fgTransparentWeak); | ||||||
|  | 
 | ||||||
|  | 		> .users { | ||||||
|  | 			> .user + .user:before { | ||||||
|  | 				content: ", "; | ||||||
|  | 				font-weight: normal; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			> .user:last-of-type:after { | ||||||
|  | 				content: " "; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .dbiokgaf.tl { | ||||||
|  | 	padding: 16px 0; | ||||||
|  | 	flex: 1; | ||||||
|  | 	min-width: 0; | ||||||
|  | 	overflow: auto; | ||||||
| 
 | 
 | ||||||
| 	> .new { | 	> .new { | ||||||
| 		position: fixed; | 		position: fixed; | ||||||
|  |  | ||||||
|  | @ -1,14 +1,17 @@ | ||||||
| import autobind from 'autobind-decorator'; | import autobind from 'autobind-decorator'; | ||||||
| import Channel from '../channel'; | import Channel from '../channel'; | ||||||
| import { Notes } from '../../../../models'; | import { Notes, Users } from '../../../../models'; | ||||||
| import { isMutedUserRelated } from '../../../../misc/is-muted-user-related'; | import { isMutedUserRelated } from '../../../../misc/is-muted-user-related'; | ||||||
| import { PackedNote } from '../../../../models/repositories/note'; | import { PackedNote } from '../../../../models/repositories/note'; | ||||||
|  | import { User } from '../../../../models/entities/user'; | ||||||
| 
 | 
 | ||||||
| export default class extends Channel { | export default class extends Channel { | ||||||
| 	public readonly chName = 'channel'; | 	public readonly chName = 'channel'; | ||||||
| 	public static shouldShare = false; | 	public static shouldShare = false; | ||||||
| 	public static requireCredential = false; | 	public static requireCredential = false; | ||||||
| 	private channelId: string; | 	private channelId: string; | ||||||
|  | 	private typers: Record<User['id'], Date> = {}; | ||||||
|  | 	private emitTypersIntervalId: ReturnType<typeof setInterval>; | ||||||
| 
 | 
 | ||||||
| 	@autobind | 	@autobind | ||||||
| 	public async init(params: any) { | 	public async init(params: any) { | ||||||
|  | @ -16,6 +19,8 @@ export default class extends Channel { | ||||||
| 
 | 
 | ||||||
| 		// Subscribe stream
 | 		// Subscribe stream
 | ||||||
| 		this.subscriber.on('notesStream', this.onNote); | 		this.subscriber.on('notesStream', this.onNote); | ||||||
|  | 		this.subscriber.on(`channelStream:${this.channelId}`, this.onEvent); | ||||||
|  | 		this.emitTypersIntervalId = setInterval(this.emitTypers, 5000); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@autobind | 	@autobind | ||||||
|  | @ -41,9 +46,41 @@ export default class extends Channel { | ||||||
| 		this.send('note', note); | 		this.send('note', note); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	@autobind | ||||||
|  | 	private onEvent(data: any) { | ||||||
|  | 		if (data.type === 'typing') { | ||||||
|  | 			const id = data.body; | ||||||
|  | 			const begin = this.typers[id] == null; | ||||||
|  | 			this.typers[id] = new Date(); | ||||||
|  | 			if (begin) { | ||||||
|  | 				this.emitTypers(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@autobind | ||||||
|  | 	private async emitTypers() { | ||||||
|  | 		const now = new Date(); | ||||||
|  | 
 | ||||||
|  | 		// Remove not typing users
 | ||||||
|  | 		for (const [userId, date] of Object.entries(this.typers)) { | ||||||
|  | 			if (now.getTime() - date.getTime() > 5000) delete this.typers[userId]; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		const users = await Users.packMany(Object.keys(this.typers), null, { detail: false }); | ||||||
|  | 
 | ||||||
|  | 		this.send({ | ||||||
|  | 			type: 'typers', | ||||||
|  | 			body: users, | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	@autobind | 	@autobind | ||||||
| 	public dispose() { | 	public dispose() { | ||||||
| 		// Unsubscribe events
 | 		// Unsubscribe events
 | ||||||
| 		this.subscriber.off('notesStream', this.onNote); | 		this.subscriber.off('notesStream', this.onNote); | ||||||
|  | 		this.subscriber.off(`channelStream:${this.channelId}`, this.onEvent); | ||||||
|  | 
 | ||||||
|  | 		clearInterval(this.emitTypersIntervalId); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ export default class extends Channel { | ||||||
| 
 | 
 | ||||||
| 	private gameId: ReversiGame['id'] | null = null; | 	private gameId: ReversiGame['id'] | null = null; | ||||||
| 	private watchers: Record<User['id'], Date> = {}; | 	private watchers: Record<User['id'], Date> = {}; | ||||||
| 	private emitWatchersIntervalId: any; | 	private emitWatchersIntervalId: ReturnType<typeof setInterval>; | ||||||
| 
 | 
 | ||||||
| 	@autobind | 	@autobind | ||||||
| 	public async init(params: any) { | 	public async init(params: any) { | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ import { Users, Followings, Mutings, UserProfiles, ChannelFollowings } from '../ | ||||||
| import { ApiError } from '../error'; | import { ApiError } from '../error'; | ||||||
| import { AccessToken } from '../../../models/entities/access-token'; | import { AccessToken } from '../../../models/entities/access-token'; | ||||||
| import { UserProfile } from '../../../models/entities/user-profile'; | import { UserProfile } from '../../../models/entities/user-profile'; | ||||||
|  | import { publishChannelStream } from '../../../services/stream'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Main stream connection |  * Main stream connection | ||||||
|  | @ -27,10 +28,10 @@ export default class Connection { | ||||||
| 	public subscriber: EventEmitter; | 	public subscriber: EventEmitter; | ||||||
| 	private channels: Channel[] = []; | 	private channels: Channel[] = []; | ||||||
| 	private subscribingNotes: any = {}; | 	private subscribingNotes: any = {}; | ||||||
| 	private followingClock: NodeJS.Timer; | 	private followingClock: ReturnType<typeof setInterval>; | ||||||
| 	private mutingClock: NodeJS.Timer; | 	private mutingClock: ReturnType<typeof setInterval>; | ||||||
| 	private followingChannelsClock: NodeJS.Timer; | 	private followingChannelsClock: ReturnType<typeof setInterval>; | ||||||
| 	private userProfileClock: NodeJS.Timer; | 	private userProfileClock: ReturnType<typeof setInterval>; | ||||||
| 
 | 
 | ||||||
| 	constructor( | 	constructor( | ||||||
| 		wsConnection: websocket.connection, | 		wsConnection: websocket.connection, | ||||||
|  | @ -93,6 +94,7 @@ export default class Connection { | ||||||
| 			case 'disconnect': this.onChannelDisconnectRequested(body); break; | 			case 'disconnect': this.onChannelDisconnectRequested(body); break; | ||||||
| 			case 'channel': this.onChannelMessageRequested(body); break; | 			case 'channel': this.onChannelMessageRequested(body); break; | ||||||
| 			case 'ch': this.onChannelMessageRequested(body); break; // alias
 | 			case 'ch': this.onChannelMessageRequested(body); break; // alias
 | ||||||
|  | 			case 'typingOnChannel': this.typingOnChannel(body.channel); break; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -258,6 +260,13 @@ export default class Connection { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	@autobind | ||||||
|  | 	private typingOnChannel(channel: ChannelModel['id']) { | ||||||
|  | 		if (this.user) { | ||||||
|  | 			publishChannelStream(channel, 'typing', this.user.id); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	@autobind | 	@autobind | ||||||
| 	private async updateFollowing() { | 	private async updateFollowing() { | ||||||
| 		const followings = await Followings.find({ | 		const followings = await Followings.find({ | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import { ReversiGame } from '../models/entities/games/reversi/game'; | ||||||
| import { UserGroup } from '../models/entities/user-group'; | import { UserGroup } from '../models/entities/user-group'; | ||||||
| import config from '../config'; | import config from '../config'; | ||||||
| import { Antenna } from '../models/entities/antenna'; | import { Antenna } from '../models/entities/antenna'; | ||||||
|  | import { Channel } from '../models/entities/channel'; | ||||||
| 
 | 
 | ||||||
| class Publisher { | class Publisher { | ||||||
| 	private publish = (channel: string, type: string | null, value?: any): void => { | 	private publish = (channel: string, type: string | null, value?: any): void => { | ||||||
|  | @ -38,6 +39,10 @@ class Publisher { | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	public publishChannelStream = (channelId: Channel['id'], type: string, value?: any): void => { | ||||||
|  | 		this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	public publishUserListStream = (listId: UserList['id'], type: string, value?: any): void => { | 	public publishUserListStream = (listId: UserList['id'], type: string, value?: any): void => { | ||||||
| 		this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); | 		this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); | ||||||
| 	} | 	} | ||||||
|  | @ -84,6 +89,7 @@ export const publishMainStream = publisher.publishMainStream; | ||||||
| export const publishDriveStream = publisher.publishDriveStream; | export const publishDriveStream = publisher.publishDriveStream; | ||||||
| export const publishNoteStream = publisher.publishNoteStream; | export const publishNoteStream = publisher.publishNoteStream; | ||||||
| export const publishNotesStream = publisher.publishNotesStream; | export const publishNotesStream = publisher.publishNotesStream; | ||||||
|  | export const publishChannelStream = publisher.publishChannelStream; | ||||||
| export const publishUserListStream = publisher.publishUserListStream; | export const publishUserListStream = publisher.publishUserListStream; | ||||||
| export const publishAntennaStream = publisher.publishAntennaStream; | export const publishAntennaStream = publisher.publishAntennaStream; | ||||||
| export const publishMessagingStream = publisher.publishMessagingStream; | export const publishMessagingStream = publisher.publishMessagingStream; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue