mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-09-15 19:32:09 +02:00
Compare commits
5 Commits
app-raw-me
...
155dd226cb
Author | SHA1 | Date | |
---|---|---|---|
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.31a
|
||||
// @license MIT
|
||||
// @namespace https://chylex.com
|
||||
// @homepageURL https://dht.chylex.com/
|
||||
@@ -21,68 +21,139 @@ var DISCORD = (function(){
|
||||
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 getMessageElementProps = function(ele) {
|
||||
const props = 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;
|
||||
};
|
||||
|
||||
var hasMoreMessages = function() {
|
||||
return document.querySelector("#messagesNavigationDescription + [class^=container]") === null;
|
||||
};
|
||||
|
||||
var getMessages = function() {
|
||||
try {
|
||||
const messages = [];
|
||||
|
||||
for (const ele of getMessageElements()) {
|
||||
const props = getMessageElementProps(ele);
|
||||
|
||||
if (props != null) {
|
||||
messages.push(props.message);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
} catch (e) {
|
||||
console.error(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 +161,75 @@ 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-'] *, [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");
|
||||
|
||||
var channel = channelListEle.querySelector("[class*='modeSelected']").parentElement;
|
||||
var props = DISCORD.getReactProps(channel).children.props;
|
||||
|
||||
if (!props){
|
||||
return null;
|
||||
}
|
||||
|
||||
var channelObj = props.channel || props.children().props.channel;
|
||||
|
||||
if (!channelObj){
|
||||
return null;
|
||||
}
|
||||
|
||||
obj = {
|
||||
else if (obj.guild_id) {
|
||||
return {
|
||||
"server": document.querySelector("nav header > h1").innerText,
|
||||
"channel": channelObj.name,
|
||||
"id": channelObj.id,
|
||||
"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 +239,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 +251,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();
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -273,11 +311,15 @@ var DISCORD = (function(){
|
||||
if (nextChannel === null){
|
||||
return false;
|
||||
}
|
||||
else{
|
||||
nextChannel.children[0].click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
return true;
|
||||
|
||||
const nextChannelLink = nextChannel.querySelector("a[href^='/channels/']");
|
||||
if (!nextChannelLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nextChannelLink.click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -594,7 +636,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.31a, released 12 Feb 2022</sub>
|
||||
</p>`);
|
||||
|
||||
// elements
|
||||
@@ -1214,7 +1256,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 +1267,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.31a"
|
||||
VERSION_FULL = VERSION_SHORT + ", released 12 Feb 2022"
|
||||
|
||||
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,4 @@ messages
|
||||
msSaveBlob
|
||||
messageReference
|
||||
message_id
|
||||
guild_id
|
||||
|
@@ -7,68 +7,139 @@ var DISCORD = (function(){
|
||||
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 getMessageElementProps = function(ele) {
|
||||
const props = 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;
|
||||
};
|
||||
|
||||
var hasMoreMessages = function() {
|
||||
return document.querySelector("#messagesNavigationDescription + [class^=container]") === null;
|
||||
};
|
||||
|
||||
var getMessages = function() {
|
||||
try {
|
||||
const messages = [];
|
||||
|
||||
for (const ele of getMessageElements()) {
|
||||
const props = getMessageElementProps(ele);
|
||||
|
||||
if (props != null) {
|
||||
messages.push(props.message);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
} catch (e) {
|
||||
console.error(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 +147,75 @@ 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-'] *, [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");
|
||||
|
||||
var channel = channelListEle.querySelector("[class*='modeSelected']").parentElement;
|
||||
var props = DISCORD.getReactProps(channel).children.props;
|
||||
|
||||
if (!props){
|
||||
return null;
|
||||
}
|
||||
|
||||
var channelObj = props.channel || props.children().props.channel;
|
||||
|
||||
if (!channelObj){
|
||||
return null;
|
||||
}
|
||||
|
||||
obj = {
|
||||
else if (obj.guild_id) {
|
||||
return {
|
||||
"server": document.querySelector("nav header > h1").innerText,
|
||||
"channel": channelObj.name,
|
||||
"id": channelObj.id,
|
||||
"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 +225,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 +237,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();
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -259,11 +297,15 @@ var DISCORD = (function(){
|
||||
if (nextChannel === null){
|
||||
return false;
|
||||
}
|
||||
else{
|
||||
nextChannel.children[0].click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
return true;
|
||||
|
||||
const nextChannelLink = nextChannel.querySelector("a[href^='/channels/']");
|
||||
if (!nextChannelLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nextChannelLink.click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -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