1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2025-08-18 13:31:42 +02:00

13 Commits

Author SHA1 Message Date
33f5ab7cce Release v37.0 2022-06-18 14:00:15 +02:00
b9a5664740 Fix not seeing messages after a Discord update
Closes #189
2022-06-18 13:59:29 +02:00
845ac1b0fa Release v36.2 (beta) 2022-06-06 17:08:37 +02:00
1bead42a0e Improve error handling and reporting when extracting message data 2022-06-06 17:05:20 +02:00
8f1c91b2cc Fix not formatting single underscores as italics in the viewer
Closes #142
2022-06-04 22:13:16 +02:00
9ae5ece24b Fix negative numbers & exception with very large numbers in attachment size limit 2022-06-04 21:55:55 +02:00
053ab5b091 Fix exception & wrong download statistics when multiple attachments have the same URL 2022-06-04 21:47:34 +02:00
71c628fdf8 Fix not recomputing download statistics after removing download items 2022-06-04 21:45:40 +02:00
af621b8d46 Fix wrong plural in the Viewer tab if the total amount of messages is zero (properly this time) 2022-06-04 21:26:50 +02:00
31fe6aed35 Stop ignoring removal filters for messages and download items if the filter matches all 2022-06-04 21:25:06 +02:00
c25426af55 Add image loading animation to viewer
Related to #79
2022-06-04 16:56:41 +02:00
59129ba20a Change image alt text in viewer to indicate when images are loading, and when loading fails
Related to #79
2022-06-04 15:50:05 +02:00
f7bfe052ca Add known sizes of images to the viewer
Related to #79
2022-06-04 15:29:42 +02:00
10 changed files with 100 additions and 43 deletions

View File

