mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-09-15 19:32:09 +02:00
Compare commits
18 Commits
v36.beta
...
ce87901088
Author | SHA1 | Date | |
---|---|---|---|
ce87901088
|
|||
ea03f285a6
|
|||
4d914f2ae7
|
|||
a20ce8ee71
|
|||
b5ae561da2
|
|||
3cca167793
|
|||
18f1ea15fa
|
|||
21e196f4fe
|
|||
ad831d89e9
|
|||
3ba4d018a9
|
|||
ab6e929da6
|
|||
5569c159d4
|
|||
3d228152c0
|
|||
155dd226cb
|
|||
4b823802d3
|
|||
7c9ab5137e
|
|||
6de55b434a
|
|||
24a240fd29
|
10
README.md
10
README.md
@@ -1,24 +1,24 @@
|
||||
# Welcome
|
||||
|
||||
All you need to **use Discord History Tracker** is either an up-to-date browser, or the [Discord desktop client](https://discord.com/download). Visit the [official website](https://dht.chylex.com) for instructions.
|
||||
This branch is dedicated to the browser-only version of **Discord History Tracker**. All you need to use it is either an up-to-date browser, or the [Discord desktop client](https://discord.com/download). Visit the [official website](https://dht.chylex.com/browser-only) for instructions.
|
||||
|
||||
To **report an issue or suggestion**, first please see the [issues](https://github.com/chylex/Discord-History-Tracker/issues) page and make sure someone else hasn't already created a similar issue report. If you do find an existing issue, comment on it or add a reaction. Otherwise, either click [New Issue](https://github.com/chylex/Discord-History-Tracker/issues/new), or contact me via email [contact@chylex.com](mailto:contact@chylex.com) or Twitter [@chylexmc](https://twitter.com/chylexmc).
|
||||
|
||||
If you are interested in **creating your own version** from the source code, continue reading the [build instructions](#Build-Instructions) below.
|
||||
If you are interested in **building from source code**, continue reading the [build instructions](#Build-Instructions) below.
|
||||
|
||||
# Build Instructions
|
||||
|
||||
Follow the steps below to create your own version of Discord History Tracker.
|
||||
|
||||
### Setup
|
||||
|
||||
Fork the repository and clone it to your computer (if you've never used git, you can download the [GitHub Desktop](https://desktop.github.com) client to get started quickly).
|
||||
|
||||
By default, cloning will default to the `master` branch which is dedicated to the desktop app. Make sure to switch to the `master-browser-only` branch.
|
||||
|
||||
Now you can modify the source code:
|
||||
* `src/tracker/` contains JS files that are automatically combined into the **tracker bookmark/script**
|
||||
* `src/viewer/` contains HTML, CSS, JS files that are then combined into the **offline viewer page**
|
||||
* `lib/` contains utilities required to build the project
|
||||
* `web/` contains source code of the [official website](https://dht.chylex.com), which can be used as a template when making your own website
|
||||
* `web/` contains source code of the [official website](https://dht.chylex.com/browser-only), which can be used as a template when making your own website
|
||||
|
||||
### Building
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
// ==UserScript==
|
||||
// @name Discord History Tracker
|
||||
// @version v.31
|
||||
// @version v.31f
|
||||
// @license MIT
|
||||
// @namespace https://chylex.com
|
||||
// @homepageURL https://dht.chylex.com/
|
||||
@@ -18,71 +18,156 @@ var DISCORD = (function(){
|
||||
};
|
||||
|
||||
var getMessageScrollerElement = function(){
|
||||
return getMessageOuterElement().querySelector("[class*='scroller-']");
|
||||
return getMessageOuterElement().querySelector("[class*='scroller_']");
|
||||
};
|
||||
|
||||
var observerTimer = 0, waitingForCleanup = 0;
|
||||
var getMessageElements = function() {
|
||||
return getMessageOuterElement().querySelectorAll("[class*='message_']");
|
||||
};
|
||||
|
||||
var getReactProps = function(ele) {
|
||||
var keys = Object.keys(ele || {});
|
||||
var key = keys.find(key => key.startsWith("__reactInternalInstance"));
|
||||
|
||||
if (key){
|
||||
return ele[key].memoizedProps;
|
||||
}
|
||||
|
||||
key = keys.find(key => key.startsWith("__reactProps$"));
|
||||
return key ? ele[key] : null;
|
||||
};
|
||||
|
||||
var tryGetReactProps = function(ele) {
|
||||
try {
|
||||
return this.getReactProps(ele);
|
||||
} catch (ignore) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
var getMessageElementProps = function(ele) {
|
||||
const props = getReactProps(ele);
|
||||
|
||||
if (props.children && props.children.length) {
|
||||
for (let i = 3; i < props.children.length; i++) {
|
||||
const childProps = props.children[i].props;
|
||||
|
||||
if (childProps && "message" in childProps && "channel" in childProps) {
|
||||
return childProps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
var hasMoreMessages = function() {
|
||||
return document.querySelector("#messagesNavigationDescription + [class^=container]") === null;
|
||||
};
|
||||
|
||||
var getMessages = function() {
|
||||
try {
|
||||
const messages = [];
|
||||
|
||||
for (const ele of getMessageElements()) {
|
||||
try {
|
||||
const props = getMessageElementProps(ele);
|
||||
|
||||
if (props != null) {
|
||||
messages.push(props.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error extracing message data, skipping it.", e, ele, tryGetReactProps(ele));
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error retrieving messages.", e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
/*
|
||||
* Sets up a callback hook to trigger whenever the list of messages is updated. The callback is given a boolean value that is true if there are more messages to load.
|
||||
/**
|
||||
* Calls the provided function with a list of messages whenever the currently loaded messages change,
|
||||
* or with `false` if there are no more messages.
|
||||
*/
|
||||
setupMessageUpdateCallback: function(callback){
|
||||
var onTimerFinished = function(){
|
||||
let view = getMessageOuterElement();
|
||||
setupMessageCallback: function(callback) {
|
||||
let skipsLeft = 0;
|
||||
let waitForCleanup = false;
|
||||
let hasReachedStart = false;
|
||||
const previousMessages = new Set();
|
||||
|
||||
if (!view){
|
||||
restartTimer(500);
|
||||
const intervalId = window.setInterval(() => {
|
||||
if (skipsLeft > 0) {
|
||||
--skipsLeft;
|
||||
return;
|
||||
}
|
||||
else{
|
||||
let anyMessage = getMessageOuterElement().querySelector("[class*='message-']");
|
||||
let messages = anyMessage ? anyMessage.parentElement.children.length : 0;
|
||||
|
||||
if (messages < 100){
|
||||
waitingForCleanup = 0;
|
||||
const view = getMessageOuterElement();
|
||||
|
||||
if (!view) {
|
||||
skipsLeft = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
const anyMessage = DOM.queryReactClass("message", getMessageOuterElement());
|
||||
const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0;
|
||||
|
||||
if (messageCount > 300) {
|
||||
if (waitForCleanup) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitingForCleanup > 0){
|
||||
--waitingForCleanup;
|
||||
restartTimer(750);
|
||||
}
|
||||
else{
|
||||
if (messages > 300){
|
||||
waitingForCleanup = 6;
|
||||
skipsLeft = 3;
|
||||
waitForCleanup = true;
|
||||
|
||||
DOM.setTimer(() => {
|
||||
let view = getMessageScrollerElement();
|
||||
view.scrollTop = view.scrollHeight/2;
|
||||
}, 1);
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
const view = getMessageScrollerElement();
|
||||
view.scrollTop = view.scrollHeight / 2;
|
||||
}, 1);
|
||||
}
|
||||
else {
|
||||
waitForCleanup = false;
|
||||
}
|
||||
|
||||
callback();
|
||||
restartTimer(200);
|
||||
const messages = getMessages();
|
||||
let hasChanged = false;
|
||||
|
||||
for (const message of messages) {
|
||||
if (!previousMessages.has(message.id)) {
|
||||
hasChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var restartTimer = function(delay){
|
||||
observerTimer = DOM.setTimer(onTimerFinished, delay);
|
||||
};
|
||||
if (!hasChanged) {
|
||||
if (!hasReachedStart && !hasMoreMessages()) {
|
||||
hasReachedStart = true;
|
||||
callback(false);
|
||||
}
|
||||
|
||||
onTimerFinished();
|
||||
window.DHT_ON_UNLOAD.push(() => window.clearInterval(observerTimer));
|
||||
return;
|
||||
}
|
||||
|
||||
previousMessages.clear();
|
||||
for (const message of messages) {
|
||||
previousMessages.add(message.id);
|
||||
}
|
||||
|
||||
hasReachedStart = false;
|
||||
callback(messages);
|
||||
}, 200);
|
||||
|
||||
window.DHT_ON_UNLOAD.push(() => window.clearInterval(intervalId));
|
||||
},
|
||||
|
||||
/*
|
||||
* Returns internal React state object of an element.
|
||||
*/
|
||||
getReactProps: function(ele){
|
||||
var keys = Object.keys(ele || {});
|
||||
var key = keys.find(key => key.startsWith("__reactInternalInstance"));
|
||||
|
||||
if (key){
|
||||
return ele[key].memoizedProps;
|
||||
}
|
||||
|
||||
key = keys.find(key => key.startsWith("__reactProps$"));
|
||||
return key ? ele[key] : null;
|
||||
return getReactProps(ele);
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -90,83 +175,88 @@ var DISCORD = (function(){
|
||||
* For types DM and GROUP, the server and channel names are identical.
|
||||
* For SERVER type, the channel has to be in view, otherwise Discord unloads it.
|
||||
*/
|
||||
getSelectedChannel: function(){
|
||||
try{
|
||||
var obj;
|
||||
var channelListEle = DOM.queryReactClass("privateChannels");
|
||||
getSelectedChannel: function() {
|
||||
try {
|
||||
let obj;
|
||||
|
||||
if (channelListEle){
|
||||
var channel = DOM.queryReactClass("selected", channelListEle);
|
||||
for (const ele of getMessageElements()) {
|
||||
const props = getMessageElementProps(ele);
|
||||
|
||||
if (!channel || !("href" in channel) || !channel.href.includes("/@me/")){
|
||||
return null;
|
||||
if (props != null) {
|
||||
obj = props.channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var linkSplit = channel.href.split("/");
|
||||
var link = linkSplit[linkSplit.length-1];
|
||||
if (!obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(/^\d+$/.test(link))){
|
||||
return null;
|
||||
}
|
||||
var dms = DOM.queryReactClass("privateChannels");
|
||||
|
||||
var name;
|
||||
if (dms){
|
||||
let name;
|
||||
|
||||
for(let ele of channel.querySelectorAll("[class^='name-'] *")){
|
||||
let node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE);
|
||||
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){
|
||||
if (node) {
|
||||
name = node.nodeValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!name){
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var icon = channel.querySelector("img[class*='avatar']");
|
||||
var iconParent = icon && icon.closest("foreignObject");
|
||||
var iconMask = iconParent && iconParent.getAttribute("mask");
|
||||
let type;
|
||||
|
||||
obj = {
|
||||
// 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;
|
||||
}
|
||||
|
||||
return {
|
||||
"server": name,
|
||||
"channel": name,
|
||||
"id": link,
|
||||
"type": (iconMask && iconMask.includes("#svg-mask-avatar-default")) ? "GROUP" : "DM",
|
||||
"id": obj.id,
|
||||
"type": type,
|
||||
"extra": {}
|
||||
};
|
||||
}
|
||||
else{
|
||||
channelListEle = document.getElementById("channels");
|
||||
else if (obj.guild_id) {
|
||||
let guild;
|
||||
|
||||
var channel = channelListEle.querySelector("[class*='modeSelected']").parentElement;
|
||||
var props = DISCORD.getReactProps(channel).children.props;
|
||||
for (const child of getReactProps(document.querySelector("nav header [class*='headerContent_']")).children) {
|
||||
if (child && child.props && child.props.guild) {
|
||||
guild = child.props.guild;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!props){
|
||||
if (!guild || typeof guild.name !== "string" || obj.guild_id !== guild.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var channelObj = props.channel || props.children().props.channel;
|
||||
|
||||
if (!channelObj){
|
||||
return null;
|
||||
}
|
||||
|
||||
obj = {
|
||||
"server": document.querySelector("nav header > h1").innerText,
|
||||
"channel": channelObj.name,
|
||||
"id": channelObj.id,
|
||||
return {
|
||||
"server": guild.name,
|
||||
"channel": obj.name,
|
||||
"id": obj.id,
|
||||
"type": "SERVER",
|
||||
"extra": {
|
||||
"position": channelObj.position,
|
||||
"topic": channelObj.topic,
|
||||
"nsfw": channelObj.nsfw
|
||||
"position": obj.position,
|
||||
"topic": obj.topic,
|
||||
"nsfw": obj.nsfw
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return obj.channel.length === 0 ? null : obj;
|
||||
}catch(e){
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
@@ -176,32 +266,7 @@ var DISCORD = (function(){
|
||||
* Returns an array containing currently loaded messages.
|
||||
*/
|
||||
getMessages: function(){
|
||||
try{
|
||||
var scroller = getMessageScrollerElement();
|
||||
var props = DISCORD.getReactProps(scroller);
|
||||
var wrappers;
|
||||
|
||||
try{
|
||||
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));
|
||||
}
|
||||
|
||||
var messages = [];
|
||||
|
||||
for(let obj of wrappers){
|
||||
let nested = obj.props;
|
||||
|
||||
if (nested && nested.message){
|
||||
messages.push(nested.message);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}catch(e){
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
return getMessages();
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -213,7 +278,7 @@ var DISCORD = (function(){
|
||||
* Returns true if there are more messages available or if they're still loading.
|
||||
*/
|
||||
hasMoreMessages: function(){
|
||||
return document.querySelector("#messagesNavigationDescription + [class^=container]") === null;
|
||||
return hasMoreMessages();
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -230,54 +295,59 @@ var DISCORD = (function(){
|
||||
/*
|
||||
* Selects the next text channel and returns true, otherwise returns false if there are no more channels.
|
||||
*/
|
||||
selectNextTextChannel: function(){
|
||||
var dms = DOM.queryReactClass("privateChannels");
|
||||
selectNextTextChannel: function() {
|
||||
const dms = DOM.queryReactClass("privateChannels");
|
||||
|
||||
if (dms){
|
||||
var currentChannel = DOM.queryReactClass("selected", dms);
|
||||
var nextChannel = currentChannel && currentChannel.nextElementSibling;
|
||||
if (dms) {
|
||||
const currentChannel = DOM.queryReactClass("selected", dms);
|
||||
const currentChannelContainer = currentChannel && currentChannel.closest("[class*='channel_']");
|
||||
const nextChannel = currentChannelContainer && currentChannelContainer.nextElementSibling;
|
||||
|
||||
if (!nextChannel || !nextChannel.getAttribute("class").includes("channel-") || !("href" in nextChannel) || !nextChannel.href.includes("/@me/")){
|
||||
if (!nextChannel || !nextChannel.getAttribute("class").includes("channel_")) {
|
||||
return false;
|
||||
}
|
||||
else{
|
||||
nextChannel.click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
return true;
|
||||
|
||||
const nextChannelLink = nextChannel.querySelector("a[href*='/@me/']");
|
||||
if (!nextChannelLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nextChannelLink.click();
|
||||
nextChannelLink.scrollIntoView(true);
|
||||
return true;
|
||||
}
|
||||
else{
|
||||
var channelIconNormal = "M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z";
|
||||
var channelIconSpecial = "M14 8C14 7.44772 13.5523 7 13 7H9.76001L10.3657 3.58738C10.4201 3.28107 10.1845 3 9.87344 3H8.88907C8.64664 3 8.43914 3.17391 8.39677 3.41262L7.76001 7H4.18011C3.93722 7 3.72946 7.17456 3.68759 7.41381L3.51259 8.41381C3.45905 8.71977 3.69449 9 4.00511 9H7.41001L6.35001 15H2.77011C2.52722 15 2.31946 15.1746 2.27759 15.4138L2.10259 16.4138C2.04905 16.7198 2.28449 17 2.59511 17H6.00001L5.39427 20.4126C5.3399 20.7189 5.57547 21 5.88657 21H6.87094C7.11337 21 7.32088 20.8261 7.36325 20.5874L8.00001 17H14L13.3943 20.4126C13.3399 20.7189 13.5755 21 13.8866 21H14.8709C15.1134 21 15.3209 20.8261 15.3632 20.5874L16 17H19.5799C19.8228 17 20.0306 16.8254 20.0724 16.5862L20.2474 15.5862C20.301 15.2802 20.0655 15 19.7549 15H16.35L16.6758 13.1558C16.7823 12.5529 16.3186 12 15.7063 12C15.2286 12 14.8199 12.3429 14.7368 12.8133L14.3504 15H8.35045L9.41045 9H13C13.5523 9 14 8.55228 14 8Z";
|
||||
|
||||
var isValidChannelClass = cls => cls.includes("wrapper-") && !cls.includes("clickable-");
|
||||
var isValidChannelType = ele => !!ele.querySelector('path[d="' + channelIconNormal + '"]') || !!ele.querySelector('path[d="' + channelIconSpecial + '"]');
|
||||
var isValidChannel = ele => ele.childElementCount > 0 && isValidChannelClass(ele.children[0].className) && isValidChannelType(ele);
|
||||
|
||||
var channelListEle = document.querySelector("div[class*='sidebar'] > nav[class*='container'] > div[class*='scroller']");
|
||||
|
||||
if (!channelListEle){
|
||||
else {
|
||||
const channelListEle = document.getElementById("channels");
|
||||
if (!channelListEle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var allChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), isValidChannel);
|
||||
var nextChannel = null;
|
||||
function getLinkElement(channel) {
|
||||
return channel.querySelector("a[href^='/channels/'][role='link']");
|
||||
}
|
||||
|
||||
for(var index = 0; index < allChannels.length-1; index++){
|
||||
if (allChannels[index].children[0].className.includes("modeSelected")){
|
||||
nextChannel = allChannels[index+1];
|
||||
const allTextChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), ele => getLinkElement(ele) !== null);
|
||||
let nextChannel = null;
|
||||
|
||||
for (let index = 0; index < allTextChannels.length - 1; index++) {
|
||||
if (allTextChannels[index].className.includes("selected_")) {
|
||||
nextChannel = allTextChannels[index + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextChannel === null){
|
||||
if (nextChannel === null) {
|
||||
return false;
|
||||
}
|
||||
else{
|
||||
nextChannel.children[0].click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
return true;
|
||||
|
||||
const nextChannelLink = getLinkElement(nextChannel);
|
||||
if (!nextChannelLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nextChannelLink.click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -301,7 +371,7 @@ var DOM = (function(){
|
||||
/*
|
||||
* Returns the first child element containing the specified obfuscated class. Parent defaults to the entire document.
|
||||
*/
|
||||
queryReactClass: (cls, parent) => (parent || document).querySelector(`[class*="${cls}-"]`),
|
||||
queryReactClass: (cls, parent) => (parent || document).querySelector(`[class*="${cls}_"]`),
|
||||
|
||||
/*
|
||||
* Creates an element, adds it to the DOM, and returns it.
|
||||
@@ -454,8 +524,8 @@ var GUI = (function(){
|
||||
// styles
|
||||
|
||||
controller.styles = DOM.createStyle(`
|
||||
#app-mount > div[class*="app-"] { margin-bottom: 48px !important; }
|
||||
#dht-ctrl { position: absolute; bottom: 0; width: 100%; height: 48px; background-color: #FFF; }
|
||||
#app-mount { height: calc(100% - 48px) !important; }
|
||||
#dht-ctrl { position: absolute; bottom: 0; width: 100%; height: 48px; background-color: #FFF; z-index: 1000000; }
|
||||
#dht-ctrl button { height: 32px; margin: 8px 0 8px 8px; font-size: 16px; padding: 0 12px; background-color: #7289DA; color: #FFF; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75); }
|
||||
#dht-ctrl button:disabled { background-color: #7A7A7A; cursor: default; }
|
||||
#dht-ctrl-close { margin: 8px 8px 8px 0 !important; float: right; }
|
||||
@@ -563,8 +633,8 @@ ${btn("close", "X")}`);
|
||||
// styles
|
||||
|
||||
settings.styles = DOM.createStyle(`
|
||||
#dht-cfg-overlay { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: #000; opacity: 0.5; display: block; z-index: 1000; }
|
||||
#dht-cfg { position: absolute; left: 50%; top: 50%; width: 800px; height: 262px; margin-left: -400px; margin-top: -131px; padding: 8px; background-color: #fff; z-index: 1001; }
|
||||
#dht-cfg-overlay { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: #000; opacity: 0.5; display: block; z-index: 1000001; }
|
||||
#dht-cfg { position: absolute; left: 50%; top: 50%; width: 800px; height: 262px; margin-left: -400px; margin-top: -131px; padding: 8px; background-color: #fff; z-index: 1000002; }
|
||||
#dht-cfg-note { margin-top: 22px; }
|
||||
#dht-cfg sub { color: #666; font-size: 13px; }`);
|
||||
|
||||
@@ -594,7 +664,7 @@ ${radio("asm", "pause", "Pause Tracking")}
|
||||
${radio("asm", "switch", "Switch to Next Channel")}
|
||||
<p id='dht-cfg-note'>
|
||||
It is recommended to disable link and image previews to avoid putting unnecessary strain on your browser.<br><br>
|
||||
<sub>v.31, released 3 April 2021</sub>
|
||||
<sub>v.31f, released 20 November 2023</sub>
|
||||
</p>`);
|
||||
|
||||
// elements
|
||||
@@ -1214,7 +1284,7 @@ let stopTrackingDelayed = function(callback){
|
||||
}, 200); // give the user visual feedback after clicking the button before switching off
|
||||
};
|
||||
|
||||
DISCORD.setupMessageUpdateCallback(() => {
|
||||
DISCORD.setupMessageCallback(messages => {
|
||||
if (STATE.isTracking() && ignoreMessageCallback.size === 0){
|
||||
let info = DISCORD.getSelectedChannel();
|
||||
|
||||
@@ -1225,28 +1295,22 @@ DISCORD.setupMessageUpdateCallback(() => {
|
||||
|
||||
STATE.addDiscordChannel(info.server, info.type, info.id, info.channel, info.extra);
|
||||
|
||||
let messages = DISCORD.getMessages();
|
||||
|
||||
if (messages == null){
|
||||
stopTrackingDelayed();
|
||||
return;
|
||||
}
|
||||
else if (!messages.length){
|
||||
if (messages !== false && !messages.length){
|
||||
DISCORD.loadOlderMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
let hasUpdatedFile = STATE.addDiscordMessages(info.id, messages);
|
||||
let hasUpdatedFile = messages !== false && STATE.addDiscordMessages(info.id, messages);
|
||||
|
||||
if (SETTINGS.autoscroll){
|
||||
let action = null;
|
||||
|
||||
if (!hasUpdatedFile && !STATE.isMessageFresh(messages[0].id)){
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
else if (!DISCORD.hasMoreMessages()){
|
||||
if (messages === false) {
|
||||
action = SETTINGS.afterFirstMsg;
|
||||
}
|
||||
else if (!hasUpdatedFile && !STATE.isMessageFresh(messages[0].id)){
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
|
||||
if (action === null){
|
||||
if (hasUpdatedFile){
|
||||
|
File diff suppressed because one or more lines are too long
4
build.py
4
build.py
@@ -8,8 +8,8 @@ import os
|
||||
import re
|
||||
import distutils.dir_util
|
||||
|
||||
VERSION_SHORT = "v.31"
|
||||
VERSION_FULL = VERSION_SHORT + ", released 3 April 2021"
|
||||
VERSION_SHORT = "v.31f"
|
||||
VERSION_FULL = VERSION_SHORT + ", released 20 November 2023"
|
||||
|
||||
EXEC_UGLIFYJS_WIN = "{2}/lib/uglifyjs.cmd --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\""
|
||||
EXEC_UGLIFYJS_AUTO = "uglifyjs --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\""
|
||||
|
@@ -70,3 +70,5 @@ messages
|
||||
msSaveBlob
|
||||
messageReference
|
||||
message_id
|
||||
guild_id
|
||||
guild
|
||||
|
@@ -4,71 +4,156 @@ var DISCORD = (function(){
|
||||
};
|
||||
|
||||
var getMessageScrollerElement = function(){
|
||||
return getMessageOuterElement().querySelector("[class*='scroller-']");
|
||||
return getMessageOuterElement().querySelector("[class*='scroller_']");
|
||||
};
|
||||
|
||||
var observerTimer = 0, waitingForCleanup = 0;
|
||||
var getMessageElements = function() {
|
||||
return getMessageOuterElement().querySelectorAll("[class*='message_']");
|
||||
};
|
||||
|
||||
var getReactProps = function(ele) {
|
||||
var keys = Object.keys(ele || {});
|
||||
var key = keys.find(key => key.startsWith("__reactInternalInstance"));
|
||||
|
||||
if (key){
|
||||
return ele[key].memoizedProps;
|
||||
}
|
||||
|
||||
key = keys.find(key => key.startsWith("__reactProps$"));
|
||||
return key ? ele[key] : null;
|
||||
};
|
||||
|
||||
var tryGetReactProps = function(ele) {
|
||||
try {
|
||||
return this.getReactProps(ele);
|
||||
} catch (ignore) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
var getMessageElementProps = function(ele) {
|
||||
const props = getReactProps(ele);
|
||||
|
||||
if (props.children && props.children.length) {
|
||||
for (let i = 3; i < props.children.length; i++) {
|
||||
const childProps = props.children[i].props;
|
||||
|
||||
if (childProps && "message" in childProps && "channel" in childProps) {
|
||||
return childProps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
var hasMoreMessages = function() {
|
||||
return document.querySelector("#messagesNavigationDescription + [class^=container]") === null;
|
||||
};
|
||||
|
||||
var getMessages = function() {
|
||||
try {
|
||||
const messages = [];
|
||||
|
||||
for (const ele of getMessageElements()) {
|
||||
try {
|
||||
const props = getMessageElementProps(ele);
|
||||
|
||||
if (props != null) {
|
||||
messages.push(props.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error extracing message data, skipping it.", e, ele, tryGetReactProps(ele));
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error retrieving messages.", e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
/*
|
||||
* Sets up a callback hook to trigger whenever the list of messages is updated. The callback is given a boolean value that is true if there are more messages to load.
|
||||
/**
|
||||
* Calls the provided function with a list of messages whenever the currently loaded messages change,
|
||||
* or with `false` if there are no more messages.
|
||||
*/
|
||||
setupMessageUpdateCallback: function(callback){
|
||||
var onTimerFinished = function(){
|
||||
let view = getMessageOuterElement();
|
||||
setupMessageCallback: function(callback) {
|
||||
let skipsLeft = 0;
|
||||
let waitForCleanup = false;
|
||||
let hasReachedStart = false;
|
||||
const previousMessages = new Set();
|
||||
|
||||
if (!view){
|
||||
restartTimer(500);
|
||||
const intervalId = window.setInterval(() => {
|
||||
if (skipsLeft > 0) {
|
||||
--skipsLeft;
|
||||
return;
|
||||
}
|
||||
else{
|
||||
let anyMessage = getMessageOuterElement().querySelector("[class*='message-']");
|
||||
let messages = anyMessage ? anyMessage.parentElement.children.length : 0;
|
||||
|
||||
if (messages < 100){
|
||||
waitingForCleanup = 0;
|
||||
const view = getMessageOuterElement();
|
||||
|
||||
if (!view) {
|
||||
skipsLeft = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
const anyMessage = DOM.queryReactClass("message", getMessageOuterElement());
|
||||
const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0;
|
||||
|
||||
if (messageCount > 300) {
|
||||
if (waitForCleanup) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitingForCleanup > 0){
|
||||
--waitingForCleanup;
|
||||
restartTimer(750);
|
||||
}
|
||||
else{
|
||||
if (messages > 300){
|
||||
waitingForCleanup = 6;
|
||||
skipsLeft = 3;
|
||||
waitForCleanup = true;
|
||||
|
||||
DOM.setTimer(() => {
|
||||
let view = getMessageScrollerElement();
|
||||
view.scrollTop = view.scrollHeight/2;
|
||||
}, 1);
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
const view = getMessageScrollerElement();
|
||||
view.scrollTop = view.scrollHeight / 2;
|
||||
}, 1);
|
||||
}
|
||||
else {
|
||||
waitForCleanup = false;
|
||||
}
|
||||
|
||||
callback();
|
||||
restartTimer(200);
|
||||
const messages = getMessages();
|
||||
let hasChanged = false;
|
||||
|
||||
for (const message of messages) {
|
||||
if (!previousMessages.has(message.id)) {
|
||||
hasChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var restartTimer = function(delay){
|
||||
observerTimer = DOM.setTimer(onTimerFinished, delay);
|
||||
};
|
||||
if (!hasChanged) {
|
||||
if (!hasReachedStart && !hasMoreMessages()) {
|
||||
hasReachedStart = true;
|
||||
callback(false);
|
||||
}
|
||||
|
||||
onTimerFinished();
|
||||
window.DHT_ON_UNLOAD.push(() => window.clearInterval(observerTimer));
|
||||
return;
|
||||
}
|
||||
|
||||
previousMessages.clear();
|
||||
for (const message of messages) {
|
||||
previousMessages.add(message.id);
|
||||
}
|
||||
|
||||
hasReachedStart = false;
|
||||
callback(messages);
|
||||
}, 200);
|
||||
|
||||
window.DHT_ON_UNLOAD.push(() => window.clearInterval(intervalId));
|
||||
},
|
||||
|
||||
/*
|
||||
* Returns internal React state object of an element.
|
||||
*/
|
||||
getReactProps: function(ele){
|
||||
var keys = Object.keys(ele || {});
|
||||
var key = keys.find(key => key.startsWith("__reactInternalInstance"));
|
||||
|
||||
if (key){
|
||||
return ele[key].memoizedProps;
|
||||
}
|
||||
|
||||
key = keys.find(key => key.startsWith("__reactProps$"));
|
||||
return key ? ele[key] : null;
|
||||
return getReactProps(ele);
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -76,83 +161,88 @@ var DISCORD = (function(){
|
||||
* For types DM and GROUP, the server and channel names are identical.
|
||||
* For SERVER type, the channel has to be in view, otherwise Discord unloads it.
|
||||
*/
|
||||
getSelectedChannel: function(){
|
||||
try{
|
||||
var obj;
|
||||
var channelListEle = DOM.queryReactClass("privateChannels");
|
||||
getSelectedChannel: function() {
|
||||
try {
|
||||
let obj;
|
||||
|
||||
if (channelListEle){
|
||||
var channel = DOM.queryReactClass("selected", channelListEle);
|
||||
for (const ele of getMessageElements()) {
|
||||
const props = getMessageElementProps(ele);
|
||||
|
||||
if (!channel || !("href" in channel) || !channel.href.includes("/@me/")){
|
||||
return null;
|
||||
if (props != null) {
|
||||
obj = props.channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var linkSplit = channel.href.split("/");
|
||||
var link = linkSplit[linkSplit.length-1];
|
||||
if (!obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(/^\d+$/.test(link))){
|
||||
return null;
|
||||
}
|
||||
var dms = DOM.queryReactClass("privateChannels");
|
||||
|
||||
var name;
|
||||
if (dms){
|
||||
let name;
|
||||
|
||||
for(let ele of channel.querySelectorAll("[class^='name-'] *")){
|
||||
let node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE);
|
||||
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){
|
||||
if (node) {
|
||||
name = node.nodeValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!name){
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var icon = channel.querySelector("img[class*='avatar']");
|
||||
var iconParent = icon && icon.closest("foreignObject");
|
||||
var iconMask = iconParent && iconParent.getAttribute("mask");
|
||||
let type;
|
||||
|
||||
obj = {
|
||||
// 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;
|
||||
}
|
||||
|
||||
return {
|
||||
"server": name,
|
||||
"channel": name,
|
||||
"id": link,
|
||||
"type": (iconMask && iconMask.includes("#svg-mask-avatar-default")) ? "GROUP" : "DM",
|
||||
"id": obj.id,
|
||||
"type": type,
|
||||
"extra": {}
|
||||
};
|
||||
}
|
||||
else{
|
||||
channelListEle = document.getElementById("channels");
|
||||
else if (obj.guild_id) {
|
||||
let guild;
|
||||
|
||||
var channel = channelListEle.querySelector("[class*='modeSelected']").parentElement;
|
||||
var props = DISCORD.getReactProps(channel).children.props;
|
||||
for (const child of getReactProps(document.querySelector("nav header [class*='headerContent_']")).children) {
|
||||
if (child && child.props && child.props.guild) {
|
||||
guild = child.props.guild;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!props){
|
||||
if (!guild || typeof guild.name !== "string" || obj.guild_id !== guild.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var channelObj = props.channel || props.children().props.channel;
|
||||
|
||||
if (!channelObj){
|
||||
return null;
|
||||
}
|
||||
|
||||
obj = {
|
||||
"server": document.querySelector("nav header > h1").innerText,
|
||||
"channel": channelObj.name,
|
||||
"id": channelObj.id,
|
||||
return {
|
||||
"server": guild.name,
|
||||
"channel": obj.name,
|
||||
"id": obj.id,
|
||||
"type": "SERVER",
|
||||
"extra": {
|
||||
"position": channelObj.position,
|
||||
"topic": channelObj.topic,
|
||||
"nsfw": channelObj.nsfw
|
||||
"position": obj.position,
|
||||
"topic": obj.topic,
|
||||
"nsfw": obj.nsfw
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return obj.channel.length === 0 ? null : obj;
|
||||
}catch(e){
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
@@ -162,32 +252,7 @@ var DISCORD = (function(){
|
||||
* Returns an array containing currently loaded messages.
|
||||
*/
|
||||
getMessages: function(){
|
||||
try{
|
||||
var scroller = getMessageScrollerElement();
|
||||
var props = DISCORD.getReactProps(scroller);
|
||||
var wrappers;
|
||||
|
||||
try{
|
||||
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));
|
||||
}
|
||||
|
||||
var messages = [];
|
||||
|
||||
for(let obj of wrappers){
|
||||
let nested = obj.props;
|
||||
|
||||
if (nested && nested.message){
|
||||
messages.push(nested.message);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}catch(e){
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
return getMessages();
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -199,7 +264,7 @@ var DISCORD = (function(){
|
||||
* Returns true if there are more messages available or if they're still loading.
|
||||
*/
|
||||
hasMoreMessages: function(){
|
||||
return document.querySelector("#messagesNavigationDescription + [class^=container]") === null;
|
||||
return hasMoreMessages();
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -216,54 +281,59 @@ var DISCORD = (function(){
|
||||
/*
|
||||
* Selects the next text channel and returns true, otherwise returns false if there are no more channels.
|
||||
*/
|
||||
selectNextTextChannel: function(){
|
||||
var dms = DOM.queryReactClass("privateChannels");
|
||||
selectNextTextChannel: function() {
|
||||
const dms = DOM.queryReactClass("privateChannels");
|
||||
|
||||
if (dms){
|
||||
var currentChannel = DOM.queryReactClass("selected", dms);
|
||||
var nextChannel = currentChannel && currentChannel.nextElementSibling;
|
||||
if (dms) {
|
||||
const currentChannel = DOM.queryReactClass("selected", dms);
|
||||
const currentChannelContainer = currentChannel && currentChannel.closest("[class*='channel_']");
|
||||
const nextChannel = currentChannelContainer && currentChannelContainer.nextElementSibling;
|
||||
|
||||
if (!nextChannel || !nextChannel.getAttribute("class").includes("channel-") || !("href" in nextChannel) || !nextChannel.href.includes("/@me/")){
|
||||
if (!nextChannel || !nextChannel.getAttribute("class").includes("channel_")) {
|
||||
return false;
|
||||
}
|
||||
else{
|
||||
nextChannel.click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
return true;
|
||||
|
||||
const nextChannelLink = nextChannel.querySelector("a[href*='/@me/']");
|
||||
if (!nextChannelLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nextChannelLink.click();
|
||||
nextChannelLink.scrollIntoView(true);
|
||||
return true;
|
||||
}
|
||||
else{
|
||||
var channelIconNormal = "M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z";
|
||||
var channelIconSpecial = "M14 8C14 7.44772 13.5523 7 13 7H9.76001L10.3657 3.58738C10.4201 3.28107 10.1845 3 9.87344 3H8.88907C8.64664 3 8.43914 3.17391 8.39677 3.41262L7.76001 7H4.18011C3.93722 7 3.72946 7.17456 3.68759 7.41381L3.51259 8.41381C3.45905 8.71977 3.69449 9 4.00511 9H7.41001L6.35001 15H2.77011C2.52722 15 2.31946 15.1746 2.27759 15.4138L2.10259 16.4138C2.04905 16.7198 2.28449 17 2.59511 17H6.00001L5.39427 20.4126C5.3399 20.7189 5.57547 21 5.88657 21H6.87094C7.11337 21 7.32088 20.8261 7.36325 20.5874L8.00001 17H14L13.3943 20.4126C13.3399 20.7189 13.5755 21 13.8866 21H14.8709C15.1134 21 15.3209 20.8261 15.3632 20.5874L16 17H19.5799C19.8228 17 20.0306 16.8254 20.0724 16.5862L20.2474 15.5862C20.301 15.2802 20.0655 15 19.7549 15H16.35L16.6758 13.1558C16.7823 12.5529 16.3186 12 15.7063 12C15.2286 12 14.8199 12.3429 14.7368 12.8133L14.3504 15H8.35045L9.41045 9H13C13.5523 9 14 8.55228 14 8Z";
|
||||
|
||||
var isValidChannelClass = cls => cls.includes("wrapper-") && !cls.includes("clickable-");
|
||||
var isValidChannelType = ele => !!ele.querySelector('path[d="' + channelIconNormal + '"]') || !!ele.querySelector('path[d="' + channelIconSpecial + '"]');
|
||||
var isValidChannel = ele => ele.childElementCount > 0 && isValidChannelClass(ele.children[0].className) && isValidChannelType(ele);
|
||||
|
||||
var channelListEle = document.querySelector("div[class*='sidebar'] > nav[class*='container'] > div[class*='scroller']");
|
||||
|
||||
if (!channelListEle){
|
||||
else {
|
||||
const channelListEle = document.getElementById("channels");
|
||||
if (!channelListEle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var allChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), isValidChannel);
|
||||
var nextChannel = null;
|
||||
function getLinkElement(channel) {
|
||||
return channel.querySelector("a[href^='/channels/'][role='link']");
|
||||
}
|
||||
|
||||
for(var index = 0; index < allChannels.length-1; index++){
|
||||
if (allChannels[index].children[0].className.includes("modeSelected")){
|
||||
nextChannel = allChannels[index+1];
|
||||
const allTextChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), ele => getLinkElement(ele) !== null);
|
||||
let nextChannel = null;
|
||||
|
||||
for (let index = 0; index < allTextChannels.length - 1; index++) {
|
||||
if (allTextChannels[index].className.includes("selected_")) {
|
||||
nextChannel = allTextChannels[index + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextChannel === null){
|
||||
if (nextChannel === null) {
|
||||
return false;
|
||||
}
|
||||
else{
|
||||
nextChannel.children[0].click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
return true;
|
||||
|
||||
const nextChannelLink = getLinkElement(nextChannel);
|
||||
if (!nextChannelLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nextChannelLink.click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -16,7 +16,7 @@ var DOM = (function(){
|
||||
/*
|
||||
* Returns the first child element containing the specified obfuscated class. Parent defaults to the entire document.
|
||||
*/
|
||||
queryReactClass: (cls, parent) => (parent || document).querySelector(`[class*="${cls}-"]`),
|
||||
queryReactClass: (cls, parent) => (parent || document).querySelector(`[class*="${cls}_"]`),
|
||||
|
||||
/*
|
||||
* Creates an element, adds it to the DOM, and returns it.
|
||||
|
@@ -83,8 +83,8 @@ var GUI = (function(){
|
||||
// styles
|
||||
|
||||
controller.styles = DOM.createStyle(`
|
||||
#app-mount > div[class*="app-"] { margin-bottom: 48px !important; }
|
||||
#dht-ctrl { position: absolute; bottom: 0; width: 100%; height: 48px; background-color: #FFF; }
|
||||
#app-mount { height: calc(100% - 48px) !important; }
|
||||
#dht-ctrl { position: absolute; bottom: 0; width: 100%; height: 48px; background-color: #FFF; z-index: 1000000; }
|
||||
#dht-ctrl button { height: 32px; margin: 8px 0 8px 8px; font-size: 16px; padding: 0 12px; background-color: #7289DA; color: #FFF; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75); }
|
||||
#dht-ctrl button:disabled { background-color: #7A7A7A; cursor: default; }
|
||||
#dht-ctrl-close { margin: 8px 8px 8px 0 !important; float: right; }
|
||||
@@ -192,8 +192,8 @@ ${btn("close", "X")}`);
|
||||
// styles
|
||||
|
||||
settings.styles = DOM.createStyle(`
|
||||
#dht-cfg-overlay { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: #000; opacity: 0.5; display: block; z-index: 1000; }
|
||||
#dht-cfg { position: absolute; left: 50%; top: 50%; width: 800px; height: 262px; margin-left: -400px; margin-top: -131px; padding: 8px; background-color: #fff; z-index: 1001; }
|
||||
#dht-cfg-overlay { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: #000; opacity: 0.5; display: block; z-index: 1000001; }
|
||||
#dht-cfg { position: absolute; left: 50%; top: 50%; width: 800px; height: 262px; margin-left: -400px; margin-top: -131px; padding: 8px; background-color: #fff; z-index: 1000002; }
|
||||
#dht-cfg-note { margin-top: 22px; }
|
||||
#dht-cfg sub { color: #666; font-size: 13px; }`);
|
||||
|
||||
|
@@ -30,7 +30,7 @@ let stopTrackingDelayed = function(callback){
|
||||
}, 200); // give the user visual feedback after clicking the button before switching off
|
||||
};
|
||||
|
||||
DISCORD.setupMessageUpdateCallback(() => {
|
||||
DISCORD.setupMessageCallback(messages => {
|
||||
if (STATE.isTracking() && ignoreMessageCallback.size === 0){
|
||||
let info = DISCORD.getSelectedChannel();
|
||||
|
||||
@@ -41,28 +41,22 @@ DISCORD.setupMessageUpdateCallback(() => {
|
||||
|
||||
STATE.addDiscordChannel(info.server, info.type, info.id, info.channel, info.extra);
|
||||
|
||||
let messages = DISCORD.getMessages();
|
||||
|
||||
if (messages == null){
|
||||
stopTrackingDelayed();
|
||||
return;
|
||||
}
|
||||
else if (!messages.length){
|
||||
if (messages !== false && !messages.length){
|
||||
DISCORD.loadOlderMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
let hasUpdatedFile = STATE.addDiscordMessages(info.id, messages);
|
||||
let hasUpdatedFile = messages !== false && STATE.addDiscordMessages(info.id, messages);
|
||||
|
||||
if (SETTINGS.autoscroll){
|
||||
let action = null;
|
||||
|
||||
if (!hasUpdatedFile && !STATE.isMessageFresh(messages[0].id)){
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
else if (!DISCORD.hasMoreMessages()){
|
||||
if (messages === false) {
|
||||
action = SETTINGS.afterFirstMsg;
|
||||
}
|
||||
else if (!hasUpdatedFile && !STATE.isMessageFresh(messages[0].id)){
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
|
||||
if (action === null){
|
||||
if (hasUpdatedFile){
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="robots" content="index,follow">
|
||||
@@ -14,14 +14,15 @@
|
||||
<body>
|
||||
<div class="inner">
|
||||
<h1>Discord History Tracker <span class="version">{{{version:web}}}</span> <span class="bar">|</span> <span class="notes"><a href="https://github.com/chylex/Discord-History-Tracker/wiki/Release-Notes">Release Notes</a></span></h1>
|
||||
<p>Discord History Tracker is a browser script that lets you locally save chat history in your servers, groups, and private conversations.</p>
|
||||
<p>When the script is active, it will load history of the selected text channel up to the first message, and let you download it for offline viewing in your browser.</p>
|
||||
|
||||
<p>Discord History Tracker lets you save chat history in your servers, groups, and private conversations, and view it offline.</p>
|
||||
<img src="img/tracker.png" width="851" class="dht bordered">
|
||||
<p>This page explains how to use Discord History Tracker entirely in your browser. While this method gets you started quicker and works on any device that has a modern web browser, it has <strong>significant limitations and fewer features</strong> than the <a href="https://dht.chylex.com">desktop app</a>.</p>
|
||||
<p>Because everything happens in your browser, if the browser tab is closed, or your browser or computer crashes, you will lose all progress. Your browser may also crash or freeze if you have too many messages. If this is a concern, <a href="https://dht.chylex.com">use the desktop app</a> instead.</p>
|
||||
|
||||
<h2>How to Save History</h2>
|
||||
<h3>Running the Script</h3>
|
||||
<h2>How to Use</h2>
|
||||
<p>A tracking script will load messages according to your settings, and temporarily save them in your browser. Once you finish tracking, the browser will create an archive file you can save to your disk, and open in an offline viewer later.</p>
|
||||
|
||||
<h3>Setup the Tracking Script</h3>
|
||||
<h4>Option 1: Userscript</h4>
|
||||
<div class="quote">
|
||||
<p><strong>Preferred option.</strong> Requires a browser addon, but DHT will stay up-to-date and be easily accessible on the Discord website.</p>
|
||||
@@ -40,19 +41,21 @@
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h4>Option 2: Browser Console</h4>
|
||||
<h4>Option 2: Browser / Discord Console</h4>
|
||||
<div class="quote">
|
||||
<p>The console is the only way to use DHT directly in the desktop app.</p>
|
||||
|
||||
<ol>
|
||||
<li>Click <a href="javascript:" id="tracker-copy-button" onauxclick="return false;">Copy to Clipboard</a> to copy the script<noscript> (requires JavaScript)</noscript></li>
|
||||
<li>Click <a href="javascript:" id="tracker-copy-button" onauxclick="return false;">Copy to Clipboard</a> to copy the tracking script
|
||||
<noscript> (requires JavaScript)</noscript>
|
||||
</li>
|
||||
<li>Press <strong>Ctrl</strong>+<strong>Shift</strong>+<strong>I</strong> in your browser or the Discord app, and select the <strong>Console</strong> tab</li>
|
||||
<li>Paste the script into the console, and press <strong>Enter</strong> to run it</li>
|
||||
<li>Press <strong>Ctrl</strong>+<strong>Shift</strong>+<strong>I</strong> again to close the console</li>
|
||||
</ol>
|
||||
|
||||
<p id="tracker-copy-issue">Your browser may not support copying to clipboard, please try copying the script manually:</p>
|
||||
<textarea id="tracker-copy-contents"><?php include "./build/track.html"; ?></textarea>
|
||||
<textarea id="tracker-copy-contents"><?php include './build/track.html'; ?></textarea>
|
||||
</div>
|
||||
|
||||
<h4>Option 3: Bookmarklet</h4>
|
||||
@@ -60,38 +63,33 @@
|
||||
<p>Requires Firefox 69 or newer.</p>
|
||||
|
||||
<ol>
|
||||
<li>Right-click <a href="<?php include "./build/track.html"; ?>" onclick="return false;" onauxclick="return false;">Discord History Tracker</a></li>
|
||||
<li>Right-click <a href="<?php include './build/track.html'; ?>" onclick="return false;" onauxclick="return false;">Discord History Tracker</a></li>
|
||||
<li>Select «Bookmark This Link» and save the bookmark</li>
|
||||
<li>Open <a href="https://discord.com/channels/@me" rel="noreferrer">Discord</a> and click the bookmark to run the script</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h4>Old Versions</h4>
|
||||
<div class="quote">
|
||||
<p>Whenever DHT is fixed to work with a recent Discord update, it will no longer work on the previous version of Discord.</p>
|
||||
<p>If you haven't received that Discord update yet, see <a href="https://github.com/chylex/Discord-History-Tracker/wiki/Release-Notes">Release Notes</a> for information about recent updates, and <a href="https://github.com/chylex/Discord-History-Tracker/wiki/Old-Versions">Old Versions</a> if you need to use an older version of DHT.</p>
|
||||
</div>
|
||||
<p>Whenever DHT is updated to work with a new version of Discord, it may no longer work with the previous version of Discord.</p>
|
||||
<p>If you haven't received that Discord update yet, see <a href="https://github.com/chylex/Discord-History-Tracker/wiki/Release-Notes">Release Notes</a> for information about recent updates, and <a href="https://github.com/chylex/Discord-History-Tracker/wiki/Old-Versions">Old Versions</a> if you need to use an older version of DHT.</p>
|
||||
|
||||
<h3>Using the Script</h3>
|
||||
<p>When running for the first time, you will see a <strong>Settings</strong> dialog where you can configure the script. These settings will be remembered as long as you don't delete cookies in your browser.</p>
|
||||
<p>By default, Discord History Tracker is set to pause tracking after it reaches a previously saved message to avoid unnecessary history loading. You may also set it to load all channels in the server or your friends list by selecting <strong>Switch to Next Channel</strong>.</p>
|
||||
<p>Once you have configured everything, upload your previously saved archive (if you have any), click <strong>Start Tracking</strong>, and let it run. After the script saves all messages, download the archive.</p>
|
||||
<h3>How to Track Messages</h3>
|
||||
<p>When using the script for the first time, you will see a <strong>Settings</strong> dialog where you can configure the script. These settings will be remembered as long as you don't delete cookies in your browser.</p>
|
||||
<p>By default, Discord History Tracker is set to automatically scroll up to load the channel history, and pause tracking if it reaches a previously saved message to avoid unnecessary history loading.</p>
|
||||
<p>Before you <strong>Start Tracking</strong>, you may use <strong>Upload & Combine</strong> to load messages from a previously saved archive file into the browser.</p>
|
||||
<p>When you click <strong>Download</strong>, the browser will generate an archive file from saved messages, and lets you save it to your computer.</p>
|
||||
|
||||
<h2>How to View History</h2>
|
||||
<p>Download the <a href="build/viewer.html">Viewer</a>, open it in your browser, and load the archive. By downloading it to your computer, you can view archives offline, and allow the browser to load image previews that might otherwise not load if the remote server prevents embedding them.</p>
|
||||
<h3>How to View History</h3>
|
||||
<p>First, save the <a href="build/viewer.html">Viewer</a> file to your computer. Then you can open the downloaded viewer in your browser, click <strong>Load File</strong>, and select the archive to view.</p>
|
||||
|
||||
<h2>External Links</h2>
|
||||
<p class="links">
|
||||
<a href="https://github.com/chylex/Discord-History-Tracker/issues">Issues & Suggestions</a> —
|
||||
<a href="https://github.com/chylex/Discord-History-Tracker">Source Code</a> —
|
||||
<a href="https://github.com/chylex/Discord-History-Tracker/tree/master-browser-only">Source Code</a> —
|
||||
<a href="https://twitter.com/chylexmc">Follow Dev on Twitter</a> —
|
||||
<a href="https://www.patreon.com/chylex">Support via Patreon</a> —
|
||||
<a href="https://ko-fi.com/chylex">Support via Ko-fi</a>
|
||||
</p>
|
||||
|
||||
<h2>Disclaimer</h2>
|
||||
<p>Discord History Tracker and the viewer are fully client-side and do not communicate with any servers – the terms 'Upload' and 'Download' only refer to your browser. If you close your browser while the script is running, all unsaved progress will be lost.</p>
|
||||
<p>Please, do not use this script for large or public servers. The script was made as a convenient way of keeping a local copy of private and group chats, as Discord is currently lacking this functionality.</p>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
@@ -99,12 +97,12 @@
|
||||
var issue = document.getElementById("tracker-copy-issue");
|
||||
var button = document.getElementById("tracker-copy-button");
|
||||
|
||||
if (document.queryCommandSupported("copy")){
|
||||
if (document.queryCommandSupported("copy")) {
|
||||
contents.style.display = "none";
|
||||
issue.style.display = "none";
|
||||
}
|
||||
|
||||
button.addEventListener("click", function(){
|
||||
button.addEventListener("click", function() {
|
||||
contents.style.display = "block";
|
||||
issue.style.display = "block";
|
||||
|
||||
@@ -116,9 +114,9 @@
|
||||
issue.style.display = "none";
|
||||
});
|
||||
|
||||
contents.addEventListener("click", function(){
|
||||
contents.addEventListener("click", function() {
|
||||
contents.select();
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1,13 +1,12 @@
|
||||
body {
|
||||
font-family: Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding: 0 0 20px;
|
||||
font-size: 18px;
|
||||
text-shadow: 1px 1px 0 #111;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background-color: #3B3E45;
|
||||
background-color: #3b3e45;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inner {
|
||||
@@ -22,7 +21,7 @@ p {
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0EB3E0;
|
||||
color: #1ecfff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -64,30 +63,31 @@ h1 span.notes {
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 36px 0 0;
|
||||
margin: 40px 0 0;
|
||||
font-size: 32px;
|
||||
color: #ffb67b;
|
||||
color: #f9d288;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 24px 0 0;
|
||||
margin: 30px 0 12px;
|
||||
font-size: 22px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
h2 + h3, h3 + h4 {
|
||||
margin-top: 12px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 22px 0 0;
|
||||
font-size: 19px;
|
||||
margin: 25px 0 0;
|
||||
font-size: 20px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-top: -6px;
|
||||
margin-left: -6px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
li {
|
||||
@@ -102,6 +102,10 @@ li > img {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
code {
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.dht {
|
||||
max-width: 100%;
|
||||
max-height: auto;
|
||||
@@ -116,11 +120,10 @@ li > img {
|
||||
border: 2px dashed rgba(255, 255, 255, 0.25);
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
}
|
||||
|
||||
.quote {
|
||||
border-left: 2px dashed rgba(255, 255, 255, 0.1);
|
||||
border-left: 2px dashed rgba(255, 253, 123, 0.5);
|
||||
margin-left: 2px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
Reference in New Issue
Block a user