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