-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrenderer.js
More file actions
408 lines (324 loc) · 14.2 KB
/
renderer.js
File metadata and controls
408 lines (324 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
// This file is required by the index.html file and will
// be executed in the renderer process for that window.
/* Potential TODO
* Allow for local embeds to be displayed
* Other search options (filters/non-exact query)
*/
// Some of this should probably be in main but whatver
const fs = require('fs');
// Icon start path
const cdn = "https://cdn.discordapp.com/";
const channel = "sample" // Set channel
const local = "file://" + __dirname + "/fetch/" + channel + '/';
const avatars = fs.existsSync(__dirname + "/fetch/" + channel + "/avatars");
const attachments = fs.existsSync(__dirname + "/fetch/" + channel + "/attachments");
const embeds = fs.existsSync(__dirname + "/fetch/" + channel + "/embeds");
const mentions = fs.existsSync(__dirname + "/fetch/" + channel + "/mentions.json");
const emojis = fs.existsSync(__dirname + "/fetch/" + channel + "/emojis");
// Read message data from local file
var messageData = JSON.parse(fs.readFileSync('fetch/' + channel + '/messages.json', 'utf8'));
if (mentions) var mentionData = JSON.parse(fs.readFileSync('fetch/' + channel + '/mentions.json', 'utf8'));
var messages = document.getElementById('messages');
var searchoutput = document.getElementById('searchout');
// Event to jump to the associated message
jump = (event) => {
// Get get ID from current node or nearest parent with ID
var target = event.target;
var id;
while (!(id = target.id)) {
target = target.parentNode;
}
var reps = 1;
for (var j = 0; j < messageData.length; j++) {
if (messageData[j]['id'] == id) {
break;
}
if (messageData[j]['referenced_message']) {
reps++;
}
}
while (i < Math.min((j + 20), messageData.length)) {
addMessage(i);
i++;
}
messages.childNodes[j + reps].scrollIntoView();
};
/* Function that takes in message content and returns a list of nodes
* Nodes are separated by styling
* Markdown does not format correctly in all cases, and does not support quotes,
* multiline code blocks, or role mentions
*/
function parse(content, jumpable = false) {
// Pain
const mention = /(?<!\\)<([@#])!?(\d+?)>/g;
const emoji = /(?<!\\)<a?:[^\s:]+:(\d+)>/g;
const italic = /((?<!\\)[*_])([^\n]+?)(\1)/g;
const bold = /(?<!\\)\*(?<!\\)\*([^\n]+?)(?<!\\)\*\*/g;
const underline = /(?<!\\)_(?<!\\)_([^\n]+?)(?<!\\)__/g;
const strike = /(?<!\\)~~([^\n]+?)(?<!\\)~~/g;
const spoiler = /(?<!\\)\|\|([^\n]+?)(?<!\\)\|\|/g
const code = /((?<!\\)\`?\`)([^\n]+?)(\1)/g;
const codeescape = /(?<=<code>)[^<]+(?=<\/code>)/g;
const escape = /\\([$-/:-?{-~!"^_`\[\]\\@#])/g;
const url = /https?:\/\/\S+(\.\S+)+\b/g;
content = content.replaceAll("<", "<").replaceAll(">", ">");
content = content.replaceAll(code, "<code>$2</code>");
[...match = content.matchAll(codeescape)].reverse().forEach(match => {
content = content.slice(0, match.index) + match[0].replaceAll(/([^\p{L}\d\s])/gu, "\\$1") + content.slice(match.index + match[0].length);
});
if (mentions) {
[...content.matchAll(mention)].reverse().forEach(match => {
var m = mentionData[match[2]];
if (m) content = content.slice(0, match.index) + "<span class='mention'>" + match[1] + mentionData[match[2]]["name"] + "</span>" + content.slice(match.index + match[0].length);
else content = content.slice(0, match.index) + "<span class='mention'><" + match[1] + match[2] + "></span>" + content.slice(match.index + match[0].length);
});
}
if (emojis) {
[...content.matchAll(emoji)].reverse().forEach(match => {
if (content.length == match[0].length) content = "<img src='" + local + "emojis/" + match[1]+ ".webp' class='emoji'>"
else content = content.slice(0, match.index) + "<img src='" + local + "emojis/" + match[1]+ ".webp' class='emoji' inline=1>" + content.slice(match.index + match[0].length);
});
}
content = content.replaceAll(/(?<!\\)((@everyone)|(@here))/g, "<span class='mention'>$1</span>");
content = content.replaceAll(bold, "<b>$1</b>");
content = content.replaceAll(underline, "<u>$1</u>");
content = content.replaceAll(strike, "<s>$1</s>");
content = content.replaceAll(spoiler, "<span class='spoiler' id='event-needed'>$1</span>")
content = content.replaceAll(italic, "<i>$2</i>");
if (!jumpable) {
[...match = content.matchAll(url)].reverse().forEach(match => {
content = content.slice(0, match.index) + "<a href='" + match[0] + "' target='_blank'>" + match[0] + "</a>" + content.slice(match.index + match[0].length);
});
}
content = content.replaceAll(escape, "$1");
return content;
}
/* Function that adds a message to the list of nodes in an element
* Currently nodes are never removed and stay loaded
* Jumpable means that the message will jump when clicked, and only had content
*/
function addMessage(index, dst = messages, jumpable = false) {
m = messageData[index];
// Create elements of message
var message = document.createElement("div");
var icon = document.createElement("img");
var content = document.createElement("span");
var name = document.createElement("h3");
var time = document.createElement("h5");
var text = document.createElement("p");
// Combine elements
message.appendChild(icon);
message.appendChild(content);
content.appendChild(name);
content.appendChild(time);
content.appendChild(text);
// Message content field
if (m['content']) {
text.innerHTML = parse(m['content']);
}
/* Attachment field
* Loop through message attachments and add the appropriate type
*/
if (m['attachments'].length && !jumpable) {
m['attachments'].forEach(attachment => {
var att;
var type;
// Get main type of content (e.g. video/audio/image)
if (attachment['content_type']) type = attachment['content_type'].split('/')[0];
else type = "image"; // Sure, good enough
// Handle type
if (type == "image") {
att = document.createElement("img");
if (attachments) att.src = local + 'attachments/' + encodeURIComponent(encodeURIComponent(attachment['url']));
else att.src = attachment['url'];
} else if (type == "video" || type == "audio") {
att = document.createElement(type);
att.setAttribute("controls", "");
var media = document.createElement("source");
if (attachments) media.src = local + 'attachments/' + encodeURIComponent(encodeURIComponent(attachment['url']));
else media.src = attachment['url'];
media.type = attachment['content_type'];
att.appendChild(media);
} else {
att = document.createElement("a");
att.href = attachment['url'];
att.textContent = attachment['url'];
att.target = "_blank";
}
if (attachment['width'] < 800) att.width = attachment['width'];
att.className = "attachment";
content.appendChild(att);
});
}
/* Embed field
* Loop through message embeds and add the appropriate type from remote source
* Images/gifs only currently
*/
if (m['embeds'].length && !jumpable) {
m['embeds'].forEach(embed => {
var emb;
var type = embed['type'];
if (type == "gifv") {
emb = document.createElement("video");
emb.setAttribute("autoplay", "");
emb.setAttribute("loop", "");
var media = document.createElement("source");
if (embeds) media.src = local + 'embeds/' + encodeURIComponent(encodeURIComponent(embed['video']['url']));
else media.src = embed['video']['url'];
media.type = "video/mp4";
emb.appendChild(media);
} else if (type == "image") {
emb = document.createElement("img");
if (embeds) emb.src = local + 'embeds/' + encodeURIComponent(encodeURIComponent(embed['url']));
else emb.src = embed['url'];
} else {
emb = document.createElement("img");
if (embeds) {
if (embed['thumbnail']) emb.src = local + 'embeds/' + encodeURIComponent(encodeURIComponent(embed['thumbnail']['url']));
else if (embed['image']) emb.src = local + 'embeds/' + encodeURIComponent(encodeURIComponent(embed['image']['url']));
} else {
if (embed['thumbnail']) emb.src = embed['thumbnail']['url'];
else if (embed['image']) emb.src = embed['image']['url'];
}
}
if (emb.children.length || emb.src) {
if (embed['video'] && embed['video']['width'] < 800) emb.width = embed['video']['width'];
else if (embed['thumbnail'] && embed['thumbnail']['width'] < 800) emb.width = embed['thumbnail']['width'];
emb.className = "embed";
content.appendChild(emb);
}
});
}
/* Message reply field
* Shows message that the current message is a reply to above
*/
if (m['referenced_message'] && !jumpable) {
var replied = document.createElement("div");
var ref = m['referenced_message'];
var reficon = document.createElement("img");
var refname = document.createElement("h4");
var reftext = document.createElement("p");
if (avatars) {
reficon.src = local + "avatars/" + ref['author']['id'] + ".webp";
} else {
if (ref['author']['avatar']) reficon.src = cdn + "avatars/" + ref['author']['id'] + "/" + ref['author']['avatar'] + ".webp?size=80";
else reficon.src = cdn + "embed/avatars/" + ref['author']['discriminator'] % 5 + ".png"
}
reficon.width = 24;
refname.textContent = ref['author']['username'];
reftext.innerHTML = parse(ref['content'], true);
reficon.classList.add('reply');
reficon.classList.add('logo');
refname.className = 'reply';
reftext.className = 'reply';
replied.appendChild(reficon);
replied.appendChild(refname);
replied.appendChild(reftext);
replied.className = 'reply';
replied.id = ref['id']
dst.appendChild(replied);
// Make reply jumpable
replied.addEventListener("click", jump);
}
/* Reaction field
* Shows reaction emoji and counts on current message below
*/
if (m['reactions'] && !jumpable) {
m['reactions'].forEach(reaction => {
var react = document.createElement("span");
var emj = document.createElement("img");
if (reaction['emoji']['id']) {
emj.src = local + "emojis/" + reaction['emoji']['id'] + ".webp";
emj.width = 18;
emj.className = 'emoji';
}
else {
emj = document.createElement("span");
emj.textContent = reaction['emoji']['name'];
};
var count = document.createElement("span");
count.textContent = reaction['count'];
count.className = "reactionCount"
react.appendChild(emj);
react.appendChild(count);
react.className = "react";
content.appendChild(react);
});
}
// Set name, pfp, and time (UTC only currently)
name.textContent = m['author']['username'];
ts = m['timestamp'];
time.textContent = ts.substring(0, 10) + " " + ts.substring(11, 23);
if (avatars) {
icon.src = local + "avatars/" + m['author']['id'] + ".webp";
} else {
// Get remote avatar if not fetched (custom/default)
if (m['author']['avatar']) icon.src = cdn + "avatars/" + m['author']['id'] + "/" + m['author']['avatar'] + ".webp?size=80";
else icon.src = cdn + "embed/avatars/" + m['author']['discriminator'] % 5 + ".png"
}
icon.width = 48;
icon.className = "logo";
text.className = "content";
message.className = "message";
dst.appendChild(message);
// Add jump event if applicable
if (jumpable) {
message.id = m['id'];
message.addEventListener("click", jump);
}
// Add spoiler clear event if needed
var spoil = document.getElementById("event-needed");
if (spoil) {
// Don't enable spoiler click on jumpable messages
if (!jumpable) spoil.addEventListener("click", spoiler => {
spoiler.path[0].classList.add("cleared");
});
spoil.id = '';
}
}
// Load first 100 messages
for (var i = 0; i < Math.min(100, messageData.length); i++) {
addMessage(i);
}
/* Search function
* Takes a query and returns index of next message containing an exact match from start
*/
function search(query, start = 0) {
query = query.toUpperCase();
for (var i = start; i < messageData.length; i++) {
var content = messageData[i]['content'].toUpperCase();
if (content.indexOf(query) != -1) {
return i;
}
}
return null;
}
// Scroll event, load new images when near bottom
window.addEventListener("scroll", () => {
if ((document.body.clientHeight - 1080) - this.scrollY < 640) {
for (var j = i; j < (i + 20) && j < messageData.length; j++) {
addMessage(i);
j++;
i++;
}
}
});
// Do search when button is clicked
document.querySelector('#searchbutton').addEventListener("click", () => {
searchoutput.innerHTML = '';
var query = document.getElementById('searchbox').value;
var res = parseInt(document.getElementById('startbox').value);
if (isNaN(res)) res = 0;
var num = 0;
while (num < 12) {
var res = search(query, res);
if (res != null) {
addMessage(res, searchoutput, true);
res++;
num++;
} else {
break;
}
}
document.getElementById('startbox').value = res;
});