1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2024-10-17 09:42:44 +02:00
Discord-History-Tracker/bld/viewer.html
2024-03-03 14:16:57 +01:00

171 lines
35 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Discord Offline History</title>
<script type="text/javascript">
var DISCORD=function(){function isImageAttachment(attachment){var ext=attachment.url.lastIndexOf(".");return".png"===(ext=-1===ext?"":attachment.url.substring(ext).toLowerCase())||".gif"===ext||".jpg"===ext||".jpeg"===ext}function getHumanReadableTime(date){return(date=new Date(date)).toLocaleDateString()+", "+date.toLocaleTimeString()}function processMessageContents(animatedEmojiExtension){var escapeHtmlMatch,processed=DOM.escapeHTML(animatedEmojiExtension.replace(REGEX_formatUrlNoEmbed,"$1"));return STATE.settings.enableFormatting&&(escapeHtmlMatch=(full,match)=>"&#"+match.charCodeAt(0)+";",processed=processed.replace(REGEX_specialEscapedBacktick,"&#96;").replace(REGEX_formatCodeBlock,(full,ignore,match)=>"<code class='block'>"+match.replace(REGEX_specialUnescaped,escapeHtmlMatch)+"</code>").replace(REGEX_formatCodeInline,(full,ignore,match)=>"<code class='inline'>"+match.replace(REGEX_specialUnescaped,escapeHtmlMatch)+"</code>").replace(REGEX_specialEscapedSingle,escapeHtmlMatch).replace(REGEX_specialEscapedDouble,full=>full.replace(/\\/g,"").replace(/(.)/g,escapeHtmlMatch)).replace(REGEX_formatBold,"<b>$1</b>").replace(REGEX_formatItalic1,"<i>$1</i>").replace(REGEX_formatItalic2,"<i>$1</i>").replace(REGEX_formatUnderline,"<u>$1</u>").replace(REGEX_formatStrike,"<s>$1</s>")),animatedEmojiExtension=STATE.settings.enableAnimatedEmoji?"gif":"png","<p>"+(processed=processed.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_mentionUser,(full,match)=>"<span class='link mention-user' title='#"+(STATE.getUserTag(match)||"????")+"'>@"+STATE.getUserName(match)+"</span>").replace(REGEX_customEmojiStatic,"<img src='https://cdn.discordapp.com/emojis/$2.png' alt=':$1:' title=':$1:' class='emoji'>").replace(REGEX_customEmojiAnimated,"<img src='https://cdn.discordapp.com/emojis/$2."+animatedEmojiExtension+"' alt=':$1:' title=':$1:' class='emoji'>"))+"</p>"}var templateChannelServer,templateChannelPrivate,templateMessageNoAvatar,templateMessageWithAvatar,templateUserAvatar,templateEmbedImage,templateEmbedRich,templateEmbedRichNoDescription,templateEmbedRichUnsupported,templateEmbedDownload,REGEX_formatBold=/\*\*([\s\S]+?)\*\*(?!\*)/g,REGEX_formatItalic1=/\*([\s\S]+?)\*(?!\*)/g,REGEX_formatItalic2=/_([\s\S]+?)_(?!_)\b/g,REGEX_formatUnderline=/__([\s\S]+?)__(?!_)/g,REGEX_formatStrike=/~~([\s\S]+?)~~(?!~)/g,REGEX_formatCodeInline=/(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/g,REGEX_formatCodeBlock=/```(?:([A-z0-9\-]+?)\n+)?\n*([^]+?)\n*```/g,REGEX_formatUrl=/(\b(?:https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi,REGEX_formatUrlNoEmbed=/<(\b(?:https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])>/gi,REGEX_specialEscapedBacktick=/\\`/g,REGEX_specialEscapedSingle=/\\([*\\])/g,REGEX_specialEscapedDouble=/\\__|_\\_|\\_\\_|\\~~|~\\~|\\~\\~/g,REGEX_specialUnescaped=/([*_~\\])/g,REGEX_mentionUser=/&lt;@!?(\d+?)&gt;/g,REGEX_mentionChannel=/&lt;#(\d+?)&gt;/g,REGEX_customEmojiStatic=/&lt;:([^:]+):(\d+?)&gt;/g,REGEX_customEmojiAnimated=/&lt;a:([^:]+):(\d+?)&gt;/g;return{setup:function(){templateChannelServer=new TEMPLATE(["<div data-channel='{id}'>","<div class='info' title='{topic}'><strong class='name'>#{name}</strong>{nsfw}<span class='tag'>{msgcount}</span></div>","<span class='server'>{server.name} ({server.type})</span>","</div>"].join("")),templateChannelPrivate=new TEMPLATE(["<div data-channel='{id}'>","<div class='info'><strong class='name'>{name}</strong><span class='tag'>{msgcount}</span></div>","<span class='server'>({server.type})</span>","</div>"].join("")),templateMessageNoAvatar=new TEMPLATE(["<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>","<div class='message'>{contents}{embeds}{attachments}</div>","<div class='reactions'>{reactions}</div>","</div>"].join("")),templateMessageWithAvatar=new TEMPLATE(["<div>","<div class='reply-message reply-message-with-avatar'>{reply}</div>","<div class='avatar-wrapper'>","<div class='avatar'>{avatar}</div>","<div>","<h2><strong class='username' title='#{user.tag}'>{user.name}</strong><span class='info time'>{timestamp}</span>{edit}{jump}</h2>","<div class='message'>{contents}{embeds}{attachments}</div>","<div class='reactions'>{reactions}</div>","</div>","</div>","</div>"].join("")),templateUserAvatar=new TEMPLATE(["<img src='https://cdn.discordapp.com/avatars/{id}/{path}.webp?size=128'>"].join("")),templateEmbedImage=new TEMPLATE(["<a href='{url}' class='embed thumbnail'><img src='{url}' alt='(image attachment not found)'></a><br>"].join("")),templateEmbedRich=new TEMPLATE(["<div class='embed download'><a href='{url}' class='title'>{t}</a><p class='desc'>{d}</p></div>"].join("")),templateEmbedRichNoDescription=new TEMPLATE(["<div class='embed download'><a href='{url}' class='title'>{t}</a></div>"].join("")),templateEmbedRichUnsupported=new TEMPLATE(["<div class='embed download'><p>(Formatted embeds are currently not supported)</p></div>"].join("")),templateEmbedDownload=new TEMPLATE(["<a href='{url}' class='embed download'>Download {filename}</a>"].join("")),templateReaction=new TEMPLATE(["<span class='reaction-wrapper'><span class='reaction-emoji'>{n}</span><span class='count'>{c}</span></span>"].join("")),templateReactionCustom=new TEMPLATE(["<span class='reaction-wrapper'><img src='https://cdn.discordapp.com/emojis/{id}.{ext}' alt=':{n}:' title=':{n}:' class='reaction-emoji-custom'><span class='count'>{c}</span></span>"].join(""))},isImageAttachment:isImageAttachment,getChannelHTML:function(channel){return("SERVER"===channel.server.type?templateChannelServer:templateChannelPrivate).apply(channel,(property,value)=>{if("server.type"===property)switch(value){case"SERVER":return"server";case"GROUP":return"group";case"DM":return"user"}else if("nsfw"===property)return value?"<span class='tag'>NSFW</span>":""})},getMessageHTML:function(message){return(STATE.settings.enableUserAvatars?templateMessageWithAvatar:templateMessageNoAvatar).apply(message,(contents,value)=>{if("avatar"===contents)return value?templateUserAvatar.apply(value):"";if("user.tag"===contents)return value||"????";if("timestamp"===contents)return getHumanReadableTime(value);if("contents"===contents)return null==value||0===value.length?"":processMessageContents(value);if("embeds"===contents)return value?value.map(embed=>{switch(embed.type){case"image":return STATE.settings.enableImagePreviews?templateEmbedImage.apply(embed):"";case"rich":return(embed.t?embed.d?templateEmbedRich:templateEmbedRichNoDescription:templateEmbedRichUnsupported).apply(embed)}}).join(""):"";if("attachments"===contents)return value?value.map(attachment=>{if(isImageAttachment(attachment)&&STATE.settings.enableImagePreviews)return templateEmbedImage.apply(attachment);var sliced=attachment.url.split("/");return templateEmbedDownload.apply({url:attachment.url,filename:sliced[sliced.length-1]})}).join(""):"";if("edit"===contents)return value?"<span class='info edited'>Edited"+(1<value?" "+getHumanReadableTime(value):"")+"</span>":"";if("jump"===contents)return STATE.hasActiveFilter?"<span class='info jump' data-jump='"+value+"'>Jump to message</span>":"";if("reply"!==contents)return"reactions"===contents?null===value?"":value.map(reaction=>"id"in reaction?(reaction.ext=reaction.an&&STATE.settings.enableAnimatedEmoji?"gif":"png",templateReactionCustom.apply(reaction)):templateReaction.apply(reaction)).join(""):void 0;if(null===value)return"";var user="<span class='reply-username' title='#"+(value.user.tag||"????")+"'>"+value.user.name+"</span>",avatar=STATE.settings.enableUserAvatars&&value.avatar?"<span class='reply-avatar'>"+templateUserAvatar.apply(value.avatar)+"</span>":"",contents=value.contents?"<span class='reply-contents'>"+processMessageContents(value.contents)+"</span>":"";return"<span class='jump' data-jump='"+value.id+"'>Jump to reply</span><span class='user'>"+avatar+user+"</span>"+contents})}}}(),DOM=function(){var createElement=(ele,parent)=>{ele=document.createElement(ele);return parent.appendChild(ele),ele},entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},entityRegex=/[&<>"']/g;return{id:(id,parent)=>(parent||document).getElementById(id),cls:(cls,parent)=>Array.prototype.slice.call((parent||document).getElementsByClassName(cls)),tag:(tag,parent)=>Array.prototype.slice.call((parent||document).getElementsByTagName(tag)),fcls:(cls,parent)=>(parent||document).getElementsByClassName(cls)[0],createElement:(tag,parent)=>createElement(tag,parent),removeElement:ele=>ele.parentNode.removeChild(ele),escapeHTML:html=>String(html).replace(entityRegex,s=>entityMap[s]),downloadTextFile:(fileName,url)=>{var ele=new Blob([url],{type:"octet/stream"});if("msSaveBlob"in window.navigator)return window.navigator.msSaveBlob(ele,fileName);url=window.URL.createObjectURL(ele),ele=createElement("a",document.body);ele.href=url,ele.download=fileName,ele.style.display="none",ele.click(),document.body.removeChild(ele),window.URL.revokeObjectURL(url)}}}(),EMBED=function(){var html,generated,enabled=!1;return{setup:function(){enabled=!0,html="<!DOCTYPE html>\n"+document.documentElement.outerHTML,DOM.id("btn-upload-file").insertAdjacentHTML("afterend",'<button id="btn-embed-file" disabled>Embed File</button>'),DOM.id("btn-embed-file").addEventListener("click",()=>function(fileName,url){var ele=new Blob([url],{type:"octet/stream"});if("msSaveBlob"in window.navigator)return window.navigator.msSaveBlob(ele,fileName);url=window.URL.createObjectURL(ele),ele=DOM.createElement("a",document.body);ele.href=url,ele.download=fileName,ele.style.display="none",ele.click(),document.body.removeChild(ele),window.URL.revokeObjectURL(url)}("embed.html",generated))},onFileRead:function(str){enabled&&(DOM.id("btn-embed-file").disabled=!1,generated=html.replace("</title>",`</title>\n<script type="text/javascript">window.DHT_EMBEDDED = "${str=str,window.btoa(unescape(encodeURIComponent(str)))}";<\/script>`).replace(`<${document.body.tagName.toLowerCase()}>`,'<body class="embedded">'))},getEmbeddedJSON:function(){var embed=window.DHT_EMBEDDED;return embed?decodeURIComponent(escape(window.atob(embed))):null}}}();document.addEventListener("DOMContentLoaded",()=>{var embedded=EMBED.getEmbeddedJSON();"?embed"!==location.search||embedded||EMBED.setup(),DISCORD.setup(),GUI.setup(),GUI.onOptionMessagesPerPageChanged(()=>{STATE.setMessagesPerPage(GUI.getOptionMessagesPerPage())}),STATE.setMessagesPerPage(GUI.getOptionMessagesPerPage()),GUI.onOptMessageFilterChanged(filter=>{STATE.setActiveFilter(filter)}),GUI.onNavigationButtonClicked(action=>{STATE.updateCurrentPage(action)}),STATE.onUsersRefreshed(users=>{GUI.updateUserList(users)}),STATE.onChannelsRefreshed((channels,selected)=>{GUI.updateChannelList(channels,selected,STATE.selectChannel)}),STATE.onMessagesRefreshed(messages=>{GUI.updateNavigation(STATE.getCurrentPage(),STATE.getPageCount()),GUI.updateMessageList(messages),GUI.scrollMessagesToTop()});function loadJSON(json,errParse,errInvalid){var obj;try{obj=JSON.parse(json),EMBED.onFileRead(json)}catch(e){return console.error(e),void alert(errParse)}SAVEFILE.isValid(obj)?STATE.uploadFile(new SAVEFILE(obj)):alert(errInvalid)}embedded?loadJSON(embedded,"Could not parse embedded file, see console for details.","Embedded file has an invalid format."):GUI.onFileUploaded(files=>{var file,reader;return 1===files.length?(file=files[0],reader=new FileReader,STATE.setUploadedFileName(file.name),reader.onload=()=>loadJSON(reader.result,"Could not parse '"+file.name+"', see console for details.","File '"+file.name+"' has an invalid format."),reader.readAsText(file,"UTF-8")):alert("Please, select only one file."),!0})});var GUI=function(){function getActiveFilter(){var active=DOM.fcls("active",DOM.id("opt-filter-list"));return active&&""!==active.value?{type:active.getAttribute("data-filter-type"),value:active.value}:null}function triggerFilterChanged(){var activeFilter=getActiveFilter();DOM.id("opt-save-filtered").classList.toggle("active",null!=activeFilter),eventOnOptMessageFilterChanged&&eventOnOptMessageFilterChanged(activeFilter)}function showModal(width,html){var dialog=DOM.id("dialog");return dialog.innerHTML=html,dialog.style.width=width+"px",dialog.style.marginLeft=-width/2+"px",DOM.id("modal").classList.add("visible"),dialog}function showSettingsModal(){function setupCheckBox(id,settingName){var ele=DOM.id(id);ele.checked=STATE.settings[settingName],ele.addEventListener("change",()=>STATE.settings[settingName]=ele.checked)}showModal(560,`
<label><input id='dht-cfg-imgpreviews' type='checkbox'> Image Previews</label><br>
<label><input id='dht-cfg-formatting' type='checkbox'> Message Formatting</label><br>
<label><input id='dht-cfg-useravatars' type='checkbox'> User Avatars</label><br>
<label><input id='dht-cfg-animemoji' type='checkbox'> Animated Emoji</label><br>`),setupCheckBox("dht-cfg-imgpreviews","enableImagePreviews"),setupCheckBox("dht-cfg-formatting","enableFormatting"),setupCheckBox("dht-cfg-useravatars","enableUserAvatars"),setupCheckBox("dht-cfg-animemoji","enableAnimatedEmoji")}var eventOnFileUploaded,eventOnOptMessagesPerPageChanged,eventOnOptMessageFilterChanged,eventOnNavButtonClicked;return{setup:function(){function resetActiveFilter(){inputMessageFilter.value="",inputMessageFilter.dispatchEvent(new Event("change")),DOM.id("opt-filter-contents").value="",DOM.id("opt-save-filtered").classList.remove("active")}var inputUploadedFile=DOM.id("uploaded-file"),inputMessageFilter=DOM.id("opt-messages-filter"),containerFilterList=DOM.id("opt-filter-list");DOM.id("btn-upload-file").addEventListener("click",()=>{inputUploadedFile.click()}),inputUploadedFile.addEventListener("change",()=>{eventOnFileUploaded&&eventOnFileUploaded(inputUploadedFile.files)&&(inputUploadedFile.value=null,resetActiveFilter())}),inputMessageFilter.value="",inputMessageFilter.addEventListener("change",()=>{DOM.cls("active",containerFilterList).forEach(ele=>ele.classList.remove("active")),inputMessageFilter.value&&containerFilterList.querySelector("[data-filter-type='"+inputMessageFilter.value+"']").classList.add("active"),triggerFilterChanged()}),Array.prototype.forEach.call(containerFilterList.children,ele=>{ele.addEventListener("SELECT"===ele.tagName?"change":"input",e=>triggerFilterChanged())}),DOM.id("opt-messages-per-page").addEventListener("change",()=>{eventOnOptMessagesPerPageChanged&&eventOnOptMessagesPerPageChanged()}),DOM.id("btn-save-filtered").addEventListener("click",()=>{confirm("Filtering only removes messages, all users and servers will remain in the new archive. Continue?")&&STATE.saveFilteredMessages()}),DOM.tag("button",DOM.fcls("nav")).forEach(button=>{button.disabled=!0,button.addEventListener("click",()=>{eventOnNavButtonClicked&&eventOnNavButtonClicked(button.getAttribute("data-nav"))})}),DOM.id("btn-settings").addEventListener("click",()=>{showSettingsModal()}),DOM.id("btn-about").addEventListener("click",()=>{var linkGH;showModal(560,`
<p>Discord History Tracker is developed by <a href='https://chylex.com'>chylex</a> as an <a href='${linkGH="https://github.com/chylex/Discord-History-Tracker"}/blob/master/LICENSE.md'>open source</a> project.</p>
<sub>v.31h, released 03 March 2024</sub>
<p>Please, report any issues and suggestions to the <a href='${linkGH}/issues'>tracker</a>. If you want to support the development, please spread the word and consider <a href='https://www.patreon.com/chylex'>becoming a patron</a> or <a href='https://ko-fi.com/chylex'>buying me a coffee</a>. Any support is appreciated!</p>
<p><a href='${linkGH}/issues'>Issue Tracker</a> &nbsp;&mdash;&nbsp; <a href='${linkGH}'>GitHub Repository</a> &nbsp;&mdash;&nbsp; <a href='https://twitter.com/chylexmc'>Developer's Twitter</a></p>`)}),DOM.id("messages").addEventListener("click",index=>{index=index.target.getAttribute("data-jump");index&&(resetActiveFilter(),index=STATE.navigateToMessage(index),DOM.id("messages").children[index].scrollIntoView())}),DOM.id("overlay").addEventListener("click",()=>{DOM.id("modal").classList.remove("visible"),DOM.id("dialog").innerHTML=""})},onFileUploaded:function(callback){eventOnFileUploaded=callback},onOptionMessagesPerPageChanged:function(callback){eventOnOptMessagesPerPageChanged=callback},onOptMessageFilterChanged:function(callback){eventOnOptMessageFilterChanged=callback},onNavigationButtonClicked:function(callback){eventOnNavButtonClicked=callback},getOptionMessagesPerPage:function(){return parseInt(DOM.id("opt-messages-per-page").value,10)},updateNavigation:function(currentPage,totalPages){DOM.id("nav-page-current").innerHTML=currentPage,DOM.id("nav-page-total").innerHTML=totalPages||"?",DOM.id("nav-first").disabled=1===currentPage,DOM.id("nav-prev").disabled=1===currentPage,DOM.id("nav-pick").disabled=(totalPages||0)<=1,DOM.id("nav-next").disabled=currentPage===(totalPages||1),DOM.id("nav-last").disabled=currentPage===(totalPages||1)},updateChannelList:function(channels,activeChannel,callback){var eleChannels=DOM.id("channels");channels?(null!=getActiveFilter()&&(channels=channels.filter(channel=>0<channel.msgcount)),eleChannels.innerHTML=channels.map(channel=>DISCORD.getChannelHTML(channel)).join(""),Array.prototype.forEach.call(eleChannels.children,ele=>{ele.addEventListener("click",e=>{var currentChannel=DOM.fcls("active",eleChannels);currentChannel&&currentChannel.classList.remove("active"),ele.classList.add("active"),callback(ele.getAttribute("data-channel"))})}),!activeChannel||(activeChannel=eleChannels.querySelector("[data-channel='"+activeChannel+"']"))&&activeChannel.classList.add("active")):eleChannels.innerHTML=""},updateMessageList:function(messages){DOM.id("messages").innerHTML=messages?messages.map(message=>DISCORD.getMessageHTML(message)).join(""):""},updateUserList:function(users){for(var eleSelect=DOM.id("opt-filter-user");1<eleSelect.length;)eleSelect.remove(1);var key,options=[];for(key of Object.keys(users)){var option=document.createElement("option");option.value=key,option.text=users[key].name,options.push(option)}options.sort((a,b)=>a.text.toLocaleLowerCase().localeCompare(b.text.toLocaleLowerCase())),options.forEach(option=>eleSelect.add(option))},scrollMessagesToTop:function(){DOM.id("messages").scrollTop=0}}}(),PROCESSOR={FILTER:{byUser:userindex=>message=>message.u===userindex,byTime:(timeStart,timeEnd)=>message=>message.t>=timeStart&&message.t<=timeEnd,byContents:substr=>message=>-1!==("m"in message?message.m:"").indexOf(substr),byRegex:regex=>message=>regex.test("m"in message?message.m:""),withImages:()=>message=>message.e&&message.e.some(embed=>"image"===embed.type)||message.a&&message.a.some(DISCORD.isImageAttachment),withDownloads:()=>message=>message.a&&message.a.some(attachment=>!DISCORD.isImageAttachment(attachment)),withEmbeds:()=>message=>message.e&&0<message.e.length,withAttachments:()=>message=>message.a&&0<message.a.length,isEdited:()=>message=>"te"in message?message.te:1==(1&message.f)},SORTER:{oldestToNewest:(key1,key2)=>key1.length===key2.length?key2<key1?1:key1<key2?-1:0:key1.length>key2.length?1:-1,newestToOldest:(key1,key2)=>key1.length===key2.length?key2<key1?-1:key1<key2?1:0:key1.length>key2.length?-1:1}};class SAVEFILE{constructor(parsedObj){var me=this;me.meta=parsedObj.meta,me.data=parsedObj.data,me.meta.users=me.meta.users||{},me.meta.userindex=me.meta.userindex||[],me.meta.servers=me.meta.servers||[],me.meta.channels=me.meta.channels||{}}static isValid(parsedObj){return parsedObj&&"object"==typeof parsedObj.meta&&"object"==typeof parsedObj.data}getServer(index){return this.meta.servers[index]||{name:"&lt;unknown&gt;",type:"ERROR"}}getChannels(){return this.meta.channels}getChannelById(channel){return this.meta.channels[channel]||{id:channel,name:channel}}getUsers(){return this.meta.users}getUser(index){return this.meta.users[this.meta.userindex[index]]||{name:"&lt;unknown&gt;"}}getUserId(index){return this.meta.userindex[index]}getUserById(user){return this.meta.users[user]||{name:user}}getUserIndex(user){return this.meta.userindex.indexOf(user)}getMessages(channel){return this.data[channel]||{}}filterToJson(filterFunction){var channel,newMeta=JSON.parse(JSON.stringify(this.meta)),newData={};for(channel of Object.keys(this.getChannels())){var key,messages=this.getMessages(channel),retained={};for(key of Object.keys(messages)){var message=messages[key];filterFunction(message)&&(retained[key]=message)}0<Object.keys(retained).length?newData[channel]=retained:delete newMeta.channels[channel]}return JSON.stringify({meta:newMeta,data:newData})}}var STATE=function(){function triggerChannelsRefreshed(selectedChannel){eventOnChannelsRefreshed&&eventOnChannelsRefreshed(ROOT.getChannelList(),selectedChannel)}function triggerMessagesRefreshed(){eventOnMessagesRefreshed&&eventOnMessagesRefreshed(ROOT.getMessageList())}var FILE,MSGS,uploadedFileName,filterFunction,selectedChannel,currentPage,messagesPerPage,eventOnChannelsRefreshed,eventOnMessagesRefreshed,eventOnUsersRefreshed,ROOT={};ROOT.onChannelsRefreshed=function(callback){eventOnChannelsRefreshed=callback},ROOT.onMessagesRefreshed=function(callback){eventOnMessagesRefreshed=callback},ROOT.onUsersRefreshed=function(callback){eventOnUsersRefreshed=callback},ROOT.uploadFile=function(file){FILE=file,selectedChannel=MSGS=null,currentPage=1,eventOnUsersRefreshed&&eventOnUsersRefreshed(ROOT.getUserList()),triggerChannelsRefreshed(),triggerMessagesRefreshed()},ROOT.setUploadedFileName=function(name){uploadedFileName=name},ROOT.getChannelName=function(channel){return FILE.getChannelById(channel).name},ROOT.getUserName=function(user){return FILE.getUserById(user).name},ROOT.getUserTag=function(user){return FILE.getUserById(user).tag};function getFilteredMessageKeys(keys){var messages=FILE.getMessages(keys),keys=Object.keys(messages);return keys=filterFunction?keys.filter(key=>filterFunction(messages[key])):keys}ROOT.getChannelList=function(){if(!FILE)return[];var channels=FILE.getChannels();return Object.keys(channels).map(key=>({id:key,name:channels[key].name,server:FILE.getServer(channels[key].server),msgcount:getFilteredMessageKeys(key).length,topic:channels[key].topic||"",nsfw:channels[key].nsfw||!1,position:channels[key].position||-1})).sort((ac,bc)=>{var as=ac.server,bs=bc.server;return as.type.localeCompare(bs.type,"en")||as.name.toLocaleLowerCase().localeCompare(bs.name.toLocaleLowerCase(),void 0,{numeric:!0})||ac.position-bc.position||ac.name.toLocaleLowerCase().localeCompare(bc.name.toLocaleLowerCase(),void 0,{numeric:!0})})},ROOT.selectChannel=function(channel){currentPage=1,MSGS=getFilteredMessageKeys(selectedChannel=channel).sort(PROCESSOR.SORTER.oldestToNewest),triggerMessagesRefreshed()},ROOT.getSelectedChannel=function(){return selectedChannel},ROOT.getMessageList=function(){if(!MSGS)return[];var messages=FILE.getMessages(selectedChannel),startIndex=messagesPerPage*(ROOT.getCurrentPage()-1);return MSGS.slice(startIndex,messagesPerPage?startIndex+messagesPerPage:void 0).map(key=>{var message=messages[key],user=FILE.getUser(message.u),avatar=user.avatar?{id:FILE.getUserId(message.u),path:user.avatar}:null,replyObj="r"in message&&message.r in messages?messages[message.r]:null,replyUser=replyObj?FILE.getUser(replyObj.u):null,replyAvatar=replyUser&&replyUser.avatar?{id:FILE.getUserId(replyObj.u),path:replyUser.avatar}:null,replyObj=replyObj?{id:message.r,user:replyUser,avatar:replyAvatar,contents:replyObj.m}:null;return{user:user,avatar:avatar,timestamp:message.t,contents:"m"in message?message.m:null,embeds:message.e,attachments:message.a,edit:"te"in message?message.te:1==(1&message.f),jump:key,reply:replyObj,reactions:"re"in message?message.re:null}})},ROOT.navigateToMessage=function(index){if(!MSGS)return 0;index=MSGS.indexOf(index);return-1==index?0:(currentPage=Math.max(1,Math.min(ROOT.getPageCount(),1+Math.floor(index/messagesPerPage))),triggerMessagesRefreshed(),index%messagesPerPage)},ROOT.hasActiveFilter=!1,ROOT.setActiveFilter=function(filter){switch(filter?filter.type:""){case"user":filterFunction=PROCESSOR.FILTER.byUser(FILE.getUserIndex(filter.value));break;case"contents":filterFunction=PROCESSOR.FILTER.byContents(filter.value);break;case"withimages":filterFunction=PROCESSOR.FILTER.withImages();break;case"withdownloads":filterFunction=PROCESSOR.FILTER.withDownloads();break;case"edited":filterFunction=PROCESSOR.FILTER.isEdited();break;default:filterFunction=null}ROOT.hasActiveFilter=null!=filterFunction,triggerChannelsRefreshed(selectedChannel),selectedChannel&&ROOT.selectChannel(selectedChannel)},ROOT.saveFilteredMessages=function(){var saveFileName="dht-filtered.txt";uploadedFileName&&(saveFileName=uploadedFileName.includes("filtered")?uploadedFileName:uploadedFileName.replace(".","-filtered.")),DOM.downloadTextFile(saveFileName,FILE.filterToJson(filterFunction))},ROOT.getUserList=function(){return FILE?FILE.getUsers():[]},ROOT.setMessagesPerPage=function(amount){messagesPerPage=amount,triggerMessagesRefreshed()},ROOT.updateCurrentPage=function(action){switch(action){case"first":currentPage=1;break;case"prev":currentPage=Math.max(1,currentPage-1);break;case"next":currentPage=Math.min(ROOT.getPageCount(),currentPage+1);break;case"last":currentPage=ROOT.getPageCount();break;case"pick":var page=parseInt(prompt("Select page:",currentPage),10);if(!page&&0!==page)return;currentPage=Math.max(1,Math.min(ROOT.getPageCount(),page))}triggerMessagesRefreshed()},ROOT.getCurrentPage=function(){var total=ROOT.getPageCount();return(currentPage=total<currentPage&&0<total?total:currentPage)||1},ROOT.getPageCount=function(){return MSGS?messagesPerPage?Math.ceil(MSGS.length/messagesPerPage):1:0},ROOT.settings={};var defineSettingProperty=(property,defaultValue,storageToValue)=>{var name="_"+property;Object.defineProperty(ROOT.settings,property,{get:()=>ROOT.settings[name],set:value=>{ROOT.settings[name]=value,triggerMessagesRefreshed(),((property,value)=>{try{localStorage.setItem(property,value)}catch(e){console.error(e)}})(property,value)}});var stored=(property=>{try{return localStorage.getItem(property)}catch(e){return console.error(e),null}})(property);null!==stored&&(stored=storageToValue(stored)),ROOT.settings[name]=null===stored?defaultValue:stored},fromBooleanString=value=>"true"===value||"false"!==value&&null;return defineSettingProperty("enableImagePreviews",!0,fromBooleanString),defineSettingProperty("enableFormatting",!0,fromBooleanString),defineSettingProperty("enableUserAvatars",!0,fromBooleanString),defineSettingProperty("enableAnimatedEmoji",!0,fromBooleanString),ROOT}(),TEMPLATE_REGEX=/{([^{}]+?)}/g;class TEMPLATE{constructor(contents){this.contents=contents}apply(obj,processor){return this.contents.replace(TEMPLATE_REGEX,(full,updated)=>{var value=updated.split(".").reduce((o,property)=>o[property],obj);if(processor){updated=processor(updated,value);return void 0===updated?DOM.escapeHTML(value):updated}return DOM.escapeHTML(value)})}} </script>
<style type="text/css">
#channels {width:15vw;min-width:215px;max-width:300px;overflow-y:auto;background-color:#1C1E22}
#channels > div {cursor:pointer;padding:10px 12px;color:#eee;font-size:15px;border-bottom:1px solid #333333}
#channels > div:hover, #channels > div.active {background-color:#282B30}
#channels .info {display:flex;height:16px;margin-bottom:4px}
#channels .name {flex-grow:1;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}
#channels .tag {flex-shrink:1;background-color:rgba(255,255,255,0.08);border-radius:4px;margin-left:4px;margin-top:1px;padding:2px 5px;font-size:11px}
body {font-family:Whitney, "Helvetica Neue", Helvetica, Verdana, "Lucida Grande", sans-serif;line-height:1;margin:0;padding:0;overflow:hidden}
body.embedded .hide-embedded {display:none}
#menu {width:100%;height:48px;display:flex;flex-direction:row}
#app {height:calc(100vh - 48px);display:flex;flex-direction:row}
#menu {background-color:#17181C;border-bottom:1px dotted #5D626B}
#menu .splitter {width:1px;margin:9px 4px;background-color:#5D626B}
#menu .separator {flex:1 1 0}
#menu :disabled {background-color:#555;cursor:default}
#menu button, #menu select, #menu input[type="text"] {margin:8px;background-color:#7289DA;color:#FFF;text-shadow:1px 1px 2px rgba(0,0,0,0.75)}
#menu button {font-size:17px;padding:0 12px;border:0;cursor:pointer}
#menu select {font-size:14px;padding:6px;border:0;cursor:pointer}
#menu input[type="text"] {font-size:14px;padding:7px 12px;border:0}
#menu .nav {display:flex;flex-direction:row;margin:0 8px}
#menu .nav > button {font-size:14px}
#menu .nav > button.icon {font-family:Lucida Console, monospace;font-size:17px;padding:0 8px}
#menu .nav > button, #menu .nav > p {margin:8px 1px}
#opt-filter-list > select, #opt-filter-list > input {display:none}
#opt-filter-list > .active {display:block}
#opt-save-filtered {display:flex}
#opt-save-filtered:not(.active) {display:none}
#messages {flex:1 1 0;overflow-y:auto;background-color:#36393E}
#messages > div {margin:0 24px;padding:4px 0 12px;border-bottom:1px solid rgba(255,255,255,0.04)}
#messages h2 {margin:0;padding:0;display:block}
#messages .avatar-wrapper {display:flex;flex-direction:row;align-items:flex-start;align-content:flex-start}
#messages .avatar-wrapper > div {flex:1 1 auto}
#messages .avatar {flex:0 0 38px!important;margin:8px 14px 0 0}
#messages .avatar img {width:38px;border-radius:50%}
#messages .username {color:#FFF;font-size:15px;font-weight:600;margin-right:3px;letter-spacing:0}
#messages .info {color:rgba(255,255,255,0.4);font-size:12px;font-weight:500;letter-spacing:0}
#messages .info::before {content:"\2022";text-align:center;display:inline-block;width:14px}
#messages .jump {cursor:pointer;text-decoration:underline;text-underline-offset:2px}
.message {margin-top:6px;color:rgba(255,255,255,0.7);font-size:15px;line-height:1.1em;white-space:pre-wrap;word-wrap:break-word}
.message .link, .reply-message .link {color:#7289DA;background-color:rgba(115,139,215,0.1)}
.message a, .reply-message a {color:#0096CF;text-decoration:none}
.message a:hover {text-decoration:underline}
.message p {margin:0}
.message .embed {display:inline-block;margin-top:8px}
.message .embed .title {font-weight:bold;display:inline-block}
.message .embed .desc {margin-top:4px}
.message .thumbnail {max-width:calc(100% - 20px);max-height:320px}
.message .thumbnail img {max-width:100%;max-height:320px;border-radius:3px}
.message .download {margin-right:8px;padding:8px 9px;border:1px solid rgba(255,255,255,0.5);border-radius:3px}
.message .embed:first-child, .message .download + .download {margin-top:0}
.message code {background-color:#2E3136;border-radius:5px;font-family:Menlo, Consolas, Monaco, monospace;font-size:14px}
.message code.inline {display:inline;padding:2px}
.message code.block {display:block;border:2px solid #282B30;margin-top:6px;padding:7px}
.message .emoji {width:22px;height:22px;margin:0 1px;vertical-align:-30%;object-fit:contain}
.reply-message {display:flex;align-items:baseline;flex-wrap:wrap;line-height:120%;white-space:nowrap}
.reply-message-with-avatar {margin:0 0 -2px 52px}
.reply-message .jump {color:rgba(255,255,255,0.4);font-size:12px;text-underline-offset:1px;margin-right:7px}
.reply-message .emoji {width:16px;height:16px;vertical-align:-20%;object-fit:contain}
.reply-message .user {margin-right:5px}
.reply-avatar {margin-right:4px}
.reply-avatar img {width:16px;border-radius:50%;vertical-align:middle}
.reply-username {color:#FFF;font-size:12px;font-weight:600;letter-spacing:0}
.reply-contents {display:inline-block;color:rgba(255,255,255,0.7);font-size:12px;max-width:calc(80%)}
.reply-contents p {margin:0;overflow:hidden;text-overflow:ellipsis}
.reply-contents code {background-color:#2E3136;font-family:Menlo, Consolas, Monaco, monospace;padding:1px 2px}
.reactions {margin-top:4px}
.reactions .reaction-wrapper {display:inline-block;border-radius:4px;margin:3px 2px 0 0;padding:3px 6px;background:#42454a;cursor:default}
.reactions .reaction-emoji {margin-right:5px;font-size:16px;display:inline-block;text-align:center;vertical-align:-5%}
.reactions .reaction-emoji-custom {height:15px;margin-right:5px;vertical-align:-10%}
.reactions .count {color:rgba(255,255,255,0.45);font-size:14px}
#modal div {position:absolute;display:none}
#modal.visible div {display:block}
#modal #overlay {left:0;top:0;width:100%;height:100%;background-color:#000}
#modal.visible #overlay {opacity:0.5}
#dialog {left:50%;top:50%;padding:16px;background-color:#fff;transform:translateY(-50%)}
#dialog p {line-height:1.2}
#dialog p:first-child, #dialog p:last-child {margin-top:1px;margin-bottom:1px}
#dialog sub {color:#999;font-size:12px}
#dialog a {color:#0096CF;text-decoration:none}
#dialog a:hover {text-decoration:underline}
</style>
</head>
<body>
<div id="menu">
<input id="uploaded-file" type="file" style="display:none">
<button id="btn-upload-file" class="hide-embedded">Load File</button>
<div class="splitter hide-embedded"></div>
<button id="btn-settings">Settings</button>
<div> <!-- needed to stop the select from messing up -->
<select id="opt-messages-per-page">
<option value="50">50 messages per page&nbsp;</option>
<option value="100">100 messages per page&nbsp;</option>
<option value="250">250 messages per page&nbsp;</option>
<option value="500">500 messages per page&nbsp;</option>
<option value="1000">1000 messages per page&nbsp;</option>
<option value="0">All messages&nbsp;</option>
</select>
</div>
<div class="nav">
<button id="nav-first" data-nav="first" class="icon">&laquo;</button>
<button id="nav-prev" data-nav="prev" class="icon">&lsaquo;</button>
<button id="nav-pick" data-nav="pick">Page <span id="nav-page-current">1</span>/<span id="nav-page-total">?</span></button>
<button id="nav-next" data-nav="next" class="icon">&rsaquo;</button>
<button id="nav-last" data-nav="last" class="icon">&raquo;</button>
</div>
<div class="splitter"></div>
<div> <!-- needed to stop the select from messing up -->
<select id="opt-messages-filter">
<option value="">No filter&nbsp;</option>
<option value="user">Filter messages by user&nbsp;</option>
<option value="contents">Filter messages by contents&nbsp;</option>
<option value="withimages">Only messages with images&nbsp;</option>
<option value="withdownloads">Only messages with downloads&nbsp;</option>
<option value="edited">Only edited messages&nbsp;</option>
</select>
</div>
<div id="opt-filter-list">
<select id="opt-filter-user" data-filter-type="user">
<option value="">Select user...</option>
</select>
<input id="opt-filter-contents" type="text" data-filter-type="contents" placeholder="Messages containing...">
<input type="hidden" data-filter-type="withimages" value="1">
<input type="hidden" data-filter-type="withdownloads" value="1">
<input type="hidden" data-filter-type="edited" value="1">
</div>
<div id="opt-save-filtered">
<div class="splitter"></div>
<button id="btn-save-filtered">Save Filtered Messages</button>
</div>
<div class="separator"></div>
<button id="btn-about">About</button>
</div>
<div id="app">
<div id="channels"></div>
<div id="messages"></div>
</div>
<div id="modal">
<div id="overlay"></div>
<div id="dialog"></div>
</div>
</body>
</html>