From 0e084c0a689460f3dcfa3437b3f254d864f2716d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 20 Oct 2020 00:23:10 +1300 Subject: [PATCH] Display ghost messages that are being sent --- build/index.html | 6 +- build/static/Timeline.js | 100 +++++++++++++++++++++++++++--- build/static/chat-input.js | 18 +----- build/static/main.css | 5 ++ build/static/room-picker.js | 2 +- src/js/Timeline.js | 100 +++++++++++++++++++++++++++--- src/js/chat-input.js | 18 +----- src/js/room-picker.js | 2 +- src/sass/components/messages.sass | 5 ++ 9 files changed, 201 insertions(+), 55 deletions(-) diff --git a/build/index.html b/build/index.html index a14a855..1fb6d0f 100644 --- a/build/index.html +++ b/build/index.html @@ -2,10 +2,10 @@ - + - - + + Carbon diff --git a/build/static/Timeline.js b/build/static/Timeline.js index 7c5a508..f412366 100644 --- a/build/static/Timeline.js +++ b/build/static/Timeline.js @@ -1,6 +1,13 @@ import {ElemJS, ejs} from "./basic.js" import {Subscribable} from "./store/Subscribable.js" import {Anchor} from "./Anchor.js" +import * as lsm from "./lsm.js" + +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} @@ -28,23 +35,35 @@ class Event extends ElemJS { 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.child(this.data.content.body) + this.element.classList[this.data.pending ? "add" : "remove"]("c-message--pending") + this.text(this.data.content.body) } } class EventGroup extends ElemJS { - constructor(list) { + constructor(reactive, list) { super("div") this.class("c-message-group") + this.reactive = reactive this.list = list this.data = { sender: list[0].data.sender, @@ -66,9 +85,20 @@ class EventGroup extends ElemJS { 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 { @@ -90,7 +120,7 @@ class ReactiveTimeline extends ElemJS { const success = indices.some(i => { if (!this.list[i]) { // if (printed++ < 100) console.log("tryadd success, created group") - const group = new EventGroup([event]) + const group = new EventGroup(this, [event]) this.list.splice(i, 0, group) this.childAt(i, group) return true @@ -103,6 +133,12 @@ class ReactiveTimeline extends ElemJS { 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)) @@ -112,36 +148,84 @@ class ReactiveTimeline extends ElemJS { } class Timeline extends Subscribable { - constructor() { + constructor(id) { super() Object.assign(this.events, { - beforeChange: [] + beforeChange: [], + afterChange: [] }) Object.assign(this.eventDeps, { - beforeChange: [] + beforeChange: [], + afterChange: [] }) + this.id = id this.list = [] this.map = new Map() this.reactiveTimeline = new ReactiveTimeline([]) 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) - if (this.map.has(eventData.event_id)) { - this.map.get(eventData.event_id).update(eventData) + 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") diff --git a/build/static/chat-input.js b/build/static/chat-input.js index d8c0a35..08ee236 100644 --- a/build/static/chat-input.js +++ b/build/static/chat-input.js @@ -3,8 +3,6 @@ import {store} from "./store/store.js" import * as lsm from "./lsm.js" import {chat} from "./chat.js" -let sentIndex = 0 - const input = q("#c-chat-textarea") store.activeRoom.subscribe("changeSelf", () => { @@ -33,21 +31,7 @@ function fixHeight() { input.style.height = (input.scrollHeight + 1) + "px" } -function getTxnId() { - return Date.now() + (sentIndex++) -} - function send(body) { if (!store.activeRoom.exists()) return - const id = store.activeRoom.value().id - return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${id}/send/m.room.message/${getTxnId()}?access_token=${lsm.get("access_token")}`, { - method: "PUT", - body: JSON.stringify({ - msgtype: "m.text", - body - }), - headers: { - "Content-Type": "application/json" - } - }) + return store.activeRoom.value().timeline.send(body) } diff --git a/build/static/main.css b/build/static/main.css index af591b6..a813b7e 100644 --- a/build/static/main.css +++ b/build/static/main.css @@ -182,6 +182,11 @@ body { .c-message { margin-top: 4px; + opacity: 1; + transition: opacity 0.2s ease-out; +} +.c-message--pending { + opacity: 0.5; } .c-message-event { diff --git a/build/static/room-picker.js b/build/static/room-picker.js index 9fc99f5..3de2ab1 100644 --- a/build/static/room-picker.js +++ b/build/static/room-picker.js @@ -68,7 +68,7 @@ class Room extends ElemJS { this.id = id this.data = data - this.timeline = new Timeline() + this.timeline = new Timeline(this.id) this.group = null this.class("c-room") diff --git a/src/js/Timeline.js b/src/js/Timeline.js index 7c5a508..f412366 100644 --- a/src/js/Timeline.js +++ b/src/js/Timeline.js @@ -1,6 +1,13 @@ import {ElemJS, ejs} from "./basic.js" import {Subscribable} from "./store/Subscribable.js" import {Anchor} from "./Anchor.js" +import * as lsm from "./lsm.js" + +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} @@ -28,23 +35,35 @@ class Event extends ElemJS { 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.child(this.data.content.body) + this.element.classList[this.data.pending ? "add" : "remove"]("c-message--pending") + this.text(this.data.content.body) } } class EventGroup extends ElemJS { - constructor(list) { + constructor(reactive, list) { super("div") this.class("c-message-group") + this.reactive = reactive this.list = list this.data = { sender: list[0].data.sender, @@ -66,9 +85,20 @@ class EventGroup extends ElemJS { 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 { @@ -90,7 +120,7 @@ class ReactiveTimeline extends ElemJS { const success = indices.some(i => { if (!this.list[i]) { // if (printed++ < 100) console.log("tryadd success, created group") - const group = new EventGroup([event]) + const group = new EventGroup(this, [event]) this.list.splice(i, 0, group) this.childAt(i, group) return true @@ -103,6 +133,12 @@ class ReactiveTimeline extends ElemJS { 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)) @@ -112,36 +148,84 @@ class ReactiveTimeline extends ElemJS { } class Timeline extends Subscribable { - constructor() { + constructor(id) { super() Object.assign(this.events, { - beforeChange: [] + beforeChange: [], + afterChange: [] }) Object.assign(this.eventDeps, { - beforeChange: [] + beforeChange: [], + afterChange: [] }) + this.id = id this.list = [] this.map = new Map() this.reactiveTimeline = new ReactiveTimeline([]) 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) - if (this.map.has(eventData.event_id)) { - this.map.get(eventData.event_id).update(eventData) + 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") diff --git a/src/js/chat-input.js b/src/js/chat-input.js index d8c0a35..08ee236 100644 --- a/src/js/chat-input.js +++ b/src/js/chat-input.js @@ -3,8 +3,6 @@ import {store} from "./store/store.js" import * as lsm from "./lsm.js" import {chat} from "./chat.js" -let sentIndex = 0 - const input = q("#c-chat-textarea") store.activeRoom.subscribe("changeSelf", () => { @@ -33,21 +31,7 @@ function fixHeight() { input.style.height = (input.scrollHeight + 1) + "px" } -function getTxnId() { - return Date.now() + (sentIndex++) -} - function send(body) { if (!store.activeRoom.exists()) return - const id = store.activeRoom.value().id - return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${id}/send/m.room.message/${getTxnId()}?access_token=${lsm.get("access_token")}`, { - method: "PUT", - body: JSON.stringify({ - msgtype: "m.text", - body - }), - headers: { - "Content-Type": "application/json" - } - }) + return store.activeRoom.value().timeline.send(body) } diff --git a/src/js/room-picker.js b/src/js/room-picker.js index 9fc99f5..3de2ab1 100644 --- a/src/js/room-picker.js +++ b/src/js/room-picker.js @@ -68,7 +68,7 @@ class Room extends ElemJS { this.id = id this.data = data - this.timeline = new Timeline() + this.timeline = new Timeline(this.id) this.group = null this.class("c-room") diff --git a/src/sass/components/messages.sass b/src/sass/components/messages.sass index 779cd58..6284254 100644 --- a/src/sass/components/messages.sass +++ b/src/sass/components/messages.sass @@ -44,6 +44,11 @@ .c-message margin-top: 4px + opacity: 1 + transition: opacity 0.2s ease-out + + &--pending + opacity: 0.5 .c-message-event padding-top: 10px