Carbon/src/js/read-marker.js

150 lines
4.1 KiB
JavaScript

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
}