Better event grouping code
This commit is contained in:
parent
aaa7305b1c
commit
4b0b5c4b39
14 changed files with 316 additions and 126 deletions
|
@ -2,12 +2,12 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" type="text/css" href="static/main.css?static=0c50689ce5">
|
<link rel="stylesheet" type="text/css" href="static/main.css?static=da499eb39c">
|
||||||
<script type="module" src="static/groups.js?static=2cc7f0daf8"></script>
|
<script type="module" src="static/groups.js?static=2cc7f0daf8"></script>
|
||||||
<script type="module" src="static/chat-input.js?static=e8b21037fa"></script>
|
<script type="module" src="static/chat-input.js?static=e8b21037fa"></script>
|
||||||
<script type="module" src="static/room-picker.js?static=1d38378110"></script>
|
<script type="module" src="static/room-picker.js?static=1d38378110"></script>
|
||||||
<script type="module" src="static/sync/sync.js?static=9e31c8a727"></script>
|
<script type="module" src="static/sync/sync.js?static=9e31c8a727"></script>
|
||||||
<script type="module" src="static/chat.js?static=d4a415f1ae"></script>
|
<script type="module" src="static/chat.js?static=8a04bee48d"></script>
|
||||||
<title>Carbon</title>
|
<title>Carbon</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="c-rooms" id="c-rooms"></div>
|
<div class="c-rooms" id="c-rooms"></div>
|
||||||
<div class="c-chat">
|
<div class="c-chat">
|
||||||
<div class="c-chat__messages">
|
<div class="c-chat__messages" id="c-chat-messages">
|
||||||
<div class="c-chat__inner" id="c-chat"></div>
|
<div class="c-chat__inner" id="c-chat"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-chat-input">
|
<div class="c-chat-input">
|
||||||
|
|
15
build/static/Anchor.js
Normal file
15
build/static/Anchor.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import {ElemJS} from "./basic.js"
|
||||||
|
|
||||||
|
class Anchor extends ElemJS {
|
||||||
|
constructor() {
|
||||||
|
super("div")
|
||||||
|
this.class("c-anchor")
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll() {
|
||||||
|
console.log("anchor scrolled")
|
||||||
|
this.element.scrollIntoView({block: "start"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {Anchor}
|
|
@ -1,5 +1,27 @@
|
||||||
import {ElemJS} from "./basic.js"
|
import {ElemJS, ejs} from "./basic.js"
|
||||||
import {Subscribable} from "./store/Subscribable.js"
|
import {Subscribable} from "./store/Subscribable.js"
|
||||||
|
import {Anchor} from "./Anchor.js"
|
||||||
|
|
||||||
|
function eventSearch(list, event, min = 0, max = -1) {
|
||||||
|
if (list.length === 0) return {success: false, i: 0}
|
||||||
|
|
||||||
|
if (max === -1) max = list.length - 1
|
||||||
|
let mid = Math.floor((max + min) / 2)
|
||||||
|
// success condition
|
||||||
|
if (list[mid] && list[mid].data.event_id === event.data.event_id) return {success: true, i: mid}
|
||||||
|
// failed condition
|
||||||
|
if (min >= max) {
|
||||||
|
while (mid !== -1 && (!list[mid] || list[mid].data.origin_server_ts > event.data.origin_server_ts)) mid--
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
i: mid + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// recurse (below)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
class Event extends ElemJS {
|
class Event extends ElemJS {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
|
@ -19,61 +41,106 @@ class Event extends ElemJS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EventGroup extends ElemJS {
|
||||||
|
constructor(list) {
|
||||||
|
super("div")
|
||||||
|
this.class("c-message-group")
|
||||||
|
this.list = list
|
||||||
|
this.data = {
|
||||||
|
sender: list[0].data.sender,
|
||||||
|
origin_server_ts: list[0].data.origin_server_ts
|
||||||
|
}
|
||||||
|
this.child(
|
||||||
|
ejs("div").class("c-message-group__avatar").child(
|
||||||
|
ejs("div").class("c-message-group__icon")
|
||||||
|
),
|
||||||
|
this.messages = ejs("div").class("c-message-group__messages").child(
|
||||||
|
ejs("div").class("c-message-group__intro").child(
|
||||||
|
ejs("div").class("c-message-group__name").text(this.data.sender),
|
||||||
|
ejs("div").class("c-message-group__date").text("at 4:20 pm")
|
||||||
|
),
|
||||||
|
...this.list
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addEvent(event) {
|
||||||
|
const index = eventSearch(this.list, event).i
|
||||||
|
this.list.splice(index, 0, event)
|
||||||
|
this.messages.childAt(index + 1, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReactiveTimeline extends ElemJS {
|
||||||
|
constructor(list) {
|
||||||
|
super("div")
|
||||||
|
this.class("c-event-groups")
|
||||||
|
this.list = list
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
addEvent(event) {
|
||||||
|
const search = eventSearch(this.list, event)
|
||||||
|
// console.log(search, this.list.map(l => l.data.sender), event.data)
|
||||||
|
if (!search.success && search.i >= 1) this.tryAddGroups(event, [search.i-1, search.i])
|
||||||
|
else this.tryAddGroups(event, [search.i])
|
||||||
|
}
|
||||||
|
|
||||||
|
tryAddGroups(event, indices) {
|
||||||
|
const success = indices.some(i => {
|
||||||
|
if (!this.list[i]) {
|
||||||
|
// if (printed++ < 100) console.log("tryadd success, created group")
|
||||||
|
const group = new EventGroup([event])
|
||||||
|
this.list.splice(i, 0, group)
|
||||||
|
this.childAt(i, group)
|
||||||
|
return true
|
||||||
|
} else if (this.list[i] && this.list[i].data.sender === event.data.sender) {
|
||||||
|
// if (printed++ < 100) console.log("tryadd success, using existing group")
|
||||||
|
this.list[i].addEvent(event)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!success) console.log("tryadd failure", indices, this.list.map(l => l.data.sender), event.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.clearChildren()
|
||||||
|
this.list.forEach(group => this.child(group))
|
||||||
|
this.anchor = new Anchor()
|
||||||
|
this.child(this.anchor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Timeline extends Subscribable {
|
class Timeline extends Subscribable {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
Object.assign(this.events, {
|
Object.assign(this.events, {
|
||||||
addItem: [],
|
beforeChange: []
|
||||||
removeItem: []
|
|
||||||
})
|
})
|
||||||
Object.assign(this.eventDeps, {
|
Object.assign(this.eventDeps, {
|
||||||
addItem: [],
|
beforeChange: []
|
||||||
removeItem: []
|
|
||||||
})
|
})
|
||||||
this.list = []
|
this.list = []
|
||||||
this.map = new Map()
|
this.map = new Map()
|
||||||
this.elementsMap = new Map()
|
this.reactiveTimeline = new ReactiveTimeline([])
|
||||||
this.elementsList = []
|
|
||||||
}
|
|
||||||
|
|
||||||
_binarySearch(event, min = 0, max = -1) {
|
|
||||||
if (this.list.length === 0) return {success: false, i: 0}
|
|
||||||
|
|
||||||
if (max === -1) max = this.list.length - 1
|
|
||||||
let mid = Math.floor((max + min) / 2)
|
|
||||||
// success condition
|
|
||||||
if (this.list[mid] && this.list[mid].event_id === event.event_id) return {success: true, i: mid}
|
|
||||||
// failed condition
|
|
||||||
if (min >= max) {
|
|
||||||
while (mid !== -1 && (!this.list[mid] || this.list[mid].origin_server_ts > event.origin_server_ts)) mid--
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
i: mid + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// recurse (below)
|
|
||||||
if (this.list[mid].origin_server_ts > event.origin_server_ts) return this._binarySearch(event, min, mid-1)
|
|
||||||
// recurse (above)
|
|
||||||
else return this._binarySearch(event, mid+1, max)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEvents(events) {
|
updateEvents(events) {
|
||||||
for (const event of events) {
|
this.broadcast("beforeChange")
|
||||||
if (this.map.has(event.event_id)) {
|
for (const eventData of events) {
|
||||||
this.map.set(event.event_id, event)
|
if (this.map.has(eventData.event_id)) {
|
||||||
this.elementsMap.get(event.event_id).update(this.map.get(event.event_id))
|
this.map.get(eventData.event_id).update(eventData)
|
||||||
} else {
|
} else {
|
||||||
const index = this._binarySearch(event).i
|
const event = new Event(eventData)
|
||||||
this.list.splice(index, 0, event)
|
this.reactiveTimeline.addEvent(event)
|
||||||
this.map.set(event.event_id, event)
|
|
||||||
const e = new Event(event)
|
|
||||||
this.elementsList.splice(index, 0, e)
|
|
||||||
this.elementsMap.set(event.event_id, e)
|
|
||||||
this.broadcast("addItem", {index, element: e})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTimeline() {
|
||||||
|
return this.reactiveTimeline
|
||||||
|
}
|
||||||
|
/*
|
||||||
getGroupedEvents() {
|
getGroupedEvents() {
|
||||||
let currentSender = Symbol("N/A")
|
let currentSender = Symbol("N/A")
|
||||||
let groups = []
|
let groups = []
|
||||||
|
@ -90,6 +157,7 @@ class Timeline extends Subscribable {
|
||||||
if (currentGroup.length) groups.push(currentGroup)
|
if (currentGroup.length) groups.push(currentGroup)
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
export {Timeline}
|
export {Timeline}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {ElemJS, q, ejs} from "./basic.js"
|
import {ElemJS, q, ejs} from "./basic.js"
|
||||||
import {store} from "./store/store.js"
|
import {store} from "./store/store.js"
|
||||||
|
|
||||||
|
const chatMessages = q("#c-chat-messages")
|
||||||
|
|
||||||
class Chat extends ElemJS {
|
class Chat extends ElemJS {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(q("#c-chat"))
|
super(q("#c-chat"))
|
||||||
|
@ -25,10 +27,20 @@ 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 = (_, {element, index}) => {
|
const subscription = () => {
|
||||||
this.childAt(index, element)
|
// 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.
|
||||||
|
let oldDifference = chatMessages.scrollHeight - chatMessages.clientHeight
|
||||||
|
setTimeout(() => {
|
||||||
|
let newDifference = chatMessages.scrollHeight - chatMessages.clientHeight
|
||||||
|
console.log("height difference", oldDifference, newDifference)
|
||||||
|
if (oldDifference < 24) { // this is jank
|
||||||
|
this.element.parentElement.scrollBy(0, 1000)
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
const name = "addItem"
|
const name = "beforeChange"
|
||||||
this.removableSubscriptions.push({name, target: timeline, subscription})
|
this.removableSubscriptions.push({name, target: timeline, subscription})
|
||||||
timeline.subscribe(name, subscription)
|
timeline.subscribe(name, subscription)
|
||||||
}
|
}
|
||||||
|
@ -38,25 +50,16 @@ class Chat extends ElemJS {
|
||||||
render() {
|
render() {
|
||||||
this.clearChildren()
|
this.clearChildren()
|
||||||
if (store.activeRoom.exists()) {
|
if (store.activeRoom.exists()) {
|
||||||
const timeline = store.activeRoom.value().timeline
|
const reactiveTimeline = store.activeRoom.value().timeline.getTimeline()
|
||||||
for (const group of timeline.getGroupedEvents()) {
|
this.child(reactiveTimeline)
|
||||||
const first = group[0]
|
setTimeout(() => {
|
||||||
this.child(
|
this.element.parentElement.scrollBy(0, 1)
|
||||||
ejs("div").class("c-message-group").child(
|
reactiveTimeline.anchor.scroll()
|
||||||
ejs("div").class("c-message-group__avatar").child(
|
}, 0)
|
||||||
ejs("div").class("c-message-group__icon")
|
|
||||||
),
|
|
||||||
ejs("div").class("c-message-group__messages").child(
|
|
||||||
ejs("div").class("c-message-group__intro").child(
|
|
||||||
ejs("div").class("c-message-group__name").text(first.sender)
|
|
||||||
),
|
|
||||||
...group.map(event => timeline.elementsMap.get(event.event_id))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new Chat()
|
const chat = new Chat()
|
||||||
|
|
||||||
|
export {chat}
|
||||||
|
|
|
@ -136,6 +136,10 @@ body {
|
||||||
border-radius: 0px 6px 6px 0px;
|
border-radius: 0px 6px 6px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-event-groups * {
|
||||||
|
overflow-anchor: none;
|
||||||
|
}
|
||||||
|
|
||||||
.c-message-group, .c-message-event {
|
.c-message-group, .c-message-event {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
|
@ -206,7 +210,7 @@ body {
|
||||||
|
|
||||||
.c-chat {
|
.c-chat {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr auto;
|
grid-template-rows: 1fr 82px;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -224,6 +228,7 @@ body {
|
||||||
.c-chat-input {
|
.c-chat-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-top: 2px solid #4b4e54;
|
border-top: 2px solid #4b4e54;
|
||||||
|
background-color: #36393e;
|
||||||
}
|
}
|
||||||
.c-chat-input__textarea {
|
.c-chat-input__textarea {
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
|
@ -241,4 +246,9 @@ body {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
resize: none;
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-anchor {
|
||||||
|
overflow-anchor: auto;
|
||||||
|
height: 1px;
|
||||||
}
|
}
|
|
@ -48,7 +48,7 @@ html
|
||||||
.c-groups__container#c-groups-list
|
.c-groups__container#c-groups-list
|
||||||
.c-rooms#c-rooms
|
.c-rooms#c-rooms
|
||||||
.c-chat
|
.c-chat
|
||||||
.c-chat__messages
|
.c-chat__messages#c-chat-messages
|
||||||
.c-chat__inner#c-chat
|
.c-chat__inner#c-chat
|
||||||
.c-chat-input
|
.c-chat-input
|
||||||
textarea(placeholder="Send a message..." autocomplete="off").c-chat-input__textarea#c-chat-textarea
|
textarea(placeholder="Send a message..." autocomplete="off").c-chat-input__textarea#c-chat-textarea
|
15
src/js/Anchor.js
Normal file
15
src/js/Anchor.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import {ElemJS} from "./basic.js"
|
||||||
|
|
||||||
|
class Anchor extends ElemJS {
|
||||||
|
constructor() {
|
||||||
|
super("div")
|
||||||
|
this.class("c-anchor")
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll() {
|
||||||
|
console.log("anchor scrolled")
|
||||||
|
this.element.scrollIntoView({block: "start"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {Anchor}
|
|
@ -1,5 +1,27 @@
|
||||||
import {ElemJS} from "./basic.js"
|
import {ElemJS, ejs} from "./basic.js"
|
||||||
import {Subscribable} from "./store/Subscribable.js"
|
import {Subscribable} from "./store/Subscribable.js"
|
||||||
|
import {Anchor} from "./Anchor.js"
|
||||||
|
|
||||||
|
function eventSearch(list, event, min = 0, max = -1) {
|
||||||
|
if (list.length === 0) return {success: false, i: 0}
|
||||||
|
|
||||||
|
if (max === -1) max = list.length - 1
|
||||||
|
let mid = Math.floor((max + min) / 2)
|
||||||
|
// success condition
|
||||||
|
if (list[mid] && list[mid].data.event_id === event.data.event_id) return {success: true, i: mid}
|
||||||
|
// failed condition
|
||||||
|
if (min >= max) {
|
||||||
|
while (mid !== -1 && (!list[mid] || list[mid].data.origin_server_ts > event.data.origin_server_ts)) mid--
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
i: mid + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// recurse (below)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
class Event extends ElemJS {
|
class Event extends ElemJS {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
|
@ -19,61 +41,106 @@ class Event extends ElemJS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EventGroup extends ElemJS {
|
||||||
|
constructor(list) {
|
||||||
|
super("div")
|
||||||
|
this.class("c-message-group")
|
||||||
|
this.list = list
|
||||||
|
this.data = {
|
||||||
|
sender: list[0].data.sender,
|
||||||
|
origin_server_ts: list[0].data.origin_server_ts
|
||||||
|
}
|
||||||
|
this.child(
|
||||||
|
ejs("div").class("c-message-group__avatar").child(
|
||||||
|
ejs("div").class("c-message-group__icon")
|
||||||
|
),
|
||||||
|
this.messages = ejs("div").class("c-message-group__messages").child(
|
||||||
|
ejs("div").class("c-message-group__intro").child(
|
||||||
|
ejs("div").class("c-message-group__name").text(this.data.sender),
|
||||||
|
ejs("div").class("c-message-group__date").text("at 4:20 pm")
|
||||||
|
),
|
||||||
|
...this.list
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addEvent(event) {
|
||||||
|
const index = eventSearch(this.list, event).i
|
||||||
|
this.list.splice(index, 0, event)
|
||||||
|
this.messages.childAt(index + 1, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReactiveTimeline extends ElemJS {
|
||||||
|
constructor(list) {
|
||||||
|
super("div")
|
||||||
|
this.class("c-event-groups")
|
||||||
|
this.list = list
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
addEvent(event) {
|
||||||
|
const search = eventSearch(this.list, event)
|
||||||
|
// console.log(search, this.list.map(l => l.data.sender), event.data)
|
||||||
|
if (!search.success && search.i >= 1) this.tryAddGroups(event, [search.i-1, search.i])
|
||||||
|
else this.tryAddGroups(event, [search.i])
|
||||||
|
}
|
||||||
|
|
||||||
|
tryAddGroups(event, indices) {
|
||||||
|
const success = indices.some(i => {
|
||||||
|
if (!this.list[i]) {
|
||||||
|
// if (printed++ < 100) console.log("tryadd success, created group")
|
||||||
|
const group = new EventGroup([event])
|
||||||
|
this.list.splice(i, 0, group)
|
||||||
|
this.childAt(i, group)
|
||||||
|
return true
|
||||||
|
} else if (this.list[i] && this.list[i].data.sender === event.data.sender) {
|
||||||
|
// if (printed++ < 100) console.log("tryadd success, using existing group")
|
||||||
|
this.list[i].addEvent(event)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!success) console.log("tryadd failure", indices, this.list.map(l => l.data.sender), event.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.clearChildren()
|
||||||
|
this.list.forEach(group => this.child(group))
|
||||||
|
this.anchor = new Anchor()
|
||||||
|
this.child(this.anchor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Timeline extends Subscribable {
|
class Timeline extends Subscribable {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
Object.assign(this.events, {
|
Object.assign(this.events, {
|
||||||
addItem: [],
|
beforeChange: []
|
||||||
removeItem: []
|
|
||||||
})
|
})
|
||||||
Object.assign(this.eventDeps, {
|
Object.assign(this.eventDeps, {
|
||||||
addItem: [],
|
beforeChange: []
|
||||||
removeItem: []
|
|
||||||
})
|
})
|
||||||
this.list = []
|
this.list = []
|
||||||
this.map = new Map()
|
this.map = new Map()
|
||||||
this.elementsMap = new Map()
|
this.reactiveTimeline = new ReactiveTimeline([])
|
||||||
this.elementsList = []
|
|
||||||
}
|
|
||||||
|
|
||||||
_binarySearch(event, min = 0, max = -1) {
|
|
||||||
if (this.list.length === 0) return {success: false, i: 0}
|
|
||||||
|
|
||||||
if (max === -1) max = this.list.length - 1
|
|
||||||
let mid = Math.floor((max + min) / 2)
|
|
||||||
// success condition
|
|
||||||
if (this.list[mid] && this.list[mid].event_id === event.event_id) return {success: true, i: mid}
|
|
||||||
// failed condition
|
|
||||||
if (min >= max) {
|
|
||||||
while (mid !== -1 && (!this.list[mid] || this.list[mid].origin_server_ts > event.origin_server_ts)) mid--
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
i: mid + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// recurse (below)
|
|
||||||
if (this.list[mid].origin_server_ts > event.origin_server_ts) return this._binarySearch(event, min, mid-1)
|
|
||||||
// recurse (above)
|
|
||||||
else return this._binarySearch(event, mid+1, max)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEvents(events) {
|
updateEvents(events) {
|
||||||
for (const event of events) {
|
this.broadcast("beforeChange")
|
||||||
if (this.map.has(event.event_id)) {
|
for (const eventData of events) {
|
||||||
this.map.set(event.event_id, event)
|
if (this.map.has(eventData.event_id)) {
|
||||||
this.elementsMap.get(event.event_id).update(this.map.get(event.event_id))
|
this.map.get(eventData.event_id).update(eventData)
|
||||||
} else {
|
} else {
|
||||||
const index = this._binarySearch(event).i
|
const event = new Event(eventData)
|
||||||
this.list.splice(index, 0, event)
|
this.reactiveTimeline.addEvent(event)
|
||||||
this.map.set(event.event_id, event)
|
|
||||||
const e = new Event(event)
|
|
||||||
this.elementsList.splice(index, 0, e)
|
|
||||||
this.elementsMap.set(event.event_id, e)
|
|
||||||
this.broadcast("addItem", {index, element: e})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTimeline() {
|
||||||
|
return this.reactiveTimeline
|
||||||
|
}
|
||||||
|
/*
|
||||||
getGroupedEvents() {
|
getGroupedEvents() {
|
||||||
let currentSender = Symbol("N/A")
|
let currentSender = Symbol("N/A")
|
||||||
let groups = []
|
let groups = []
|
||||||
|
@ -90,6 +157,7 @@ class Timeline extends Subscribable {
|
||||||
if (currentGroup.length) groups.push(currentGroup)
|
if (currentGroup.length) groups.push(currentGroup)
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
export {Timeline}
|
export {Timeline}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {ElemJS, q, ejs} from "./basic.js"
|
import {ElemJS, q, ejs} from "./basic.js"
|
||||||
import {store} from "./store/store.js"
|
import {store} from "./store/store.js"
|
||||||
|
|
||||||
|
const chatMessages = q("#c-chat-messages")
|
||||||
|
|
||||||
class Chat extends ElemJS {
|
class Chat extends ElemJS {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(q("#c-chat"))
|
super(q("#c-chat"))
|
||||||
|
@ -25,10 +27,20 @@ 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 = (_, {element, index}) => {
|
const subscription = () => {
|
||||||
this.childAt(index, element)
|
// 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.
|
||||||
|
let oldDifference = chatMessages.scrollHeight - chatMessages.clientHeight
|
||||||
|
setTimeout(() => {
|
||||||
|
let newDifference = chatMessages.scrollHeight - chatMessages.clientHeight
|
||||||
|
console.log("height difference", oldDifference, newDifference)
|
||||||
|
if (oldDifference < 24) { // this is jank
|
||||||
|
this.element.parentElement.scrollBy(0, 1000)
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
const name = "addItem"
|
const name = "beforeChange"
|
||||||
this.removableSubscriptions.push({name, target: timeline, subscription})
|
this.removableSubscriptions.push({name, target: timeline, subscription})
|
||||||
timeline.subscribe(name, subscription)
|
timeline.subscribe(name, subscription)
|
||||||
}
|
}
|
||||||
|
@ -38,25 +50,16 @@ class Chat extends ElemJS {
|
||||||
render() {
|
render() {
|
||||||
this.clearChildren()
|
this.clearChildren()
|
||||||
if (store.activeRoom.exists()) {
|
if (store.activeRoom.exists()) {
|
||||||
const timeline = store.activeRoom.value().timeline
|
const reactiveTimeline = store.activeRoom.value().timeline.getTimeline()
|
||||||
for (const group of timeline.getGroupedEvents()) {
|
this.child(reactiveTimeline)
|
||||||
const first = group[0]
|
setTimeout(() => {
|
||||||
this.child(
|
this.element.parentElement.scrollBy(0, 1)
|
||||||
ejs("div").class("c-message-group").child(
|
reactiveTimeline.anchor.scroll()
|
||||||
ejs("div").class("c-message-group__avatar").child(
|
}, 0)
|
||||||
ejs("div").class("c-message-group__icon")
|
|
||||||
),
|
|
||||||
ejs("div").class("c-message-group__messages").child(
|
|
||||||
ejs("div").class("c-message-group__intro").child(
|
|
||||||
ejs("div").class("c-message-group__name").text(first.sender)
|
|
||||||
),
|
|
||||||
...group.map(event => timeline.elementsMap.get(event.event_id))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new Chat()
|
const chat = new Chat()
|
||||||
|
|
||||||
|
export {chat}
|
||||||
|
|
3
src/sass/components/anchor.sass
Normal file
3
src/sass/components/anchor.sass
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.c-anchor
|
||||||
|
overflow-anchor: auto
|
||||||
|
height: 1px
|
|
@ -8,6 +8,7 @@
|
||||||
.c-chat-input
|
.c-chat-input
|
||||||
width: 100%
|
width: 100%
|
||||||
border-top: 2px solid c.$divider
|
border-top: 2px solid c.$divider
|
||||||
|
background-color: c.$dark
|
||||||
|
|
||||||
&__textarea
|
&__textarea
|
||||||
width: calc(100% - 40px)
|
width: calc(100% - 40px)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
.c-chat
|
.c-chat
|
||||||
display: grid
|
display: grid
|
||||||
grid-template-rows: 1fr auto
|
grid-template-rows: 1fr 82px // fixed so that input box height adjustment doesn't mess up scroll
|
||||||
align-items: end
|
align-items: end
|
||||||
flex: 1
|
flex: 1
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
@use "../colors" as c
|
@use "../colors" as c
|
||||||
|
|
||||||
|
.c-event-groups *
|
||||||
|
overflow-anchor: none
|
||||||
|
|
||||||
.c-message-group, .c-message-event
|
.c-message-group, .c-message-event
|
||||||
margin-top: 12px
|
margin-top: 12px
|
||||||
padding-top: 12px
|
padding-top: 12px
|
||||||
|
|
|
@ -4,3 +4,4 @@
|
||||||
@use "./components/messages"
|
@use "./components/messages"
|
||||||
@use "./components/chat"
|
@use "./components/chat"
|
||||||
@use "./components/chat-input"
|
@use "./components/chat-input"
|
||||||
|
@use "./components/anchor"
|
||||||
|
|
Loading…
Reference in a new issue