@@ -9,7 +9,7 @@ using DHT.Utils.Tasks;
namespace DHT.Desktop.Main.Controls { namespace DHT.Desktop.Main.Controls {
sealed class AttachmentFilterPanelModel : BaseModel, IDisposable { sealed class AttachmentFilterPanelModel : BaseModel, IDisposable {
public sealed record Unit(string Name, int Scale); public sealed record Unit(string Name, uint Scale);
private static readonly Unit[] AllUnits = { private static readonly Unit[] AllUnits = {
new ("B", 1), new ("B", 1),
@@ -26,7 +26,7 @@ namespace DHT.Desktop.Main.Controls {
public string FilterStatisticsText { get; private set; } = ""; public string FilterStatisticsText { get; private set; } = "";
private bool limitSize = false; private bool limitSize = false;
private int maximumSize = 0; private ulong maximumSize = 0L;
private Unit maximumSizeUnit = AllUnits[0]; private Unit maximumSizeUnit = AllUnits[0];
public bool LimitSize { public bool LimitSize {
@@ -34,7 +34,7 @@ namespace DHT.Desktop.Main.Controls {
set => Change(ref limitSize, value); set => Change(ref limitSize, value);
} }
public int MaximumSize { public ulong MaximumSize {
get => maximumSize; get => maximumSize;
set => Change(ref maximumSize, value); set => Change(ref maximumSize, value);
} }
@@ -116,7 +116,11 @@ namespace DHT.Desktop.Main.Controls {
AttachmentFilter filter = new(); AttachmentFilter filter = new();
if (LimitSize) { if (LimitSize) {
filter.MaxBytes = maximumSize * maximumSizeUnit.Scale; try {
filter.MaxBytes = maximumSize * maximumSizeUnit.Scale;
} catch (ArithmeticException) {
// set no size limit, because the overflown size is larger than any file could possibly be
}
} }
return filter; return filter;

View File

@@ -169,7 +169,7 @@ namespace DHT.Desktop.Main.Controls {
var exportedMessageCountStr = exportedMessageCount?.Format() ?? "(...)"; var exportedMessageCountStr = exportedMessageCount?.Format() ?? "(...)";
var totalMessageCountStr = totalMessageCount?.Format() ?? "(...)"; var totalMessageCountStr = totalMessageCount?.Format() ?? "(...)";
FilterStatisticsText = verb + " " + exportedMessageCountStr + " out of " + totalMessageCountStr + " message" + (totalMessageCount is null or 0 ? "." : "s."); FilterStatisticsText = verb + " " + exportedMessageCountStr + " out of " + totalMessageCountStr + " message" + (totalMessageCount is null or 1 ? "." : "s.");
OnPropertyChanged(nameof(FilterStatisticsText)); OnPropertyChanged(nameof(FilterStatisticsText));
} }

View File

@@ -175,7 +175,6 @@ namespace DHT.Desktop.Main.Pages {
}; };
db.RemoveDownloadItems(allExceptFailedFilter, FilterRemovalMode.KeepMatching); db.RemoveDownloadItems(allExceptFailedFilter, FilterRemovalMode.KeepMatching);
downloadStatisticsComputer.Recompute();
if (IsDownloading) { if (IsDownloading) {
EnqueueDownloadItems(); EnqueueDownloadItems();

View File

@@ -91,11 +91,13 @@ class DISCORD {
static getMessageElementProps(ele) { static getMessageElementProps(ele) {
const props = DOM.getReactProps(ele); const props = DOM.getReactProps(ele);
if (props.children && props.children.length >= 4) { if (props.children && props.children.length) {
const childProps = props.children[3].props; for (let i = 3; i < props.children.length; i++) {
const childProps = props.children[i].props;
if ("message" in childProps && "channel" in childProps) {
return childProps; if (childProps && "message" in childProps && "channel" in childProps) {
return childProps;
}
} }
} }
@@ -110,16 +112,20 @@ class DISCORD {
const messages = []; const messages = [];
for (const ele of this.getMessageElements()) { for (const ele of this.getMessageElements()) {
const props = this.getMessageElementProps(ele); try {
const props = this.getMessageElementProps(ele);
if (props != null) {
messages.push(props.message); if (props != null) {
messages.push(props.message);
}
} catch (e) {
console.error("[DHT] Error extracing message data, skipping it.", e, ele, DOM.tryGetReactProps(ele));
} }
} }
return messages; return messages;
} catch (e) { } catch (e) {
console.error(e); console.error("[DHT] Error retrieving messages.", e);
return []; return [];
} }
} }

View File

@@ -71,4 +71,15 @@ class DOM {
key = keys.find(key => key.startsWith("__reactProps$")); key = keys.find(key => key.startsWith("__reactProps$"));
return key ? ele[key] : null; return key ? ele[key] : null;
} }
/**
* Returns internal React state object of an element, or null if the retrieval throws.
*/
static tryGetReactProps(ele) {
try {
return this.getReactProps(ele);
} catch (e) {
return null;
}
}
} }

View File

@@ -1,7 +1,7 @@
const DISCORD = (function() { const DISCORD = (function() {
const regex = { const regex = {
formatBold: /\*\*([\s\S]+?)\*\*(?!\*)/g, formatBold: /\*\*([\s\S]+?)\*\*(?!\*)/g,
formatItalic: /(.)?\*([\s\S]+?)\*(?!\*)/g, formatItalic: /(.)?([_*])([\s\S]+?)\2(?!\2)/g,
formatUnderline: /__([\s\S]+?)__(?!_)/g, formatUnderline: /__([\s\S]+?)__(?!_)/g,
formatStrike: /~~([\s\S]+?)~~(?!~)/g, formatStrike: /~~([\s\S]+?)~~(?!~)/g,
formatCodeInline: /(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/g, formatCodeInline: /(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/g,
@@ -9,7 +9,7 @@ const DISCORD = (function() {
formatUrl: /(\b(?:https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig, formatUrl: /(\b(?:https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig,
formatUrlNoEmbed: /<(\b(?:https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])>/ig, formatUrlNoEmbed: /<(\b(?:https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])>/ig,
specialEscapedBacktick: /\\`/g, specialEscapedBacktick: /\\`/g,
specialEscapedSingle: /\\([*\\])/g, specialEscapedSingle: /\\([*_\\])/g,
specialEscapedDouble: /\\__|_\\_|\\_\\_|\\~~|~\\~|\\~\\~/g, specialEscapedDouble: /\\__|_\\_|\\_\\_|\\~~|~\\~|\\~\\~/g,
specialUnescaped: /([*_~\\])/g, specialUnescaped: /([*_~\\])/g,
mentionRole: /&lt;@&(\d+?)&gt;/g, mentionRole: /&lt;@&(\d+?)&gt;/g,
@@ -26,6 +26,7 @@ const DISCORD = (function() {
let templateUserAvatar; let templateUserAvatar;
let templateAttachmentDownload; let templateAttachmentDownload;
let templateEmbedImage; let templateEmbedImage;
let templateEmbedImageWithSize;
let templateEmbedRich; let templateEmbedRich;
let templateEmbedRichNoDescription; let templateEmbedRichNoDescription;
let templateEmbedUrl; let templateEmbedUrl;
@@ -46,8 +47,8 @@ const DISCORD = (function() {
.replace(regex.specialEscapedSingle, escapeHtmlMatch) .replace(regex.specialEscapedSingle, escapeHtmlMatch)
.replace(regex.specialEscapedDouble, full => full.replace(/\\/g, "").replace(/(.)/g, escapeHtmlMatch)) .replace(regex.specialEscapedDouble, full => full.replace(/\\/g, "").replace(/(.)/g, escapeHtmlMatch))
.replace(regex.formatBold, "<b>$1</b>") .replace(regex.formatBold, "<b>$1</b>")
.replace(regex.formatItalic, (full, pre, match) => pre === "\\" ? full : (pre || "") + "<i>" + match + "</i>")
.replace(regex.formatUnderline, "<u>$1</u>") .replace(regex.formatUnderline, "<u>$1</u>")
.replace(regex.formatItalic, (full, pre, char, match) => pre === "\\" ? full : (pre || "") + "<i>" + match + "</i>")
.replace(regex.formatStrike, "<s>$1</s>"); .replace(regex.formatStrike, "<s>$1</s>");
} }
@@ -64,6 +65,19 @@ const DISCORD = (function() {
return "<p>" + processed + "</p>"; return "<p>" + processed + "</p>";
}; };
const getImageEmbed = function(url, image) {
if (!SETTINGS.enableImagePreviews) {
return "";
}
if (image.width && image.height) {
return templateEmbedImageWithSize.apply({ url, src: image.url, width: image.width, height: image.height });
}
else {
return templateEmbedImage.apply({ url, src: image.url });
}
};
return { return {
setup() { setup() {
templateChannelServer = new TEMPLATE([ templateChannelServer = new TEMPLATE([
@@ -114,7 +128,12 @@ const DISCORD = (function() {
// noinspection HtmlUnknownTarget // noinspection HtmlUnknownTarget
templateEmbedImage = new TEMPLATE([ templateEmbedImage = new TEMPLATE([
"<a href='{url}' class='embed thumbnail'><img src='{src}' alt='(image attachment not found)'></a><br>" "<a href='{url}' class='embed thumbnail loading'><img src='{src}' alt='' onload='DISCORD.handleImageLoad(this)' onerror='DISCORD.handleImageLoadError(this)'></a><br>"
].join(""));
// noinspection HtmlUnknownTarget
templateEmbedImageWithSize = new TEMPLATE([
"<a href='{url}' class='embed thumbnail loading'><img src='{src}' width='{width}' height='{height}' alt='' onload='DISCORD.handleImageLoad(this)' onerror='DISCORD.handleImageLoadError(this)'></a><br>"
].join("")); ].join(""));
// noinspection HtmlUnknownTarget // noinspection HtmlUnknownTarget
@@ -145,6 +164,17 @@ const DISCORD = (function() {
].join("")); ].join(""));
}, },
handleImageLoad(ele) {
ele.parentElement.classList.remove("loading");
},
handleImageLoadError(ele) {
// noinspection JSUnusedGlobalSymbols
ele.onerror = null;
ele.parentElement.classList.remove("loading");
ele.setAttribute("alt", "(image attachment not found)");
},
isImageAttachment(attachment) { isImageAttachment(attachment) {
const dot = attachment.url.lastIndexOf("."); const dot = attachment.url.lastIndexOf(".");
const ext = dot === -1 ? "" : attachment.url.substring(dot).toLowerCase(); const ext = dot === -1 ? "" : attachment.url.substring(dot).toLowerCase();
@@ -183,10 +213,10 @@ const DISCORD = (function() {
return templateEmbedUnsupported.apply(embed); return templateEmbedUnsupported.apply(embed);
} }
else if ("image" in embed && embed.image.url) { else if ("image" in embed && embed.image.url) {
return SETTINGS.enableImagePreviews ? templateEmbedImage.apply({ url: embed.url, src: embed.image.url }) : ""; return getImageEmbed(embed.url, embed.image);
} }
else if ("thumbnail" in embed && embed.thumbnail.url) { else if ("thumbnail" in embed && embed.thumbnail.url) {
return SETTINGS.enableImagePreviews ? templateEmbedImage.apply({ url: embed.url, src: embed.thumbnail.url }) : ""; return getImageEmbed(embed.url, embed.thumbnail);
} }
else if ("title" in embed && "description" in embed) { else if ("title" in embed && "description" in embed) {
return templateEmbedRich.apply(embed); return templateEmbedRich.apply(embed);

View File

@@ -107,11 +107,25 @@
} }
.message .thumbnail { .message .thumbnail {
position: relative;
max-width: calc(100% - 20px); max-width: calc(100% - 20px);
max-height: 320px; max-height: 320px;
} }
.message .thumbnail.loading::after {
content: "";
background: rgba(0, 0, 0, 0.75)
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300' preserveAspectRatio='xMidYMid'%3E %3Ccircle cx='150' cy='150' fill='none' stroke='%237983f5' stroke-width='8' r='42' stroke-dasharray='198 68'%3E %3CanimateTransform attributeName='transform' type='rotate' repeatCount='indefinite' dur='1.25s' values='0 150 150;360 150 150' keyTimes='0;1' /%3E %3C/circle%3E %3C/svg%3E")
no-repeat center center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.message .thumbnail img { .message .thumbnail img {
width: auto;
max-width: 100%; max-width: 100%;
max-height: 320px; max-height: 320px;
border-radius: 3px; border-radius: 3px;

View File

@@ -1,6 +1,6 @@
namespace DHT.Server.Data.Filters { namespace DHT.Server.Data.Filters {
public sealed class AttachmentFilter { public sealed class AttachmentFilter {
public long? MaxBytes { get; set; } = null; public ulong? MaxBytes { get; set; } = null;
public DownloadItemRules? DownloadItemRule { get; set; } = null; public DownloadItemRules? DownloadItemRule { get; set; } = null;

View File

@@ -403,21 +403,17 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
} }
public void RemoveMessages(MessageFilter filter, FilterRemovalMode mode) { public void RemoveMessages(MessageFilter filter, FilterRemovalMode mode) {
var whereClause = filter.GenerateWhereClause(invert: mode == FilterRemovalMode.KeepMatching); var perf = log.Start();
if (!string.IsNullOrEmpty(whereClause)) { DeleteFromTable("messages", filter.GenerateWhereClause(invert: mode == FilterRemovalMode.KeepMatching));
var perf = log.Start(); totalMessagesComputer.Recompute();
DeleteFromTable("messages", whereClause); perf.End();
totalMessagesComputer.Recompute();
perf.End();
}
} }
public int CountAttachments(AttachmentFilter? filter = null) { public int CountAttachments(AttachmentFilter? filter = null) {
using var conn = pool.Take(); using var conn = pool.Take();
using var cmd = conn.Command("SELECT COUNT(*) FROM attachments a" + filter.GenerateWhereClause("a")); using var cmd = conn.Command("SELECT COUNT(DISTINCT url) FROM attachments a" + filter.GenerateWhereClause("a"));
using var reader = cmd.ExecuteReader(); using var reader = cmd.ExecuteReader();
return reader.Read() ? reader.GetInt32(0) : 0; return reader.Read() ? reader.GetInt32(0) : 0;
@@ -476,7 +472,7 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
public void EnqueueDownloadItems(AttachmentFilter? filter = null) { public void EnqueueDownloadItems(AttachmentFilter? filter = null) {
using var conn = pool.Take(); using var conn = pool.Take();
using var cmd = conn.Command("INSERT INTO downloads (url, status, size) SELECT a.url, :enqueued, a.size FROM attachments a" + filter.GenerateWhereClause("a")); using var cmd = conn.Command("INSERT INTO downloads (url, status, size) SELECT a.url, :enqueued, MAX(a.size) FROM attachments a" + filter.GenerateWhereClause("a") + " GROUP BY a.url");
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued); cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
@@ -502,16 +498,13 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
} }
public void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode) { public void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode) {
var whereClause = filter.GenerateWhereClause(invert: mode == FilterRemovalMode.KeepMatching); DeleteFromTable("downloads", filter.GenerateWhereClause(invert: mode == FilterRemovalMode.KeepMatching));
totalDownloadsComputer.Recompute();
if (!string.IsNullOrEmpty(whereClause)) {
DeleteFromTable("downloads", whereClause);
}
} }
public DownloadStatusStatistics GetDownloadStatusStatistics() { public DownloadStatusStatistics GetDownloadStatusStatistics() {
static void LoadUndownloadedStatistics(ISqliteConnection conn, DownloadStatusStatistics result) { static void LoadUndownloadedStatistics(ISqliteConnection conn, DownloadStatusStatistics result) {
using var cmd = conn.Command("SELECT IFNULL(COUNT(filtered.size), 0), IFNULL(SUM(filtered.size), 0) FROM (SELECT DISTINCT a.url, a.size FROM attachments a WHERE a.url NOT IN (SELECT d.url FROM downloads d)) filtered"); using var cmd = conn.Command("SELECT IFNULL(COUNT(size), 0), IFNULL(SUM(size), 0) FROM (SELECT MAX(a.size) size FROM attachments a WHERE a.url NOT IN (SELECT d.url FROM downloads d) GROUP BY a.url)");
using var reader = cmd.ExecuteReader(); using var reader = cmd.ExecuteReader();
if (reader.Read()) { if (reader.Read()) {
@@ -655,7 +648,7 @@ FROM downloads");
private long ComputeAttachmentStatistics() { private long ComputeAttachmentStatistics() {
using var conn = pool.Take(); using var conn = pool.Take();
return conn.SelectScalar("SELECT COUNT(*) FROM attachments") as long? ?? 0L; return conn.SelectScalar("SELECT COUNT(DISTINCT url) FROM attachments") as long? ?? 0L;
} }
private void UpdateAttachmentStatistics(long totalAttachments) { private void UpdateAttachmentStatistics(long totalAttachments) {

View File

@@ -7,6 +7,6 @@ using DHT.Utils;
namespace DHT.Utils { namespace DHT.Utils {
static class Version { static class Version {
public const string Tag = "36.1.0.0"; public const string Tag = "37.0.0.0";
} }
} }