|
|
|
@ -1,4 +1,4 @@
|
|
|
|
|
const {ElemJS, ejs} = require("./basic.js")
|
|
|
|
|
const {ElemJS, ejs, q} = require("./basic.js")
|
|
|
|
|
const {Subscribable} = require("./store/subscribable.js")
|
|
|
|
|
const {SubscribeValue} = require("./store/subscribe_value.js")
|
|
|
|
|
const {SubscribeMap} = require("./store/subscribe_map.js")
|
|
|
|
@ -20,6 +20,16 @@ function getTxnId() {
|
|
|
|
|
return Date.now() + (sentIndex++)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function markFullyRead(roomID, eventID) {
|
|
|
|
|
return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${roomID}/read_markers?access_token=${lsm.get("access_token")}`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
"m.fully_read": eventID,
|
|
|
|
|
"m.read": eventID
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function eventSearch(list, event, min = 0, max = NO_MAX) {
|
|
|
|
|
if (list.length === 0) return {success: false, i: 0}
|
|
|
|
|
|
|
|
|
@ -174,6 +184,62 @@ class ReactiveTimeline extends ElemJS {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ReadMarker extends ElemJS {
|
|
|
|
|
constructor(timeline) {
|
|
|
|
|
super("div")
|
|
|
|
|
|
|
|
|
|
this.class("c-read-marker")
|
|
|
|
|
this.loadingIcon = ejs("div")
|
|
|
|
|
.class("c-read-marker__loading", "loading-icon")
|
|
|
|
|
.style("display", "none")
|
|
|
|
|
this.child(
|
|
|
|
|
ejs("div").class("c-read-marker__inner").child(
|
|
|
|
|
ejs("div").class("c-read-marker__text").child(this.loadingIcon).addText("New")
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
let processing = false
|
|
|
|
|
const observer = new IntersectionObserver(entries => {
|
|
|
|
|
const entry = entries[0]
|
|
|
|
|
if (!entry.isIntersecting) return
|
|
|
|
|
if (processing) return
|
|
|
|
|
processing = true
|
|
|
|
|
this.loadingIcon.style("display", "")
|
|
|
|
|
markFullyRead(this.timeline.id, this.timeline.latestEventID).then(() => {
|
|
|
|
|
this.loadingIcon.style("display", "none")
|
|
|
|
|
processing = false
|
|
|
|
|
})
|
|
|
|
|
}, {
|
|
|
|
|
root: document.getElementById("c-chat-messages"),
|
|
|
|
|
rootMargin: "-80px 0px 0px 0px", // marker must be this distance inside the top of the screen to be counted as read
|
|
|
|
|
threshold: 0.01
|
|
|
|
|
})
|
|
|
|
|
observer.observe(this.element)
|
|
|
|
|
|
|
|
|
|
this.timeline = timeline
|
|
|
|
|
this.timeline.userReads.get(lsm.get("mx_user_id")).subscribe("changeSelf", (_, eventID) => {
|
|
|
|
|
// read marker updated, attach to it
|
|
|
|
|
const event = this.timeline.map.get(eventID)
|
|
|
|
|
this.attach(event)
|
|
|
|
|
})
|
|
|
|
|
this.timeline.subscribe("afterChange", () => {
|
|
|
|
|
// timeline has new events, attach to last read one
|
|
|
|
|
const eventID = this.timeline.userReads.get(lsm.get("mx_user_id")).value()
|
|
|
|
|
const event = this.timeline.map.get(eventID)
|
|
|
|
|
this.attach(event)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attach(event) {
|
|
|
|
|
if (event && event.data.origin_server_ts !== this.timeline.latest) {
|
|
|
|
|
this.class("c-read-marker--attached")
|
|
|
|
|
event.element.insertAdjacentElement("beforeend", this.element)
|
|
|
|
|
} else {
|
|
|
|
|
this.removeClass("c-read-marker--attached")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Timeline extends Subscribable {
|
|
|
|
|
constructor(room) {
|
|
|
|
|
super()
|
|
|
|
@ -195,10 +261,12 @@ class Timeline extends Subscribable {
|
|
|
|
|
this.map = new Map()
|
|
|
|
|
this.reactiveTimeline = new ReactiveTimeline(this.id, [])
|
|
|
|
|
this.latest = 0
|
|
|
|
|
this.latestEventID = null
|
|
|
|
|
this.pending = new Set()
|
|
|
|
|
this.pendingEdits = []
|
|
|
|
|
this.typing = new SubscribeValue().set([])
|
|
|
|
|
this.userReads = new SubscribeMap()
|
|
|
|
|
this.userReads = new SubscribeMap(SubscribeValue)
|
|
|
|
|
this.readMarker = new ReadMarker(this)
|
|
|
|
|
this.from = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -224,13 +292,21 @@ class Timeline extends Subscribable {
|
|
|
|
|
this.updateStateEvents(events)
|
|
|
|
|
for (const eventData of events) {
|
|
|
|
|
// set variables
|
|
|
|
|
this.latest = Math.max(this.latest, eventData.origin_server_ts)
|
|
|
|
|
let id = eventData.event_id
|
|
|
|
|
if (eventData.origin_server_ts > this.latest) {
|
|
|
|
|
this.latest = eventData.origin_server_ts
|
|
|
|
|
this.latestEventID = id
|
|
|
|
|
}
|
|
|
|
|
// handle local echoes
|
|
|
|
|
if (eventData.sender === lsm.get("mx_user_id") && eventData.content && this.pending.has(eventData.content["chat.carbon.message.pending_id"])) {
|
|
|
|
|
const target = this.map.get(eventData.content["chat.carbon.message.pending_id"])
|
|
|
|
|
this.map.set(id, target)
|
|
|
|
|
this.map.delete(eventData.content["chat.carbon.message.pending_id"])
|
|
|
|
|
const pendingID = eventData.content["chat.carbon.message.pending_id"]
|
|
|
|
|
if (id !== pendingID) {
|
|
|
|
|
const target = this.map.get(pendingID)
|
|
|
|
|
this.map.set(id, target)
|
|
|
|
|
this.map.delete(pendingID)
|
|
|
|
|
// update fully read marker - assume we have fully read up to messages we send
|
|
|
|
|
markFullyRead(this.id, id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// handle timeline events
|
|
|
|
|
if (this.map.has(id)) {
|
|
|
|
@ -259,6 +335,8 @@ class Timeline extends Subscribable {
|
|
|
|
|
const event = renderEvent(eventData)
|
|
|
|
|
this.map.set(id, event)
|
|
|
|
|
this.reactiveTimeline.addEvent(event)
|
|
|
|
|
// update read receipt for sender on their own event
|
|
|
|
|
this.moveReadReceipt(eventData.sender, id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// apply edits
|
|
|
|
@ -285,7 +363,7 @@ class Timeline extends Subscribable {
|
|
|
|
|
if (eventData.type === "m.receipt") {
|
|
|
|
|
for (const eventID of Object.keys(eventData.content)) {
|
|
|
|
|
for (const user of Object.keys(eventData.content[eventID]["m.read"])) {
|
|
|
|
|
this.userReads.set(user, eventID)
|
|
|
|
|
this.moveReadReceipt(user, eventID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// console.log("Updated read receipts:", this.userReads)
|
|
|
|
@ -293,6 +371,23 @@ class Timeline extends Subscribable {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
moveReadReceipt(user, eventID) {
|
|
|
|
|
if (!this.map.has(eventID)) return // ignore receipts we don't have events for
|
|
|
|
|
// check for a previous event to move from
|
|
|
|
|
const prev = this.userReads.get(user)
|
|
|
|
|
if (prev.exists()) {
|
|
|
|
|
const prevID = prev.value()
|
|
|
|
|
if (this.map.has(prevID)) {
|
|
|
|
|
// ensure new message came later
|
|
|
|
|
if (this.map.get(eventID).data.origin_server_ts < this.map.get(prevID).data.origin_server_ts) return
|
|
|
|
|
this.map.get(prevID).readBy.delete(user)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// set on new message
|
|
|
|
|
this.userReads.set(user, eventID)
|
|
|
|
|
if (this.map.has(eventID)) this.map.get(eventID).readBy.add(user)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateUnreadCount(count) {
|
|
|
|
|
this.room.number.update({unreads: count})
|
|
|
|
|
}
|
|
|
|
|