diff --git a/app/Resources/Tracker/bootstrap.js b/app/Resources/Tracker/bootstrap.js index 881159a..a4c03e3 100644 --- a/app/Resources/Tracker/bootstrap.js +++ b/app/Resources/Tracker/bootstrap.js @@ -97,7 +97,7 @@ const info = DISCORD.getSelectedChannel(); if (!info) { - GUI.setStatus("Stopped"); + GUI.setStatus("Error (Unknown Channel)"); stopTrackingDelayed(); return; } @@ -130,17 +130,9 @@ STATE.onTrackingStateChanged(enabled => { if (enabled) { - if (DISCORD.getSelectedChannel() == null) { - const message = document.querySelector("div[class*='modeSelected-'][class*='typeThread-']") == null - ? "Cannot find selected channel. Ensure it is visible in the channel list." - : "Cannot find selected thread. Ensure it is visible in the channel list. If it is, try switching to a different server and back, or restarting Discord."; - stopTrackingDelayed(() => alert(message)); - return; - } - const messages = DISCORD.getMessages(); - if (messages == null) { + if (messages.length === 0) { stopTrackingDelayed(() => alert("Cannot see any messages.")); return; } diff --git a/app/Resources/Tracker/scripts/discord.js b/app/Resources/Tracker/scripts/discord.js index 2c66233..38e225a 100644 --- a/app/Resources/Tracker/scripts/discord.js +++ b/app/Resources/Tracker/scripts/discord.js @@ -5,7 +5,11 @@ class DISCORD { } static getMessageScrollerElement() { - return this.getMessageOuterElement().querySelector("[class*='scroller-']"); + return DOM.queryReactClass("scroller", this.getMessageOuterElement()); + } + + static getMessageElements() { + return this.getMessageOuterElement().querySelectorAll("[class*='message-']"); } static hasMoreMessages() { @@ -42,7 +46,7 @@ class DISCORD { return; } - const anyMessage = this.getMessageOuterElement().querySelector("[class*='message-']"); + const anyMessage = DOM.queryReactClass("message", this.getMessageOuterElement()); const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0; if (messageCount > 300) { @@ -86,32 +90,74 @@ class DISCORD { }, 200); } + /** + * Returns the property object of a message element. + * @returns { null | { message: DiscordMessage, channel: Object } } + */ + static getMessageElementProps(ele) { + const props = DOM.getReactProps(ele); + + if (props.children && props.children.length >= 4) { + const childProps = props.children[3].props; + + if ("message" in childProps && "channel" in childProps) { + return childProps; + } + } + + return null; + } + + /** + * Returns an array containing currently loaded messages. + */ + static getMessages() { + try { + const messages = []; + + for (const ele of this.getMessageElements()) { + const props = this.getMessageElementProps(ele); + + if (props != null) { + messages.push(props.message); + } + } + + return messages; + } catch (e) { + console.error(e); + return []; + } + } + /** * Returns an object containing the selected server and channel information. * For types DM and GROUP, the server and channel ids and names are identical. - * @returns {{}|null} + * @returns { {} | null } */ static getSelectedChannel() { try { - let channelListEle = DOM.queryReactClass("privateChannels"); + let obj; - if (channelListEle) { - const channel = DOM.queryReactClass("selected", channelListEle); + for (const ele of this.getMessageElements()) { + const props = this.getMessageElementProps(ele); - if (!channel || !("href" in channel) || !channel.href.includes("/@me/")) { - return null; + if (props != null) { + obj = props.channel; + break; } - - const linkSplit = channel.href.split("/"); - const id = linkSplit[linkSplit.length - 1]; - - if (!(/^\d+$/.test(id))) { - return null; - } - + } + + if (!obj) { + return null; + } + + const dms = DOM.queryReactClass("privateChannels"); + + if (dms) { let name; - for (const ele of channel.querySelectorAll("[class^='name-'] *")) { + for (const ele of dms.querySelectorAll("[class*='channel-'][class*='selected-'] [class^='name-'] *")) { const node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE); if (node) { @@ -124,118 +170,49 @@ class DISCORD { return null; } - const icon = channel.querySelector("img[class*='avatar']"); - const iconParent = icon && icon.closest("foreignObject"); - const iconMask = iconParent && iconParent.getAttribute("mask"); + let type; - return { - "server": { id, name, type: (iconMask && iconMask.includes("#svg-mask-avatar-default")) ? "GROUP" : "DM" }, - "channel": { id, name } - }; + // https://discord.com/developers/docs/resources/channel#channel-object-channel-types + switch (obj.type) { + case 1: type = "DM"; break; + case 3: type = "GROUP"; break; + default: return null; + } + + const id = obj.id; + const server = { id, name, type }; + const channel = { id, name }; + + return { server, channel }; } - else { - channelListEle = document.getElementById("channels"); + else if (obj.guild_id) { + const server = { + "id": obj.guild_id, + "name": document.querySelector("nav header > h1").innerText, + "type": "SERVER" + }; - const channel = channelListEle.querySelector("[class*='modeSelected']").parentElement; - const props = DOM.getReactProps(channel).children.props; - - if (!props) { - return null; - } - - let channelObj; - - try { - channelObj = props.channel || props.children().props.channel; - } catch (ignored) { - channelObj = null; - } - - if (channelObj) { - return { - "server": { - "id": channelObj.guild_id, - "name": document.querySelector("nav header > h1").innerText, - "type": "SERVER" - }, - "channel": { - "id": channelObj.id, - "name": channelObj.name, - "extra": { - "position": channelObj.position, - "topic": channelObj.topic, - "nsfw": channelObj.nsfw - } - } - }; - } - - const parentProps = DOM.getReactProps(channel.parentElement); - const parentPropsChildren = parentProps.children; - - if (!Array.isArray(parentPropsChildren) || parentPropsChildren.length < 3) { - return null; - } - - const threadItems = parentPropsChildren[2]; - const threadItem = Array.isArray(threadItems) && threadItems.find(item => item.props && item.props.thread && channel.querySelector("div[data-list-item-id='channels___" + item.props.thread.id + "']") !== null); - - if (!threadItem) { - return null; - } - - const thread = threadItem.props.thread; - - return { - "server": { - "id": thread.guild_id, - "name": document.querySelector("nav header > h1").innerText, - "type": "SERVER" - }, - "channel": { - "id": thread.id, - "name": thread.name, - "extra": { - "parent": thread.parent_id, - "nsfw": thread.nsfw - } + const channel = { + "id": obj.id, + "name": obj.name, + "extra": { + "nsfw": obj.nsfw } }; - } - } catch (e) { - console.error(e); - return null; - } - } - - /** - * Returns an array containing currently loaded messages, or null if the messages cannot be retrieved. - */ - static getMessages() { - try { - const scroller = this.getMessageScrollerElement(); - const props = DOM.getReactProps(scroller); - let wrappers; - - try { - // noinspection JSUnresolvedVariable - wrappers = props.children.props.children.props.children.props.children.find(ele => Array.isArray(ele)); - } catch (e) { // old version compatibility - wrappers = props.children.find(ele => Array.isArray(ele)); - } - - const messages = []; - - for (const obj of wrappers) { - // noinspection JSUnresolvedVariable - const nested = obj.props; - if (nested && nested.message) { - messages.push(nested.message); + if (obj.parent_id) { + channel["extra"]["parent"] = obj.parent_id; } + else { + channel["extra"]["position"] = obj.position; + channel["extra"]["topic"] = obj.topic; + } + + return { server, channel }; + } + else { + return null; } - - return messages; } catch (e) { console.error(e); return null;