WIP read receipt improvements

This commit is contained in:
Andrew Ferrazzutti 2021-04-23 03:38:13 -04:00
parent c8d1d38d21
commit 7f937d34e2
2 changed files with 162 additions and 119 deletions

View File

@ -27,6 +27,12 @@ window.__chronoParseDate = function (text, ref, option) {}
* @return {Promise<void>}
*/
window.__mautrixReceiveChanges = function (changes) {}
/**
* @param {string} messages - The ID of the chat receiving messages.
* @param {MessageData[]} messages - The messages added to a chat.
* @return {Promise<void>}
*/
window.__mautrixReceiveMessages = function (chatID, messages) {}
/**
* @param {str} chatID - The ID of the chat whose receipts are being processed.
* @param {str} receipt_id - The ID of the most recently-read message for the current chat.
@ -78,6 +84,7 @@ class MautrixController {
constructor(ownID) {
this.chatListObserver = null
this.msgListObserver = null
this.receiptObserver = null
this.qrChangeObserver = null
this.qrAppearObserver = null
this.emailAppearObserver = null
@ -106,9 +113,9 @@ class MautrixController {
}
}
getCurrentChatId() {
getCurrentChatID() {
const chatListElement = document.querySelector("#_chat_list_body > .ExSelected > .chatList")
return chatListElement ? this.getChatListItemId(chatListElement) : null
return chatListElement ? this.getChatListItemID(chatListElement) : null
}
/**
@ -182,7 +189,7 @@ class MautrixController {
* @return {MessageData}
* @private
*/
async _tryParseMessage(date, element, chatType) {
async _parseMessage(date, element, chatType) {
const is_outgoing = element.classList.contains("mdRGT07Own")
let sender = {}
@ -386,40 +393,37 @@ class MautrixController {
})
}
/**
* Parse the message list of whatever the currently-viewed chat is.
*
* @param {?string} chatId - The ID of the currently-viewed chat, if known.
* @return {[MessageData]} - A list of messages.
*/
async parseMessageList(chatId) {
if (!chatId) {
chatId = this.getCurrentChatId()
}
const chatType = this.getChatType(chatId)
const msgList = document.querySelector("#_chat_room_msg_list")
async _tryParseMessages(msgList, chatType) {
const messages = []
let refDate = null
for (const child of msgList.children) {
if (child.tagName == "DIV") {
for (const child of msgList) {
if (child.classList.contains("mdRGT10Date")) {
refDate = await this._tryParseDayDate(child.firstElementChild.innerText)
}
else if (child.classList.contains("MdRGT07Cont")) {
} else if (child.classList.contains("MdRGT07Cont")) {
// TODO :not(.MdNonDisp) to exclude not-yet-posted messages,
// but that is unlikely to be a problem here.
// Also, offscreen times may have .MdNonDisp on them
const timeElement = child.querySelector("time")
if (timeElement) {
const messageDate = await this._tryParseDate(timeElement.innerText, refDate)
messages.push(await this._tryParseMessage(messageDate, child, chatType))
}
messages.push(await this._parseMessage(messageDate, child, chatType))
}
}
}
return messages
}
/**
* Parse the message list of whatever the currently-viewed chat is.
*
* @return {[MessageData]} - A list of messages.
*/
async parseMessageList() {
const msgList = Array.from(document.querySelectorAll("#_chat_room_msg_list > div[data-local-id]"))
msgList.sort((a,b) => a.getAttribute("data-local-id") - b.getAttribute("data-local-id"))
return await this._tryParseMessages(msgList, this.getChatType(this.getCurrentChatID()))
}
/**
* @typedef PathImage
* @type object
@ -457,7 +461,7 @@ class MautrixController {
return this._getPathImage(element.querySelector(".mdRGT13Img img[src]"))
}
getParticipantListItemId(element) {
getParticipantListItemID(element) {
// TODO Cache own ID
return element.getAttribute("data-mid")
}
@ -483,7 +487,7 @@ class MautrixController {
return [ownParticipant].concat(Array.from(element.children).slice(1).map(child => {
const name = this.getParticipantListItemName(child)
const id = this.getParticipantListItemId(child) || this.getUserIdFromFriendsList(name)
const id = this.getParticipantListItemID(child) || this.getUserIdFromFriendsList(name)
return {
id: id, // NOTE Don't want non-own user's ID to ever be null.
avatar: this.getParticipantListItemAvatar(child),
@ -504,7 +508,7 @@ class MautrixController {
* (e.g. "7:16 PM", "Thu" or "Aug 4")
*/
getChatListItemId(element) {
getChatListItemID(element) {
return element.getAttribute("data-chatid")
}
@ -528,12 +532,12 @@ class MautrixController {
* Parse a conversation list item element.
*
* @param {Element} element - The element to parse.
* @param {?string} knownId - The ID of this element, if it is known.
* @param {?string} knownID - The ID of this element, if it is known.
* @return {ChatListInfo} - The info in the element.
*/
parseChatListItem(element, knownId) {
parseChatListItem(element, knownID) {
return !element.classList.contains("chatList") ? null : {
id: knownId || this.getChatListItemId(element),
id: knownID || this.getChatListItemID(element),
name: this.getChatListItemName(element),
icon: this.getChatListItemIcon(element),
lastMsg: this.getChatListItemLastMsg(element),
@ -599,9 +603,11 @@ class MautrixController {
for (const node of change.addedNodes) {
}
*/
} else if (change.target.tagName == "LI") {
if (!change.target.classList.contains("ExSelected")) {
console.log("Not using chat list mutation response for currently-active chat")
continue
}
else if (change.target.tagName == "LI")
{
for (const node of change.addedNodes) {
const chat = this.parseChatListItem(node)
if (chat) {
@ -626,9 +632,7 @@ class MautrixController {
* Add a mutation observer to the chat list.
*/
addChatListObserver() {
if (this.chatListObserver !== null) {
this.removeChatListObserver()
}
this.chatListObserver = new MutationObserver(mutations => {
try {
this._observeChatListMutations(mutations)
@ -686,80 +690,108 @@ class MautrixController {
* @private
*/
_observeReceiptsMulti(mutations, chat_id) {
const ids = new Set()
const receipts = []
for (const change of mutations) {
let success = false
if (change.type == "attributes") {
if ( change.target.classList.contains("mdRGT07Read") &&
!change.target.classList.contains("MdNonDisp")) {
success = true
}
} else if (change.type == "characterData") {
success = true
}
if (success) {
const msgElement = change.target.closest(".mdRGT07Own")
if (msgElement) {
const id = +msgElement.getAttribute("data-local-id")
if (!ids.has(id)) {
ids.add(id)
receipts.push({
id: +msgElement.getAttribute("data-local-id"),
count: this._getReceiptCount(msgElement),
id: id,
count: this._getReceiptCount(change.target),
})
}
}
}
}
if (receipts.length > 0) {
window.__mautrixReceiveReceiptMulti(chat_id, receipts).then(
() => console.debug(`Receipt sent for message ${receipt_id}`),
err => console.error(`Error sending receipt for message ${receipt_id}:`, err))
() => console.debug(`Receipts sent for ${receipts.length} messages`),
err => console.error(`Error sending receipts for ${receipts.length} messages`, err))
}
}
/**
* Add a mutation observer to the message list.
* Used for observing read receipts.
* TODO Should also use for observing messages of the currently-viewed chat.
* Add a mutation observer to the message list of the current chat.
* Used for observing new messages & read receipts.
*/
addMsgListObserver(forceCreate) {
addMsgListObserver() {
const chat_room_msg_list = document.querySelector("#_chat_room_msg_list")
if (!chat_room_msg_list) {
console.debug("Could not start msg list observer: no msg list available!")
return
}
if (this.msgListObserver !== null) {
this.removeMsgListObserver()
} else if (!forceCreate) {
console.debug("No pre-existing msg list observer to replace")
return
}
const observeReadReceipts =
this.getChatType(this.getCurrentChatId()) == ChatTypeEnum.DIRECT ?
const chatID = this.getCurrentChatID()
const chatType = this.getChatType(chatID)
this.msgListObserver = new MutationObserver(async (changes) => {
for (const change of changes) {
const msgList = Array.from(change.addedNodes).filter(
child => child.tagName == "DIV" && child.hasAttribute("data-local-id"))
msgList.sort((a,b) => a.getAttribute("data-local-id") - b.getAttribute("data-local-id"))
window.__mautrixReceiveMessages(chatID, await this._tryParseMessages(msgList, chatType))
}
})
this.msgListObserver.observe(chat_room_msg_list,
{ childList: true })
console.debug("Started msg list observer")
const observeReadReceipts = (
chatType == ChatTypeEnum.DIRECT ?
this._observeReceiptsDirect :
this._observeReceiptsMulti
).bind(this)
const chat_id = this.getCurrentChatId()
this.msgListObserver = new MutationObserver(mutations => {
this.receiptObserver = new MutationObserver(changes => {
try {
observeReadReceipts(mutations, chat_id)
observeReadReceipts(changes, chatID)
} catch (err) {
console.error("Error observing msg list mutations:", err)
}
})
this.msgListObserver.observe(
this.receiptObserver.observe(
chat_room_msg_list,
{ subtree: true, attributes: true, attributeFilter: ["class"], characterData: true })
console.debug("Started msg list observer")
{ subtree: true, attributes: true, attributeFilter: ["class"] })
console.debug("Started receipt observer")
}
/**
* Disconnect the most recently added mutation observer.
*/
removeMsgListObserver() {
let result = false
if (this.msgListObserver !== null) {
this.msgListObserver.disconnect()
this.msgListObserver = null
console.debug("Disconnected msg list observer")
result = true
}
if (this.receiptObserver !== null) {
this.receiptObserver.disconnect()
this.receiptObserver = null
console.debug("Disconnected receipt observer")
result = true
}
return result
}
addQRChangeObserver(element) {
if (this.qrChangeObserver !== null) {
this.removeQRChangeObserver()
}
this.qrChangeObserver = new MutationObserver(changes => {
for (const change of changes) {
if (change.attributeName === "title" && change.target instanceof Element) {
@ -781,9 +813,7 @@ class MautrixController {
}
addQRAppearObserver(element) {
if (this.qrAppearObserver !== null) {
this.removeQRAppearObserver()
}
this.qrAppearObserver = new MutationObserver(changes => {
for (const change of changes) {
for (const node of change.addedNodes) {
@ -809,9 +839,7 @@ class MautrixController {
}
addEmailAppearObserver(element) {
if (this.emailAppearObserver !== null) {
this.removeEmailAppearObserver()
}
this.emailAppearObserver = new MutationObserver(changes => {
for (const change of changes) {
for (const node of change.addedNodes) {
@ -836,9 +864,7 @@ class MautrixController {
}
addPINAppearObserver(element) {
if (this.pinAppearObserver !== null) {
this.removePINAppearObserver()
}
this.pinAppearObserver = new MutationObserver(changes => {
for (const change of changes) {
for (const node of change.addedNodes) {
@ -863,9 +889,7 @@ class MautrixController {
}
addExpiryObserver(element) {
if (this.expiryObserver !== null) {
this.removeExpiryObserver()
}
const button = element.querySelector("dialog button")
this.expiryObserver = new MutationObserver(changes => {
if (changes.length == 1 && !changes[0].target.classList.contains("MdNonDisp")) {

View File

@ -97,6 +97,8 @@ export default class MessagesPuppeteer {
id => this.sentMessageIDs.add(id))
await this.page.exposeFunction("__mautrixReceiveChanges",
this._receiveChatListChanges.bind(this))
await this.page.exposeFunction("__mautrixReceiveMessages",
this._receiveMessages.bind(this))
await this.page.exposeFunction("__mautrixReceiveReceiptDirectLatest",
this._receiveReceiptDirectLatest.bind(this))
await this.page.exposeFunction("__mautrixReceiveReceiptMulti",
@ -366,6 +368,21 @@ export default class MessagesPuppeteer {
return { id: await this.taskQueue.push(() => this._sendMessageUnsafe(chatID, text)) }
}
_filterMessages(chatID, messages) {
const minID = this.mostRecentMessages.get(chatID) || 0
const filtered_messages = messages.filter(msg => msg.id > minID && !this.sentMessageIDs.has(msg.id))
let range = 0
if (filtered_messages.length > 0) {
const newFirstID = filtered_messages[0].id
const newLastID = filtered_messages[filtered_messages.length - 1].id
this.mostRecentMessages.set(chatID, newLastID)
range = newFirstID === newLastID ? newFirstID : `${newFirstID}-${newLastID}`
}
this.log(`Loaded ${messages.length} messages in ${chatID}: got ${range} newer than ${minID}`)
return filtered_messages
}
/**
* Get messages in a chat.
*
@ -376,16 +393,10 @@ export default class MessagesPuppeteer {
return this.taskQueue.push(async () => {
const messages = await this._getMessagesUnsafe(chatID)
if (messages.length > 0) {
// TODO Commonize this
const newFirstID = messages[0].id
const newLastID = messages[messages.length - 1].id
this.mostRecentMessages.set(chatID, newLastID)
const range = newFirstID === newLastID ? newFirstID : `${newFirstID}-${newLastID}`
this.log(`Loaded ${messages.length} messages in ${chatID}: got ${range}`)
}
for (const message of messages) {
message.chat_id = chatID
}
}
return messages
})
}
@ -449,7 +460,7 @@ export default class MessagesPuppeteer {
await this.page.evaluate(
() => window.__mautrixController.addChatListObserver())
await this.page.evaluate(
() => window.__mautrixController.addMsgListObserver(true))
() => window.__mautrixController.addMsgListObserver())
}
async stopObserving() {
@ -482,6 +493,10 @@ export default class MessagesPuppeteer {
if (await this.page.evaluate(isCorrectChatVisible, chatName)) {
this.log("Already viewing chat, no need to switch")
} else {
this.log("Switching chat, so remove msg list observer")
const hadMsgListObserver = await this.page.evaluate(
() => window.__mautrixController.removeMsgListObserver())
await chatListItem.click()
this.log(`Waiting for chat header title to be "${chatName}"`)
await this.page.waitForFunction(
@ -495,8 +510,13 @@ export default class MessagesPuppeteer {
{},
await this.page.$("#_chat_detail_area"))
if (hadMsgListObserver) {
this.log("Restoring msg list observer")
await this.page.evaluate(
() => window.__mautrixController.addMsgListObserver(false))
() => window.__mautrixController.addMsgListObserver())
} else {
this.log("Not restoring msg list observer, as there never was one")
}
}
}
@ -591,21 +611,29 @@ export default class MessagesPuppeteer {
}
}
// TODO Inbound read receipts
// Probably use a MutationObserver mapped to msgID
_receiveMessages(chatID, messages) {
if (this.client) {
messages = this._filterMessages(chatID, messages)
if (messages.length > 0) {
for (const message of messages) {
message.chat_id = chatID
this.client.sendMessage(message).catch(err =>
this.error("Failed to send message", message.id, "to client:", err))
}
}
} else {
this.log("No client connected, not sending messages")
}
}
async _getMessagesUnsafe(chatID) {
// TODO Also handle "decrypting" state
// TODO Handle unloaded messages. Maybe scroll up
// TODO This will mark the chat as "read"!
await this._switchChat(chatID)
const minID = this.mostRecentMessages.get(chatID) || 0
this.log(`Waiting for messages newer than ${minID}`)
const messages = await this.page.evaluate(
chatID => window.__mautrixController.parseMessageList(chatID), chatID)
const filtered_messages = messages.filter(msg => msg.id > minID && !this.sentMessageIDs.has(msg.id))
this.log(`Found messages: ${messages.length} total, ${filtered_messages.length} new`)
return filtered_messages
const messages = await this.page.evaluate(() =>
window.__mautrixController.parseMessageList())
return this._filterMessages(chatID, messages)
}
async _processChatListChangeUnsafe(chatID) {
@ -616,11 +644,6 @@ export default class MessagesPuppeteer {
this.log("No new messages found in", chatID)
return
}
const newFirstID = messages[0].id
const newLastID = messages[messages.length - 1].id
this.mostRecentMessages.set(chatID, newLastID)
const range = newFirstID === newLastID ? newFirstID : `${newFirstID}-${newLastID}`
this.log(`Loaded ${messages.length} messages in ${chatID}: got ${range}`)
if (this.client) {
for (const message of messages) {
@ -652,14 +675,10 @@ export default class MessagesPuppeteer {
_receiveReceiptMulti(chat_id, receipts) {
this.log(`Received bulk read receipts for chat ${chat_id}:`, receipts)
this.taskQueue.push(() => this._receiveReceiptMulti(chat_id, receipts))
.catch(err => this.error("Error handling read receipt changes:", err))
}
async _receiveReceiptMultiUnsafe(chat_id, receipts) {
for (receipt of receipts) {
for (const receipt of receipts) {
receipt.chat_id = chat_id
await this.client.sendReceipt(receipt)
this.taskQueue.push(() => this.client.sendReceipt(receipt))
.catch(err => this.error("Error handling read receipt changes:", err))
}
}