document.addEventListener("DOMContentLoaded", function(event) { formatDates(); getPostTexts(); getUserDetails(); handleAvatarHover(); initFileUpload(); setupUploadPostPreview(); }); function formatDates() { let timestampElements = document.querySelectorAll('.postdate[ts]'); timestampElements.forEach(function(el) { let d = new Date(el.attributes['ts'].value * 1000); el.innerText = d.toLocaleString(); }); } function getPostTexts() { let boxes = document.querySelectorAll('.post-box'); boxes.forEach(function(el) { let id = el.querySelector('.post-meta-box').querySelector('span').innerText.replace('#',''); get('get_posttext.php?id='+id).then(function(response) { let info; try { info = JSON.parse(response); } catch (error) { showError(`Unknown response from server: ${response}`); return; } let postTextBox = el.querySelector('.post-text-box'); let loader = postTextBox.querySelector('.loader'); if (loader !== null) { postTextBox.removeChild(loader); } if (info.success) { postTextBox.innerHTML = info.text; } else { if (info.error == 404) { console.log('Post ', id, ' not found'); } else { console.log('Error fetching post ', id, 'Error code: ', info.error); } } }, function(error) { console.log('Error fetching post ', id, error); }); }); } function getUserDetails() { let boxes = document.querySelectorAll('.user-box'); boxes.forEach(function(el) { let userBox = el.querySelector('.username'); if (userBox == null) { userBox = el.querySelector('span'); } let id = userBox.innerText; get('get_userdetails.php?id='+id).then(function(response) { let info; try { info = JSON.parse(response); } catch (error) { showError(`Unknown response from server: ${response}`); return; } let loader = userBox.querySelector('.loader-small'); if (loader !== null) { userBox.removeChild(loader); } if (info.success) { if (info.name != null) { el.innerHTML = '' + info.name + '' + id + ''; } let presenceIndicator = document.createElement('div'); presenceIndicator.classList.add('presence-indicator'); switch (info.presence) { case 0: presenceIndicator.style.borderColor = "red"; presenceIndicator.style.setProperty('border-color', 'var(--red)'); break; case 1: presenceIndicator.style.borderColor = "green"; presenceIndicator.style.setProperty('border-color', 'var(--green)'); break; } el.appendChild(presenceIndicator); } else { if (info.error == 404) { console.log('User ', id, ' not found'); } else { console.log('Error fetching user ', id, 'Error code: ', info.error); } } }, function(error) { console.log('Error fetching user ', id, error); }); }); } function handleAvatarHover() { let avatars = document.querySelectorAll('img.avatar'); avatars.forEach(function(el) { el.addEventListener('mouseenter', function() { // Blur non hovered let nonHovered = document.querySelectorAll('img.avatar:not(:hover)'); nonHovered.forEach(function(img) { img.classList.add('blur'); //img.classList.remove('unblur'); //img.style.filter = "blur(5px)"; }); }); el.addEventListener('mouseleave', function() { // Unblur let avtrs = document.querySelectorAll('img.avatar'); avtrs.forEach(function(img) { img.classList.remove('blur'); //img.classList.add('unblur'); //img.style.filter = "blur(0px)"; }); }); }); } function showLoginForm(authUrl) { const authDiv = document.getElementById("authorizeDiv"); authDiv.classList.remove("hidden"); authDiv.querySelector("a").href = authUrl; } function initUploadForm(formId) { document.querySelectorAll('.upload-form').forEach(function(el) { el.style.display = "none"; }); const form = document.getElementById(formId); form.style.display = form.offsetHeight == 0 ? "inherit" : "none"; form.reset(); removeImage(); document.getElementById('uploadProgress').style.display = "none"; } function showAvatarResetDate(e) { const val = e.valueAsNumber || parseInt(e.value) || 3; let d = new Date(); d.setDate(d.getDate() + val); e.parentElement.querySelector('span').innerText = d.toLocaleDateString(); } function checkLoginStatus() { return new Promise(function(resolve, reject) { let req = new XMLHttpRequest(); req.open('GET', 'set_temp_avatar.php?status'); req.onload = function() { // This is called even on 404 etc // so check the status console.log("onload", req); if (req.status == 200) { // Resolve the promise with the response text //resolve(req.response); let result = req.response; if (result.length < 1) { var error = new Error("No response"); error.name = "NoResponse"; reject(error); } try { result = JSON.parse(result); } catch (err) { reject(err); return; } resolve(result); } else { // Otherwise reject with the status text // which will hopefully be a meaningful error reject(Error(req.statusText)); } }; // Handle network errors req.onerror = function() { console.error("Network error"); reject(Error('Network Error')); }; // Make the request req.send(); }); } function transitionEndEventName () { var i, undefined, el = document.createElement('div'), transitions = { 'transition':'transitionend', 'OTransition':'otransitionend', // oTransitionEnd in very old Opera 'MozTransition':'transitionend', 'WebkitTransition':'webkitTransitionEnd' }; for (i in transitions) { if (transitions.hasOwnProperty(i) && el.style[i] !== undefined) { return transitions[i]; } } //TODO: throw 'TransitionEnd event is not supported in this browser'; } function initFileUpload() { document.getElementById('showUploadForm').addEventListener('click', e => { initUploadForm('manuallyAddAvatarForm'); }); document.getElementById('addTempAvatar').addEventListener('click', e => { document.getElementById('loadingLoginStatus').classList.remove('hidden'); checkLoginStatus().then(result => { document.getElementById('loadingLoginStatus').classList.add('hidden'); console.log("Authentication status: ", result, !!result.authenticated); if(result.error) { console.error(`Error checking authentication status: ${result.error.message}`); return; } if (!!result.authenticated) { initUploadForm('uploadTempAvatar'); document.getElementById('uploadTempAvatar').querySelector('textarea').maxlength = result.maxPostLength; } else { showLoginForm(result.authUrl); } }, error => { console.error("Error checking authentication status", error); }); }); document.getElementById('cbShouldPostAvatar').addEventListener('click', e => { document.getElementById('uploadTempAvatar').querySelector('.posttext').style.display = e.target.checked ? 'inherit' : 'none'; }); document.querySelectorAll('.posttext').forEach(el => { el.addEventListener('focus', e => { if (e.target.selectionStart == e.target.selectionEnd && !document.firstTextboxFocus) { document.firstTextboxFocus = true; setTimeout(function() { console.log("Selecting start", e, e.target); e.target.selectionStart = 0; e.target.selectionEnd = 0; }); } }); }); const durationInput = document.querySelector('#uploadTempAvatar input[name=avatarDuration]'); showAvatarResetDate(durationInput); durationInput.addEventListener('change', e => { showAvatarResetDate(e.target); }); var isAdvancedUpload = function() { var div = document.createElement('div'); return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window && 'FileReader' in window; }(); if (isAdvancedUpload) { window.droppedFile = false; const dragOverEvents = ['dragover', 'dragenter']; const dragEndEvents = ['dragleave', 'dragend', 'drop']; const events = dragOverEvents.concat(dragEndEvents).concat(['drag', 'dragstart']); const zone = document.querySelectorAll('.file-upload').forEach(function(form) { form.classList.add('advanced-upload'); const fileSelect = form.querySelector('input[type=file]'); events.forEach(event => { form.addEventListener(event, e => { // preventing the unwanted behaviours e.preventDefault(); e.stopPropagation(); }); }); dragOverEvents.forEach(event => { form.addEventListener(event, e => { form.classList.add('is-dragover'); }); }); dragEndEvents.forEach(event => { form.addEventListener(event, e => { form.classList.remove('is-dragover'); }); }); form.addEventListener('drop', e => { imageAdded(e.dataTransfer.files, e); }); fileSelect.addEventListener('change', e => { imageAdded(e.target.files, e); }); }); } console.log(document.querySelectorAll('.removeImage')); document.querySelectorAll('.removeImage').forEach(function(el) { el.style.zIndex = 1000; el.addEventListener('click', e => { removeImage(); }); }); } function removeImage(e) { console.log(e); window.droppedFile = null; //document.getElementById('removeImage').style.display = "none"; document.querySelectorAll('.removeImage').forEach(function(el) { el.style.display = "none"; }); document.querySelectorAll('.avatarPreview').forEach(function(el) { window.URL.revokeObjectURL(el.src); el.style.display = "none"; el.src = ""; }); } function handleManualAvatarUpload(e) { startUpload(e).then(result => { notify("Avatar added successfully", {type: "success"}); showNewlyAddedPicture(result); e.style.display = "none"; }, error => { notify(`Error saving avatar: ${error.message}`, {type: "error"}); e.style.display = "none"; }); return false; } function handleTempAvatarUpload(e) { startUpload(e).then(result => { if (result.warn) { notify(result.warn, {type: "warn"}); } else { notify("Your temporary theme avatar has been saved succesfully", {type: "success"}); } e.style.display = "none"; }, error => { console.error(error); notify(`Error saving avatar: ${error.message}`, {type: "error"}); }); return false; } function startUpload(e) { console.log(window.droppedFile, e); const progress = e.querySelector('.uploadProgress'); return new Promise(function(resolve, reject) { postForm(e, (position, total) => { progress.max = total; progress.value = position; progress.style.display = "inherit"; }).then(result => { if (result.length < 1) { reject({'message': 'Received no response from server :( '}); return; } try { result = JSON.parse(result); } catch (err) { reject({'message': `Unknown response from server: ${result}`}); return; } console.log("Done", result); if (result.error) { reject(result.error); } else { resolve(result); } }, error => { console.error(error); let serverError; if (typeof(error) == "object") { serverError = error; } else { try { serverError = JSON.parse(error); } catch (err) { serverError = {message: "Unknown error"}; } } reject(serverError); }); }); } function showNewlyAddedPicture(postDetails) { // TODO: Consolidate together with getUserDetails() // TODO: Check if user already has a picture for this theme document.querySelector('.avatar-grid').appendChild(createPostBox(info)); } function createPostBox(info) { /* * info.succes: true (hopefully) * info.error: Error object, containing "code" and "message" * info.src: Avatar image * info.presence: -1: gray, 0: red, 1: green * info.user: @username. Might be null or empty string * info.realname: Realname. Might be null or empty string * info.posttext: HTML or pure text content of the post * info.postid: ID of the post * info.timestamp: timestamp of the post */ console.log(info); const grid = document.querySelector('.avatar-grid'); let postBox = null; // If there is already oen in the grid, clone that if (grid.querySelectorAll(".avatar-box").length) { postBox = grid.children[grid.children.length - 1].cloneNode(true); } else { console.error("No images in grid yet. Not fully implemented. TODO"); } // New image postBox.querySelector('.avatar').src = info.img; // User info const userDetails = postBox.querySelector('.user-box'); if (info.realname != null) { userDetails.innerHTML = '' + info.realname + '' + info.user + ''; } else { userDetails.innerHTML = '' + info.user + ''; } /* console.log(userDetails.outerHTML); let userBox = userDetails.querySelector('.username'); if (userBox == null) { userBox = userDetails.querySelector('span'); } console.log(userBox.outerHTML); userBox.innerText = info.user; console.log(userBox.outerHTML); if (info.realname != null) { userBox.innerHTML = '' + info.realname + '' + info.user + ''; }*/ let presenceIndicator = userDetails.querySelector('.presence-indicator'); if (presenceIndicator == null) { presenceIndicator = document.createElement('div'); presenceIndicator.classList.add('presence-indicator'); userDetails.appendChild(presenceIndicator); } switch (info.presence) { case -1: presenceIndicator.style.borderColor = "gray"; presenceIndicator.style.setProperty('border-color', 'var(--gray-dark)'); break; case 0: presenceIndicator.style.borderColor = "red"; presenceIndicator.style.setProperty('border-color', 'var(--red)'); break; case 1: presenceIndicator.style.borderColor = "green"; presenceIndicator.style.setProperty('border-color', 'var(--green)'); break; } // Post info const postTextBox = postBox.querySelector('.post-text-box'); postTextBox.innerHTML = info.posttext; const postMetaBox = postBox.querySelector('.post-meta-box'); const postLink = postMetaBox.querySelector('a'); postLink.href = "https://posts.pnut.io/" + info.postid; postLink.innerText = '#' + info.postid; let d = new Date(info.timestamp * 1000); postMetaBox.querySelector('.postdate').innerText = d.toLocaleString(); return postBox; } function postForm(form, progressCallback) { // Return a new promise. console.log('Form: ', form); return new Promise(function(resolve, reject) { //const promise = new Promise(); if (form.id == 'manuallyAddAvatarForm') { const idInput = form.querySelector('[name=postID]'); const pID = getPostID(idInput); if (pID == -1) { reject("Invalid post id"); } else { idInput.value = pID; } } var ajaxData = new FormData(form); if (window.droppedFile) { ajaxData.append(form.querySelector('input[type=file]').getAttribute('name'), window.droppedFile); } const id = getQueries().id; if (!id) { reject("Unknown theme id"); } else { ajaxData.append('theme', id); ajaxData.append('submit', 'submit'); console.log(ajaxData); // Do the usual XHR stuff let req = new XMLHttpRequest(); req.open(form.method, form.action); req.onload = function() { // This is called even on 404 etc // so check the status console.log("onload", req); if (req.status == 200) { // Resolve the promise with the response text resolve(req.response); } else { // Otherwise reject with the status text // which will hopefully be a meaningful error reject(Error(req.statusText)); } }; // Handle network errors req.onerror = function() { console.error("Network error"); reject(Error('Network Error')); }; if (progressCallback) { var eventSource = req.upload || req; eventSource.addEventListener("progress", function(e) { // normalize position attributes across XMLHttpRequest versions and browsers var position = e.position || e.loaded; var total = e.totalSize || e.total; progressCallback(position, total); }); } // Make the request req.send(ajaxData); } }); } function imageAdded(arr, evt) { if (!arr.length) { return; } console.log("Dropped image", arr, evt); window.droppedFile = arr[0]; const imgSize = window.droppedFile.size; console.log("Size in Bytes", imgSize); const maxSize = parseInt(document.querySelector('input[name=MAX_FILE_SIZE]').value); let errorSpan = document.getElementById('imgTooLarge'); if (imgSize > maxSize) { //Image too large if (errorSpan == null) { errorSpan = document.createElement('span'); errorSpan.classList.add('imgTooLarge'); errorSpan.innerText = `Image is too large. It is ${(imgSize/1024/1024).toFixed(2)}MiB, but should be <= ${(maxSize/1024/1024).toFixed(2)}MiB`; errorSpan.style.display = 'inherit'; errorSpan.id = 'imgTooLarge'; document.querySelector('#uploadTempAvatar table tr td:nth-child(2)').appendChild(errorSpan); } else { errorSpan.style.display = 'inherit'; } } else if (errorSpan != null) { errorSpan.style.display = 'none'; } getOrientation(function(e){ console.log("Orientation", e); let prv = evt.target.parentElement.querySelector('.avatarPreview'); switch (e) { case 1: prv.style.transform = "rotate(0)"; break; case 3: prv.style.transform = "rotate(180deg)"; break; case 6: prv.style.transform = "rotate(90deg)"; break; case 8: prv.style.transform = "rotate(270deg)"; break; default: prv.style.transform = "rotate(0)"; break; } if (prv.src) { window.URL.revokeObjectURL(prv.src); } prv.style.display = "inherit"; prv.src = window.URL.createObjectURL(droppedFile); evt.target.parentElement.querySelector('.removeImage').style.display = "inherit"; evt.target.parentElement.querySelector('.removeImage').style.zIndex = 1000;zIndex = 1000; }); } function getOrientation(callback) { var reader = new FileReader(); reader.onload = function(e) { var view = new DataView(e.target.result); const magic = view.getUint16(0, false); if (magic != 0xFFD8) { console.warn("Unknown magic bytes: ", magic.toString(16)) return callback(-2); } var length = view.byteLength, offset = 2; console.debug("Length: ", length); while (offset < length) { const firstBytesTest = view.getUint16(offset+2, false); if (firstBytesTest <= 8) { console.warn("firstBytesTest <=8", firstBytesTest); return callback(-1); } var marker = view.getUint16(offset, false); offset += 2; if (marker == 0xFFE1) { console.debug("Found marker"); if (view.getUint32(offset += 2, false) != 0x45786966) { console.debug("Invalid sequence following marker: ", view.getUint32(offset += 2, false).toString(16)); return callback(-1); } var little = view.getUint16(offset += 6, false) == 0x4949; offset += view.getUint32(offset + 4, little); var tags = view.getUint16(offset, little); offset += 2; for (var i = 0; i < tags; i++) { if (view.getUint16(offset + (i * 12), little) == 0x0112) { console.debug("Found rotation tag"); return callback(view.getUint16(offset + (i * 12) + 8, little)); } } } else if ((marker & 0xFF00) != 0xFF00) { break; } else { offset += view.getUint16(offset, false); } } return callback(-1); }; reader.readAsArrayBuffer(window.droppedFile); } function setupUploadPostPreview() { const postIdInput = document.querySelector('input[name=postID]'); window.lastPostIDKey = 0; postIdInput.onkeyup = function() { window.lastPostIDKey = Date.now(); setTimeout(function() { const now = Date.now(); const diff = now - window.lastPostIDKey; console.log(diff); if (diff < 500) { return; } const pID = getPostID(postIdInput); get(`get_posttext.php?id=${pID}&avatarWidth=40`).then(function(response) { try { response = JSON.parse(response); } catch (error) { showError(`Unknown response from server: ${response}`); return; } if (response.error) { showError(`Error loading post #${pID}: ${response.error}`); return; } const tbl = document.querySelector('.upload-form').querySelector('table'); let i = tbl.rows[1].cells.length - 1; // Remove existing post boxes while (tbl.rows[1].cells[i].querySelector('.post-box')) { tbl.rows[1].removeChild(tbl.rows[1].cells[i]); i = tbl.rows[1].cells.length - 1; } const cell = tbl.rows[1].insertCell(); const postBox = createPostBox(response); const ab = postBox.querySelector('.avatar'); ab.style.width = '20px'; postBox.removeChild(ab.parentElement); const ub = postBox.querySelector('.post-box').querySelector('.user-box'); ub.insertBefore(ab, ub.firstChild); ab.className = ""; ab.style.borderRadius = "3px"; cell.style.verticalAlign = 'top'; cell.appendChild(postBox); // Post box overflows horizontically (long links, etc). Remove set width and let it goooo if (postBox.scrollWidth > postBox.clientWidth) { postBox.style.width = "initial"; } }); }, 500); }; } function getPostID(input) { const parsed = parseInt(input.value); if (isNaN(parsed)) { matches = input.value.match(/(?:https?:\/\/)?((posts.*pnut.*)|.*pnut.*posts)\/(\d+)/); if (!matches || Number.isInteger(matches[matches.length-1])) { return -1; } return parseInt(matches[matches.length-1]); } return parsed; } function showError(message, params = {}) { params.type = 'error'; notify(message, params); } function notify(message, params) { defaultOptions = { class: 'notif', position: 'top|right', duration: 5, type: 'info' }; const mergedOptions = { ...defaultOptions, ...params, }; mergedOptions.duration *= 1000; switch (mergedOptions.type) { case 'success': console.log(message); toast.success(message, mergedOptions); break; case 'warn': console.warn(message); toast.warn(message, mergedOptions); break; case 'error': console.error(message); toast.alert(message, mergedOptions); break; default: console.log(message); toast.message(message, mergedOptions); break; } }