mirror of
				https://github.com/chylex/Discord-History-Tracker.git
				synced 2025-11-04 03:40:12 +01:00 
			
		
		
		
	Compare commits
	
		
			6 Commits
		
	
	
		
			v42.0
			...
			50d2288203
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						50d2288203
	
				 | 
					
					
						|||
| 
						
						
							
						
						c3d4fa5532
	
				 | 
					
					
						|||
| 
						
						
							
						
						a6225b9721
	
				 | 
					
					
						|||
| 
						
						
							
						
						943163473a
	
				 | 
					
					
						|||
| 
						
						
							
						
						fa00df10d8
	
				 | 
					
					
						|||
| 
						
						
							
						
						f54465e5fe
	
				 | 
					
					
						
@@ -249,11 +249,8 @@ sealed partial class MessageFilterPanelModel : ObservableObject, IDisposable {
 | 
				
			|||||||
			var checkBoxItems = new List<CheckBoxItem<ulong>>();
 | 
								var checkBoxItems = new List<CheckBoxItem<ulong>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await foreach (var user in state.Db.Users.Get()) {
 | 
								await foreach (var user in state.Db.Users.Get()) {
 | 
				
			||||||
				var name = user.Name;
 | 
					 | 
				
			||||||
				var discriminator = user.Discriminator;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				checkBoxItems.Add(new CheckBoxItem<ulong>(user.Id) {
 | 
									checkBoxItems.Add(new CheckBoxItem<ulong>(user.Id) {
 | 
				
			||||||
					Title = discriminator == null ? name : name + " #" + discriminator,
 | 
										Title = user.DisplayName == null ? user.Name : $"{user.DisplayName} ({user.Name})",
 | 
				
			||||||
					IsChecked = IncludedUsers == null || IncludedUsers.Contains(user.Id)
 | 
										IsChecked = IncludedUsers == null || IncludedUsers.Contains(user.Id)
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -73,6 +73,7 @@ sealed class DebugPageModel {
 | 
				
			|||||||
		var users = Enumerable.Range(0, userCount).Select(_ => new User {
 | 
							var users = Enumerable.Range(0, userCount).Select(_ => new User {
 | 
				
			||||||
			Id = RandomId(rand),
 | 
								Id = RandomId(rand),
 | 
				
			||||||
			Name = RandomName("u"),
 | 
								Name = RandomName("u"),
 | 
				
			||||||
 | 
								DisplayName = RandomName("u"),
 | 
				
			||||||
			AvatarUrl = null,
 | 
								AvatarUrl = null,
 | 
				
			||||||
			Discriminator = rand.Next(0, 9999).ToString(),
 | 
								Discriminator = rand.Next(0, 9999).ToString(),
 | 
				
			||||||
		}).ToArray();
 | 
							}).ToArray();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,8 @@ items:
 | 
				
			|||||||
          pattern: "^[0-9]+$"
 | 
					          pattern: "^[0-9]+$"
 | 
				
			||||||
        name:
 | 
					        name:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
 | 
					        displayName:
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
        avatar:
 | 
					        avatar:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
        discriminator:
 | 
					        discriminator:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										47
									
								
								app/Resources/Tracker/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										47
									
								
								app/Resources/Tracker/bootstrap.js
									
									
									
									
										vendored
									
									
								
							@@ -10,11 +10,16 @@
 | 
				
			|||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
						/*[IMPORTS]*/
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						if (!DISCORD.isCompatible()) {
 | 
				
			||||||
 | 
							alert("Discord History Tracker is not compatible with this version of Discord.");
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	window.DHT_LOADED = true;
 | 
						window.DHT_LOADED = true;
 | 
				
			||||||
	window.DHT_ON_UNLOAD = [];
 | 
						window.DHT_ON_UNLOAD = [];
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	/*[IMPORTS]*/
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	const port = 0; /*[PORT]*/
 | 
						const port = 0; /*[PORT]*/
 | 
				
			||||||
	const token = "/*[TOKEN]*/";
 | 
						const token = "/*[TOKEN]*/";
 | 
				
			||||||
	STATE.setup(port, token);
 | 
						STATE.setup(port, token);
 | 
				
			||||||
@@ -46,7 +51,7 @@
 | 
				
			|||||||
		return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
 | 
							return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	const onTrackingContinued = function(anyNewMessages) {
 | 
						const onTrackingContinued = function(anyNewMessages, hasMoreBefore) {
 | 
				
			||||||
		if (!STATE.isTracking()) {
 | 
							if (!STATE.isTracking()) {
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -63,7 +68,7 @@
 | 
				
			|||||||
		if (SETTINGS.autoscroll) {
 | 
							if (SETTINGS.autoscroll) {
 | 
				
			||||||
			let action = null;
 | 
								let action = null;
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			if (!DISCORD.hasMoreMessages()) {
 | 
								if (!hasMoreBefore) {
 | 
				
			||||||
				console.debug("[DHT] Reached first message.");
 | 
									console.debug("[DHT] Reached first message.");
 | 
				
			||||||
				action = SETTINGS.afterFirstMsg;
 | 
									action = SETTINGS.afterFirstMsg;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -84,7 +89,7 @@
 | 
				
			|||||||
	
 | 
						
 | 
				
			||||||
	let waitUntilSendingFinishedTimer = null;
 | 
						let waitUntilSendingFinishedTimer = null;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	const onMessagesUpdated = async messages => {
 | 
						const onMessagesUpdated = async (server, channel, messages, hasMoreBefore) => {
 | 
				
			||||||
		if (!STATE.isTracking() || delayedStopRequests > 0) {
 | 
							if (!STATE.isTracking() || delayedStopRequests > 0) {
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -94,24 +99,16 @@
 | 
				
			|||||||
			
 | 
								
 | 
				
			||||||
			waitUntilSendingFinishedTimer = window.setTimeout(() => {
 | 
								waitUntilSendingFinishedTimer = window.setTimeout(() => {
 | 
				
			||||||
				waitUntilSendingFinishedTimer = null;
 | 
									waitUntilSendingFinishedTimer = null;
 | 
				
			||||||
				onMessagesUpdated(messages);
 | 
									onMessagesUpdated(server, channel, messages, hasMoreBefore);
 | 
				
			||||||
			}, 100);
 | 
								}, 100);
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		const info = DISCORD.getSelectedChannel();
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		if (!info) {
 | 
					 | 
				
			||||||
			GUI.setStatus("Error (Unknown Channel)");
 | 
					 | 
				
			||||||
			stopTrackingDelayed();
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		isSending = true;
 | 
							isSending = true;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			await STATE.addDiscordChannel(info.server, info.channel);
 | 
								await STATE.addDiscordChannel(server, channel);
 | 
				
			||||||
		} catch (e) {
 | 
							} catch (e) {
 | 
				
			||||||
			onError(e);
 | 
								onError(e);
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
@@ -120,32 +117,28 @@
 | 
				
			|||||||
		try {
 | 
							try {
 | 
				
			||||||
			if (!messages.length) {
 | 
								if (!messages.length) {
 | 
				
			||||||
				isSending = false;
 | 
									isSending = false;
 | 
				
			||||||
				onTrackingContinued(false);
 | 
									onTrackingContinued(false, hasMoreBefore);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else {
 | 
								else {
 | 
				
			||||||
				const anyNewMessages = await STATE.addDiscordMessages(messages);
 | 
									const anyNewMessages = await STATE.addDiscordMessages(messages);
 | 
				
			||||||
				onTrackingContinued(anyNewMessages);
 | 
									onTrackingContinued(anyNewMessages, hasMoreBefore);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} catch (e) {
 | 
							} catch (e) {
 | 
				
			||||||
			onError(e);
 | 
								onError(e);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	DISCORD.setupMessageCallback(onMessagesUpdated);
 | 
						const starter = DISCORD.setupMessageCallback(onMessagesUpdated);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	STATE.onTrackingStateChanged(enabled => {
 | 
						STATE.onTrackingStateChanged(enabled => {
 | 
				
			||||||
		if (enabled) {
 | 
							if (enabled) {
 | 
				
			||||||
			const messages = DISCORD.getMessages();
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			if (messages.length === 0) {
 | 
					 | 
				
			||||||
				stopTrackingDelayed(() => alert("Cannot see any messages."));
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			GUI.setStatus("Starting");
 | 
								GUI.setStatus("Starting");
 | 
				
			||||||
			hasJustStarted = true;
 | 
								hasJustStarted = true;
 | 
				
			||||||
			// noinspection JSIgnoredPromiseFromCall
 | 
								
 | 
				
			||||||
			onMessagesUpdated(messages);
 | 
								if (!starter()) {
 | 
				
			||||||
 | 
									stopTrackingDelayed(() => alert("Cannot see any messages."));
 | 
				
			||||||
 | 
									hasJustStarted = false;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else {
 | 
							else {
 | 
				
			||||||
			isSending = false;
 | 
								isSending = false;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,11 +4,26 @@ class DISCORD {
 | 
				
			|||||||
	
 | 
						
 | 
				
			||||||
	// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
 | 
						// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
 | 
				
			||||||
	static CHANNEL_TYPE = {
 | 
						static CHANNEL_TYPE = {
 | 
				
			||||||
 | 
							GUILD_TEXT: 0,
 | 
				
			||||||
		DM: 1,
 | 
							DM: 1,
 | 
				
			||||||
		GROUP_DM: 3,
 | 
							GROUP_DM: 3,
 | 
				
			||||||
 | 
							GUILD_ANNOUNCEMENT: 5,
 | 
				
			||||||
		ANNOUNCEMENT_THREAD: 10,
 | 
							ANNOUNCEMENT_THREAD: 10,
 | 
				
			||||||
		PUBLIC_THREAD: 11,
 | 
							PUBLIC_THREAD: 11,
 | 
				
			||||||
		PRIVATE_THREAD: 12
 | 
							PRIVATE_THREAD: 12,
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							isPrivate(type) {
 | 
				
			||||||
 | 
								return type === this.DM
 | 
				
			||||||
 | 
									|| type === this.GROUP_DM;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							isNavigableGuildChannel(type) {
 | 
				
			||||||
 | 
								return type === this.GUILD_TEXT
 | 
				
			||||||
 | 
									|| type === this.GUILD_ANNOUNCEMENT
 | 
				
			||||||
 | 
									|| type === this.ANNOUNCEMENT_THREAD
 | 
				
			||||||
 | 
									|| type === this.PUBLIC_THREAD
 | 
				
			||||||
 | 
									|| type === this.PRIVATE_THREAD;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	// https://discord.com/developers/docs/resources/channel#message-object-message-types
 | 
						// https://discord.com/developers/docs/resources/channel#message-object-message-types
 | 
				
			||||||
@@ -18,6 +33,74 @@ class DISCORD {
 | 
				
			|||||||
		THREAD_STARTER: 21
 | 
							THREAD_STARTER: 21
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
						// https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags
 | 
				
			||||||
 | 
						static PERMISSION = {
 | 
				
			||||||
 | 
							VIEW_CHANNEL: 1n << 10n
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @type {Object}
 | 
				
			||||||
 | 
						 * @property {function(String): ?DiscordGuild} getGuild
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						static #guildStore = WEBPACK.findModule(WEBPACK.filterByProps("getGuild", "getGuilds", "getGuildIds"));
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @type {Object}
 | 
				
			||||||
 | 
						 * @property {function(String): Boolean} isOptInEnabled
 | 
				
			||||||
 | 
						 * @property {function(String): Set<String>} getOptedInChannels
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						static #guildSettings = WEBPACK.findModule(WEBPACK.filterByProps("isOptInEnabled", "getOptedInChannels"));
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @type {Object}
 | 
				
			||||||
 | 
						 * @property {function(String): ?DiscordChannel} getChannel
 | 
				
			||||||
 | 
						 * @property {function(String): Array<DiscordChannel>} getMutableGuildChannelsForGuild
 | 
				
			||||||
 | 
						 * @property {function(): Array<DiscordChannel>} getSortedPrivateChannels
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						static #channelStore = WEBPACK.findModule(WEBPACK.filterByProps("getChannel", "getMutableGuildChannelsForGuild", "getSortedPrivateChannels"));
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @type {function(BigInt, Object): Boolean}
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						static #hasPermission = WEBPACK.findFunction("can", [ "getGuildPermissions", "getChannelPermissions" ]);
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @type {function(String): MessageData}
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						static #getMessages = WEBPACK.findFunction("getMessages");
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @type {function(String): void}
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						static #jumpToMessage = WEBPACK.findFunction("jumpToMessage");
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @type {function(): String}
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						static #getCurrentlySelectedChannelId = WEBPACK.findFunction("getCurrentlySelectedChannelId");
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @type {function(String): void}
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						static #selectPrivateChannel = WEBPACK.findFunction("selectPrivateChannel", [ "selectChannel" ]);
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @type {function(String, String, String=null): void}
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						static #transitionToGuild = WEBPACK.findFunction("transitionToGuild", [ "transitionTo" ]);
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						static isCompatible() {
 | 
				
			||||||
 | 
							return !!this.#guildStore
 | 
				
			||||||
 | 
								&& !!this.#guildSettings
 | 
				
			||||||
 | 
								&& !!this.#channelStore
 | 
				
			||||||
 | 
								&& !!this.#hasPermission
 | 
				
			||||||
 | 
								&& !!this.#getMessages
 | 
				
			||||||
 | 
								&& !!this.#jumpToMessage
 | 
				
			||||||
 | 
								&& !!this.#getCurrentlySelectedChannelId
 | 
				
			||||||
 | 
								&& !!this.#selectPrivateChannel
 | 
				
			||||||
 | 
								&& !!this.#transitionToGuild;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	static getMessageOuterElement() {
 | 
						static getMessageOuterElement() {
 | 
				
			||||||
		return DOM.queryReactClass("messagesWrapper");
 | 
							return DOM.queryReactClass("messagesWrapper");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -26,14 +109,6 @@ class DISCORD {
 | 
				
			|||||||
		return DOM.queryReactClass("scroller", this.getMessageOuterElement());
 | 
							return DOM.queryReactClass("scroller", this.getMessageOuterElement());
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	static getMessageElements() {
 | 
					 | 
				
			||||||
		return this.getMessageOuterElement().querySelectorAll("[class*='message_']");
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	static hasMoreMessages() {
 | 
					 | 
				
			||||||
		return document.querySelector("#messagesNavigationDescription + [class^=container]") === null;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	static loadOlderMessages() {
 | 
						static loadOlderMessages() {
 | 
				
			||||||
		const view = this.getMessageScrollerElement();
 | 
							const view = this.getMessageScrollerElement();
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
@@ -48,20 +123,36 @@ class DISCORD {
 | 
				
			|||||||
	static setupMessageCallback(callback) {
 | 
						static setupMessageCallback(callback) {
 | 
				
			||||||
		const previousMessages = new Set();
 | 
							const previousMessages = new Set();
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		const onMessageElementsChanged = function() {
 | 
							const onMessageElementsChanged = force => {
 | 
				
			||||||
			const messages = DISCORD.getMessages();
 | 
								const channelId = this.#getCurrentlySelectedChannelId();
 | 
				
			||||||
			const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !DISCORD.hasMoreMessages();
 | 
								if (!channelId) {
 | 
				
			||||||
 | 
									return false;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
 | 
								const messages = this.#getMessages(channelId);
 | 
				
			||||||
 | 
								if (!messages || !messages.ready || messages.loadingMore) {
 | 
				
			||||||
 | 
									return false;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								const channel = this.#channelStore.getChannel(channelId);
 | 
				
			||||||
 | 
								if (!channel) {
 | 
				
			||||||
 | 
									return false;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								const hasChanged = force || !messages.hasMoreBefore || messages.some(message => !previousMessages.has(message.id));
 | 
				
			||||||
			if (!hasChanged) {
 | 
								if (!hasChanged) {
 | 
				
			||||||
				return;
 | 
									return false;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			previousMessages.clear();
 | 
								previousMessages.clear();
 | 
				
			||||||
			for (const message of messages) {
 | 
								for (const message of messages._array) {
 | 
				
			||||||
				previousMessages.add(message.id);
 | 
									previousMessages.add(message.id);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			callback(messages);
 | 
								const server = this.#guildStore.getGuild(channel.guild_id);
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								callback(server, channel, messages._array, messages.hasMoreBefore);
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		let debounceTimer;
 | 
							let debounceTimer;
 | 
				
			||||||
@@ -74,7 +165,7 @@ class DISCORD {
 | 
				
			|||||||
			debounceTimer = window.setTimeout(onMessageElementsChanged, 100);
 | 
								debounceTimer = window.setTimeout(onMessageElementsChanged, 100);
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		const observer = new MutationObserver(function () {
 | 
							const observer = new MutationObserver(function() {
 | 
				
			||||||
			onMessageElementsChangedLater();
 | 
								onMessageElementsChangedLater();
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
@@ -112,216 +203,55 @@ class DISCORD {
 | 
				
			|||||||
			observedElement = null;
 | 
								observedElement = null;
 | 
				
			||||||
			window.clearInterval(observerTimer);
 | 
								window.clearInterval(observerTimer);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
	/**
 | 
							return () => onMessageElementsChanged(true);
 | 
				
			||||||
	 * Returns the message from a message element.
 | 
					 | 
				
			||||||
	 * @returns { null | DiscordMessage } }
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	static getMessageFromElement(ele) {
 | 
					 | 
				
			||||||
		const props = DOM.getReactProps(ele);
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		if (props && Array.isArray(props.children)) {
 | 
					 | 
				
			||||||
			for (const child of props.children) {
 | 
					 | 
				
			||||||
				if (!(child instanceof Object)) {
 | 
					 | 
				
			||||||
					continue;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				const childProps = child.props;
 | 
					 | 
				
			||||||
				if (childProps instanceof Object && "message" in childProps) {
 | 
					 | 
				
			||||||
					return childProps.message;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		return null;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Returns an array containing currently loaded messages.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	static getMessages() {
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			const messages = [];
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			for (const ele of this.getMessageElements()) {
 | 
					 | 
				
			||||||
				try {
 | 
					 | 
				
			||||||
					const message = this.getMessageFromElement(ele);
 | 
					 | 
				
			||||||
					
 | 
					 | 
				
			||||||
					if (message != null) {
 | 
					 | 
				
			||||||
						messages.push(message);
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				} catch (e) {
 | 
					 | 
				
			||||||
					console.error("[DHT] Error extracing message data, skipping it.", e, ele, DOM.tryGetReactProps(ele));
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			return messages;
 | 
					 | 
				
			||||||
		} catch (e) {
 | 
					 | 
				
			||||||
			console.error("[DHT] Error retrieving messages.", e);
 | 
					 | 
				
			||||||
			return [];
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Returns an object containing the selected server and channel information.
 | 
					 | 
				
			||||||
	 * For types DM and GROUP, the server and channel ids and names are identical.
 | 
					 | 
				
			||||||
	 * @returns { {} | null }
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	static getSelectedChannel() {
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			let obj = null;
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			try {
 | 
					 | 
				
			||||||
				for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) {
 | 
					 | 
				
			||||||
					if (child && child.props && child.props.channel) {
 | 
					 | 
				
			||||||
						obj = child.props.channel;
 | 
					 | 
				
			||||||
						break;
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			} catch (e) {
 | 
					 | 
				
			||||||
				console.error("[DHT] Error retrieving selected channel from 'chatContent' element.", e);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			if (!obj || typeof obj.id !== "string") {
 | 
					 | 
				
			||||||
				return null;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			const dms = DOM.queryReactClass("privateChannels");
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			if (dms) {
 | 
					 | 
				
			||||||
				let name;
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				for (const ele of dms.querySelectorAll("[class*='channel_'] [class*='selected_'] [class^='name_'] *")) {
 | 
					 | 
				
			||||||
					const node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE);
 | 
					 | 
				
			||||||
					
 | 
					 | 
				
			||||||
					if (node) {
 | 
					 | 
				
			||||||
						name = node.nodeValue;
 | 
					 | 
				
			||||||
						break;
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				if (!name) {
 | 
					 | 
				
			||||||
					return null;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				let type;
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
 | 
					 | 
				
			||||||
				switch (obj.type) {
 | 
					 | 
				
			||||||
					case DISCORD.CHANNEL_TYPE.DM: type = "DM"; break;
 | 
					 | 
				
			||||||
					case DISCORD.CHANNEL_TYPE.GROUP_DM: type = "GROUP"; break;
 | 
					 | 
				
			||||||
					default: return null;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				const id = obj.id;
 | 
					 | 
				
			||||||
				const server = { id, name, type };
 | 
					 | 
				
			||||||
				const channel = { id, name };
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				return { server, channel };
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			else if (obj.guild_id) {
 | 
					 | 
				
			||||||
				let guild;
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				for (const child of DOM.getReactProps(document.querySelector("nav header [class*='headerContent_']")).children) {
 | 
					 | 
				
			||||||
					if (child && child.props && child.props.guild) {
 | 
					 | 
				
			||||||
						guild = child.props.guild;
 | 
					 | 
				
			||||||
						break;
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				if (!guild || typeof guild.name !== "string" || obj.guild_id !== guild.id) {
 | 
					 | 
				
			||||||
					return null;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				const server = {
 | 
					 | 
				
			||||||
					"id": guild.id,
 | 
					 | 
				
			||||||
					"name": guild.name,
 | 
					 | 
				
			||||||
					"type": "SERVER"
 | 
					 | 
				
			||||||
				};
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				const channel = {
 | 
					 | 
				
			||||||
					"id": obj.id,
 | 
					 | 
				
			||||||
					"name": obj.name,
 | 
					 | 
				
			||||||
					"extra": {
 | 
					 | 
				
			||||||
						"nsfw": obj.nsfw
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				};
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				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 {
 | 
					 | 
				
			||||||
					channel["extra"]["position"] = obj.position;
 | 
					 | 
				
			||||||
					channel["extra"]["topic"] = obj.topic;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				return { server, channel };
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			else {
 | 
					 | 
				
			||||||
				return null;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} catch (e) {
 | 
					 | 
				
			||||||
			console.error("[DHT] Error retrieving selected channel.", e);
 | 
					 | 
				
			||||||
			return null;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Selects the next text channel and returns true, otherwise returns false if there are no more channels.
 | 
						 * Selects the next text channel and returns true, otherwise returns false if there are no more channels.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	static selectNextTextChannel() {
 | 
						static selectNextTextChannel() {
 | 
				
			||||||
		const dms = DOM.queryReactClass("privateChannels");
 | 
							const currentChannel = this.#channelStore.getChannel(this.#getCurrentlySelectedChannelId());
 | 
				
			||||||
 | 
							if (!currentChannel) {
 | 
				
			||||||
 | 
								return false;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		if (dms) {
 | 
							if (this.CHANNEL_TYPE.isPrivate(currentChannel.type)) {
 | 
				
			||||||
			const currentChannel = DOM.queryReactClass("selected", dms);
 | 
								const privateChannel = this.#channelStore.getSortedPrivateChannels();
 | 
				
			||||||
			const currentChannelContainer = currentChannel && currentChannel.closest("[class*='channel_']");
 | 
								const currentIndex = privateChannel.findIndex(channel => channel.id === currentChannel.id);
 | 
				
			||||||
			const nextChannel = currentChannelContainer && currentChannelContainer.nextElementSibling;
 | 
					 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			if (!nextChannel || !nextChannel.getAttribute("class").includes("channel_")) {
 | 
								if (currentIndex === -1 || currentIndex === privateChannel.length - 1) {
 | 
				
			||||||
				return false;
 | 
									return false;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			const nextChannelLink = nextChannel.querySelector("a[href*='/@me/']");
 | 
								this.#selectPrivateChannel(privateChannel[currentIndex + 1].id);
 | 
				
			||||||
			if (!nextChannelLink) {
 | 
					 | 
				
			||||||
				return false;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			nextChannelLink.click();
 | 
					 | 
				
			||||||
			nextChannelLink.scrollIntoView(true);
 | 
					 | 
				
			||||||
			return true;
 | 
								return true;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else {
 | 
							else {
 | 
				
			||||||
			const channelListEle = document.getElementById("channels");
 | 
								const guildId = currentChannel.guild_id;
 | 
				
			||||||
			if (!channelListEle) {
 | 
								
 | 
				
			||||||
 | 
								let isChannelOptedIn;
 | 
				
			||||||
 | 
								if (this.#guildSettings.isOptInEnabled(guildId)) {
 | 
				
			||||||
 | 
									const optedInChannels = this.#guildSettings.getOptedInChannels(guildId);
 | 
				
			||||||
 | 
									isChannelOptedIn = channel => optedInChannels.has(channel.id);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								else {
 | 
				
			||||||
 | 
									isChannelOptedIn = _ => true;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								const guildChannelMap = this.#channelStore.getMutableGuildChannelsForGuild(guildId);
 | 
				
			||||||
 | 
								const guildChannels = Object.values(guildChannelMap)
 | 
				
			||||||
 | 
									.filter(channel => this.CHANNEL_TYPE.isNavigableGuildChannel(channel.type) && isChannelOptedIn(channel) && this.#hasPermission(this.PERMISSION.VIEW_CHANNEL, channel))
 | 
				
			||||||
 | 
									.sort((a, b) => a.position - b.position);
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								debugger;
 | 
				
			||||||
 | 
								const currentIndex = guildChannels.findIndex(channel => channel.id === currentChannel.id);
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								if (currentIndex === -1 || currentIndex === guildChannels.length - 1) {
 | 
				
			||||||
				return false;
 | 
									return false;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			function getLinkElement(channel) {
 | 
								this.#transitionToGuild(guildId, guildChannels[currentIndex + 1].id);
 | 
				
			||||||
				return channel.querySelector("a[href^='/channels/'][role='link']");
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			const allTextChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), ele => getLinkElement(ele) !== null);
 | 
					 | 
				
			||||||
			let nextChannel = null;
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			for (let index = 0; index < allTextChannels.length - 1; index++) {
 | 
					 | 
				
			||||||
				if (allTextChannels[index].className.includes("selected_")) {
 | 
					 | 
				
			||||||
					nextChannel = allTextChannels[index + 1];
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			if (nextChannel === null) {
 | 
					 | 
				
			||||||
				return false;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			const nextChannelLink = getLinkElement(nextChannel);
 | 
					 | 
				
			||||||
			if (!nextChannelLink) {
 | 
					 | 
				
			||||||
				return false;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			nextChannelLink.click();
 | 
					 | 
				
			||||||
			nextChannel.scrollIntoView(true);
 | 
					 | 
				
			||||||
			return true;
 | 
								return true;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,31 +55,4 @@ class DOM {
 | 
				
			|||||||
		const value = document.cookie.replace(new RegExp("(?:(?:^|.*;\\s*)" + name + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1");
 | 
							const value = document.cookie.replace(new RegExp("(?:(?:^|.*;\\s*)" + name + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1");
 | 
				
			||||||
		return value.length ? JSON.parse(decodeURIComponent(value)) : null;
 | 
							return value.length ? JSON.parse(decodeURIComponent(value)) : null;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Returns internal React state object of an element.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	static getReactProps(ele) {
 | 
					 | 
				
			||||||
		const keys = Object.keys(ele || {});
 | 
					 | 
				
			||||||
		let key = keys.find(key => key.startsWith("__reactInternalInstance"));
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		if (key) {
 | 
					 | 
				
			||||||
			// noinspection JSUnresolvedVariable
 | 
					 | 
				
			||||||
			return ele[key].memoizedProps;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		key = keys.find(key => key.startsWith("__reactProps$"));
 | 
					 | 
				
			||||||
		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;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,64 +58,39 @@ const STATE = (function() {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @param {DiscordChannel} channel
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						const getPrivateChannelName = function(channel) {
 | 
				
			||||||
 | 
							if (channel.name === "") {
 | 
				
			||||||
 | 
								return channel.rawRecipients.map(user => user.username).join(", ");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
 | 
								return channel.name;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @param {Number} type
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						const getChannelTypeName = function(type) {
 | 
				
			||||||
 | 
							if (type === DISCORD.CHANNEL_TYPE.DM) {
 | 
				
			||||||
 | 
								return "DM";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else if (type === DISCORD.CHANNEL_TYPE.GROUP_DM) {
 | 
				
			||||||
 | 
								return "GROUP";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
 | 
								return "SERVER";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	const trackingStateChangedListeners = [];
 | 
						const trackingStateChangedListeners = [];
 | 
				
			||||||
	let isTracking = false;
 | 
						let isTracking = false;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	const addedChannels = new Set();
 | 
						const addedChannels = new Set();
 | 
				
			||||||
	const addedUsers = new Set();
 | 
						const addedUsers = new Set();
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * @name DiscordUser
 | 
					 | 
				
			||||||
	 * @property {String} id
 | 
					 | 
				
			||||||
	 * @property {String} username
 | 
					 | 
				
			||||||
	 * @property {String} discriminator
 | 
					 | 
				
			||||||
	 * @property {String} [avatar]
 | 
					 | 
				
			||||||
	 * @property {Boolean} [bot]
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * @name DiscordMessage
 | 
					 | 
				
			||||||
	 * @property {String} id
 | 
					 | 
				
			||||||
	 * @property {String} channel_id
 | 
					 | 
				
			||||||
	 * @property {DiscordUser} author
 | 
					 | 
				
			||||||
	 * @property {String} content
 | 
					 | 
				
			||||||
	 * @property {Date} timestamp
 | 
					 | 
				
			||||||
	 * @property {Date|null} editedTimestamp
 | 
					 | 
				
			||||||
	 * @property {DiscordAttachment[]} attachments
 | 
					 | 
				
			||||||
	 * @property {Object[]} embeds
 | 
					 | 
				
			||||||
	 * @property {DiscordMessageReaction[]} [reactions]
 | 
					 | 
				
			||||||
	 * @property {DiscordMessageReference} [messageReference]
 | 
					 | 
				
			||||||
	 * @property {Number} type
 | 
					 | 
				
			||||||
	 * @property {String} state
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * @name DiscordAttachment
 | 
					 | 
				
			||||||
	 * @property {String} id
 | 
					 | 
				
			||||||
	 * @property {String} filename
 | 
					 | 
				
			||||||
	 * @property {String} [content_type]
 | 
					 | 
				
			||||||
	 * @property {String} size
 | 
					 | 
				
			||||||
	 * @property {String} url
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * @name DiscordMessageReaction
 | 
					 | 
				
			||||||
	 * @property {DiscordEmoji} emoji
 | 
					 | 
				
			||||||
	 * @property {Number} count
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * @name DiscordMessageReference
 | 
					 | 
				
			||||||
	 * @property {String} [message_id]
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * @name DiscordEmoji
 | 
					 | 
				
			||||||
	 * @property {String|null} id
 | 
					 | 
				
			||||||
	 * @property {String|null} name
 | 
					 | 
				
			||||||
	 * @property {Boolean} animated
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		setup(port, token) {
 | 
							setup(port, token) {
 | 
				
			||||||
			serverPort = port;
 | 
								serverPort = port;
 | 
				
			||||||
@@ -146,32 +121,51 @@ const STATE = (function() {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 | 
							/**
 | 
				
			||||||
 | 
							 * @param {?DiscordGuild} serverInfo
 | 
				
			||||||
 | 
							 * @param {DiscordChannel} channelInfo
 | 
				
			||||||
 | 
							 */
 | 
				
			||||||
		async addDiscordChannel(serverInfo, channelInfo) {
 | 
							async addDiscordChannel(serverInfo, channelInfo) {
 | 
				
			||||||
			if (addedChannels.has(channelInfo.id)) {
 | 
								if (addedChannels.has(channelInfo.id)) {
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			const server = {
 | 
								const server = {
 | 
				
			||||||
				id: serverInfo.id,
 | 
									type: getChannelTypeName(channelInfo.type)
 | 
				
			||||||
				name: serverInfo.name,
 | 
					 | 
				
			||||||
				type: serverInfo.type
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			const channel = {
 | 
								const channel = {
 | 
				
			||||||
				id: channelInfo.id,
 | 
									id: channelInfo.id,
 | 
				
			||||||
				name: channelInfo.name
 | 
									extra: {}
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			if ("extra" in channelInfo) {
 | 
								if (DISCORD.CHANNEL_TYPE.isPrivate(channelInfo.type)) {
 | 
				
			||||||
				const extra = channelInfo.extra;
 | 
									server.id = channelInfo.id;
 | 
				
			||||||
 | 
									server.name = channel.name = getPrivateChannelName(channelInfo);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								else if (serverInfo) {
 | 
				
			||||||
 | 
									server.id = serverInfo.id;
 | 
				
			||||||
 | 
									server.name = serverInfo.name;
 | 
				
			||||||
 | 
									channel.name = channelInfo.name;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								else {
 | 
				
			||||||
 | 
									return;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
				if ("parent" in extra) {
 | 
								if ("nsfw" in channelInfo) {
 | 
				
			||||||
					channel.parent = extra.parent;
 | 
									channel.extra.nsfw = channelInfo.nsfw;
 | 
				
			||||||
				}
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
				channel.position = extra.position;
 | 
								if ("topic" in channelInfo) {
 | 
				
			||||||
				channel.topic = extra.topic;
 | 
									channel.extra.topic = channelInfo.topic;
 | 
				
			||||||
				channel.nsfw = extra.nsfw;
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								if ("position" in channelInfo) {
 | 
				
			||||||
 | 
									channel.extra.position = channelInfo.position;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								if (channelInfo.type === DISCORD.CHANNEL_TYPE.ANNOUNCEMENT_THREAD || channelInfo.type === DISCORD.CHANNEL_TYPE.PUBLIC_THREAD || channelInfo.type === DISCORD.CHANNEL_TYPE.PRIVATE_THREAD) {
 | 
				
			||||||
 | 
									channel.extra.parent = channelInfo.parent_id;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			await post("/track-channel", { server, channel });
 | 
								await post("/track-channel", { server, channel });
 | 
				
			||||||
@@ -200,6 +194,10 @@ const STATE = (function() {
 | 
				
			|||||||
						name: user.username
 | 
											name: user.username
 | 
				
			||||||
					};
 | 
										};
 | 
				
			||||||
					
 | 
										
 | 
				
			||||||
 | 
										if (user.globalName) {
 | 
				
			||||||
 | 
											obj.displayName = user.globalName;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										
 | 
				
			||||||
					if (user.avatar) {
 | 
										if (user.avatar) {
 | 
				
			||||||
						obj.avatar = user.avatar;
 | 
											obj.avatar = user.avatar;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										81
									
								
								app/Resources/Tracker/scripts/types.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								app/Resources/Tracker/scripts/types.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @name DiscordGuild
 | 
				
			||||||
 | 
					 * @property {String} id
 | 
				
			||||||
 | 
					 * @property {String} name
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @name DiscordChannel
 | 
				
			||||||
 | 
					 * @property {String} id
 | 
				
			||||||
 | 
					 * @property {String} name
 | 
				
			||||||
 | 
					 * @property {Number} type
 | 
				
			||||||
 | 
					 * @property {String} [guild_id]
 | 
				
			||||||
 | 
					 * @property {String} [parent_id]
 | 
				
			||||||
 | 
					 * @property {Number} [position]
 | 
				
			||||||
 | 
					 * @property {String} [topic]
 | 
				
			||||||
 | 
					 * @property {Boolean} [nsfw]
 | 
				
			||||||
 | 
					 * @property {DiscordUser[]} [rawRecipients]
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @name DiscordUser
 | 
				
			||||||
 | 
					 * @property {String} id
 | 
				
			||||||
 | 
					 * @property {String} username
 | 
				
			||||||
 | 
					 * @property {String} discriminator
 | 
				
			||||||
 | 
					 * @property {String} [globalName]
 | 
				
			||||||
 | 
					 * @property {String} [avatar]
 | 
				
			||||||
 | 
					 * @property {Boolean} [bot]
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @name DiscordMessage
 | 
				
			||||||
 | 
					 * @property {String} id
 | 
				
			||||||
 | 
					 * @property {String} channel_id
 | 
				
			||||||
 | 
					 * @property {DiscordUser} author
 | 
				
			||||||
 | 
					 * @property {String} content
 | 
				
			||||||
 | 
					 * @property {Date} timestamp
 | 
				
			||||||
 | 
					 * @property {Date|null} editedTimestamp
 | 
				
			||||||
 | 
					 * @property {DiscordAttachment[]} attachments
 | 
				
			||||||
 | 
					 * @property {Object[]} embeds
 | 
				
			||||||
 | 
					 * @property {DiscordMessageReaction[]} [reactions]
 | 
				
			||||||
 | 
					 * @property {DiscordMessageReference} [messageReference]
 | 
				
			||||||
 | 
					 * @property {Number} type
 | 
				
			||||||
 | 
					 * @property {String} state
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @name DiscordAttachment
 | 
				
			||||||
 | 
					 * @property {String} id
 | 
				
			||||||
 | 
					 * @property {String} filename
 | 
				
			||||||
 | 
					 * @property {String} [content_type]
 | 
				
			||||||
 | 
					 * @property {String} size
 | 
				
			||||||
 | 
					 * @property {String} url
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @name DiscordMessageReaction
 | 
				
			||||||
 | 
					 * @property {DiscordEmoji} emoji
 | 
				
			||||||
 | 
					 * @property {Number} count
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @name DiscordMessageReference
 | 
				
			||||||
 | 
					 * @property {String} [message_id]
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @name DiscordEmoji
 | 
				
			||||||
 | 
					 * @property {String|null} id
 | 
				
			||||||
 | 
					 * @property {String|null} name
 | 
				
			||||||
 | 
					 * @property {Boolean} animated
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @name MessageData
 | 
				
			||||||
 | 
					 * @type {Object}
 | 
				
			||||||
 | 
					 * @property {Boolean} ready
 | 
				
			||||||
 | 
					 * @property {Boolean} loadingMore
 | 
				
			||||||
 | 
					 * @property {Boolean} hasMoreAfter
 | 
				
			||||||
 | 
					 * @property {Boolean} hasMoreBefore
 | 
				
			||||||
 | 
					 * @property {Array<DiscordMessage>} _array
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
							
								
								
									
										93
									
								
								app/Resources/Tracker/scripts/webpack.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								app/Resources/Tracker/scripts/webpack.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Parts copied from Better Discord, licensed under Apache License 2.0.
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * https://github.com/BetterDiscord/BetterDiscord/blob/78edeb77c60542a57884686c4ba98f997c886fad/renderer/src/modules/webpackmodules.js
 | 
				
			||||||
 | 
					 * https://github.com/BetterDiscord/BetterDiscord/blob/78edeb77c60542a57884686c4ba98f997c886fad/LICENSE
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class WEBPACK {
 | 
				
			||||||
 | 
						static get require() {
 | 
				
			||||||
 | 
							if (this._require) {
 | 
				
			||||||
 | 
								return this._require;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							/**
 | 
				
			||||||
 | 
							 * @type {Object}
 | 
				
			||||||
 | 
							 * @property {Object} m
 | 
				
			||||||
 | 
							 * @property {Object} c
 | 
				
			||||||
 | 
							 */
 | 
				
			||||||
 | 
							let hookedRequire;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							const id = "dht-webpackmodules-" + new Date().getTime();
 | 
				
			||||||
 | 
							if (typeof (window["webpackChunkdiscord_app"]) !== "undefined") {
 | 
				
			||||||
 | 
								window["webpackChunkdiscord_app"].push([ [ id ], {}, internalRequire => hookedRequire = internalRequire ]);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							delete hookedRequire.m[id];
 | 
				
			||||||
 | 
							delete hookedRequire.c[id];
 | 
				
			||||||
 | 
							return this._require = hookedRequire;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						static getAllModules() {
 | 
				
			||||||
 | 
							return this.require.c;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						static filterByProps(...props) {
 | 
				
			||||||
 | 
							return module => props.every(prop => prop in module);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						static filterByPropsWithPredicate(predicate, ...props) {
 | 
				
			||||||
 | 
							return module => props.every(prop => prop in module && predicate(module[prop]));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						static findModules(filter) {
 | 
				
			||||||
 | 
							const defaultExport = true;
 | 
				
			||||||
 | 
							const moduleFilter = module => (typeof module === "object" || typeof module === "function") && filter(module);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							const results = [];
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							for (const module of Object.values(this.getAllModules())) {
 | 
				
			||||||
 | 
								/**
 | 
				
			||||||
 | 
								 * @type {Object}
 | 
				
			||||||
 | 
								 * @property [Z]
 | 
				
			||||||
 | 
								 * @property [ZP]
 | 
				
			||||||
 | 
								 * @property [__esModule]
 | 
				
			||||||
 | 
								 * @property [default]
 | 
				
			||||||
 | 
								 */
 | 
				
			||||||
 | 
								const exports = module.exports;
 | 
				
			||||||
 | 
								if (!exports || exports === window || exports === document.documentElement || exports[Symbol.toStringTag] === "DOMTokenList") {
 | 
				
			||||||
 | 
									continue;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								let foundModule = null;
 | 
				
			||||||
 | 
								if (exports.Z && moduleFilter(exports.Z)) {
 | 
				
			||||||
 | 
									foundModule = defaultExport ? exports.Z : exports;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (exports.ZP && moduleFilter(exports.ZP)) {
 | 
				
			||||||
 | 
									foundModule = defaultExport ? exports.ZP : exports;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (exports.__esModule && exports.default && moduleFilter(exports.default)) {
 | 
				
			||||||
 | 
									foundModule = defaultExport ? exports.default : exports;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (moduleFilter(exports)) {
 | 
				
			||||||
 | 
									foundModule = exports;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								if (foundModule) {
 | 
				
			||||||
 | 
									results.push(foundModule);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							return results;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						static findModule(filter) {
 | 
				
			||||||
 | 
							const modules = this.findModules(filter);
 | 
				
			||||||
 | 
							return modules.length === 1 ? modules[0] : null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						static findFunction(name, additionalRequiredProps) {
 | 
				
			||||||
 | 
							const searchedProps = additionalRequiredProps ? [name, ...additionalRequiredProps] : [name];
 | 
				
			||||||
 | 
							const matchingModule = this.findModule(this.filterByPropsWithPredicate(prop => typeof(prop) === "function", ...searchedProps));
 | 
				
			||||||
 | 
							return matchingModule == null ? null : matchingModule[name].bind(matchingModule);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -25,6 +25,8 @@
 | 
				
			|||||||
    <div id="menu">
 | 
					    <div id="menu">
 | 
				
			||||||
      <button id="btn-settings">Settings</button>
 | 
					      <button id="btn-settings">Settings</button>
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
 | 
					      <div class="splitter"></div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
      <div> <!-- needed to stop the select from messing up -->
 | 
					      <div> <!-- needed to stop the select from messing up -->
 | 
				
			||||||
        <select id="opt-messages-per-page">
 | 
					        <select id="opt-messages-per-page">
 | 
				
			||||||
          <option value="50">50 messages per page </option>
 | 
					          <option value="50">50 messages per page </option>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import discord from "./discord.mjs";
 | 
					import discord from "./discord.mjs";
 | 
				
			||||||
import gui from "./gui.mjs";
 | 
					import gui from "./gui.mjs";
 | 
				
			||||||
import state from "./state.mjs";
 | 
					import state from "./state.mjs";
 | 
				
			||||||
 | 
					import "./polyfills.mjs";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
window.DISCORD = discord;
 | 
					window.DISCORD = discord;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -80,7 +80,7 @@ export default (function() {
 | 
				
			|||||||
		processed = processed
 | 
							processed = processed
 | 
				
			||||||
			.replace(regex.formatUrl, "<a href='$1' target='_blank' rel='noreferrer'>$1</a>")
 | 
								.replace(regex.formatUrl, "<a href='$1' target='_blank' rel='noreferrer'>$1</a>")
 | 
				
			||||||
			.replace(regex.mentionChannel, (full, match) => "<span class='link mention-chat'>#" + state.getChannelName(match) + "</span>")
 | 
								.replace(regex.mentionChannel, (full, match) => "<span class='link mention-chat'>#" + state.getChannelName(match) + "</span>")
 | 
				
			||||||
			.replace(regex.mentionUser, (full, match) => "<span class='link mention-user' title='#" + (state.getUserTag(match) || "????") + "'>@" + state.getUserName(match) + "</span>")
 | 
								.replace(regex.mentionUser, (full, match) => "<span class='link mention-user' title='" + state.getUserName(match) + "'>@" + state.getUserDisplayName(match) + "</span>")
 | 
				
			||||||
			.replace(regex.customEmojiStatic, (full, m1, m2) => getEmoji(m1, m2, "webp"))
 | 
								.replace(regex.customEmojiStatic, (full, m1, m2) => getEmoji(m1, m2, "webp"))
 | 
				
			||||||
			.replace(regex.customEmojiAnimated, (full, m1, m2) => getEmoji(m1, m2, animatedEmojiExtension));
 | 
								.replace(regex.customEmojiAnimated, (full, m1, m2) => getEmoji(m1, m2, animatedEmojiExtension));
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
@@ -129,7 +129,7 @@ export default (function() {
 | 
				
			|||||||
			templateMessageNoAvatar = new template([
 | 
								templateMessageNoAvatar = new template([
 | 
				
			||||||
				"<div>",
 | 
									"<div>",
 | 
				
			||||||
				"<div class='reply-message'>{reply}</div>",
 | 
									"<div class='reply-message'>{reply}</div>",
 | 
				
			||||||
				"<h2><strong class='username' title='#{user.tag}'>{user.name}</strong><span class='info time'>{timestamp}</span>{edit}{jump}</h2>",
 | 
									"<h2><strong class='username' title='{user.name}'>{user.displayName}</strong><span class='info time'>{timestamp}</span>{edit}{jump}</h2>",
 | 
				
			||||||
				"<div class='message'>{contents}{embeds}{attachments}</div>",
 | 
									"<div class='message'>{contents}{embeds}{attachments}</div>",
 | 
				
			||||||
				"{reactions}",
 | 
									"{reactions}",
 | 
				
			||||||
				"</div>"
 | 
									"</div>"
 | 
				
			||||||
@@ -141,7 +141,7 @@ export default (function() {
 | 
				
			|||||||
				"<div class='avatar-wrapper'>",
 | 
									"<div class='avatar-wrapper'>",
 | 
				
			||||||
				"<div class='avatar'>{avatar}</div>",
 | 
									"<div class='avatar'>{avatar}</div>",
 | 
				
			||||||
				"<div>",
 | 
									"<div>",
 | 
				
			||||||
				"<h2><strong class='username' title='#{user.tag}'>{user.name}</strong><span class='info time'>{timestamp}</span>{edit}{jump}</h2>",
 | 
									"<h2><strong class='username' title='{user.name}'>{user.displayName}</strong><span class='info time'>{timestamp}</span>{edit}{jump}</h2>",
 | 
				
			||||||
				"<div class='message'>{contents}{embeds}{attachments}</div>",
 | 
									"<div class='message'>{contents}{embeds}{attachments}</div>",
 | 
				
			||||||
				"{reactions}",
 | 
									"{reactions}",
 | 
				
			||||||
				"</div>",
 | 
									"</div>",
 | 
				
			||||||
@@ -227,8 +227,8 @@ export default (function() {
 | 
				
			|||||||
				if (property === "avatar") {
 | 
									if (property === "avatar") {
 | 
				
			||||||
					return value ? templateUserAvatar.apply(getAvatarUrlObject(value)) : "";
 | 
										return value ? templateUserAvatar.apply(getAvatarUrlObject(value)) : "";
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				else if (property === "user.tag") {
 | 
									else if (property === "user.displayName") {
 | 
				
			||||||
					return value ? value : "????";
 | 
										return value ? value : message.user.name;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				else if (property === "timestamp") {
 | 
									else if (property === "timestamp") {
 | 
				
			||||||
					return dom.getHumanReadableTime(value);
 | 
										return dom.getHumanReadableTime(value);
 | 
				
			||||||
@@ -292,7 +292,7 @@ export default (function() {
 | 
				
			|||||||
						return value === null ? "<span class='reply-contents reply-missing'>(replies to an unknown message)</span>" : "";
 | 
											return value === null ? "<span class='reply-contents reply-missing'>(replies to an unknown message)</span>" : "";
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					
 | 
										
 | 
				
			||||||
					const user = "<span class='reply-username' title='#" + (value.user.tag ? value.user.tag : "????") + "'>" + value.user.name + "</span>";
 | 
										const user = "<span class='reply-username' title='" + value.user.name + "'>" + (value.user.displayName ?? value.user.name) + "</span>";
 | 
				
			||||||
					const avatar = settings.enableUserAvatars && value.avatar ? "<span class='reply-avatar'>" + templateUserAvatar.apply(getAvatarUrlObject(value.avatar)) + "</span>" : "";
 | 
										const avatar = settings.enableUserAvatars && value.avatar ? "<span class='reply-avatar'>" + templateUserAvatar.apply(getAvatarUrlObject(value.avatar)) + "</span>" : "";
 | 
				
			||||||
					const contents = value.contents ? "<span class='reply-contents'>" + processMessageContents(value.contents) + "</span>" : "";
 | 
										const contents = value.contents ? "<span class='reply-contents'>" + processMessageContents(value.contents) + "</span>" : "";
 | 
				
			||||||
					
 | 
										
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -243,10 +243,11 @@ export default (function() {
 | 
				
			|||||||
			
 | 
								
 | 
				
			||||||
			const options = [];
 | 
								const options = [];
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			for (const key of Object.keys(users)) {
 | 
								for (const id of Object.keys(users)) {
 | 
				
			||||||
 | 
									const user = users[id];
 | 
				
			||||||
				const option = document.createElement("option");
 | 
									const option = document.createElement("option");
 | 
				
			||||||
				option.value = key;
 | 
									option.value = id;
 | 
				
			||||||
				option.text = users[key].name;
 | 
									option.text = user.displayName ? `${user.displayName} (${user.name})` : user.name;
 | 
				
			||||||
				options.push(option);
 | 
									options.push(option);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										35
									
								
								app/Resources/Viewer/scripts/polyfills.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/Resources/Viewer/scripts/polyfills.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					// https://gist.github.com/MattiasBuelens/496fc1d37adb50a733edd43853f2f60e/088f061ab79b296f29225467ae9ba86ff990195d
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ReadableStream.prototype.values ??= function({ preventCancel = false } = {}) {
 | 
				
			||||||
 | 
						const reader = this.getReader();
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							async next() {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									const result = await reader.read();
 | 
				
			||||||
 | 
									if (result.done) {
 | 
				
			||||||
 | 
										reader.releaseLock();
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return result;
 | 
				
			||||||
 | 
								} catch (e) {
 | 
				
			||||||
 | 
									reader.releaseLock();
 | 
				
			||||||
 | 
									throw e;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							async return(value) {
 | 
				
			||||||
 | 
								if (!preventCancel) {
 | 
				
			||||||
 | 
									const cancelPromise = reader.cancel(value);
 | 
				
			||||||
 | 
									reader.releaseLock();
 | 
				
			||||||
 | 
									await cancelPromise;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								else {
 | 
				
			||||||
 | 
									reader.releaseLock();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return { done: true, value };
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							[Symbol.asyncIterator]() {
 | 
				
			||||||
 | 
								return this;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ReadableStream.prototype[Symbol.asyncIterator] ??= ReadableStream.prototype.values;
 | 
				
			||||||
@@ -316,16 +316,16 @@ export default (function() {
 | 
				
			|||||||
			return (channelObj && channelObj.name) || channel;
 | 
								return (channelObj && channelObj.name) || channel;
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		getUserTag(user) {
 | 
					 | 
				
			||||||
			const userObj = loadedFileMeta.users[user];
 | 
					 | 
				
			||||||
			return (userObj && userObj.tag) || "????";
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		getUserName(user) {
 | 
							getUserName(user) {
 | 
				
			||||||
			const userObj = loadedFileMeta.users[user];
 | 
								const userObj = loadedFileMeta.users[user];
 | 
				
			||||||
			return (userObj && userObj.name) || user;
 | 
								return (userObj && userObj.name) || user;
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 | 
							getUserDisplayName(user) {
 | 
				
			||||||
 | 
								const userObj = loadedFileMeta.users[user];
 | 
				
			||||||
 | 
								return (userObj && (userObj.displayName || userObj.name)) || user;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							
 | 
				
			||||||
		selectChannel(channel) {
 | 
							selectChannel(channel) {
 | 
				
			||||||
			currentPage = 1;
 | 
								currentPage = 1;
 | 
				
			||||||
			selectedChannel = channel;
 | 
								selectedChannel = channel;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,16 @@
 | 
				
			|||||||
#menu {
 | 
					#menu {
 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  height: 48px;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-direction: row;
 | 
					  flex-direction: row;
 | 
				
			||||||
 | 
					  align-items: stretch;
 | 
				
			||||||
 | 
					  gap: 8px;
 | 
				
			||||||
 | 
					  padding: 8px;
 | 
				
			||||||
  background-color: #17181c;
 | 
					  background-color: #17181c;
 | 
				
			||||||
  border-bottom: 1px dotted #5d626b;
 | 
					  border-bottom: 1px dotted #5d626b;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#menu .splitter {
 | 
					#menu .splitter {
 | 
				
			||||||
  width: 1px;
 | 
					  flex: 0 0 1px;
 | 
				
			||||||
  margin: 9px 4px;
 | 
					  margin: 9px 1px;
 | 
				
			||||||
  background-color: #5d626b;
 | 
					  background-color: #5d626b;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -23,7 +24,8 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#menu button, #menu select, #menu input[type="text"] {
 | 
					#menu button, #menu select, #menu input[type="text"] {
 | 
				
			||||||
  margin: 8px;
 | 
					  height: 31px;
 | 
				
			||||||
 | 
					  padding: 0 10px;
 | 
				
			||||||
  background-color: #7289da;
 | 
					  background-color: #7289da;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75);
 | 
					  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75);
 | 
				
			||||||
@@ -31,28 +33,25 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#menu button {
 | 
					#menu button {
 | 
				
			||||||
  font-size: 17px;
 | 
					  font-size: 17px;
 | 
				
			||||||
  padding: 0 12px;
 | 
					 | 
				
			||||||
  border: 0;
 | 
					  border: 0;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#menu select {
 | 
					#menu select {
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  padding: 6px;
 | 
					 | 
				
			||||||
  border: 0;
 | 
					  border: 0;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#menu input[type="text"] {
 | 
					#menu input[type="text"] {
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  padding: 7px 12px;
 | 
					 | 
				
			||||||
  border: 0;
 | 
					  border: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#menu .nav {
 | 
					#menu .nav {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-direction: row;
 | 
					  flex-direction: row;
 | 
				
			||||||
  margin: 0 8px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#menu .nav > button {
 | 
					#menu .nav > button {
 | 
				
			||||||
@@ -66,7 +65,7 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#menu .nav > button, #menu .nav > p {
 | 
					#menu .nav > button, #menu .nav > p {
 | 
				
			||||||
  margin: 8px 1px;
 | 
					  margin: 0 1px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#opt-filter-list > select, #opt-filter-list > input {
 | 
					#opt-filter-list > select, #opt-filter-list > input {
 | 
				
			||||||
@@ -76,3 +75,7 @@
 | 
				
			|||||||
#opt-filter-list > .active {
 | 
					#opt-filter-list > .active {
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#btn-about {
 | 
				
			||||||
 | 
					  margin-left: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ namespace DHT.Server.Data;
 | 
				
			|||||||
public readonly struct User {
 | 
					public readonly struct User {
 | 
				
			||||||
	public ulong Id { get; init; }
 | 
						public ulong Id { get; init; }
 | 
				
			||||||
	public string Name { get; init; }
 | 
						public string Name { get; init; }
 | 
				
			||||||
 | 
						public string? DisplayName { get; init; }
 | 
				
			||||||
	public string? AvatarUrl { get; init; }
 | 
						public string? AvatarUrl { get; init; }
 | 
				
			||||||
	public string? Discriminator { get; init; }
 | 
						public string? Discriminator { get; init; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,10 +14,10 @@ static class ViewerJson {
 | 
				
			|||||||
		public required string Name { get; init; }
 | 
							public required string Name { get; init; }
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
 | 
							[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
 | 
				
			||||||
		public string? Avatar { get; init; }
 | 
							public string? DisplayName { get; init; }
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
 | 
							[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
 | 
				
			||||||
		public string? Tag { get; init; }
 | 
							public string? Avatar { get; init; }
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public sealed class JsonServer {
 | 
						public sealed class JsonServer {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,8 +68,8 @@ static class ViewerJsonExport {
 | 
				
			|||||||
		await foreach (var user in db.Users.Get(cancellationToken)) {
 | 
							await foreach (var user in db.Users.Get(cancellationToken)) {
 | 
				
			||||||
			users[user.Id] = new ViewerJson.JsonUser {
 | 
								users[user.Id] = new ViewerJson.JsonUser {
 | 
				
			||||||
				Name = user.Name,
 | 
									Name = user.Name,
 | 
				
			||||||
 | 
									DisplayName = user.DisplayName,
 | 
				
			||||||
				Avatar = user.AvatarUrl,
 | 
									Avatar = user.AvatarUrl,
 | 
				
			||||||
				Tag = user.Discriminator
 | 
					 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,6 +29,7 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository {
 | 
				
			|||||||
			await using var cmd = conn.Upsert("users", [
 | 
								await using var cmd = conn.Upsert("users", [
 | 
				
			||||||
				("id", SqliteType.Integer),
 | 
									("id", SqliteType.Integer),
 | 
				
			||||||
				("name", SqliteType.Text),
 | 
									("name", SqliteType.Text),
 | 
				
			||||||
 | 
									("display_name", SqliteType.Text),
 | 
				
			||||||
				("avatar_url", SqliteType.Text),
 | 
									("avatar_url", SqliteType.Text),
 | 
				
			||||||
				("discriminator", SqliteType.Text)
 | 
									("discriminator", SqliteType.Text)
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@@ -38,6 +39,7 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository {
 | 
				
			|||||||
			foreach (var user in users) {
 | 
								foreach (var user in users) {
 | 
				
			||||||
				cmd.Set(":id", user.Id);
 | 
									cmd.Set(":id", user.Id);
 | 
				
			||||||
				cmd.Set(":name", user.Name);
 | 
									cmd.Set(":name", user.Name);
 | 
				
			||||||
 | 
									cmd.Set(":display_name", user.DisplayName);
 | 
				
			||||||
				cmd.Set(":avatar_url", user.AvatarUrl);
 | 
									cmd.Set(":avatar_url", user.AvatarUrl);
 | 
				
			||||||
				cmd.Set(":discriminator", user.Discriminator);
 | 
									cmd.Set(":discriminator", user.Discriminator);
 | 
				
			||||||
				await cmd.ExecuteNonQueryAsync();
 | 
									await cmd.ExecuteNonQueryAsync();
 | 
				
			||||||
@@ -62,15 +64,16 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository {
 | 
				
			|||||||
	public async IAsyncEnumerable<User> Get([EnumeratorCancellation] CancellationToken cancellationToken) {
 | 
						public async IAsyncEnumerable<User> Get([EnumeratorCancellation] CancellationToken cancellationToken) {
 | 
				
			||||||
		await using var conn = await pool.Take();
 | 
							await using var conn = await pool.Take();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		await using var cmd = conn.Command("SELECT id, name, avatar_url, discriminator FROM users");
 | 
							await using var cmd = conn.Command("SELECT id, name, display_name, avatar_url, discriminator FROM users");
 | 
				
			||||||
		await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
 | 
							await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		while (await reader.ReadAsync(cancellationToken)) {
 | 
							while (await reader.ReadAsync(cancellationToken)) {
 | 
				
			||||||
			yield return new User {
 | 
								yield return new User {
 | 
				
			||||||
				Id = reader.GetUint64(0),
 | 
									Id = reader.GetUint64(0),
 | 
				
			||||||
				Name = reader.GetString(1),
 | 
									Name = reader.GetString(1),
 | 
				
			||||||
				AvatarUrl = reader.IsDBNull(2) ? null : reader.GetString(2),
 | 
									DisplayName = reader.IsDBNull(2) ? null : reader.GetString(2),
 | 
				
			||||||
				Discriminator = reader.IsDBNull(3) ? null : reader.GetString(3),
 | 
									AvatarUrl = reader.IsDBNull(3) ? null : reader.GetString(3),
 | 
				
			||||||
 | 
									Discriminator = reader.IsDBNull(4) ? null : reader.GetString(4),
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								app/Server/Database/Sqlite/Schema/SqliteSchemaUpgradeTo8.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/Server/Database/Sqlite/Schema/SqliteSchemaUpgradeTo8.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using DHT.Server.Database.Sqlite.Utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace DHT.Server.Database.Sqlite.Schema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sealed class SqliteSchemaUpgradeTo8 : ISchemaUpgrade {
 | 
				
			||||||
 | 
						async Task ISchemaUpgrade.Run(ISqliteConnection conn, ISchemaUpgradeCallbacks.IProgressReporter reporter) {
 | 
				
			||||||
 | 
							await reporter.MainWork("Applying schema changes...", 0, 1);
 | 
				
			||||||
 | 
							await conn.ExecuteAsync("ALTER TABLE users ADD display_name TEXT");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -8,7 +8,7 @@ using DHT.Utils.Logging;
 | 
				
			|||||||
namespace DHT.Server.Database.Sqlite;
 | 
					namespace DHT.Server.Database.Sqlite;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sealed class SqliteSchema {
 | 
					sealed class SqliteSchema {
 | 
				
			||||||
	internal const int Version = 7;
 | 
						internal const int Version = 8;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static readonly Log Log = Log.ForType<SqliteSchema>();
 | 
						private static readonly Log Log = Log.ForType<SqliteSchema>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -48,6 +48,7 @@ sealed class SqliteSchema {
 | 
				
			|||||||
		                        CREATE TABLE users (
 | 
							                        CREATE TABLE users (
 | 
				
			||||||
		                        	id            INTEGER PRIMARY KEY NOT NULL,
 | 
							                        	id            INTEGER PRIMARY KEY NOT NULL,
 | 
				
			||||||
		                        	name          TEXT NOT NULL,
 | 
							                        	name          TEXT NOT NULL,
 | 
				
			||||||
 | 
							                        	display_name  TEXT,
 | 
				
			||||||
		                        	avatar_url    TEXT,
 | 
							                        	avatar_url    TEXT,
 | 
				
			||||||
		                        	discriminator TEXT
 | 
							                        	discriminator TEXT
 | 
				
			||||||
		                        )
 | 
							                        )
 | 
				
			||||||
@@ -171,6 +172,7 @@ sealed class SqliteSchema {
 | 
				
			|||||||
			{ 4, new SqliteSchemaUpgradeTo5() },
 | 
								{ 4, new SqliteSchemaUpgradeTo5() },
 | 
				
			||||||
			{ 5, new SqliteSchemaUpgradeTo6() },
 | 
								{ 5, new SqliteSchemaUpgradeTo6() },
 | 
				
			||||||
			{ 6, new SqliteSchemaUpgradeTo7() },
 | 
								{ 6, new SqliteSchemaUpgradeTo7() },
 | 
				
			||||||
 | 
								{ 7, new SqliteSchemaUpgradeTo8() },
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		var perf = Log.Start("from version " + dbVersion);
 | 
							var perf = Log.Start("from version " + dbVersion);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,7 @@ sealed class GetTrackingScriptEndpoint(IDatabaseFile db, ServerParameters parame
 | 
				
			|||||||
		string bootstrap = await resources.ReadTextAsync("Tracker/bootstrap.js");
 | 
							string bootstrap = await resources.ReadTextAsync("Tracker/bootstrap.js");
 | 
				
			||||||
		string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + parameters.Port + ";")
 | 
							string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + parameters.Port + ";")
 | 
				
			||||||
		                         .Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(parameters.Token))
 | 
							                         .Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(parameters.Token))
 | 
				
			||||||
		                         .Replace("/*[IMPORTS]*/", await resources.ReadJoinedAsync("Tracker/scripts/", '\n'))
 | 
							                         .Replace("/*[IMPORTS]*/", await resources.ReadJoinedAsync("Tracker/scripts/", '\n', [ "/webpack.js" ]))
 | 
				
			||||||
		                         .Replace("/*[CSS-CONTROLLER]*/", await resources.ReadTextAsync("Tracker/styles/controller.css"))
 | 
							                         .Replace("/*[CSS-CONTROLLER]*/", await resources.ReadTextAsync("Tracker/styles/controller.css"))
 | 
				
			||||||
		                         .Replace("/*[CSS-SETTINGS]*/", await resources.ReadTextAsync("Tracker/styles/settings.css"))
 | 
							                         .Replace("/*[CSS-SETTINGS]*/", await resources.ReadTextAsync("Tracker/styles/settings.css"))
 | 
				
			||||||
		                         .Replace("/*[DEBUGGER]*/", request.Query.ContainsKey("debug") ? "debugger;" : "");
 | 
							                         .Replace("/*[DEBUGGER]*/", request.Query.ContainsKey("debug") ? "debugger;" : "");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,7 @@ sealed class TrackUsersEndpoint(IDatabaseFile db) : BaseEndpoint(db) {
 | 
				
			|||||||
	private static User ReadUser(JsonElement json, string path) => new () {
 | 
						private static User ReadUser(JsonElement json, string path) => new () {
 | 
				
			||||||
		Id = json.RequireSnowflake("id", path),
 | 
							Id = json.RequireSnowflake("id", path),
 | 
				
			||||||
		Name = json.RequireString("name", path),
 | 
							Name = json.RequireString("name", path),
 | 
				
			||||||
 | 
							DisplayName = json.HasKey("displayName") ? json.RequireString("displayName", path) : null,
 | 
				
			||||||
		AvatarUrl = json.HasKey("avatar") ? json.RequireString("avatar", path) : null,
 | 
							AvatarUrl = json.HasKey("avatar") ? json.RequireString("avatar", path) : null,
 | 
				
			||||||
		Discriminator = json.HasKey("discriminator") ? json.RequireString("discriminator", path) : null
 | 
							Discriminator = json.HasKey("discriminator") ? json.RequireString("discriminator", path) : null
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,7 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.IO;
 | 
					using System.IO;
 | 
				
			||||||
 | 
					using System.Linq;
 | 
				
			||||||
using System.Reflection;
 | 
					using System.Reflection;
 | 
				
			||||||
using System.Text;
 | 
					using System.Text;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
@@ -43,15 +45,27 @@ public sealed class ResourceLoader(Assembly assembly) {
 | 
				
			|||||||
		return TryGetEmbeddedStream(filename) is {} stream ? await ReadBytesAsync(stream) : null;
 | 
							return TryGetEmbeddedStream(filename) is {} stream ? await ReadBytesAsync(stream) : null;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async Task<string> ReadJoinedAsync(string path, char separator) {
 | 
						public async Task<string> ReadJoinedAsync(string path, char separator, string[] order) {
 | 
				
			||||||
		StringBuilder joined = new ();
 | 
							List<(string, Stream)> resourceNames = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		foreach (var embeddedName in assembly.GetManifestResourceNames()) {
 | 
							foreach (var embeddedName in assembly.GetManifestResourceNames()) {
 | 
				
			||||||
			if (embeddedName.Replace('\\', '/').StartsWith(path)) {
 | 
								var embeddedNameNormalized = embeddedName.Replace('\\', '/');
 | 
				
			||||||
				joined.Append(await ReadTextAsync(assembly.GetManifestResourceStream(embeddedName)!)).Append(separator);
 | 
								if (embeddedNameNormalized.StartsWith(path)) {
 | 
				
			||||||
 | 
									resourceNames.Add((embeddedNameNormalized, assembly.GetManifestResourceStream(embeddedName)!));
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 | 
							StringBuilder joined = new ();
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							int GetOrderKey(string name) {
 | 
				
			||||||
 | 
								int key = Array.FindIndex(order, name.EndsWith);
 | 
				
			||||||
 | 
								return key == -1 ? order.Length : key;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							foreach(var (_, stream) in resourceNames.OrderBy(item => GetOrderKey(item.Item1))) {
 | 
				
			||||||
 | 
								joined.Append(await ReadTextAsync(stream)).Append(separator);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return joined.ToString(0, Math.Max(0, joined.Length - 1));
 | 
							return joined.ToString(0, Math.Max(0, joined.Length - 1));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,5 +8,5 @@ using DHT.Utils;
 | 
				
			|||||||
namespace DHT.Utils;
 | 
					namespace DHT.Utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static class Version {
 | 
					static class Version {
 | 
				
			||||||
	public const string Tag = "42.0.0.0";
 | 
						public const string Tag = "42.1.0.0";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								app/empty.dht
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/empty.dht
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user