mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-09-15 19:32:09 +02:00
Compare commits
12 Commits
72b23ee4f8
...
master-bro
Author | SHA1 | Date | |
---|---|---|---|
83603eceb8
|
|||
5277f28318
|
|||
![]() |
515825f7a4 | ||
af48bf60ce
|
|||
3ac968aa38
|
|||
5debfa9ec6
|
|||
92b8450c80
|
|||
ff6e21186c
|
|||
f1bbe6d13c
|
|||
4eb78def90
|
|||
4e8df28dc2
|
|||
![]() |
6ca386b741 |
@@ -31,6 +31,7 @@ After you've done changes to the source code, you will need to build it. Before
|
||||
|
||||
Now open the folder that contains `build.py` in a command line, and run `python build.py` to create a build with default settings. The following files will be created:
|
||||
* `bld/track.js` is the raw tracker script that can be pasted into a browser console
|
||||
* `bld/track.html` is the tracker script but sanitized for inclusion in HTML (see `web/index.php` for examples)
|
||||
* `bld/viewer.html` is the complete offline viewer
|
||||
|
||||
You can tweak the build process using the following flags:
|
||||
|
1484
bld/track.js
1484
bld/track.js
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
// ==UserScript==
|
||||
// @name Discord History Tracker
|
||||
// @version v.31f
|
||||
// @version v.31h
|
||||
// @license MIT
|
||||
// @namespace https://chylex.com
|
||||
// @homepageURL https://dht.chylex.com/
|
||||
@@ -12,80 +12,42 @@
|
||||
|
||||
const start = function(){
|
||||
|
||||
// NOTE: Currently unused. See `.createStyle()` calls in `gui.js`. Included in branch so a rebase isn't needed for `git` to track this if it's used later.
|
||||
/*
|
||||
const css_controller = `
|
||||
#app-mount {
|
||||
height: calc(100% - 48px) !important;
|
||||
// noinspection JSAnnotator
|
||||
|
||||
const url = window.location.href;
|
||||
|
||||
if (!url.includes("discord.com/") && !url.includes("discordapp.com/") && !confirm("Could not detect Discord in the URL, do you want to run the script anyway?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
#dht-ctrl {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background-color: #fff;
|
||||
z-index: 1000000;
|
||||
if (window.DHT_LOADED) {
|
||||
alert("Discord History Tracker is already loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
#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 p {
|
||||
display: inline-block;
|
||||
margin: 14px 12px;
|
||||
}
|
||||
`
|
||||
*/
|
||||
|
||||
// NOTE: Currently unused. See `.createStyle()` calls in `gui.js`. Included in branch so a rebase isn't needed for `git` to track this if it's used later.
|
||||
/*
|
||||
const css_settings = `
|
||||
#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;
|
||||
}
|
||||
`
|
||||
*/
|
||||
window.DHT_LOADED = true;
|
||||
window.DHT_ON_UNLOAD = [];
|
||||
|
||||
// noinspection JSUnresolvedVariable
|
||||
// noinspection LocalVariableNamingConventionJS
|
||||
class DISCORD {
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
static CHANNEL_TYPE = {
|
||||
DM: 1,
|
||||
GROUP_DM: 3,
|
||||
ANNOUNCEMENT_THREAD: 10,
|
||||
PUBLIC_THREAD: 11,
|
||||
PRIVATE_THREAD: 12
|
||||
};
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-types
|
||||
static MESSAGE_TYPE = {
|
||||
DEFAULT: 0,
|
||||
REPLY: 19,
|
||||
THREAD_STARTER: 21
|
||||
};
|
||||
|
||||
static getMessageOuterElement() {
|
||||
return DOM.queryReactClass("messagesWrapper");
|
||||
}
|
||||
@@ -114,46 +76,11 @@ class DISCORD {
|
||||
* Calls the provided function with a list of messages whenever the currently loaded messages change.
|
||||
*/
|
||||
static setupMessageCallback(callback) {
|
||||
let skipsLeft = 0;
|
||||
let waitForCleanup = false;
|
||||
const previousMessages = new Set();
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
if (skipsLeft > 0) {
|
||||
--skipsLeft;
|
||||
return;
|
||||
}
|
||||
|
||||
const view = this.getMessageOuterElement();
|
||||
|
||||
if (!view) {
|
||||
skipsLeft = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
const anyMessage = DOM.queryReactClass("message", this.getMessageOuterElement());
|
||||
const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0;
|
||||
|
||||
if (messageCount > 300) {
|
||||
if (waitForCleanup) {
|
||||
return;
|
||||
}
|
||||
|
||||
skipsLeft = 3;
|
||||
waitForCleanup = true;
|
||||
|
||||
window.setTimeout(() => {
|
||||
const view = this.getMessageScrollerElement();
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
view.scrollTop = view.scrollHeight / 2;
|
||||
}, 1);
|
||||
}
|
||||
else {
|
||||
waitForCleanup = false;
|
||||
}
|
||||
|
||||
const messages = this.getMessages();
|
||||
const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !this.hasMoreMessages();
|
||||
const onMessageElementsChanged = function() {
|
||||
const messages = DISCORD.getMessages();
|
||||
const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !DISCORD.hasMoreMessages();
|
||||
|
||||
if (!hasChanged) {
|
||||
return;
|
||||
@@ -165,24 +92,74 @@ class DISCORD {
|
||||
}
|
||||
|
||||
callback(messages);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
window.DHT_ON_UNLOAD.push(() => window.clearInterval(timer));
|
||||
let debounceTimer;
|
||||
|
||||
/**
|
||||
* Do not trigger the callback too often due to autoscrolling.
|
||||
*/
|
||||
const onMessageElementsChangedLater = function() {
|
||||
window.clearTimeout(debounceTimer);
|
||||
debounceTimer = window.setTimeout(onMessageElementsChanged, 100);
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(function () {
|
||||
onMessageElementsChangedLater();
|
||||
});
|
||||
|
||||
let skipsLeft = 0;
|
||||
let observedElement = null;
|
||||
|
||||
const observerTimer = window.setInterval(() => {
|
||||
if (skipsLeft > 0) {
|
||||
--skipsLeft;
|
||||
return;
|
||||
}
|
||||
|
||||
const view = this.getMessageOuterElement();
|
||||
|
||||
if (!view) {
|
||||
skipsLeft = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (observedElement !== null && observedElement.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
observedElement = view.querySelector("[data-list-id='chat-messages']");
|
||||
|
||||
if (observedElement) {
|
||||
console.debug("[DHT] Observed message container.");
|
||||
observer.observe(observedElement, { childList: true });
|
||||
onMessageElementsChangedLater();
|
||||
}
|
||||
}, 400);
|
||||
|
||||
window.DHT_ON_UNLOAD.push(() => {
|
||||
observer.disconnect();
|
||||
observedElement = null;
|
||||
window.clearInterval(observerTimer);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the property object of a message element.
|
||||
* @returns { null | { message: DiscordMessage, channel: Object } }
|
||||
* Returns the message from a message element.
|
||||
* @returns { null | DiscordMessage } }
|
||||
*/
|
||||
static getMessageElementProps(ele) {
|
||||
static getMessageFromElement(ele) {
|
||||
const props = DOM.getReactProps(ele);
|
||||
|
||||
if (props.children && props.children.length) {
|
||||
for (let i = 3; i < props.children.length; i++) {
|
||||
const childProps = props.children[i].props;
|
||||
if (props && Array.isArray(props.children)) {
|
||||
for (const child of props.children) {
|
||||
if (!(child instanceof Object)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (childProps && "message" in childProps && "channel" in childProps) {
|
||||
return childProps;
|
||||
const childProps = child.props;
|
||||
if (childProps instanceof Object && "message" in childProps) {
|
||||
return childProps.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,10 +176,10 @@ class DISCORD {
|
||||
|
||||
for (const ele of this.getMessageElements()) {
|
||||
try {
|
||||
const props = this.getMessageElementProps(ele);
|
||||
const message = this.getMessageFromElement(ele);
|
||||
|
||||
if (props != null) {
|
||||
messages.push(props.message);
|
||||
if (message != null) {
|
||||
messages.push(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error extracing message data, skipping it.", e, ele, DOM.tryGetReactProps(ele));
|
||||
@@ -223,7 +200,7 @@ class DISCORD {
|
||||
*/
|
||||
static getSelectedChannel() {
|
||||
try {
|
||||
let obj;
|
||||
let obj = null;
|
||||
|
||||
try {
|
||||
for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) {
|
||||
@@ -234,15 +211,6 @@ class DISCORD {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error retrieving selected channel from 'chatContent' element.", e);
|
||||
|
||||
for (const ele of this.getMessageElements()) {
|
||||
const props = this.getMessageElementProps(ele);
|
||||
|
||||
if (props != null) {
|
||||
obj = props.channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!obj || typeof obj.id !== "string") {
|
||||
@@ -271,8 +239,8 @@ class DISCORD {
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
switch (obj.type) {
|
||||
case 1: type = "DM"; break;
|
||||
case 3: type = "GROUP"; break;
|
||||
case DISCORD.CHANNEL_TYPE.DM: type = "DM"; break;
|
||||
case DISCORD.CHANNEL_TYPE.GROUP_DM: type = "GROUP"; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
@@ -310,7 +278,7 @@ class DISCORD {
|
||||
}
|
||||
};
|
||||
|
||||
if (obj.parent_id) {
|
||||
if (obj.type === DISCORD.CHANNEL_TYPE.ANNOUNCEMENT_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PUBLIC_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PRIVATE_THREAD) {
|
||||
channel["extra"]["parent"] = obj.parent_id;
|
||||
}
|
||||
else {
|
||||
@@ -475,72 +443,6 @@ class DOM {
|
||||
}
|
||||
}
|
||||
|
||||
const CONSTANTS = {
|
||||
AUTOSCROLL_ACTION_NOTHING: "optNothing",
|
||||
AUTOSCROLL_ACTION_PAUSE: "optPause",
|
||||
AUTOSCROLL_ACTION_SWITCH: "optSwitch"
|
||||
};
|
||||
|
||||
let IS_FIRST_RUN = false;
|
||||
|
||||
const SETTINGS = (function() {
|
||||
const settingsChangedEvents = [];
|
||||
|
||||
const saveSettings = function() {
|
||||
DOM.saveToCookie("DHT_SETTINGS", root, 60 * 60 * 24 * 365 * 5);
|
||||
};
|
||||
|
||||
const triggerSettingsChanged = function(property) {
|
||||
for (const callback of settingsChangedEvents) {
|
||||
callback(property);
|
||||
}
|
||||
|
||||
saveSettings();
|
||||
};
|
||||
|
||||
const defineTriggeringProperty = function(obj, property, value) {
|
||||
const name = "_" + property;
|
||||
|
||||
Object.defineProperty(obj, property, {
|
||||
get: (() => obj[name]),
|
||||
set: (value => {
|
||||
obj[name] = value;
|
||||
triggerSettingsChanged(property);
|
||||
})
|
||||
});
|
||||
|
||||
obj[name] = value;
|
||||
};
|
||||
|
||||
let loaded = DOM.loadFromCookie("DHT_SETTINGS");
|
||||
|
||||
if (!loaded) {
|
||||
loaded = {
|
||||
"_autoscroll": true,
|
||||
"_afterFirstMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE,
|
||||
"_afterSavedMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE
|
||||
};
|
||||
|
||||
IS_FIRST_RUN = true;
|
||||
}
|
||||
|
||||
const root = {
|
||||
onSettingsChanged(callback) {
|
||||
settingsChangedEvents.push(callback);
|
||||
}
|
||||
};
|
||||
|
||||
defineTriggeringProperty(root, "autoscroll", loaded._autoscroll);
|
||||
defineTriggeringProperty(root, "afterFirstMsg", loaded._afterFirstMsg);
|
||||
defineTriggeringProperty(root, "afterSavedMsg", loaded._afterSavedMsg);
|
||||
|
||||
if (IS_FIRST_RUN) {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
return root;
|
||||
})();
|
||||
|
||||
var GUI = (function(){
|
||||
var controller;
|
||||
var settings;
|
||||
@@ -627,14 +529,44 @@ 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; 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; }
|
||||
#dht-ctrl p { display: inline-block; margin: 14px 12px; }
|
||||
#dht-ctrl input { display: none; }`);
|
||||
controller.styles = DOM.createStyle(`#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 p {
|
||||
display: inline-block;
|
||||
margin: 14px 12px;
|
||||
}
|
||||
|
||||
#dht-ctrl-close {
|
||||
margin: 8px 8px 8px 0 !important;
|
||||
float: right;
|
||||
}
|
||||
`);
|
||||
|
||||
// main
|
||||
|
||||
@@ -647,7 +579,7 @@ ${btn("track", "")}
|
||||
${btn("download", "Download")}
|
||||
${btn("reset", "Reset")}
|
||||
<p id='dht-ctrl-status'></p>
|
||||
<input id='dht-ctrl-upload-input' type='file' multiple>
|
||||
<input id='dht-ctrl-upload-input' type='file' multiple style="display: none">
|
||||
${btn("close", "X")}`);
|
||||
|
||||
// elements
|
||||
@@ -736,11 +668,40 @@ ${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: 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; }`);
|
||||
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: 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;
|
||||
}
|
||||
`);
|
||||
|
||||
// overlay
|
||||
|
||||
@@ -758,17 +719,17 @@ ${btn("close", "X")}`);
|
||||
<label><input id='dht-cfg-autoscroll' type='checkbox'> Autoscroll</label><br>
|
||||
<br>
|
||||
<label>After reaching the first message in channel...</label><br>
|
||||
${radio("afm", "nothing", "Do Nothing")}
|
||||
${radio("afm", "nothing", "Continue Tracking")}
|
||||
${radio("afm", "pause", "Pause Tracking")}
|
||||
${radio("afm", "switch", "Switch to Next Channel")}
|
||||
<br>
|
||||
<label>After reaching a previously saved message...</label><br>
|
||||
${radio("asm", "nothing", "Do Nothing")}
|
||||
${radio("asm", "nothing", "Continue Tracking")}
|
||||
${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.31f, released 20 November 2023</sub>
|
||||
<sub>v.31h, released 03 March 2024</sub>
|
||||
</p>`);
|
||||
|
||||
// elements
|
||||
@@ -817,10 +778,7 @@ It is recommended to disable link and image previews to avoid putting unnecessar
|
||||
}
|
||||
},
|
||||
|
||||
setStatus: function(state){
|
||||
console.log("Status: " + state)
|
||||
// TODO I guess.
|
||||
}
|
||||
setStatus: function(status) {}
|
||||
};
|
||||
|
||||
return root;
|
||||
@@ -942,6 +900,16 @@ class SAVEFILE{
|
||||
return parsedObj && typeof parsedObj.meta === "object" && typeof parsedObj.data === "object";
|
||||
}
|
||||
|
||||
static getDate(date){
|
||||
if (date instanceof Date) {
|
||||
return date;
|
||||
}
|
||||
else {
|
||||
// noinspection JSUnresolvedReference
|
||||
return date.toDate();
|
||||
}
|
||||
};
|
||||
|
||||
findOrRegisterUser(userId, userName, userDiscriminator, userAvatar){
|
||||
var wasPresent = userId in this.meta.users;
|
||||
var userObj = wasPresent ? this.meta.users[userId] : {};
|
||||
@@ -995,15 +963,15 @@ class SAVEFILE{
|
||||
|
||||
channelObj.name = channelName;
|
||||
|
||||
if (extraInfo.position){
|
||||
if (extraInfo.position) {
|
||||
channelObj.position = extraInfo.position;
|
||||
}
|
||||
|
||||
if (extraInfo.topic){
|
||||
if (extraInfo.topic) {
|
||||
channelObj.topic = extraInfo.topic;
|
||||
}
|
||||
|
||||
if (extraInfo.nsfw){
|
||||
if (extraInfo.nsfw) {
|
||||
channelObj.nsfw = extraInfo.nsfw;
|
||||
}
|
||||
|
||||
@@ -1031,7 +999,7 @@ class SAVEFILE{
|
||||
|
||||
var obj = {
|
||||
u: this.findOrRegisterUser(author.id, author.username, author.bot ? null : author.discriminator, author.avatar),
|
||||
t: discordMessage.timestamp.toDate().getTime()
|
||||
t: SAVEFILE.getDate(discordMessage.timestamp).getTime()
|
||||
};
|
||||
|
||||
if (discordMessage.content.length > 0){
|
||||
@@ -1039,7 +1007,7 @@ class SAVEFILE{
|
||||
}
|
||||
|
||||
if (discordMessage.editedTimestamp !== null){
|
||||
obj.te = discordMessage.editedTimestamp.toDate().getTime();
|
||||
obj.te = SAVEFILE.getDate(discordMessage.editedTimestamp).getTime();
|
||||
}
|
||||
|
||||
if (discordMessage.embeds.length > 0){
|
||||
@@ -1097,11 +1065,9 @@ class SAVEFILE{
|
||||
|
||||
addMessagesFromDiscord(discordMessageArray){
|
||||
var hasNewMessages = false;
|
||||
for(var discordMessage of discordMessageArray){
|
||||
var type = discordMessage.type;
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure
|
||||
if ((type === 0 || type === 19) && discordMessage.state === "SENT" && this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){
|
||||
for(var discordMessage of discordMessageArray){
|
||||
if (this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){
|
||||
hasNewMessages = true;
|
||||
}
|
||||
}
|
||||
@@ -1168,6 +1134,72 @@ class SAVEFILE{
|
||||
}
|
||||
}
|
||||
|
||||
const CONSTANTS = {
|
||||
AUTOSCROLL_ACTION_NOTHING: "optNothing",
|
||||
AUTOSCROLL_ACTION_PAUSE: "optPause",
|
||||
AUTOSCROLL_ACTION_SWITCH: "optSwitch"
|
||||
};
|
||||
|
||||
let IS_FIRST_RUN = false;
|
||||
|
||||
const SETTINGS = (function() {
|
||||
const settingsChangedEvents = [];
|
||||
|
||||
const saveSettings = function() {
|
||||
DOM.saveToCookie("DHT_SETTINGS", root, 60 * 60 * 24 * 365 * 5);
|
||||
};
|
||||
|
||||
const triggerSettingsChanged = function(property) {
|
||||
for (const callback of settingsChangedEvents) {
|
||||
callback(property);
|
||||
}
|
||||
|
||||
saveSettings();
|
||||
};
|
||||
|
||||
const defineTriggeringProperty = function(obj, property, value) {
|
||||
const name = "_" + property;
|
||||
|
||||
Object.defineProperty(obj, property, {
|
||||
get: (() => obj[name]),
|
||||
set: (value => {
|
||||
obj[name] = value;
|
||||
triggerSettingsChanged(property);
|
||||
})
|
||||
});
|
||||
|
||||
obj[name] = value;
|
||||
};
|
||||
|
||||
let loaded = DOM.loadFromCookie("DHT_SETTINGS");
|
||||
|
||||
if (!loaded) {
|
||||
loaded = {
|
||||
"_autoscroll": true,
|
||||
"_afterFirstMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE,
|
||||
"_afterSavedMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE
|
||||
};
|
||||
|
||||
IS_FIRST_RUN = true;
|
||||
}
|
||||
|
||||
const root = {
|
||||
onSettingsChanged(callback) {
|
||||
settingsChangedEvents.push(callback);
|
||||
}
|
||||
};
|
||||
|
||||
defineTriggeringProperty(root, "autoscroll", loaded._autoscroll);
|
||||
defineTriggeringProperty(root, "afterFirstMsg", loaded._afterFirstMsg);
|
||||
defineTriggeringProperty(root, "afterSavedMsg", loaded._afterSavedMsg);
|
||||
|
||||
if (IS_FIRST_RUN) {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
return root;
|
||||
})();
|
||||
|
||||
// noinspection FunctionWithInconsistentReturnsJS
|
||||
const STATE = (function() {
|
||||
|
||||
@@ -1280,23 +1312,25 @@ const STATE = (function() {
|
||||
* Registers a Discord server and channel.
|
||||
*/
|
||||
addDiscordChannel(serverInfo, channelInfo){
|
||||
let serverName = serverInfo.name
|
||||
let serverType = serverInfo.type
|
||||
let channelId = channelInfo.id
|
||||
let channelName = channelInfo.name
|
||||
let extraInfo = channelInfo.extra
|
||||
var serverName = serverInfo.name;
|
||||
var serverType = serverInfo.type;
|
||||
var channelId = channelInfo.id;
|
||||
var channelName = channelInfo.name;
|
||||
var extraInfo = channelInfo.extra || {};
|
||||
|
||||
var serverIndex = this.getSavefile().findOrRegisterServer(serverName, serverType);
|
||||
|
||||
if (this.getSavefile().tryRegisterChannel(serverIndex, channelId, channelName, extraInfo) === true){
|
||||
this._triggerStateChanged("data", "channel");
|
||||
}
|
||||
}
|
||||
// Right. Upstream desktop `bootstrap.js` expects an `async` here. I think it's fine.
|
||||
|
||||
/*
|
||||
* Adds all messages from the array to the specified channel. Returns true if the savefile was updated.
|
||||
*/
|
||||
addDiscordMessages(discordMessageArray){
|
||||
discordMessageArray = discordMessageArray.filter(msg => (msg.type === DISCORD.MESSAGE_TYPE.DEFAULT || msg.type === DISCORD.MESSAGE_TYPE.REPLY || msg.type === DISCORD.MESSAGE_TYPE.THREAD_STARTER) && msg.state === "SENT");
|
||||
|
||||
if (this.getSavefile().addMessagesFromDiscord(discordMessageArray)){
|
||||
this._triggerStateChanged("data", "messages");
|
||||
return true;
|
||||
@@ -1320,43 +1354,14 @@ const STATE = (function() {
|
||||
this._trackingStateChangedListeners.push(callback);
|
||||
callback(this._isTracking);
|
||||
}
|
||||
|
||||
/*
|
||||
* Shim for code from the desktop app.
|
||||
*/
|
||||
setup(port, token) {
|
||||
console.log("Placeholder port and token: " + port + " " + token);
|
||||
}
|
||||
}
|
||||
|
||||
return new CLS();
|
||||
})();
|
||||
|
||||
// NOTE: STOP! This file must be kept in sync with the upstream desktop app branch in order for future changes/fixes to the desktop script to be cleanly merged here.
|
||||
// Changes for the browser version should be done in another file to maintain mergeablity.
|
||||
(function() {
|
||||
const url = window.location.href;
|
||||
|
||||
if (!url.includes("discord.com/") && !url.includes("discordapp.com/") && !confirm("Could not detect Discord in the URL, do you want to run the script anyway?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.DHT_LOADED) {
|
||||
alert("Discord History Tracker is already loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
window.DHT_LOADED = true;
|
||||
window.DHT_ON_UNLOAD = [];
|
||||
|
||||
/*[IMPORTS]*/
|
||||
|
||||
const port = 0; /*[PORT]*/
|
||||
const token = "/*[TOKEN]*/";
|
||||
STATE.setup(port, token);
|
||||
|
||||
let delayedStopRequests = 0;
|
||||
const stopTrackingDelayed = function(callback) {
|
||||
let delayedStopRequests = 0;
|
||||
const stopTrackingDelayed = function(callback) {
|
||||
delayedStopRequests++;
|
||||
|
||||
window.setTimeout(() => {
|
||||
@@ -1367,22 +1372,22 @@ const STATE = (function() {
|
||||
callback();
|
||||
}
|
||||
}, 200); // give the user visual feedback after clicking the button before switching off
|
||||
};
|
||||
};
|
||||
|
||||
let hasJustStarted = false;
|
||||
let isSending = false;
|
||||
let hasJustStarted = false;
|
||||
let isSending = false;
|
||||
|
||||
const onError = function(e) {
|
||||
const onError = function(e) {
|
||||
console.log(e);
|
||||
GUI.setStatus(e.status === "DISCONNECTED" ? "Disconnected" : "Error");
|
||||
stopTrackingDelayed(() => isSending = false);
|
||||
};
|
||||
};
|
||||
|
||||
const isNoAction = function(action) {
|
||||
const isNoAction = function(action) {
|
||||
return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
|
||||
};
|
||||
};
|
||||
|
||||
const onTrackingContinued = function(anyNewMessages) {
|
||||
const onTrackingContinued = function(anyNewMessages) {
|
||||
if (!STATE.isTracking()) {
|
||||
return;
|
||||
}
|
||||
@@ -1400,9 +1405,11 @@ const STATE = (function() {
|
||||
let action = null;
|
||||
|
||||
if (!DISCORD.hasMoreMessages()) {
|
||||
console.debug("[DHT] Reached first message.");
|
||||
action = SETTINGS.afterFirstMsg;
|
||||
}
|
||||
if (isNoAction(action) && !anyNewMessages) {
|
||||
console.debug("[DHT] No new messages.");
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
|
||||
@@ -1414,11 +1421,11 @@ const STATE = (function() {
|
||||
STATE.setIsTracking(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let waitUntilSendingFinishedTimer = null;
|
||||
let waitUntilSendingFinishedTimer = null;
|
||||
|
||||
const onMessagesUpdated = async messages => {
|
||||
const onMessagesUpdated = async messages => {
|
||||
if (!STATE.isTracking() || delayedStopRequests > 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1445,7 +1452,7 @@ const STATE = (function() {
|
||||
isSending = true;
|
||||
|
||||
try {
|
||||
await STATE.addDiscordChannel(info.server, info.channel);
|
||||
STATE.addDiscordChannel(info.server, info.channel);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
return;
|
||||
@@ -1457,17 +1464,17 @@ const STATE = (function() {
|
||||
onTrackingContinued(false);
|
||||
}
|
||||
else {
|
||||
const anyNewMessages = await STATE.addDiscordMessages(messages);
|
||||
const anyNewMessages = STATE.addDiscordMessages(messages);
|
||||
onTrackingContinued(anyNewMessages);
|
||||
}
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||
DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||
|
||||
STATE.onTrackingStateChanged(enabled => {
|
||||
STATE.onTrackingStateChanged(enabled => {
|
||||
if (enabled) {
|
||||
const messages = DISCORD.getMessages();
|
||||
|
||||
@@ -1484,15 +1491,13 @@ const STATE = (function() {
|
||||
else {
|
||||
isSending = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
GUI.showController();
|
||||
GUI.showController();
|
||||
|
||||
if (IS_FIRST_RUN) {
|
||||
if (IS_FIRST_RUN) {
|
||||
GUI.showSettings();
|
||||
}
|
||||
})();
|
||||
/*[DEBUGGER]*/
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
1268
bld/viewer.html
1268
bld/viewer.html
File diff suppressed because one or more lines are too long
96
build.py
96
build.py
@@ -8,11 +8,11 @@ import os
|
||||
import re
|
||||
import distutils.dir_util
|
||||
|
||||
VERSION_SHORT = "v.31f"
|
||||
VERSION_FULL = VERSION_SHORT + ", released 20 November 2023"
|
||||
VERSION_SHORT = "v.31h"
|
||||
VERSION_FULL = VERSION_SHORT + ", released 03 March 2024"
|
||||
|
||||
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}\""
|
||||
EXEC_UGLIFYJS_WIN = "{2}/lib/uglifyjs.cmd --parse bare_returns --compress --output \"{1}\" \"{0}\""
|
||||
EXEC_UGLIFYJS_AUTO = "uglifyjs --parse bare_returns --compress --output \"{1}\" \"{0}\""
|
||||
|
||||
USE_UGLIFYJS = "--nominify" not in sys.argv
|
||||
USE_MINIFICATION = "--nominify" not in sys.argv
|
||||
@@ -32,17 +32,13 @@ else:
|
||||
USE_UGLIFYJS = False
|
||||
print("Could not find 'uglifyjs', JS minification will be disabled")
|
||||
|
||||
if USE_UGLIFYJS:
|
||||
with open("reserve.txt", "r") as reserved:
|
||||
RESERVED_PROPS = ",".join(line.strip() for line in reserved.readlines())
|
||||
|
||||
|
||||
# File Utilities
|
||||
|
||||
def combine_files(input_pattern, output_file):
|
||||
is_first_file = True
|
||||
|
||||
with fileinput.input(sorted(glob.glob(input_pattern, recursive=True))) as stream:
|
||||
with fileinput.input(sorted(glob.glob(input_pattern))) as stream:
|
||||
for line in stream:
|
||||
if stream.isfirstline():
|
||||
if is_first_file:
|
||||
@@ -53,6 +49,23 @@ def combine_files(input_pattern, output_file):
|
||||
output_file.write(line.replace("{{{version:full}}}", VERSION_FULL))
|
||||
|
||||
|
||||
def combine_files_to_str(input_pattern):
|
||||
is_first_file = True
|
||||
output = []
|
||||
|
||||
with fileinput.input(sorted(glob.glob(input_pattern))) as stream:
|
||||
for line in stream:
|
||||
if stream.isfirstline():
|
||||
if is_first_file:
|
||||
is_first_file = False
|
||||
else:
|
||||
output.append("\n")
|
||||
|
||||
output.append(line.replace("{{{version:full}}}", VERSION_FULL))
|
||||
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def minify_css(input_file, output_file):
|
||||
if not USE_MINIFICATION:
|
||||
if input_file != output_file:
|
||||
@@ -77,24 +90,39 @@ def minify_css(input_file, output_file):
|
||||
|
||||
# Build System
|
||||
|
||||
def build_tracker_html():
|
||||
def build_tracker():
|
||||
output_file_raw = "bld/track.js"
|
||||
output_file_html = "bld/track.html"
|
||||
output_file_userscript = "bld/track.user.js"
|
||||
|
||||
output_file_tmp = "bld/track.tmp.js"
|
||||
input_pattern = "src/tracker/**/*.js"
|
||||
with open("src/tracker/styles/controller.css", "r") as f:
|
||||
controller_css = f.read()
|
||||
|
||||
with open("src/tracker/styles/settings.css", "r") as f:
|
||||
settings_css = f.read()
|
||||
|
||||
with open("src/tracker/bootstrap.js", "r") as f:
|
||||
bootstrap_js = f.read()
|
||||
|
||||
combined_tracker_js = combine_files_to_str("src/tracker/scripts/*.js")
|
||||
combined_tracker_js = combined_tracker_js.replace("/*[CSS-CONTROLLER]*/", controller_css)
|
||||
combined_tracker_js = combined_tracker_js.replace("/*[CSS-SETTINGS]*/", settings_css)
|
||||
|
||||
full_tracker_js = bootstrap_js.replace("/*[IMPORTS]*/", combined_tracker_js)
|
||||
|
||||
with open(output_file_raw, "w") as out:
|
||||
if not USE_UGLIFYJS:
|
||||
out.write("(function(){\n")
|
||||
|
||||
combine_files(input_pattern, out)
|
||||
out.write(full_tracker_js)
|
||||
|
||||
if not USE_UGLIFYJS:
|
||||
out.write("})()")
|
||||
|
||||
if USE_UGLIFYJS:
|
||||
os.system(EXEC_UGLIFYJS.format(output_file_raw, output_file_tmp, WORKING_DIR, RESERVED_PROPS))
|
||||
output_file_tmp = "bld/track.tmp.js"
|
||||
|
||||
os.system(EXEC_UGLIFYJS.format(output_file_raw, output_file_tmp, WORKING_DIR))
|
||||
|
||||
with open(output_file_raw, "w") as out:
|
||||
out.write("javascript:(function(){")
|
||||
@@ -107,25 +135,28 @@ def build_tracker_html():
|
||||
os.remove(output_file_tmp)
|
||||
|
||||
with open(output_file_raw, "r") as raw:
|
||||
script_contents = raw.read().replace("&", "&").replace('"', """).replace("'", "'").replace("<", "<").replace(">", ">")
|
||||
minified_tracker_js = raw.read()
|
||||
|
||||
with open(output_file_html, "w") as out:
|
||||
out.write(script_contents)
|
||||
write_tracker_html(output_file_html, minified_tracker_js)
|
||||
write_tracker_userscript(output_file_userscript, full_tracker_js)
|
||||
|
||||
|
||||
def build_tracker_userscript():
|
||||
output_file = "bld/track.user.js"
|
||||
|
||||
input_pattern = "src/tracker/**/*.js"
|
||||
userscript_base = "src/base/track.user.js"
|
||||
|
||||
with open(userscript_base, "r") as base:
|
||||
userscript_contents = base.read().replace("{{{version}}}", VERSION_SHORT).split("{{{contents}}}")
|
||||
def write_tracker_html(output_file, tracker_js):
|
||||
tracker_js = tracker_js.replace("&", "&").replace('"', """).replace("'", "'").replace("<", "<").replace(">", ">")
|
||||
|
||||
with open(output_file, "w") as out:
|
||||
out.write(userscript_contents[0])
|
||||
combine_files(input_pattern, out)
|
||||
out.write(userscript_contents[1])
|
||||
out.write(tracker_js)
|
||||
|
||||
|
||||
def write_tracker_userscript(output_file, full_tracker_js):
|
||||
with open("src/base/track.user.js", "r") as f:
|
||||
userscript_js = f.read()
|
||||
|
||||
userscript_js = userscript_js.replace("{{{version}}}", VERSION_SHORT)
|
||||
userscript_js = userscript_js.replace("{{{contents}}}", full_tracker_js)
|
||||
|
||||
with open(output_file, "w") as out:
|
||||
out.write(userscript_js)
|
||||
|
||||
|
||||
def build_viewer():
|
||||
@@ -150,7 +181,7 @@ def build_viewer():
|
||||
combine_files(input_js_pattern, out)
|
||||
|
||||
if USE_UGLIFYJS:
|
||||
os.system(EXEC_UGLIFYJS.format(tmp_js_file_combined, tmp_js_file_minified, WORKING_DIR, RESERVED_PROPS))
|
||||
os.system(EXEC_UGLIFYJS.format(tmp_js_file_combined, tmp_js_file_minified, WORKING_DIR))
|
||||
else:
|
||||
shutil.copyfile(tmp_js_file_combined, tmp_js_file_minified)
|
||||
|
||||
@@ -202,11 +233,8 @@ def build_website():
|
||||
|
||||
os.makedirs("bld", exist_ok = True)
|
||||
|
||||
print("Building tracker html...")
|
||||
build_tracker_html()
|
||||
|
||||
print("Building tracker userscript...")
|
||||
build_tracker_userscript()
|
||||
print("Building tracker...")
|
||||
build_tracker()
|
||||
|
||||
print("Building viewer...")
|
||||
build_viewer()
|
||||
|
74
reserve.txt
74
reserve.txt
@@ -1,74 +0,0 @@
|
||||
autoscroll
|
||||
_autoscroll
|
||||
afterFirstMsg
|
||||
_afterFirstMsg
|
||||
afterSavedMsg
|
||||
_afterSavedMsg
|
||||
enableImagePreviews
|
||||
_enableImagePreviews
|
||||
enableFormatting
|
||||
_enableFormatting
|
||||
enableAnimatedEmoji
|
||||
_enableAnimatedEmoji
|
||||
enableUserAvatars
|
||||
_enableUserAvatars
|
||||
DHT_LOADED
|
||||
DHT_EMBEDDED
|
||||
meta
|
||||
data
|
||||
users
|
||||
userindex
|
||||
servers
|
||||
channels
|
||||
u
|
||||
t
|
||||
m
|
||||
f
|
||||
e
|
||||
a
|
||||
t
|
||||
te
|
||||
d
|
||||
r
|
||||
re
|
||||
c
|
||||
n
|
||||
an
|
||||
tag
|
||||
avatar
|
||||
author
|
||||
type
|
||||
state
|
||||
name
|
||||
position
|
||||
topic
|
||||
nsfw
|
||||
id
|
||||
username
|
||||
bot
|
||||
discriminator
|
||||
timestamp
|
||||
content
|
||||
editedTimestamp
|
||||
mentions
|
||||
embeds
|
||||
attachments
|
||||
title
|
||||
description
|
||||
reply
|
||||
reactions
|
||||
emoji
|
||||
count
|
||||
animated
|
||||
ext
|
||||
toDate
|
||||
memoizedProps
|
||||
props
|
||||
children
|
||||
channel
|
||||
messages
|
||||
msSaveBlob
|
||||
messageReference
|
||||
message_id
|
||||
guild_id
|
||||
guild
|
@@ -1,5 +0,0 @@
|
||||
**STOP!**
|
||||
|
||||
These files must be kept in sync with the upstream desktop app branch in order for future changes/fixes to the desktop script to be cleanly merged here.
|
||||
|
||||
Changes for the browser version should be done in another file to maintain mergeablity.
|
73
src/tracker/bootstrap.js
vendored
73
src/tracker/bootstrap.js
vendored
@@ -1,28 +1,23 @@
|
||||
// NOTE: STOP! This file must be kept in sync with the upstream desktop app branch in order for future changes/fixes to the desktop script to be cleanly merged here.
|
||||
// Changes for the browser version should be done in another file to maintain mergeablity.
|
||||
(function() {
|
||||
const url = window.location.href;
|
||||
// noinspection JSAnnotator
|
||||
|
||||
if (!url.includes("discord.com/") && !url.includes("discordapp.com/") && !confirm("Could not detect Discord in the URL, do you want to run the script anyway?")) {
|
||||
const url = window.location.href;
|
||||
|
||||
if (!url.includes("discord.com/") && !url.includes("discordapp.com/") && !confirm("Could not detect Discord in the URL, do you want to run the script anyway?")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (window.DHT_LOADED) {
|
||||
if (window.DHT_LOADED) {
|
||||
alert("Discord History Tracker is already loaded.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
window.DHT_LOADED = true;
|
||||
window.DHT_ON_UNLOAD = [];
|
||||
window.DHT_LOADED = true;
|
||||
window.DHT_ON_UNLOAD = [];
|
||||
|
||||
/*[IMPORTS]*/
|
||||
/*[IMPORTS]*/
|
||||
|
||||
const port = 0; /*[PORT]*/
|
||||
const token = "/*[TOKEN]*/";
|
||||
STATE.setup(port, token);
|
||||
|
||||
let delayedStopRequests = 0;
|
||||
const stopTrackingDelayed = function(callback) {
|
||||
let delayedStopRequests = 0;
|
||||
const stopTrackingDelayed = function(callback) {
|
||||
delayedStopRequests++;
|
||||
|
||||
window.setTimeout(() => {
|
||||
@@ -33,22 +28,22 @@
|
||||
callback();
|
||||
}
|
||||
}, 200); // give the user visual feedback after clicking the button before switching off
|
||||
};
|
||||
};
|
||||
|
||||
let hasJustStarted = false;
|
||||
let isSending = false;
|
||||
let hasJustStarted = false;
|
||||
let isSending = false;
|
||||
|
||||
const onError = function(e) {
|
||||
const onError = function(e) {
|
||||
console.log(e);
|
||||
GUI.setStatus(e.status === "DISCONNECTED" ? "Disconnected" : "Error");
|
||||
stopTrackingDelayed(() => isSending = false);
|
||||
};
|
||||
};
|
||||
|
||||
const isNoAction = function(action) {
|
||||
const isNoAction = function(action) {
|
||||
return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
|
||||
};
|
||||
};
|
||||
|
||||
const onTrackingContinued = function(anyNewMessages) {
|
||||
const onTrackingContinued = function(anyNewMessages) {
|
||||
if (!STATE.isTracking()) {
|
||||
return;
|
||||
}
|
||||
@@ -66,9 +61,11 @@
|
||||
let action = null;
|
||||
|
||||
if (!DISCORD.hasMoreMessages()) {
|
||||
console.debug("[DHT] Reached first message.");
|
||||
action = SETTINGS.afterFirstMsg;
|
||||
}
|
||||
if (isNoAction(action) && !anyNewMessages) {
|
||||
console.debug("[DHT] No new messages.");
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
|
||||
@@ -80,11 +77,11 @@
|
||||
STATE.setIsTracking(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let waitUntilSendingFinishedTimer = null;
|
||||
let waitUntilSendingFinishedTimer = null;
|
||||
|
||||
const onMessagesUpdated = async messages => {
|
||||
const onMessagesUpdated = async messages => {
|
||||
if (!STATE.isTracking() || delayedStopRequests > 0) {
|
||||
return;
|
||||
}
|
||||
@@ -111,7 +108,7 @@
|
||||
isSending = true;
|
||||
|
||||
try {
|
||||
await STATE.addDiscordChannel(info.server, info.channel);
|
||||
STATE.addDiscordChannel(info.server, info.channel);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
return;
|
||||
@@ -123,17 +120,17 @@
|
||||
onTrackingContinued(false);
|
||||
}
|
||||
else {
|
||||
const anyNewMessages = await STATE.addDiscordMessages(messages);
|
||||
const anyNewMessages = STATE.addDiscordMessages(messages);
|
||||
onTrackingContinued(anyNewMessages);
|
||||
}
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||
DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||
|
||||
STATE.onTrackingStateChanged(enabled => {
|
||||
STATE.onTrackingStateChanged(enabled => {
|
||||
if (enabled) {
|
||||
const messages = DISCORD.getMessages();
|
||||
|
||||
@@ -150,12 +147,10 @@
|
||||
else {
|
||||
isSending = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
GUI.showController();
|
||||
GUI.showController();
|
||||
|
||||
if (IS_FIRST_RUN) {
|
||||
if (IS_FIRST_RUN) {
|
||||
GUI.showSettings();
|
||||
}
|
||||
})();
|
||||
/*[DEBUGGER]*/
|
||||
}
|
||||
|
@@ -1,5 +1,23 @@
|
||||
// noinspection JSUnresolvedVariable
|
||||
// noinspection LocalVariableNamingConventionJS
|
||||
class DISCORD {
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
static CHANNEL_TYPE = {
|
||||
DM: 1,
|
||||
GROUP_DM: 3,
|
||||
ANNOUNCEMENT_THREAD: 10,
|
||||
PUBLIC_THREAD: 11,
|
||||
PRIVATE_THREAD: 12
|
||||
};
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-types
|
||||
static MESSAGE_TYPE = {
|
||||
DEFAULT: 0,
|
||||
REPLY: 19,
|
||||
THREAD_STARTER: 21
|
||||
};
|
||||
|
||||
static getMessageOuterElement() {
|
||||
return DOM.queryReactClass("messagesWrapper");
|
||||
}
|
||||
@@ -28,46 +46,11 @@ class DISCORD {
|
||||
* Calls the provided function with a list of messages whenever the currently loaded messages change.
|
||||
*/
|
||||
static setupMessageCallback(callback) {
|
||||
let skipsLeft = 0;
|
||||
let waitForCleanup = false;
|
||||
const previousMessages = new Set();
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
if (skipsLeft > 0) {
|
||||
--skipsLeft;
|
||||
return;
|
||||
}
|
||||
|
||||
const view = this.getMessageOuterElement();
|
||||
|
||||
if (!view) {
|
||||
skipsLeft = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
const anyMessage = DOM.queryReactClass("message", this.getMessageOuterElement());
|
||||
const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0;
|
||||
|
||||
if (messageCount > 300) {
|
||||
if (waitForCleanup) {
|
||||
return;
|
||||
}
|
||||
|
||||
skipsLeft = 3;
|
||||
waitForCleanup = true;
|
||||
|
||||
window.setTimeout(() => {
|
||||
const view = this.getMessageScrollerElement();
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
view.scrollTop = view.scrollHeight / 2;
|
||||
}, 1);
|
||||
}
|
||||
else {
|
||||
waitForCleanup = false;
|
||||
}
|
||||
|
||||
const messages = this.getMessages();
|
||||
const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !this.hasMoreMessages();
|
||||
const onMessageElementsChanged = function() {
|
||||
const messages = DISCORD.getMessages();
|
||||
const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !DISCORD.hasMoreMessages();
|
||||
|
||||
if (!hasChanged) {
|
||||
return;
|
||||
@@ -79,24 +62,74 @@ class DISCORD {
|
||||
}
|
||||
|
||||
callback(messages);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
window.DHT_ON_UNLOAD.push(() => window.clearInterval(timer));
|
||||
let debounceTimer;
|
||||
|
||||
/**
|
||||
* Do not trigger the callback too often due to autoscrolling.
|
||||
*/
|
||||
const onMessageElementsChangedLater = function() {
|
||||
window.clearTimeout(debounceTimer);
|
||||
debounceTimer = window.setTimeout(onMessageElementsChanged, 100);
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(function () {
|
||||
onMessageElementsChangedLater();
|
||||
});
|
||||
|
||||
let skipsLeft = 0;
|
||||
let observedElement = null;
|
||||
|
||||
const observerTimer = window.setInterval(() => {
|
||||
if (skipsLeft > 0) {
|
||||
--skipsLeft;
|
||||
return;
|
||||
}
|
||||
|
||||
const view = this.getMessageOuterElement();
|
||||
|
||||
if (!view) {
|
||||
skipsLeft = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (observedElement !== null && observedElement.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
observedElement = view.querySelector("[data-list-id='chat-messages']");
|
||||
|
||||
if (observedElement) {
|
||||
console.debug("[DHT] Observed message container.");
|
||||
observer.observe(observedElement, { childList: true });
|
||||
onMessageElementsChangedLater();
|
||||
}
|
||||
}, 400);
|
||||
|
||||
window.DHT_ON_UNLOAD.push(() => {
|
||||
observer.disconnect();
|
||||
observedElement = null;
|
||||
window.clearInterval(observerTimer);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the property object of a message element.
|
||||
* @returns { null | { message: DiscordMessage, channel: Object } }
|
||||
* Returns the message from a message element.
|
||||
* @returns { null | DiscordMessage } }
|
||||
*/
|
||||
static getMessageElementProps(ele) {
|
||||
static getMessageFromElement(ele) {
|
||||
const props = DOM.getReactProps(ele);
|
||||
|
||||
if (props.children && props.children.length) {
|
||||
for (let i = 3; i < props.children.length; i++) {
|
||||
const childProps = props.children[i].props;
|
||||
if (props && Array.isArray(props.children)) {
|
||||
for (const child of props.children) {
|
||||
if (!(child instanceof Object)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (childProps && "message" in childProps && "channel" in childProps) {
|
||||
return childProps;
|
||||
const childProps = child.props;
|
||||
if (childProps instanceof Object && "message" in childProps) {
|
||||
return childProps.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,10 +146,10 @@ class DISCORD {
|
||||
|
||||
for (const ele of this.getMessageElements()) {
|
||||
try {
|
||||
const props = this.getMessageElementProps(ele);
|
||||
const message = this.getMessageFromElement(ele);
|
||||
|
||||
if (props != null) {
|
||||
messages.push(props.message);
|
||||
if (message != null) {
|
||||
messages.push(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error extracing message data, skipping it.", e, ele, DOM.tryGetReactProps(ele));
|
||||
@@ -137,7 +170,7 @@ class DISCORD {
|
||||
*/
|
||||
static getSelectedChannel() {
|
||||
try {
|
||||
let obj;
|
||||
let obj = null;
|
||||
|
||||
try {
|
||||
for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) {
|
||||
@@ -148,15 +181,6 @@ class DISCORD {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error retrieving selected channel from 'chatContent' element.", e);
|
||||
|
||||
for (const ele of this.getMessageElements()) {
|
||||
const props = this.getMessageElementProps(ele);
|
||||
|
||||
if (props != null) {
|
||||
obj = props.channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!obj || typeof obj.id !== "string") {
|
||||
@@ -185,8 +209,8 @@ class DISCORD {
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
switch (obj.type) {
|
||||
case 1: type = "DM"; break;
|
||||
case 3: type = "GROUP"; break;
|
||||
case DISCORD.CHANNEL_TYPE.DM: type = "DM"; break;
|
||||
case DISCORD.CHANNEL_TYPE.GROUP_DM: type = "GROUP"; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
@@ -224,7 +248,7 @@ class DISCORD {
|
||||
}
|
||||
};
|
||||
|
||||
if (obj.parent_id) {
|
||||
if (obj.type === DISCORD.CHANNEL_TYPE.ANNOUNCEMENT_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PUBLIC_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PRIVATE_THREAD) {
|
||||
channel["extra"]["parent"] = obj.parent_id;
|
||||
}
|
||||
else {
|
@@ -84,14 +84,7 @@ 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; 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; }
|
||||
#dht-ctrl p { display: inline-block; margin: 14px 12px; }
|
||||
#dht-ctrl input { display: none; }`);
|
||||
controller.styles = DOM.createStyle(`/*[CSS-CONTROLLER]*/`);
|
||||
|
||||
// main
|
||||
|
||||
@@ -104,7 +97,7 @@ ${btn("track", "")}
|
||||
${btn("download", "Download")}
|
||||
${btn("reset", "Reset")}
|
||||
<p id='dht-ctrl-status'></p>
|
||||
<input id='dht-ctrl-upload-input' type='file' multiple>
|
||||
<input id='dht-ctrl-upload-input' type='file' multiple style="display: none">
|
||||
${btn("close", "X")}`);
|
||||
|
||||
// elements
|
||||
@@ -193,11 +186,7 @@ ${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: 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; }`);
|
||||
settings.styles = DOM.createStyle(`/*[CSS-SETTINGS]*/`);
|
||||
|
||||
// overlay
|
||||
|
||||
@@ -215,12 +204,12 @@ ${btn("close", "X")}`);
|
||||
<label><input id='dht-cfg-autoscroll' type='checkbox'> Autoscroll</label><br>
|
||||
<br>
|
||||
<label>After reaching the first message in channel...</label><br>
|
||||
${radio("afm", "nothing", "Do Nothing")}
|
||||
${radio("afm", "nothing", "Continue Tracking")}
|
||||
${radio("afm", "pause", "Pause Tracking")}
|
||||
${radio("afm", "switch", "Switch to Next Channel")}
|
||||
<br>
|
||||
<label>After reaching a previously saved message...</label><br>
|
||||
${radio("asm", "nothing", "Do Nothing")}
|
||||
${radio("asm", "nothing", "Continue Tracking")}
|
||||
${radio("asm", "pause", "Pause Tracking")}
|
||||
${radio("asm", "switch", "Switch to Next Channel")}
|
||||
<p id='dht-cfg-note'>
|
||||
@@ -274,10 +263,7 @@ It is recommended to disable link and image previews to avoid putting unnecessar
|
||||
}
|
||||
},
|
||||
|
||||
setStatus: function(state){
|
||||
console.log("Status: " + state)
|
||||
// TODO I guess.
|
||||
}
|
||||
setStatus: function(status) {}
|
||||
};
|
||||
|
||||
return root;
|
@@ -114,6 +114,16 @@ class SAVEFILE{
|
||||
return parsedObj && typeof parsedObj.meta === "object" && typeof parsedObj.data === "object";
|
||||
}
|
||||
|
||||
static getDate(date){
|
||||
if (date instanceof Date) {
|
||||
return date;
|
||||
}
|
||||
else {
|
||||
// noinspection JSUnresolvedReference
|
||||
return date.toDate();
|
||||
}
|
||||
};
|
||||
|
||||
findOrRegisterUser(userId, userName, userDiscriminator, userAvatar){
|
||||
var wasPresent = userId in this.meta.users;
|
||||
var userObj = wasPresent ? this.meta.users[userId] : {};
|
||||
@@ -167,15 +177,15 @@ class SAVEFILE{
|
||||
|
||||
channelObj.name = channelName;
|
||||
|
||||
if (extraInfo.position){
|
||||
if (extraInfo.position) {
|
||||
channelObj.position = extraInfo.position;
|
||||
}
|
||||
|
||||
if (extraInfo.topic){
|
||||
if (extraInfo.topic) {
|
||||
channelObj.topic = extraInfo.topic;
|
||||
}
|
||||
|
||||
if (extraInfo.nsfw){
|
||||
if (extraInfo.nsfw) {
|
||||
channelObj.nsfw = extraInfo.nsfw;
|
||||
}
|
||||
|
||||
@@ -203,7 +213,7 @@ class SAVEFILE{
|
||||
|
||||
var obj = {
|
||||
u: this.findOrRegisterUser(author.id, author.username, author.bot ? null : author.discriminator, author.avatar),
|
||||
t: discordMessage.timestamp.toDate().getTime()
|
||||
t: SAVEFILE.getDate(discordMessage.timestamp).getTime()
|
||||
};
|
||||
|
||||
if (discordMessage.content.length > 0){
|
||||
@@ -211,7 +221,7 @@ class SAVEFILE{
|
||||
}
|
||||
|
||||
if (discordMessage.editedTimestamp !== null){
|
||||
obj.te = discordMessage.editedTimestamp.toDate().getTime();
|
||||
obj.te = SAVEFILE.getDate(discordMessage.editedTimestamp).getTime();
|
||||
}
|
||||
|
||||
if (discordMessage.embeds.length > 0){
|
||||
@@ -269,11 +279,9 @@ class SAVEFILE{
|
||||
|
||||
addMessagesFromDiscord(discordMessageArray){
|
||||
var hasNewMessages = false;
|
||||
for(var discordMessage of discordMessageArray){
|
||||
var type = discordMessage.type;
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure
|
||||
if ((type === 0 || type === 19) && discordMessage.state === "SENT" && this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){
|
||||
for(var discordMessage of discordMessageArray){
|
||||
if (this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){
|
||||
hasNewMessages = true;
|
||||
}
|
||||
}
|
@@ -110,23 +110,25 @@ const STATE = (function() {
|
||||
* Registers a Discord server and channel.
|
||||
*/
|
||||
addDiscordChannel(serverInfo, channelInfo){
|
||||
let serverName = serverInfo.name
|
||||
let serverType = serverInfo.type
|
||||
let channelId = channelInfo.id
|
||||
let channelName = channelInfo.name
|
||||
let extraInfo = channelInfo.extra
|
||||
var serverName = serverInfo.name;
|
||||
var serverType = serverInfo.type;
|
||||
var channelId = channelInfo.id;
|
||||
var channelName = channelInfo.name;
|
||||
var extraInfo = channelInfo.extra || {};
|
||||
|
||||
var serverIndex = this.getSavefile().findOrRegisterServer(serverName, serverType);
|
||||
|
||||
if (this.getSavefile().tryRegisterChannel(serverIndex, channelId, channelName, extraInfo) === true){
|
||||
this._triggerStateChanged("data", "channel");
|
||||
}
|
||||
}
|
||||
// Right. Upstream desktop `bootstrap.js` expects an `async` here. I think it's fine.
|
||||
|
||||
/*
|
||||
* Adds all messages from the array to the specified channel. Returns true if the savefile was updated.
|
||||
*/
|
||||
addDiscordMessages(discordMessageArray){
|
||||
discordMessageArray = discordMessageArray.filter(msg => (msg.type === DISCORD.MESSAGE_TYPE.DEFAULT || msg.type === DISCORD.MESSAGE_TYPE.REPLY || msg.type === DISCORD.MESSAGE_TYPE.THREAD_STARTER) && msg.state === "SENT");
|
||||
|
||||
if (this.getSavefile().addMessagesFromDiscord(discordMessageArray)){
|
||||
this._triggerStateChanged("data", "messages");
|
||||
return true;
|
||||
@@ -150,13 +152,6 @@ const STATE = (function() {
|
||||
this._trackingStateChangedListeners.push(callback);
|
||||
callback(this._isTracking);
|
||||
}
|
||||
|
||||
/*
|
||||
* Shim for code from the desktop app.
|
||||
*/
|
||||
setup(port, token) {
|
||||
console.log("Placeholder port and token: " + port + " " + token);
|
||||
}
|
||||
}
|
||||
|
||||
return new CLS();
|
@@ -1,6 +1,3 @@
|
||||
// NOTE: Currently unused. See `.createStyle()` calls in `gui.js`.
|
||||
/*
|
||||
const css_controller = `
|
||||
#app-mount {
|
||||
height: calc(100% - 48px) !important;
|
||||
}
|
||||
@@ -33,5 +30,8 @@ const css_controller = `
|
||||
display: inline-block;
|
||||
margin: 14px 12px;
|
||||
}
|
||||
`
|
||||
*/
|
||||
|
||||
#dht-ctrl-close {
|
||||
margin: 8px 8px 8px 0 !important;
|
||||
float: right;
|
||||
}
|
@@ -1,6 +1,3 @@
|
||||
// NOTE: Currently unused. See `.createStyle()` calls in `gui.js`.
|
||||
/*
|
||||
const css_settings = `
|
||||
#dht-cfg-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -29,5 +26,8 @@ const css_settings = `
|
||||
#dht-cfg-note {
|
||||
margin-top: 22px;
|
||||
}
|
||||
`
|
||||
*/
|
||||
|
||||
#dht-cfg sub {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
@@ -1,7 +1,8 @@
|
||||
var DISCORD = (function(){
|
||||
var REGEX = {
|
||||
formatBold: /\*\*([\s\S]+?)\*\*(?!\*)/g,
|
||||
formatItalic: /(.)?\*([\s\S]+?)\*(?!\*)/g,
|
||||
formatItalic1: /\*([\s\S]+?)\*(?!\*)/g,
|
||||
formatItalic2: /_([\s\S]+?)_(?!_)\b/g,
|
||||
formatUnderline: /__([\s\S]+?)__(?!_)/g,
|
||||
formatStrike: /~~([\s\S]+?)~~(?!~)/g,
|
||||
formatCodeInline: /(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/g,
|
||||
@@ -54,7 +55,8 @@ var DISCORD = (function(){
|
||||
.replace(REGEX.specialEscapedSingle, escapeHtmlMatch)
|
||||
.replace(REGEX.specialEscapedDouble, full => full.replace(/\\/g, "").replace(/(.)/g, escapeHtmlMatch))
|
||||
.replace(REGEX.formatBold, "<b>$1</b>")
|
||||
.replace(REGEX.formatItalic, (full, pre, match) => pre === '\\' ? full : (pre || "")+"<i>"+match+"</i>")
|
||||
.replace(REGEX.formatItalic1, "<i>$1</i>")
|
||||
.replace(REGEX.formatItalic2, "<i>$1</i>")
|
||||
.replace(REGEX.formatUnderline, "<u>$1</u>")
|
||||
.replace(REGEX.formatStrike, "<s>$1</s>");
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB |
106
web/index.php
106
web/index.php
@@ -13,110 +13,10 @@
|
||||
</head>
|
||||
<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 lets you save chat history in your servers, groups, and private conversations, and view it offline.</p>
|
||||
<h1>Discord History Tracker <span class="bar">|</span> <span class="notes"><a href="https://github.com/chylex/Discord-History-Tracker/wiki/Release-Notes">Release Notes</a></span> <span class="bar">|</span> <span class="notes"><a href="https://github.com/chylex/Discord-History-Tracker/wiki/Old-Versions">Old Versions</a></span></h1>
|
||||
<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 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>
|
||||
|
||||
<ol>
|
||||
<li>Install a userscript manager addon:
|
||||
<ul>
|
||||
<li><a href="https://violentmonkey.github.io/get-it/">Violentmonkey</a> (Chrome)</li>
|
||||
<li><a href="https://tampermonkey.net/">Tampermonkey</a> (Firefox, Edge, Chrome, Opera)</li>
|
||||
<li>Due to browser bugs / limitations, DHT will not work in <strong>Firefox</strong> with Greasemonkey / Violentmonkey, and in <strong>Safari</strong> at all</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Click <a href="build/track.user.js">Install Userscript</a> to prompt an installation into the userscript manager</li>
|
||||
<li>Open <a href="https://discord.com/channels/@me" rel="noreferrer">Discord</a>, and view any server, group, or private conversation (it will not appear in Friends list)</li>
|
||||
<li>Click <strong>DHT</strong> in the top right corner:<br><img src="img/button.png" class="bordered"></li>
|
||||
</ol>
|
||||
<p>The browser-only version of Discord History Tracker has been superseded by a <a href="https://dht.chylex.com">desktop app version</a>, and is no longer supported.</p>
|
||||
<p>You can import archives from the browser-only version into the desktop app, or view them by downloading the <a href="build/viewer.html">Viewer</a>.</p>
|
||||
</div>
|
||||
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
<h4>Option 3: Bookmarklet</h4>
|
||||
<div class="quote">
|
||||
<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>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>
|
||||
<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>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>
|
||||
|
||||
<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/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>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var contents = document.getElementById("tracker-copy-contents");
|
||||
var issue = document.getElementById("tracker-copy-issue");
|
||||
var button = document.getElementById("tracker-copy-button");
|
||||
|
||||
if (document.queryCommandSupported("copy")) {
|
||||
contents.style.display = "none";
|
||||
issue.style.display = "none";
|
||||
}
|
||||
|
||||
button.addEventListener("click", function() {
|
||||
contents.style.display = "block";
|
||||
issue.style.display = "block";
|
||||
|
||||
contents.select();
|
||||
document.execCommand("copy");
|
||||
|
||||
button.innerHTML = "Copied to Clipboard";
|
||||
contents.style.display = "none";
|
||||
issue.style.display = "none";
|
||||
});
|
||||
|
||||
contents.addEventListener("click", function() {
|
||||
contents.select();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Reference in New Issue
Block a user