Discord-History-Tracker/app/Resources/Viewer/scripts/state.js

456 lines
10 KiB
JavaScript

// noinspection FunctionWithInconsistentReturnsJS
const STATE = (function() {
/**
* @type {{}}
* @property {{}} users
* @property {String[]} userindex
* @property {{}[]} servers
* @property {{}} channels
*/
let loadedFileMeta;
let loadedFileData;
let loadedMessages;
let filterFunction;
let selectedChannel;
let currentPage;
let messagesPerPage;
const getUser = function(index) {
return loadedFileMeta.users[loadedFileMeta.userindex[index]] || { "name": "<unknown>" };
};
const getUserId = function(index) {
return loadedFileMeta.userindex[index];
};
const getUserList = function() {
return loadedFileMeta ? loadedFileMeta.users : [];
};
const getServer = function(index) {
return loadedFileMeta.servers[index] || { "name": "<unknown>", "type": "unknown" };
};
const generateChannelHierarchy = function() {
/**
* @type {Map<string, Set>}
*/
const hierarchy = new Map();
if (!loadedFileMeta) {
return hierarchy;
}
/**
* @returns {Set}
*/
function getChildren(parentId) {
let children = hierarchy.get(parentId);
if (!children) {
children = new Set();
hierarchy.set(parentId, children);
}
return children;
}
for (const [ id, channel ] of Object.entries(loadedFileMeta.channels)) {
getChildren(channel.parent || "").add(id);
}
const unreachableIds = new Set(hierarchy.keys());
function reachIds(parentId) {
unreachableIds.delete(parentId);
const children = hierarchy.get(parentId);
if (children) {
for (const id of children) {
reachIds(id);
}
}
}
reachIds("");
const rootChildren = getChildren("");
for (const unreachableId of unreachableIds) {
for (const id of hierarchy.get(unreachableId)) {
rootChildren.add(id);
}
hierarchy.delete(unreachableId);
}
return hierarchy;
};
const generateChannelOrder = function() {
if (!loadedFileMeta) {
return {};
}
const channels = loadedFileMeta.channels;
const hierarchy = generateChannelHierarchy();
function getSortedSubTree(parentId) {
const children = hierarchy.get(parentId);
if (!children) {
return [];
}
const sortedChildren = Array.from(children);
sortedChildren.sort((id1, id2) => {
const c1 = channels[id1];
const c2 = channels[id2];
const s1 = getServer(c1.server);
const s2 = getServer(c2.server);
return s1.type.localeCompare(s2.type, "en") ||
s1.name.toLocaleLowerCase().localeCompare(s2.name.toLocaleLowerCase(), undefined, { numeric: true }) ||
(c1.position || -1) - (c2.position || -1) ||
c1.name.toLocaleLowerCase().localeCompare(c2.name.toLocaleLowerCase(), undefined, { numeric: true });
});
const subTree = [];
for (const id of sortedChildren) {
subTree.push(id);
subTree.push(...getSortedSubTree(id));
}
return subTree;
}
const orderArray = getSortedSubTree("");
const orderMap = {};
for (let i = 0; i < orderArray.length; i++) {
orderMap[orderArray[i]] = i;
}
return orderMap;
};
const getChannelList = function() {
if (!loadedFileMeta) {
return [];
}
const channels = loadedFileMeta.channels;
const channelOrder = generateChannelOrder();
return Object.keys(channels).map(key => ({
"id": key,
"name": channels[key].name,
"server": getServer(channels[key].server),
"msgcount": getFilteredMessageKeys(key).length,
"topic": channels[key].topic || "",
"nsfw": channels[key].nsfw || false,
})).sort((ac, bc) => {
return channelOrder[ac.id] - channelOrder[bc.id];
});
};
const getMessages = function(channel) {
return loadedFileData[channel] || {};
};
const getMessageById = function(id) {
for (const messages of Object.values(loadedFileData)) {
if (id in messages) {
return messages[id];
}
}
return null;
};
const getMessageChannel = function(id) {
for (const [ channel, messages ] of Object.entries(loadedFileData)) {
if (id in messages) {
return channel;
}
}
return null;
};
const getMessageList = function() {
if (!loadedMessages) {
return [];
}
const messages = getMessages(selectedChannel);
const startIndex = messagesPerPage * (root.getCurrentPage() - 1);
return loadedMessages.slice(startIndex, !messagesPerPage ? undefined : startIndex + messagesPerPage).map(key => {
/**
* @type {{}}
* @property {Number} u
* @property {Number} t
* @property {String} m
* @property {Number} [te]
* @property {String} [r]
* @property {{}[]} [a]
* @property {String[]} [e]
* @property {{}[]} [re]
*/
const message = messages[key];
const user = getUser(message.u);
const avatar = user.avatar ? { id: getUserId(message.u), path: user.avatar } : null;
const obj = {
user,
avatar,
"timestamp": message.t,
"jump": key,
};
if ("m" in message) {
obj["contents"] = message.m;
}
if ("e" in message) {
obj["embeds"] = message.e.map(embed => JSON.parse(embed));
}
if ("a" in message) {
obj["attachments"] = message.a;
}
if ("te" in message) {
obj["edit"] = message.te;
}
if ("r" in message) {
const replyMessage = getMessageById(message.r);
const replyUser = replyMessage ? getUser(replyMessage.u) : null;
const replyAvatar = replyUser && replyUser.avatar ? { id: getUserId(replyMessage.u), path: replyUser.avatar } : null;
obj["reply"] = replyMessage ? {
"id": message.r,
"user": replyUser,
"avatar": replyAvatar,
"contents": replyMessage.m
} : null;
}
if ("re" in message) {
obj["reactions"] = message.re;
}
return obj;
});
};
let eventOnUsersRefreshed;
let eventOnChannelsRefreshed;
let eventOnMessagesRefreshed;
const triggerUsersRefreshed = function() {
eventOnUsersRefreshed && eventOnUsersRefreshed(getUserList());
};
const triggerChannelsRefreshed = function(selectedChannel) {
eventOnChannelsRefreshed && eventOnChannelsRefreshed(getChannelList(), selectedChannel);
};
const triggerMessagesRefreshed = function() {
eventOnMessagesRefreshed && eventOnMessagesRefreshed(getMessageList());
};
const getFilteredMessageKeys = function(channel) {
const messages = getMessages(channel);
let keys = Object.keys(messages);
if (filterFunction) {
keys = keys.filter(key => filterFunction(messages[key]));
}
return keys;
};
const root = {
onChannelsRefreshed(callback) {
eventOnChannelsRefreshed = callback;
},
onMessagesRefreshed(callback) {
eventOnMessagesRefreshed = callback;
},
onUsersRefreshed(callback) {
eventOnUsersRefreshed = callback;
},
/**
* @param {{ meta, data }} file
*/
uploadFile(file) {
if (loadedFileMeta != null) {
throw "A file is already loaded!";
}
if (!file || typeof file.meta !== "object" || typeof file.data !== "object") {
throw "Invalid file format!";
}
loadedFileMeta = file.meta;
loadedFileData = file.data;
loadedMessages = null;
selectedChannel = null;
currentPage = 1;
triggerUsersRefreshed();
triggerChannelsRefreshed();
triggerMessagesRefreshed();
SETTINGS.onSettingsChanged(() => triggerMessagesRefreshed());
},
getChannelName(channel) {
const channelObj = loadedFileMeta.channels[channel];
return (channelObj && channelObj.name) || channel;
},
getUserTag(user) {
const userObj = loadedFileMeta.users[user];
return (userObj && userObj.tag) || "????";
},
getUserName(user) {
const userObj = loadedFileMeta.users[user];
return (userObj && userObj.name) || user;
},
selectChannel(channel) {
currentPage = 1;
selectedChannel = channel;
loadedMessages = getFilteredMessageKeys(channel).sort(PROCESSOR.SORTER.oldestToNewest);
triggerMessagesRefreshed();
},
setMessagesPerPage(amount) {
messagesPerPage = amount;
triggerMessagesRefreshed();
},
updateCurrentPage(action) {
switch (action) {
case "first":
currentPage = 1;
break;
case "prev":
currentPage = Math.max(1, currentPage - 1);
break;
case "next":
currentPage = Math.min(this.getPageCount(), currentPage + 1);
break;
case "last":
currentPage = this.getPageCount();
break;
case "pick":
const page = parseInt(prompt("Select page:", currentPage), 10);
if (!page && page !== 0) {
return;
}
currentPage = Math.max(1, Math.min(this.getPageCount(), page));
break;
}
triggerMessagesRefreshed();
},
getCurrentPage() {
const total = this.getPageCount();
if (currentPage > total && total > 0) {
currentPage = total;
}
return currentPage || 1;
},
getPageCount() {
return !loadedMessages ? 0 : (!messagesPerPage ? 1 : Math.ceil(loadedMessages.length / messagesPerPage));
},
navigateToMessage(id) {
if (!loadedMessages) {
return -1;
}
const channel = getMessageChannel(id);
if (channel !== null && channel !== selectedChannel) {
triggerChannelsRefreshed(channel);
this.selectChannel(channel);
}
const index = loadedMessages.indexOf(id);
if (index === -1) {
return -1;
}
currentPage = Math.max(1, Math.min(this.getPageCount(), 1 + Math.floor(index / messagesPerPage)));
triggerMessagesRefreshed();
return index % messagesPerPage;
},
setActiveFilter(filter) {
switch (filter ? filter.type : "") {
case "user":
filterFunction = PROCESSOR.FILTER.byUser(loadedFileMeta.userindex.indexOf(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;
break;
}
this.hasActiveFilter = filterFunction != null;
triggerChannelsRefreshed(selectedChannel);
if (selectedChannel) {
this.selectChannel(selectedChannel); // resets current page and updates messages
}
}
};
root.hasActiveFilter = false;
return root;
})();