diff --git a/src/js/chat.js b/src/js/chat.js index 9f8035f..050ff8b 100644 --- a/src/js/chat.js +++ b/src/js/chat.js @@ -27,7 +27,7 @@ class Chat extends ElemJS { // connect to the new room's timeline updater if (store.activeRoom.exists()) { const timeline = store.activeRoom.value().timeline - const subscription = () => { + const beforeChangeSubscription = () => { // 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. // 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) } - const name = "beforeChange" - this.removableSubscriptions.push({name, target: timeline, subscription}) - timeline.subscribe(name, subscription) + this.addSubscription("beforeChange", timeline, beforeChangeSubscription) + + //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() } + addSubscription(name, target, subscription) { + this.removableSubscriptions.push({name, target, subscription}) + target.subscribe(name, subscription) + } render() { this.clearChildren() diff --git a/src/js/timeline.js b/src/js/timeline.js index 0819ff8..270b5c8 100644 --- a/src/js/timeline.js +++ b/src/js/timeline.js @@ -33,9 +33,9 @@ function eventSearch(list, event, min = 0, max = NO_MAX) { } } // 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) - else return eventSearch(list, event, mid+1, max) + else return eventSearch(list, event, mid + 1, max) } 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 { constructor(id, list) { super("div") this.class("c-event-groups") this.id = id this.list = list + this.loadMore = new LoadMore(this.id) this.render() } addEvent(event) { + this.loadMore.remove() // if (debug) console.log("running search", this.list, event) // if (debug) debugger; const search = eventSearch(this.list, event) @@ -193,7 +220,7 @@ class ReactiveTimeline extends ElemJS { if (!search.success) { if (search.i >= 1) { // add at end - this.tryAddGroups(event, [search.i-1, search.i]) + this.tryAddGroups(event, [search.i - 1, search.i]) } else { // add at start this.tryAddGroups(event, [0, -1]) @@ -201,6 +228,8 @@ class ReactiveTimeline extends ElemJS { } else { this.tryAddGroups(event, [search.i]) } + this.loadMore = new LoadMore(this.id) + this.childAt(0, this.loadMore) } tryAddGroups(event, indices) { @@ -233,6 +262,7 @@ class ReactiveTimeline extends ElemJS { render() { this.clearChildren() + this.child(this.loadMore) this.list.forEach(group => this.child(group)) this.anchor = new Anchor() this.child(this.anchor) @@ -244,11 +274,15 @@ class Timeline extends Subscribable { super() Object.assign(this.events, { beforeChange: [], - afterChange: [] + afterChange: [], + beforeScrollbackLoad: [], + afterScrollbackLoad: [], }) Object.assign(this.eventDeps, { beforeChange: [], - afterChange: [] + afterChange: [], + beforeScrollbackLoad: [], + afterScrollbackLoad: [], }) this.room = room 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("from", this.from) url.searchParams.set("dir", "b") - url.searchParams.set("limit", 10) + url.searchParams.set("limit", "20") const filter = { lazy_load_members: true } url.searchParams.set("filter", JSON.stringify(filter)) + const root = await fetch(url.toString()).then(res => res.json()) + + this.broadcast("beforeScrollbackLoad") + this.from = root.end - console.log(this.updateEvents, root.chunk) + // console.log(this.updateEvents, root.chunk) if (root.state) this.updateStateEvents(root.state) this.updateEvents(root.chunk) + this.broadcast("afterScrollbackLoad") } send(body) { @@ -385,32 +424,8 @@ class Timeline extends Subscribable { headers: { "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} diff --git a/src/sass/loading.sass b/src/sass/loading.sass new file mode 100644 index 0000000..9705bbe --- /dev/null +++ b/src/sass/loading.sass @@ -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 diff --git a/src/sass/login.sass b/src/sass/login.sass index 235fad4..74d08ac 100644 --- a/src/sass/login.sass +++ b/src/sass/login.sass @@ -1,6 +1,8 @@ @use "./base" +@use "./loading.sass" @use "./colors.sass" as c + .main justify-content: center align-items: center @@ -41,19 +43,6 @@ .form-error 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 font-family: inherit diff --git a/src/sass/main.sass b/src/sass/main.sass index d342bbb..150af73 100644 --- a/src/sass/main.sass +++ b/src/sass/main.sass @@ -5,3 +5,4 @@ @use "./components/chat" @use "./components/chat-input" @use "./components/anchor" +@use "./loading"