import {ElemJS, ejs} from "./basic.js" import {Subscribable} from "./store/Subscribable.js" import {store} from "./store/store.js" import {Anchor} from "./Anchor.js" import * as lsm from "./lsm.js" import {resolveMxc} from "./functions.js" const dateFormatter = Intl.DateTimeFormat("default", {hour: "numeric", minute: "numeric", day: "numeric", month: "short", year: "numeric"}) let sentIndex = 0 function getTxnId() { return Date.now() + (sentIndex++) } 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 { constructor(data) { super("div") this.class("c-message") this.data = null this.group = null this.update(data) } setGroup(group) { this.group = group } update(data) { this.data = data this.render() } removeEvent() { if (this.group) this.group.removeEvent(this) else this.remove() } render() { this.element.classList[this.data.pending ? "add" : "remove"]("c-message--pending") this.text(this.data.content.body) } } class Sender { constructor(roomID, mxid) { this.sender = store.rooms.get(roomID).value().members.get(mxid) this.sender.subscribe("changeSelf", this.update.bind(this)) this.name = new ElemJS("div").class("c-message-group__name") this.avatar = new ElemJS("div").class("c-message-group__avatar") this.update() } update() { if (this.sender.exists()) { // name this.name.text(this.sender.value().content.displayname) // avatar this.avatar.clearChildren() if (this.sender.value().content.avatar_url) { this.avatar.child( ejs("img").class("c-message-group__icon").attribute("src", resolveMxc(this.sender.value().content.avatar_url, 32, "crop")) ) } else { this.avatar.child( ejs("div").class("c-message-group__icon") ) } } } } class EventGroup extends ElemJS { constructor(reactive, list) { super("div") this.class("c-message-group") this.reactive = reactive this.list = list this.data = { sender: list[0].data.sender, origin_server_ts: list[0].data.origin_server_ts } this.sender = new Sender(this.reactive.id, this.data.sender) this.child( this.sender.avatar, this.messages = ejs("div").class("c-message-group__messages").child( ejs("div").class("c-message-group__intro").child( this.sender.name, ejs("div").class("c-message-group__date").text(dateFormatter.format(this.data.origin_server_ts)) ), ...this.list ) ) } addEvent(event) { const index = eventSearch(this.list, event).i event.setGroup(this) this.list.splice(index, 0, event) this.messages.childAt(index + 1, event) } removeEvent(event) { const search = eventSearch(this.list, event) if (!search.success) throw new Error(`Event ${event.data.event_id} not found in this group`) const index = search.i // actually remove the event this.list.splice(index, 1) event.remove() // should get everything else if (this.list.length === 0) this.reactive.removeGroup(this) } } class ReactiveTimeline extends ElemJS { constructor(id, list) { super("div") this.class("c-event-groups") this.id = id 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(this, [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) } removeGroup(group) { const index = this.list.indexOf(group) this.list.splice(index, 1) group.remove() // should get everything else } render() { this.clearChildren() this.list.forEach(group => this.child(group)) this.anchor = new Anchor() this.child(this.anchor) } } class Timeline extends Subscribable { constructor(id) { super() Object.assign(this.events, { beforeChange: [], afterChange: [] }) Object.assign(this.eventDeps, { beforeChange: [], afterChange: [] }) this.id = id this.list = [] this.map = new Map() this.reactiveTimeline = new ReactiveTimeline(id, []) this.latest = 0 this.pending = new Set() } updateEvents(events) { this.broadcast("beforeChange") for (const eventData of events) { this.latest = Math.max(this.latest, eventData.origin_server_ts) let id = eventData.event_id if (eventData.sender === lsm.get("mx_user_id") && eventData.content && this.pending.has(eventData.content["chat.carbon.message.pending_id"])) { id = eventData.content["chat.carbon.message.pending_id"] } if (this.map.has(id)) { this.map.get(id).update(eventData) } else { const event = new Event(eventData) this.map.set(id, event) this.reactiveTimeline.addEvent(event) } } this.broadcast("afterChange") } removeEvent(id) { if (!this.map.has(id)) throw new Error(`Tried to delete event ID ${id} which does not exist`) this.map.get(id).removeEvent() this.map.delete(id) } getTimeline() { return this.reactiveTimeline } send(body) { const tx = getTxnId() const id = `pending$${tx}` this.pending.add(id) const content = { msgtype: "m.text", body, "chat.carbon.message.pending_id": id } const fakeEvent = { origin_server_ts: Date.now(), event_id: id, sender: lsm.get("mx_user_id"), content, pending: true } this.updateEvents([fakeEvent]) return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${this.id}/send/m.room.message/${tx}?access_token=${lsm.get("access_token")}`, { method: "PUT", body: JSON.stringify(content), 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 } */ } export {Timeline}