Merge pull request 'Add scrollback' (#22) from scrollback into princess
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			Reviewed-on: #22
This commit is contained in:
		
						commit
						6da9f41519
					
				
					 5 changed files with 84 additions and 49 deletions
				
			
		| 
						 | 
					@ -27,7 +27,7 @@ class Chat extends ElemJS {
 | 
				
			||||||
		// connect to the new room's timeline updater
 | 
							// connect to the new room's timeline updater
 | 
				
			||||||
		if (store.activeRoom.exists()) {
 | 
							if (store.activeRoom.exists()) {
 | 
				
			||||||
			const timeline = store.activeRoom.value().timeline
 | 
								const timeline = store.activeRoom.value().timeline
 | 
				
			||||||
			const subscription = () => {
 | 
								const beforeChangeSubscription = () => {
 | 
				
			||||||
				// scroll anchor does not work if the timeline is scrolled to the top.
 | 
									// scroll anchor does not work if the timeline is scrolled to the top.
 | 
				
			||||||
				// at the start, when there are not enough messages for a full screen, this is the case.
 | 
									// at the start, when there are not enough messages for a full screen, this is the case.
 | 
				
			||||||
				// once there are enough messages that scrolling is necessary, we initiate a scroll down to activate the scroll anchor.
 | 
									// once there are enough messages that scrolling is necessary, we initiate a scroll down to activate the scroll anchor.
 | 
				
			||||||
| 
						 | 
					@ -40,12 +40,29 @@ class Chat extends ElemJS {
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}, 0)
 | 
									}, 0)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			const name = "beforeChange"
 | 
								this.addSubscription("beforeChange", timeline, beforeChangeSubscription)
 | 
				
			||||||
			this.removableSubscriptions.push({name, target: timeline, subscription})
 | 
					
 | 
				
			||||||
			timeline.subscribe(name, subscription)
 | 
								//Make sure after loading scrollback we don't move the scroll position
 | 
				
			||||||
 | 
								const beforeScrollbackLoadSubscription = () => {
 | 
				
			||||||
 | 
									const lastScrollHeight = chatMessages.scrollHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									const afterScrollbackLoadSub = () => {
 | 
				
			||||||
 | 
										const scrollDiff = chatMessages.scrollHeight - lastScrollHeight;
 | 
				
			||||||
 | 
										chatMessages.scrollTop += scrollDiff;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										timeline.unsubscribe("afterScrollbackLoad", afterScrollbackLoadSub)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									timeline.subscribe("afterScrollbackLoad", afterScrollbackLoadSub)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								this.addSubscription("beforeScrollbackLoad", timeline, beforeScrollbackLoadSubscription)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		this.render()
 | 
							this.render()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						addSubscription(name, target, subscription) {
 | 
				
			||||||
 | 
							this.removableSubscriptions.push({name, target, subscription})
 | 
				
			||||||
 | 
							target.subscribe(name, subscription)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render() {
 | 
						render() {
 | 
				
			||||||
		this.clearChildren()
 | 
							this.clearChildren()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,9 +33,9 @@ function eventSearch(list, event, min = 0, max = NO_MAX) {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// recurse (below)
 | 
						// recurse (below)
 | 
				
			||||||
	if (list[mid].data.origin_server_ts > event.data.origin_server_ts) return eventSearch(list, event, min, mid-1)
 | 
						if (list[mid].data.origin_server_ts > event.data.origin_server_ts) return eventSearch(list, event, min, mid - 1)
 | 
				
			||||||
	// recurse (above)
 | 
						// recurse (above)
 | 
				
			||||||
	else return eventSearch(list, event, mid+1, max)
 | 
						else return eventSearch(list, event, mid + 1, max)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Event extends ElemJS {
 | 
					class Event extends ElemJS {
 | 
				
			||||||
| 
						 | 
					@ -176,16 +176,43 @@ class EventGroup extends ElemJS {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Displays a spinner and creates an event to notify timeline to load more messages */
 | 
				
			||||||
 | 
					class LoadMore extends ElemJS {
 | 
				
			||||||
 | 
						constructor(id) {
 | 
				
			||||||
 | 
							super("div")
 | 
				
			||||||
 | 
							this.class("c-message-notice")
 | 
				
			||||||
 | 
							this.id = id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.child(
 | 
				
			||||||
 | 
								ejs("div").class("c-message-notice__inner").child(
 | 
				
			||||||
 | 
									ejs("span").class("loading-icon"),
 | 
				
			||||||
 | 
									ejs("span").text("Loading more...")
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							const intersection_observer = new IntersectionObserver(e => this.intersectionHandler(e))
 | 
				
			||||||
 | 
							intersection_observer.observe(this.element)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						intersectionHandler(e) {
 | 
				
			||||||
 | 
							if (e.some(e => e.isIntersecting)) {
 | 
				
			||||||
 | 
								store.rooms.get(this.id).value().timeline.loadScrollback()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReactiveTimeline extends ElemJS {
 | 
					class ReactiveTimeline extends ElemJS {
 | 
				
			||||||
	constructor(id, list) {
 | 
						constructor(id, list) {
 | 
				
			||||||
		super("div")
 | 
							super("div")
 | 
				
			||||||
		this.class("c-event-groups")
 | 
							this.class("c-event-groups")
 | 
				
			||||||
		this.id = id
 | 
							this.id = id
 | 
				
			||||||
		this.list = list
 | 
							this.list = list
 | 
				
			||||||
 | 
							this.loadMore = new LoadMore(this.id)
 | 
				
			||||||
		this.render()
 | 
							this.render()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addEvent(event) {
 | 
						addEvent(event) {
 | 
				
			||||||
 | 
							this.loadMore.remove()
 | 
				
			||||||
		// if (debug) console.log("running search", this.list, event)
 | 
							// if (debug) console.log("running search", this.list, event)
 | 
				
			||||||
		// if (debug) debugger;
 | 
							// if (debug) debugger;
 | 
				
			||||||
		const search = eventSearch(this.list, event)
 | 
							const search = eventSearch(this.list, event)
 | 
				
			||||||
| 
						 | 
					@ -193,7 +220,7 @@ class ReactiveTimeline extends ElemJS {
 | 
				
			||||||
		if (!search.success) {
 | 
							if (!search.success) {
 | 
				
			||||||
			if (search.i >= 1) {
 | 
								if (search.i >= 1) {
 | 
				
			||||||
				// add at end
 | 
									// add at end
 | 
				
			||||||
				this.tryAddGroups(event, [search.i-1, search.i])
 | 
									this.tryAddGroups(event, [search.i - 1, search.i])
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				// add at start
 | 
									// add at start
 | 
				
			||||||
				this.tryAddGroups(event, [0, -1])
 | 
									this.tryAddGroups(event, [0, -1])
 | 
				
			||||||
| 
						 | 
					@ -201,6 +228,8 @@ class ReactiveTimeline extends ElemJS {
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			this.tryAddGroups(event, [search.i])
 | 
								this.tryAddGroups(event, [search.i])
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							this.loadMore = new LoadMore(this.id)
 | 
				
			||||||
 | 
							this.childAt(0, this.loadMore)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tryAddGroups(event, indices) {
 | 
						tryAddGroups(event, indices) {
 | 
				
			||||||
| 
						 | 
					@ -233,6 +262,7 @@ class ReactiveTimeline extends ElemJS {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render() {
 | 
						render() {
 | 
				
			||||||
		this.clearChildren()
 | 
							this.clearChildren()
 | 
				
			||||||
 | 
							this.child(this.loadMore)
 | 
				
			||||||
		this.list.forEach(group => this.child(group))
 | 
							this.list.forEach(group => this.child(group))
 | 
				
			||||||
		this.anchor = new Anchor()
 | 
							this.anchor = new Anchor()
 | 
				
			||||||
		this.child(this.anchor)
 | 
							this.child(this.anchor)
 | 
				
			||||||
| 
						 | 
					@ -244,11 +274,15 @@ class Timeline extends Subscribable {
 | 
				
			||||||
		super()
 | 
							super()
 | 
				
			||||||
		Object.assign(this.events, {
 | 
							Object.assign(this.events, {
 | 
				
			||||||
			beforeChange: [],
 | 
								beforeChange: [],
 | 
				
			||||||
			afterChange: []
 | 
								afterChange: [],
 | 
				
			||||||
 | 
								beforeScrollbackLoad: [],
 | 
				
			||||||
 | 
								afterScrollbackLoad: [],
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		Object.assign(this.eventDeps, {
 | 
							Object.assign(this.eventDeps, {
 | 
				
			||||||
			beforeChange: [],
 | 
								beforeChange: [],
 | 
				
			||||||
			afterChange: []
 | 
								afterChange: [],
 | 
				
			||||||
 | 
								beforeScrollbackLoad: [],
 | 
				
			||||||
 | 
								afterScrollbackLoad: [],
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		this.room = room
 | 
							this.room = room
 | 
				
			||||||
		this.id = this.room.id
 | 
							this.id = this.room.id
 | 
				
			||||||
| 
						 | 
					@ -349,16 +383,21 @@ class Timeline extends Subscribable {
 | 
				
			||||||
		url.searchParams.set("access_token", lsm.get("access_token"))
 | 
							url.searchParams.set("access_token", lsm.get("access_token"))
 | 
				
			||||||
		url.searchParams.set("from", this.from)
 | 
							url.searchParams.set("from", this.from)
 | 
				
			||||||
		url.searchParams.set("dir", "b")
 | 
							url.searchParams.set("dir", "b")
 | 
				
			||||||
		url.searchParams.set("limit", 10)
 | 
							url.searchParams.set("limit", "20")
 | 
				
			||||||
		const filter = {
 | 
							const filter = {
 | 
				
			||||||
			lazy_load_members: true
 | 
								lazy_load_members: true
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		url.searchParams.set("filter", JSON.stringify(filter))
 | 
							url.searchParams.set("filter", JSON.stringify(filter))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const root = await fetch(url.toString()).then(res => res.json())
 | 
							const root = await fetch(url.toString()).then(res => res.json())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.broadcast("beforeScrollbackLoad")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.from = root.end
 | 
							this.from = root.end
 | 
				
			||||||
		console.log(this.updateEvents, root.chunk)
 | 
							// console.log(this.updateEvents, root.chunk)
 | 
				
			||||||
		if (root.state) this.updateStateEvents(root.state)
 | 
							if (root.state) this.updateStateEvents(root.state)
 | 
				
			||||||
		this.updateEvents(root.chunk)
 | 
							this.updateEvents(root.chunk)
 | 
				
			||||||
 | 
							this.broadcast("afterScrollbackLoad")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	send(body) {
 | 
						send(body) {
 | 
				
			||||||
| 
						 | 
					@ -385,32 +424,8 @@ class Timeline extends Subscribable {
 | 
				
			||||||
			headers: {
 | 
								headers: {
 | 
				
			||||||
				"Content-Type": "application/json"
 | 
									"Content-Type": "application/json"
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		})/*.then(() => {
 | 
							})
 | 
				
			||||||
			const subscription = () => {
 | 
					 | 
				
			||||||
				this.removeEvent(id)
 | 
					 | 
				
			||||||
				this.unsubscribe("afterChange", subscription)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			this.subscribe("afterChange", subscription)
 | 
					 | 
				
			||||||
		})*/
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	getGroupedEvents() {
 | 
					 | 
				
			||||||
		let currentSender = Symbol("N/A")
 | 
					 | 
				
			||||||
		let groups = []
 | 
					 | 
				
			||||||
		let currentGroup = []
 | 
					 | 
				
			||||||
		for (const event of this.list) {
 | 
					 | 
				
			||||||
			if (event.sender === currentSender) {
 | 
					 | 
				
			||||||
				currentGroup.push(event)
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				if (currentGroup.length) groups.push(currentGroup)
 | 
					 | 
				
			||||||
				currentGroup = [event]
 | 
					 | 
				
			||||||
				currentSender = event.sender
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if (currentGroup.length) groups.push(currentGroup)
 | 
					 | 
				
			||||||
		return groups
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {Timeline}
 | 
					module.exports = {Timeline}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								src/sass/loading.sass
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/sass/loading.sass
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					@keyframes spin
 | 
				
			||||||
 | 
					  0%
 | 
				
			||||||
 | 
					    transform: rotate(0deg)
 | 
				
			||||||
 | 
					  100%
 | 
				
			||||||
 | 
					    transform: rotate(180deg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.loading-icon
 | 
				
			||||||
 | 
					  display: inline-block
 | 
				
			||||||
 | 
					  background-color: #ccc
 | 
				
			||||||
 | 
					  width: 12px
 | 
				
			||||||
 | 
					  height: 12px
 | 
				
			||||||
 | 
					  margin-right: 6px
 | 
				
			||||||
 | 
					  animation: spin 0.7s infinite
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
@use "./base"
 | 
					@use "./base"
 | 
				
			||||||
 | 
					@use "./loading.sass"
 | 
				
			||||||
@use "./colors.sass" as c
 | 
					@use "./colors.sass" as c
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.main
 | 
					.main
 | 
				
			||||||
  justify-content: center
 | 
					  justify-content: center
 | 
				
			||||||
  align-items: center
 | 
					  align-items: center
 | 
				
			||||||
| 
						 | 
					@ -41,19 +43,6 @@
 | 
				
			||||||
.form-error
 | 
					.form-error
 | 
				
			||||||
  color: red
 | 
					  color: red
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@keyframes spin
 | 
					 | 
				
			||||||
  0%
 | 
					 | 
				
			||||||
    transform: rotate(0deg)
 | 
					 | 
				
			||||||
  100%
 | 
					 | 
				
			||||||
    transform: rotate(180deg)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.loading-icon
 | 
					 | 
				
			||||||
  display: inline-block
 | 
					 | 
				
			||||||
  background-color: #ccc
 | 
					 | 
				
			||||||
  width: 12px
 | 
					 | 
				
			||||||
  height: 12px
 | 
					 | 
				
			||||||
  margin-right: 6px
 | 
					 | 
				
			||||||
  animation: spin 0.7s infinite
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
input, button
 | 
					input, button
 | 
				
			||||||
  font-family: inherit
 | 
					  font-family: inherit
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,3 +5,4 @@
 | 
				
			||||||
@use "./components/chat"
 | 
					@use "./components/chat"
 | 
				
			||||||
@use "./components/chat-input"
 | 
					@use "./components/chat-input"
 | 
				
			||||||
@use "./components/anchor"
 | 
					@use "./components/anchor"
 | 
				
			||||||
 | 
					@use "./loading"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue