|
|
|
@ -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}) |
|
|
|
|
} |
|
|
|
|