(function runTwitchNotes() { function save(filename, data) { const blob = new Blob([data], {type: 'text/json'}); const elem = window.document.createElement('a'); elem.href = window.URL.createObjectURL(blob); elem.download = filename; document.body.appendChild(elem); elem.click(); document.body.removeChild(elem); } const Notes = { inputData: {}, getSavedUserList: () => { return JSON.parse(localStorage.getItem(LS_UserList) || "[]"); }, addUserToSavedList: (username) => { let userList = Notes.getSavedUserList(); if (!userList.includes(username)) { userList.push(username); localStorage.setItem(LS_UserList, JSON.stringify(userList)); } }, removeUserFromSavedList: (username) => { let userList = Notes.getSavedUserList(); userList = userList.filter(u => u !== username); if (userList.length) { localStorage.setItem(LS_UserList, JSON.stringify(userList)); } else { localStorage.removeItem(LS_UserList); } }, saveNote: (username, note) => { Notes.addUserToSavedList(username); localStorage.setItem(LS_Prefix + username, note); }, deleteNote: (username) => { Notes.removeUserFromSavedList(username); localStorage.removeItem(LS_Prefix + username); }, getNote: (username) => { return localStorage.getItem(LS_Prefix + username) || ""; }, exportNotes: () => { save('twitch-notes-' + (Date.now()) + '.json', JSON.stringify({ users: Notes.getSavedUserList(), notes: Notes.getSavedUserList().map(user => { return {user: user, note: Notes.getNote(user)}; }), settings: {}, })); }, showAllNotes: () => { let activeNote = null; const blurredBackground = createElement('div', null, 'twitch-notes-blurred-background'); const container = createElement('div', null, 'twitch-notes-center-floating-container', { padding: '0', paddingTop: '1rem', width: '100%', height: 'calc(320px + 5rem)' }); const closeButton = createElement('button', xButton, 'twitch-notes-settings-close-button', { position: 'absolute', top: '1rem', right: '1rem', }); const title = createElement('div', 'Notes:', 'twitch-note-title', { borderBottom: '1px solid #FFFFFF19', paddingBottom: '1rem', }); container.appendChild(title); const content = createElement('div', null, null, { display: 'flex', flexDirection: 'row', flexWrap: 'no-wrap', width: '100%', height: '320px' }); container.appendChild(content); closeButton.addEventListener('click', () => { blurredBackground.remove(); }); container.appendChild(closeButton); const userList = createElement('div', null, null, { borderRight: '1px solid #FFFFFF19', height: '320px', minWidth: '240px', overflowY: 'auto', overflowX: 'hidden', }); const noteContainer = createElement('div', null, null, { height: '320px', padding: '1rem', width: '100%', opacity: '0', }); const note = createElement('div', null, null, { marginBottom: '3rem', border: '1px solid #FFFFFF19', height: 'calc(320px - 7rem)', width: '100%', padding: '6px', outline: 'none', overflowY: 'auto', overflowX: 'hidden', }, { contentEditable: 'true', }); const saveNote = createElement('button', 'Save Note', 'twitch-notes-settings-button', { position: 'absolute', bottom: '1rem', left: '1rem' }); saveNote.addEventListener('click', () => { if(!activeNote) { return; } Notes.saveNote(activeNote, note.innerHTML); }) for(let user of Notes.getSavedUserList()) { const userLine = createElement('div', user, 'twitch-notes-user-list-user'); userLine.addEventListener('click', () => { const list = document.getElementsByClassName('twitch-notes-user-list-user'); for(let i = 0; i < list.length; i++) { list.item(i).removeAttribute('data-active'); } userLine.setAttribute('data-active', 'true'); noteContainer.style.opacity = '1'; note.innerHTML = Notes.getNote(user); activeNote = user; }); userList.appendChild(userLine); } noteContainer.appendChild(note); noteContainer.appendChild(saveNote); content.appendChild(userList); content.appendChild(noteContainer); blurredBackground.appendChild(container); document.body.appendChild(blurredBackground); }, importNotesConflictResolve: (user, resultsContainer, container, blurredBackground) => { const diffBlurredBackground = createElement('div', null, 'twitch-notes-blurred-background'); const diffContainer = createElement('div', 'Select which note to keep for ' + user + '
You can modify notes to merge them and keep updated one

', 'twitch-notes-center-floating-container'); const inputContainer = createElement('div', null, null, { display: 'flex', flexDirection: 'row', flexWrap: 'no-wrap', width: '100%', }); const valueStyle = { minWidth: '320px', padding: '6px', border: '1px solid #FFFFFF19', outline: 'none', }; const valueAttr = {contentEditable: 'true'}; const localValueContainer = createElement('div'); const pLocal = createElement('p', 'Locally saved note'); localValueContainer.appendChild(pLocal); const localValue = createElement('div', Notes.getNote(user), null, valueStyle, valueAttr, 'local-value-container'); const localValueSave = createElement('button', 'Save this', 'twitch-notes-settings-button'); localValueSave.addEventListener('click', () => { const val = document.getElementById('local-value-container').innerHTML; Notes.inputData.notes = Notes.inputData.notes.map(n => n.user === user ? { user: n.user, note: val, resolved: true } : n); diffBlurredBackground.remove(); resultsContainer.remove(); Notes.importNotesResult(container, blurredBackground); }); localValueContainer.appendChild(localValue); localValueContainer.innerHTML += '
'; localValueContainer.appendChild(localValueSave); const importedValueContainer = createElement('div'); const iLocal = createElement('p', 'Imported note'); importedValueContainer.appendChild(iLocal); const importedValue = createElement('div', Notes.inputData.notes.filter(n => n.user === user)[0].note || '', null, valueStyle, valueAttr, 'import-value-container'); const importedValueSave = createElement('button', 'Save this', 'twitch-notes-settings-button'); importedValueSave.addEventListener('click', () => { const val = document.getElementById('import-value-container').innerHTML; Notes.inputData.notes = Notes.inputData.notes.map(n => n.user === user ? { user: n.user, note: val, resolved: true } : n); diffBlurredBackground.remove(); resultsContainer.remove(); Notes.importNotesResult(container, blurredBackground); }); importedValueContainer.appendChild(importedValue); importedValueContainer.innerHTML += '
'; importedValueContainer.appendChild(importedValueSave); inputContainer.appendChild(localValueContainer); inputContainer.appendChild(importedValueContainer); diffContainer.appendChild(inputContainer); diffBlurredBackground.appendChild(diffContainer); document.body.appendChild(diffBlurredBackground); }, importNotesResult: (container, blurredBackground) => { const willBeAdded = []; let willBeOverwritten = []; const noChanges = []; for (let user of Notes.inputData.users) { if (Notes.getNote(user)) { if (Notes.inputData.notes.find(note => note.user === user).resolved || Notes.getNote(user) !== Notes.inputData.notes.find(note => note.user === user).note) { willBeOverwritten.push(user); } else { noChanges.push(user); } } else { willBeAdded.push(user); } } const resultsContainer = createElement('div', null); if (willBeOverwritten.length) { const overwrittenContainer = createElement('div', 'Note conflicts found for:
', null, { marginTop: '1rem', }); for (let item of willBeOverwritten) { const row = createElement('div', item + ' ', null, { color: Notes.inputData.notes.find(note => note.user === item).resolved ? 'green' : 'red', }); const resolve = createElement('a', Notes.inputData.notes.find(note => note.user === item).resolved ? '[update]' : '[resolve]', null, { cursor: 'pointer', }); resolve.addEventListener('click', () => { Notes.importNotesConflictResolve(item, resultsContainer, container, blurredBackground); }) row.appendChild(resolve); overwrittenContainer.appendChild(row); } resultsContainer.appendChild(overwrittenContainer); } if (willBeAdded.length) { const addedContainer = createElement('div', 'Note will be added for:
' + willBeAdded.join('
'), null, { marginTop: '1rem', color: 'white' }) resultsContainer.appendChild(addedContainer); } if (noChanges.length) { const noChangesContainer = createElement('div', 'No changes for:
' + noChanges.join('
'), null, { marginTop: '1rem', color: 'gray' }) resultsContainer.appendChild(noChangesContainer); } const spacer = createElement('div', null, null, {height: '24px'}); resultsContainer.appendChild(spacer); willBeOverwritten = willBeOverwritten.filter(x => !Notes.inputData.notes.find(n => n.user === x).resolved); const saveButton = createElement('button', 'Complete import', 'twitch-notes-settings-button', willBeOverwritten.length > 0 ? {background: 'gray'} : null, willBeOverwritten.length > 0 ? {disabled: 'true'} : null) resultsContainer.appendChild(saveButton) if (willBeOverwritten.length > 0) { const note = createElement('div', 'Import cannot be completed while there are unresolved conflicts', null, { color: 'gray', }); resultsContainer.appendChild(note); } else { saveButton.addEventListener('click', () => { for (let note of Notes.inputData.notes) { Notes.saveNote(note.user, note.note); } blurredBackground.remove(); Notes.inputData = {}; }); } container.appendChild(resultsContainer); }, importNotes: () => { const blurredBackground = createElement('div', null, 'twitch-notes-blurred-background'); const container = createElement('div', 'WARNING! This action might override existing data!

', 'twitch-notes-center-floating-container'); const inputBlock = createElement('div', 'Select exported twitch notes file

'); const input = createElement('input', null, 'twitch-notes-file-input', null, { type: 'file', accept: '.json' }); const settingsCloseButton = createElement('button', xButton, 'twitch-notes-settings-close-button', { position: 'absolute', top: '1rem', right: '1rem', }); inputBlock.appendChild(input); settingsCloseButton.addEventListener('click', () => { blurredBackground.remove(); }); input.addEventListener('change', () => { let file = input.files[0]; if (!file) { return; } let reader = new FileReader(); reader.readAsText(file, "UTF-8"); reader.onload = function (evt) { Notes.inputData = JSON.parse(evt.target.result.toString()); if (Notes.inputData) { inputBlock.style.display = 'none'; Notes.importNotesResult(container, blurredBackground); } } reader.onerror = function () { console.error("error reading file"); } }); container.appendChild(settingsCloseButton); container.appendChild(inputBlock); blurredBackground.appendChild(container); document.body.appendChild(blurredBackground); }, clearAllData: () => { const blurredBackground = createElement('div', null, 'twitch-notes-blurred-background'); const container = createElement('div', 'This action will delete all saved notes!

Consider exporting notes before performing this action in case you will need to use notes again', 'twitch-notes-center-floating-container'); const spacer = createElement('div', null, null, { height: '24px' }); const deleteAction = createElement('button', 'DELETE ALL NOTES', 'twitch-notes-settings-button', { background: 'red' }); deleteAction.addEventListener('click', () => { const users = Notes.getSavedUserList(); for(let user of users) { Notes.deleteNote(user); } blurredBackground.remove(); }) const cancelAction = createElement('button', 'Cancel', 'twitch-notes-settings-button'); cancelAction.addEventListener('click', () => { blurredBackground.remove(); }) container.appendChild(spacer); container.appendChild(deleteAction); container.appendChild(cancelAction); blurredBackground.appendChild(container); document.body.appendChild(blurredBackground); }, openAllNotes: () => { Notes.showAllNotes(); } } const Mouse = { mousePosition: {x: 0, y: 0}, lastMousePosition: {x: 0, y: 0}, handleMouseMove: (event) => { if (isMouseDown) { if (activeContainer) { const dx = Mouse.mousePosition.x - Mouse.lastMousePosition.x; const dy = Mouse.mousePosition.y - Mouse.lastMousePosition.y; openContainers[activeContainer].style.top = parseInt(openContainers[activeContainer].style.top) + dy + "px"; openContainers[activeContainer].style.left = parseInt(openContainers[activeContainer].style.left) + dx + "px"; } } Mouse.updateMousePosition(event.clientX, event.clientY); }, handleMouseUp: () => { activeContainer = null; isMouseDown = false; }, updateMousePosition: (x, y) => { Mouse.lastMousePosition = {...Mouse.mousePosition}; Mouse.mousePosition = {x, y}; } } function createElement(tag, body, classes, styles, attributes, id) { const element = document.createElement(tag); if (body) { element.innerHTML = body; } if (classes) { if (typeof classes === 'object') { for (let className of classes) { element.classList.add((className)); } } else { element.classList.add(classes); } } if (styles) { if (typeof styles === 'object') { for (let style in styles) { element.style[style] = styles[style]; } } } if (attributes) { if (typeof attributes === 'object') { for (let attribute in attributes) { element.setAttribute(attribute, attributes[attribute]); } } } element.setAttribute('id', id ? id : 'element-' + Math.floor((Math.random() * 10000000))) return element; } function createSettingsButton(buttonContainer) { const c1 = createElement('div', null, null, {marginLeft: "0.5rem !important"}); const c2 = createElement('div', null, null, {display: "inline-flex !important"}); const settingsButton = createElement('button', 'Notes', 'twitch-notes-settings-button'); settingsButton.addEventListener("click", toggleSettings); c2.appendChild(settingsButton); c1.appendChild(c2); buttonContainer.lastChild.insertBefore( c1, buttonContainer.lastChild.lastChild ); } function removeContainer(username) { if (!openContainers[username]) return; openContainers[username].remove(); delete openContainers[username]; } function addContainer(username, container) { if (openContainers[username]) return; openContainers[username] = container; document.body.appendChild(container); } function openTwitchNote(username) { const container = createElement('div', null, 'twitch-note-container', { left: Mouse.mousePosition.x + 'px', top: Mouse.mousePosition.y + 10 + 'px' }); const header = createElement('div', null, 'twitch-note-header'); header.addEventListener("mousedown", function close() { activeContainer = username; isMouseDown = true; }); const closeButton = createElement('span', xButton, 'twitch-note-close-button') closeButton.addEventListener("click", function close() { removeContainer(username); }); const title = createElement('span', username, 'twitch-note-title'); const content = createElement('div', Notes.getNote(username), 'twitch-note-content', [], { contentEditable: 'true' }); const saveButton = createElement('div', 'SAVE', 'twitch-note-save-button') saveButton.addEventListener("click", function () { Notes.saveNote(username, content.innerHTML); removeContainer(username); }); header.appendChild(closeButton); header.appendChild(title); container.appendChild(header); container.appendChild(content); container.appendChild(saveButton); addContainer(username, container); } function addNoteBadges() { let nodes = document.getElementsByClassName("chat-line__message"); let users = []; for (let node of nodes) { const n = node.querySelector(".chat-author__display-name"); if (!n) continue; const usernameContainer = node.querySelector( ".chat-line__username-container" ); const username = n.attributes["data-a-user"].value; if (username) { let noteButton = usernameContainer.querySelector(".twitch-note"); if (!noteButton) { const twitchNote = createElement('span', null, 'twitch-note', { cursor: 'pointer', }) twitchNote.addEventListener("click", () => { openTwitchNote(username); }); const img = createElement('img', null, null, { height: '18px', paddingRight: '4px' }, { src: "https://cdn.rdarius.lt/icons/32-id-card.png" }) twitchNote.appendChild(img); usernameContainer.insertBefore( twitchNote, usernameContainer.firstChild ); } if (!users[username]) { users[username] = 0; } users[username]++; } } } function createChatSettingsLine(text) { const element = createElement('div', null, 'twitch-notes-settings-option-line'); const button = createElement('button', null, 'twitch-notes-settings-option-line-button'); const buttonContainer = createElement('div', null, 'twitch-notes-settings-option-line-button-container'); const buttonContainerContent = createElement('div', text, 'twitch-notes-settings-option-line-button-container-content'); buttonContainer.appendChild(buttonContainerContent); button.appendChild(buttonContainer); element.appendChild(button); return element; } function createChatSettingsSeparator() { return createElement('div', null, 'twitch-note-settings-separator'); } function toggleSettings() { if (!settingsWindow) { const settingsContainer = createElement('div', null, 'twitch-notes-settings-container'); const settingsBalloon = createElement('div', null, 'twitch-notes-settings-balloon'); const settingsPopover = createElement('div', null, 'twitch-notes-settings-popover'); // startOf: settings header const settingsHeader = createElement('div', null, 'twitch-notes-settings-header'); const settingsHeaderLeftElement = createElement('div', null, 'twitch-notes-settings-header-left-element') settingsHeader.appendChild(settingsHeaderLeftElement); const settingsHeaderCenterElement = createElement('div', null, 'twitch-notes-settings-header-center-element') settingsHeader.appendChild(settingsHeaderCenterElement); const settingsHeaderCenterElementContent = createElement('p', 'Twitch Notes Settings', 'twitch-notes-settings-header-center-element-content') settingsHeaderCenterElement.appendChild(settingsHeaderCenterElementContent); const settingsHeaderRightElement = createElement('div', null, 'twitch-notes-settings-header-right-element') settingsHeader.appendChild(settingsHeaderRightElement); const settingsCloseButton = createElement('button', xButton, 'twitch-notes-settings-close-button'); settingsHeaderRightElement.appendChild(settingsCloseButton); settingsPopover.appendChild(settingsHeader); // endOf: settings header settingsCloseButton.addEventListener('click', toggleSettings); // startOf: scrollable area const settingsScrollableArea = createElement('div', null, 'twitch-notes-settings-scrollable-area'); const settingsContent = createElement('div', null, 'twitch-notes-settings-content'); settingsScrollableArea.appendChild(settingsContent); const exportNotes = createChatSettingsLine('Export Notes'); settingsContent.appendChild(exportNotes); const importNotes = createChatSettingsLine('Import Notes'); settingsContent.appendChild(importNotes); const clearData = createChatSettingsLine('Clear Data'); settingsContent.appendChild(clearData); settingsContent.appendChild(createChatSettingsSeparator()); const openAllNotes = createChatSettingsLine('View All Notes'); settingsContent.appendChild(openAllNotes); exportNotes.addEventListener('click', () => {Notes.exportNotes(); toggleSettings()}); importNotes.addEventListener('click', () => {Notes.importNotes(); toggleSettings()}); clearData.addEventListener('click', () => {Notes.clearAllData(); toggleSettings()}); openAllNotes.addEventListener('click', () => {Notes.openAllNotes(); toggleSettings()}); settingsPopover.appendChild(settingsScrollableArea); // endOf: scrollable area settingsBalloon.appendChild(settingsPopover); settingsContainer.appendChild(settingsBalloon); settingsWindow = settingsContainer; document.body.appendChild(settingsWindow); settingsWindowOpen = true; return; } if (settingsWindowOpen) { settingsWindow.style.display = 'none'; settingsWindowOpen = false; } else { settingsWindow.style.display = 'block'; settingsWindowOpen = true; } } const xButton = ``; let openContainers = {}; let isMouseDown = false; let activeContainer = null; let settingsWindowOpen = false; let settingsWindow = null; let LS_UserList = "twitch-note-all-users-list"; let LS_Prefix = "twitch-note-"; document.addEventListener("mousemove", Mouse.handleMouseMove); document.addEventListener("mouseup", Mouse.handleMouseUp); let giveUpAt = Date.now() + 20000; let changeInProgress = false; let targetNode; let buttonContainer; do { targetNode = document.getElementsByClassName( "chat-scrollable-area__message-container" )[0]; } while (!targetNode || giveUpAt < Date.now()); do { buttonContainer = document.getElementsByClassName( "chat-input__buttons-container" )[0]; } while (!buttonContainer || giveUpAt < Date.now()); createSettingsButton(buttonContainer); // Options for the observer (which mutations to observe) const config = {attributes: true, childList: true, subtree: true}; // Callback function to execute when mutations are observed const callback = (mutationList) => { if (!changeInProgress) { for (const mutation of mutationList) { if (mutation.type === "childList") { changeInProgress = true; addNoteBadges(); changeInProgress = false; } } } }; // Create an observer instance linked to the callback function const observer = new MutationObserver(callback); // Start observing the target node for configured mutations observer.observe(targetNode, config); })();