Display ghost messages that are being sent

This commit is contained in:
Cadence Ember 2020-10-20 00:23:10 +13:00
parent f9662e31a2
commit 0e084c0a68
Signed by: cadence
GPG key ID: BC1C2C61CF521B17
9 changed files with 201 additions and 55 deletions

View file

@ -2,10 +2,10 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="static/main.css?static=9aad8398d2"> <link rel="stylesheet" type="text/css" href="static/main.css?static=f7c0898b94">
<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=92188b34f9"></script> <script type="module" src="static/chat-input.js?static=16321d4eb4"></script>
<script type="module" src="static/room-picker.js?static=7bc94b38d3"></script> <script type="module" src="static/room-picker.js?static=46999be5e5"></script>
<script type="module" src="static/sync/sync.js?static=56e374b23d"></script> <script type="module" src="static/sync/sync.js?static=56e374b23d"></script>
<script type="module" src="static/chat.js?static=fc121d3d23"></script> <script type="module" src="static/chat.js?static=fc121d3d23"></script>
<title>Carbon</title> <title>Carbon</title>

View file

@ -1,6 +1,13 @@
import {ElemJS, ejs} 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" 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) { function eventSearch(list, event, min = 0, max = -1) {
if (list.length === 0) return {success: false, i: 0} if (list.length === 0) return {success: false, i: 0}
@ -28,23 +35,35 @@ class Event extends ElemJS {
super("div") super("div")
this.class("c-message") this.class("c-message")
this.data = null this.data = null
this.group = null
this.update(data) this.update(data)
} }
setGroup(group) {
this.group = group
}
update(data) { update(data) {
this.data = data this.data = data
this.render() this.render()
} }
removeEvent() {
if (this.group) this.group.removeEvent(this)
else this.remove()
}
render() { 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 { class EventGroup extends ElemJS {
constructor(list) { constructor(reactive, list) {
super("div") super("div")
this.class("c-message-group") this.class("c-message-group")
this.reactive = reactive
this.list = list this.list = list
this.data = { this.data = {
sender: list[0].data.sender, sender: list[0].data.sender,
@ -66,9 +85,20 @@ class EventGroup extends ElemJS {
addEvent(event) { addEvent(event) {
const index = eventSearch(this.list, event).i const index = eventSearch(this.list, event).i
event.setGroup(this)
this.list.splice(index, 0, event) this.list.splice(index, 0, event)
this.messages.childAt(index + 1, 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 { class ReactiveTimeline extends ElemJS {
@ -90,7 +120,7 @@ class ReactiveTimeline extends ElemJS {
const success = indices.some(i => { const success = indices.some(i => {
if (!this.list[i]) { if (!this.list[i]) {
// if (printed++ < 100) console.log("tryadd success, created group") // 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.list.splice(i, 0, group)
this.childAt(i, group) this.childAt(i, group)
return true 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) 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() { render() {
this.clearChildren() this.clearChildren()
this.list.forEach(group => this.child(group)) this.list.forEach(group => this.child(group))
@ -112,36 +148,84 @@ class ReactiveTimeline extends ElemJS {
} }
class Timeline extends Subscribable { class Timeline extends Subscribable {
constructor() { constructor(id) {
super() super()
Object.assign(this.events, { Object.assign(this.events, {
beforeChange: [] beforeChange: [],
afterChange: []
}) })
Object.assign(this.eventDeps, { Object.assign(this.eventDeps, {
beforeChange: [] beforeChange: [],
afterChange: []
}) })
this.id = id
this.list = [] this.list = []
this.map = new Map() this.map = new Map()
this.reactiveTimeline = new ReactiveTimeline([]) this.reactiveTimeline = new ReactiveTimeline([])
this.latest = 0 this.latest = 0
this.pending = new Set()
} }
updateEvents(events) { updateEvents(events) {
this.broadcast("beforeChange") this.broadcast("beforeChange")
for (const eventData of events) { for (const eventData of events) {
this.latest = Math.max(this.latest, eventData.origin_server_ts) this.latest = Math.max(this.latest, eventData.origin_server_ts)
if (this.map.has(eventData.event_id)) { let id = eventData.event_id
this.map.get(eventData.event_id).update(eventData) 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 { } else {
const event = new Event(eventData) const event = new Event(eventData)
this.map.set(id, event)
this.reactiveTimeline.addEvent(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() { getTimeline() {
return this.reactiveTimeline 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() { getGroupedEvents() {
let currentSender = Symbol("N/A") let currentSender = Symbol("N/A")

View file

@ -3,8 +3,6 @@ import {store} from "./store/store.js"
import * as lsm from "./lsm.js" import * as lsm from "./lsm.js"
import {chat} from "./chat.js" import {chat} from "./chat.js"
let sentIndex = 0
const input = q("#c-chat-textarea") const input = q("#c-chat-textarea")
store.activeRoom.subscribe("changeSelf", () => { store.activeRoom.subscribe("changeSelf", () => {
@ -33,21 +31,7 @@ function fixHeight() {
input.style.height = (input.scrollHeight + 1) + "px" input.style.height = (input.scrollHeight + 1) + "px"
} }
function getTxnId() {
return Date.now() + (sentIndex++)
}
function send(body) { function send(body) {
if (!store.activeRoom.exists()) return if (!store.activeRoom.exists()) return
const id = store.activeRoom.value().id return store.activeRoom.value().timeline.send(body)
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"
}
})
} }

View file

@ -182,6 +182,11 @@ body {
.c-message { .c-message {
margin-top: 4px; margin-top: 4px;
opacity: 1;
transition: opacity 0.2s ease-out;
}
.c-message--pending {
opacity: 0.5;
} }
.c-message-event { .c-message-event {

View file

@ -68,7 +68,7 @@ class Room extends ElemJS {
this.id = id this.id = id
this.data = data this.data = data
this.timeline = new Timeline() this.timeline = new Timeline(this.id)
this.group = null this.group = null
this.class("c-room") this.class("c-room")

View file

@ -1,6 +1,13 @@
import {ElemJS, ejs} 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" 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) { function eventSearch(list, event, min = 0, max = -1) {
if (list.length === 0) return {success: false, i: 0} if (list.length === 0) return {success: false, i: 0}
@ -28,23 +35,35 @@ class Event extends ElemJS {
super("div") super("div")
this.class("c-message") this.class("c-message")
this.data = null this.data = null
this.group = null
this.update(data) this.update(data)
} }
setGroup(group) {
this.group = group
}
update(data) { update(data) {
this.data = data this.data = data
this.render() this.render()
} }
removeEvent() {
if (this.group) this.group.removeEvent(this)
else this.remove()
}
render() { 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 { class EventGroup extends ElemJS {
constructor(list) { constructor(reactive, list) {
super("div") super("div")
this.class("c-message-group") this.class("c-message-group")
this.reactive = reactive
this.list = list this.list = list
this.data = { this.data = {
sender: list[0].data.sender, sender: list[0].data.sender,
@ -66,9 +85,20 @@ class EventGroup extends ElemJS {
addEvent(event) { addEvent(event) {
const index = eventSearch(this.list, event).i const index = eventSearch(this.list, event).i
event.setGroup(this)
this.list.splice(index, 0, event) this.list.splice(index, 0, event)
this.messages.childAt(index + 1, 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 { class ReactiveTimeline extends ElemJS {
@ -90,7 +120,7 @@ class ReactiveTimeline extends ElemJS {
const success = indices.some(i => { const success = indices.some(i => {
if (!this.list[i]) { if (!this.list[i]) {
// if (printed++ < 100) console.log("tryadd success, created group") // 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.list.splice(i, 0, group)
this.childAt(i, group) this.childAt(i, group)
return true 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) 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() { render() {
this.clearChildren() this.clearChildren()
this.list.forEach(group => this.child(group)) this.list.forEach(group => this.child(group))
@ -112,36 +148,84 @@ class ReactiveTimeline extends ElemJS {
} }
class Timeline extends Subscribable { class Timeline extends Subscribable {
constructor() { constructor(id) {
super() super()
Object.assign(this.events, { Object.assign(this.events, {
beforeChange: [] beforeChange: [],
afterChange: []
}) })
Object.assign(this.eventDeps, { Object.assign(this.eventDeps, {
beforeChange: [] beforeChange: [],
afterChange: []
}) })
this.id = id
this.list = [] this.list = []
this.map = new Map() this.map = new Map()
this.reactiveTimeline = new ReactiveTimeline([]) this.reactiveTimeline = new ReactiveTimeline([])
this.latest = 0 this.latest = 0
this.pending = new Set()
} }
updateEvents(events) { updateEvents(events) {
this.broadcast("beforeChange") this.broadcast("beforeChange")
for (const eventData of events) { for (const eventData of events) {
this.latest = Math.max(this.latest, eventData.origin_server_ts) this.latest = Math.max(this.latest, eventData.origin_server_ts)
if (this.map.has(eventData.event_id)) { let id = eventData.event_id
this.map.get(eventData.event_id).update(eventData) 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 { } else {
const event = new Event(eventData) const event = new Event(eventData)
this.map.set(id, event)
this.reactiveTimeline.addEvent(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() { getTimeline() {
return this.reactiveTimeline 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() { getGroupedEvents() {
let currentSender = Symbol("N/A") let currentSender = Symbol("N/A")

View file

@ -3,8 +3,6 @@ import {store} from "./store/store.js"
import * as lsm from "./lsm.js" import * as lsm from "./lsm.js"
import {chat} from "./chat.js" import {chat} from "./chat.js"
let sentIndex = 0
const input = q("#c-chat-textarea") const input = q("#c-chat-textarea")
store.activeRoom.subscribe("changeSelf", () => { store.activeRoom.subscribe("changeSelf", () => {
@ -33,21 +31,7 @@ function fixHeight() {
input.style.height = (input.scrollHeight + 1) + "px" input.style.height = (input.scrollHeight + 1) + "px"
} }
function getTxnId() {
return Date.now() + (sentIndex++)
}
function send(body) { function send(body) {
if (!store.activeRoom.exists()) return if (!store.activeRoom.exists()) return
const id = store.activeRoom.value().id return store.activeRoom.value().timeline.send(body)
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"
}
})
} }

View file

@ -68,7 +68,7 @@ class Room extends ElemJS {
this.id = id this.id = id
this.data = data this.data = data
this.timeline = new Timeline() this.timeline = new Timeline(this.id)
this.group = null this.group = null
this.class("c-room") this.class("c-room")

View file

@ -44,6 +44,11 @@
.c-message .c-message
margin-top: 4px margin-top: 4px
opacity: 1
transition: opacity 0.2s ease-out
&--pending
opacity: 0.5
.c-message-event .c-message-event
padding-top: 10px padding-top: 10px