const {ElemJS, ejs, q} = require("./basic.js") const {store} = require("./store/store.js") const lsm = require("./lsm.js") 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 }) }) } class ReadBanner extends ElemJS { constructor() { super(q("#c-chat-banner")) this.newMessages = ejs("span") this.child( ejs("div").class("c-chat-banner__inner").child( ejs("button").class("c-chat-banner__part").on("click", this.jumpTo.bind(this)).child( ejs("div").class("c-chat-banner__part-inner") .child(this.newMessages) .addText(" new messages") ), ejs("button").class("c-chat-banner__part", "c-chat-banner__last").on("click", this.markRead.bind(this)).child( ejs("div").class("c-chat-banner__part-inner").text("Mark as read") ) ) ) store.activeRoom.subscribe("changeSelf", this.render.bind(this)) store.notificationsChange.subscribe("changeSelf", this.render.bind(this)) this.render() } async jumpTo() { if (!store.activeRoom.exists()) return const timeline = store.activeRoom.value().timeline const readMarker = timeline.readMarker while (true) { if (readMarker.attached) { readMarker.element.scrollIntoView({behavior: "smooth", block: "center"}) return } else { q("#c-chat-messages").scrollTo({ top: 0, left: 0, behavior: "smooth" }) await new Promise(resolve => { const unsubscribe = timeline.subscribe("afterScrollbackLoad", () => { unsubscribe() resolve() }) }) } } } markRead() { if (!store.activeRoom.exists()) return const timeline = store.activeRoom.value().timeline markFullyRead(timeline.id, timeline.latestEventID) } render() { let count = 0 if (store.activeRoom.exists()) { count = store.activeRoom.value().number.state.unreads } if (count !== 0) { this.newMessages.text(count) this.class("c-chat-banner--active") } else { this.removeClass("c-chat-banner--active") } } } const readBanner = new ReadBanner() 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.attached = false 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) this.attached = true } else { this.removeClass("c-read-marker--attached") this.attached = false } if (store.activeRoom.value() === this.timeline.room) { readBanner.render() } } } module.exports = { ReadMarker, readBanner, markFullyRead }