// I'm not a React developer. Be advised! // If you are, and want to rewrite this the "right way", go ahead and do it :) // This is plain Javascript and only requires basic calls to implement customizations // Do simple XHR requests to the API on Mastodon // make sure you control errors correctly, as a Mastodon Server might not setup CORS correctly function mastodon_get(path, payload, callbk) { return mastodon_request('GET', path, payload, callbk); } function mastodon_post(path, payload, callbk) { return mastodon_request('POST', path, payload, callbk); } function mastodon_request(method, path, payload, callbk) { payload = payload || null; var url = 'https://'+fediloveApi.getCurrentInstance()+path; var oReq = new XMLHttpRequest(); oReq.addEventListener("load", function() { callbk(this.responseText); }); oReq.open(method, url); oReq.setRequestHeader('Authorization', 'Bearer '+fediloveApi.getAccessToken()); oReq.send(payload); } function api_status_fav(dom, status_id) { var dislike = false; var path = `/api/v1/statuses/${status_id}/favourite`; if (dom.getAttribute('data-liked') === "1") { path = `/api/v1/statuses/${status_id}/unfavourite`; dislike = true; } mastodon_post(path, {}, function(data) { // to-do: check "data" if the POST succeded (for now we expect it did xD) if (!dislike) { $(dom).addClass('liked-msg'); $(dom).attr('data-liked', 1); dom.style.animation = "spin2 .5s linear 1"; } else { $(dom).removeClass('liked-msg'); $(dom).attr('aria-label', '0 '+$(dom).attr('aria-label')); $(dom).removeAttr('data-liked'); dom.style.animation = undefined; } }); } // The api to send a message on the current chat thread has been improved a lot // and now is much more rock-solid, and we reuse the React functions function api_send_message() { // the message is composed from the current chat Acct (@user@domain) + the compose textarea value const text = fediloveData.chatAvatarCache.acct + ' ' + $('div#chat-compose-global textarea').val(); //async function postStatus(realm, text, inReplyToId, mediaIds, sensitive, spoilerText, visibility, mediaDescriptions, inReplyToUuid, poll, mediaFocalPoints) const lastStatus = fediloveApi.getChatLastMessageId(true); fediloveFunctions.postStatus(lastStatus.id, text, lastStatus.id, [], false, undefined, 'direct', undefined, lastStatus.uuid, undefined, undefined); // empty the current compose box $('div#chat-compose-global textarea').val(''); // scroll to the end as all chat Apps do fediloveUI.scrollChatToLastItem(); } var fediloveUI = { registerSwipeOnElementEvents: { elements: [], vars: { startX: null, startY: null, dist: null, threshold: 30, allowedTime: 200, elapsedTime: 0, startTime: null }, touchstart: function(e) { const vars = fediloveUI.registerSwipeOnElementEvents.vars; const touchobj = e.changedTouches[0]; vars.dist = 0; vars.startX = touchobj.pageX; vars.startY = touchobj.pageY; vars.startTime = new Date().getTime(); e.preventDefault(); }, touchmove: function(e) { e.preventDefault() }, touchend: function(e) { if (fediloveUI.registerSwipeOnElementEvents.touchendCallback != null) { const vars = fediloveUI.registerSwipeOnElementEvents.vars; const touchobj = e.changedTouches[0]; vars.dist = touchobj.pageX - vars.startX; vars.elapsedTime = new Date().getTime() - vars.startTime; if (Math.abs(vars.dist) < 20) { return e.preventDefault(); } const swiperightBol = (vars.elapsedTime <= vars.allowedTime && vars.dist >= vars.threshold); fediloveUI.registerSwipeOnElementEvents.touchendCallback(swiperightBol); e.preventDefault(); } }, touchendCallback: null }, unregisterSwipeOnElement: function(selector) { const element = document.querySelector(selector); if (element === null || element === undefined) return; fediloveUI.registerSwipeOnElementEvents.elements = fediloveUI.registerSwipeOnElementEvents.elements.filter( function(item) { return item !== selector }); element.removeEventListener('touchstart', fediloveUI.registerSwipeOnElementEvents.touchstart); element.removeEventListener('touchmove', fediloveUI.registerSwipeOnElementEvents.touchmove); element.removeEventListener('touchend', fediloveUI.registerSwipeOnElementEvents.touchend); }, registerSwipeOnElement: function(selector, cbackFunc) { const element = document.querySelector(selector); if (element === null || element === undefined) return; if (fediloveUI.registerSwipeOnElementEvents.elements.includes(selector)) return true; fediloveUI.registerSwipeOnElementEvents.elements.push(selector); fediloveUI.registerSwipeOnElementEvents.touchendCallback = cbackFunc; element.addEventListener('touchstart', fediloveUI.registerSwipeOnElementEvents.touchstart, false); element.addEventListener('touchmove', fediloveUI.registerSwipeOnElementEvents.touchmove, false); element.addEventListener('touchend', fediloveUI.registerSwipeOnElementEvents.touchend, false); }, meetAccountImageDirection: function(direction) { if (!window.location.pathname.startsWith('/accounts') || fediloveUI.meetAccountImageLocked) return; fediloveUI.meetAccountImageLocked = true; var dontMove = false; if (direction === 'prev') fediloveData.meetAccountCurrentImg -= 1; else if (direction === 'next') fediloveData.meetAccountCurrentImg += 1; else dontMove = true; const last = $('div.virtual-list > div.virtual-list-item').length - 1; if (fediloveData.meetAccountCurrentImg > last) { fediloveData.meetAccountCurrentImg = last > 0 ? last : 0; dontMove = true; } else if (fediloveData.meetAccountCurrentImg < 0) { fediloveData.meetAccountCurrentImg = 0; dontMove = true; } document.querySelector('#meet-navigation > #profile-nav > #next').style = ''; document.querySelector('#meet-navigation > #profile-nav > #prev').style = ''; if (fediloveData.meetAccountCurrentImg === last) document.querySelector('#meet-navigation > #profile-nav > #next').style = 'display: none'; else if (fediloveData.meetAccountCurrentImg === 0) document.querySelector('#meet-navigation > #profile-nav > #prev').style = 'display: none'; const _setSelectedVisible = function() { if ($('div.virtual-list > div.virtual-list-item')[fediloveData.meetAccountCurrentImg] !== undefined) { $('div.virtual-list > div.virtual-list-item').attr('style', 'display: none'); $('div.virtual-list > div.virtual-list-item')[fediloveData.meetAccountCurrentImg].style = ''; } fediloveUI.meetAccountImageLocked = false; }; if (dontMove === false) { if (direction === 'next') $('div.virtual-list')[0].style.animation = 'dismissMeet .25s linear 1'; else if (direction === 'prev') $('div.virtual-list')[0].style.animation = 'acceptMeet .25s linear 1'; setTimeout(function() { if ($('div.virtual-list')[0] !== undefined) { $('div.virtual-list')[0].style.animation = ''; _setSelectedVisible(); } }, 220); } else { _setSelectedVisible() } }, meetPageGoToCurrentAccount: function(isRightSwipe) { isRightSwipe = isRightSwipe || false; const elements = $('div.virtual-list > div.virtual-list-item > article'); if (!isRightSwipe && elements.length > 0) { const accId = elements.first().data('account'); elements.first()[0].style.animation = 'dismissMeet .25s linear 1'; setTimeout(function() { fediloveApi.redirect(`/accounts/${accId}`) }, 200); } }, scrollChatToLastItem: function() { const startLen = $('div.the-list > div').length; var count = 0; var _this = setInterval(function() { count++; var newLen = $('div.the-list > div').length; if (count >= 100 || newLen != startLen) { document.querySelector('div.the-list > div:last-child').scrollIntoView(); clearInterval(_this); } }, 150); }, paintChatAvatarAndName: function(accid, acct, avatar, name) { accid = accid || null; acct = acct || null; avatar = avatar || null; name = name || null; if ((accid+acct+avatar+name) === 0) { accid = 0; avatar = '/missing.png'; name = '...'; } else { fediloveData.chatAvatarCache = { id: accid, acct: acct, avatar: avatar, name: name }; // add domain part on acct on painting it to be clear if (acct.split('@').length-1 === 1) { acct += `@${fediloveApi.getCurrentInstance()}`; } } // to-do: check is it XSS safe to add it like this :) ?? $('div#chat-party-global > div#image > img').attr('src', avatar); $('div#chat-party-global > div#name > a > span').html(name); $('div#chat-party-global > div#name > span').text(acct); if (accid != 0) $('div#chat-party-global > div#name > a').attr('href', `/accounts/${accid}`); } }; // objects to access from React code var fediloveApi = { redirect: function(url) { const _id = 'link-' + Date.now(); var a = document.createElement('a'); a.href = url; a.id = _id; document.body.append(a); document.getElementById(_id).click(); document.body.removeChild(a); }, getChatMessageId: function() { var parts = window.location.pathname.split('/'); return parts[parts.length-1]; }, getChatLastMessageId: function(andUuid) { andUuid = andUuid || false; const lastUuid = $('div.the-list article.status-article').last().attr('id'); if (lastUuid === undefined) { return undefined; } const parts = lastUuid.split('/'); return andUuid? { id: parts[parts.length-1], uuid: lastUuid } : parts[parts.length-1]; }, getAccessToken: function() { var instance = fediloveApi.getCurrentInstance(); return JSON.parse(localStorage.store_loggedInInstances)[instance].access_token; }, getCurrentInstance: function() { return JSON.parse(localStorage.store_currentInstance); } }; var fediloveFunctions = { filterItemsForFedilove: function(items) { let newItems = []; for (var item of items) { if (item.account === undefined) continue; if (item.account.display_name.toLowerCase().includes('#fedilove') && item.content.replace(/<[^<>]+>/g, '').toLowerCase().includes('#fedilove')) { newItems.push(item); } } return newItems; } }; var fediloveData = { chatAvatarCache: undefined, meetAccountCurrentImg: 0, meetAccountImageLocked: false, currentAccount: null, currentAccountIsEmpty: false, gotEmojifyTextFunction: false, composeTxtKeypressEvent: false }; var fediloveEvents = { onGotEmojifyTextFunction: function() { fediloveData.gotEmojifyTextFunction = true; }, onChatGetData: function(data) { // dont do anything if avatar and name is cached if (fediloveData.chatAvatarCache !== undefined) return; // waits for the React code to call the "onGotEmojifyTextFunction" so we can use it var waitForEmojifyAndDo = function(accid, acct, avatar, dname, emojis) { var count = 0; var _this = setInterval(function() { if (count > 100) { clearInterval(_this); return; } if (fediloveData.gotEmojifyTextFunction) { dname = dname.replace(/#fedilove/gi, '').trim(); dname = dname.replace(/\s+/g, ' ').trim().replace(/\s/g, ' '); fediloveUI.paintChatAvatarAndName(accid, acct, avatar, fediloveFunctions.emojifyText(dname, emojis)); clearInterval(_this); } count++; }, 150); }; if (data) { // if the message is mine, search which account is doing it for, // and do the same with the party's data if (localStorage.store_userAccountId == data.account.id) { var account_id = data.in_reply_to_account_id; if (account_id === null && data.mentions.length > 0) { account_id = data.mentions[0].id; } if (account_id === null || account_id === undefined) return; mastodon_get(`/api/v1/accounts/${account_id}`, {}, function(newData) { var json = JSON.parse(newData); if (json !== undefined) { waitForEmojifyAndDo( account_id, `@${json.acct}`, json.avatar, json.display_name, json.emojis ); } }); } else { // this means the message we are loading is from the other party, so we continue as normal waitForEmojifyAndDo( data.account.id, `@${data.account.acct}`, data.account.avatar, data.account.display_name, data.account.emojis ); } } }, onEmojiPicked: function(emoji) { var $txt = $("#chat-compose-global textarea"); var caretPos = $txt[0].selectionStart; var textAreaTxt = $txt.val(); $txt.val(textAreaTxt.substring(0, caretPos) + emoji + textAreaTxt.substring(caretPos)); }, onNewNotification: function (dataItems) { if (window.location.pathname.startsWith('/statuses/')) { if (fediloveFunctions.updateStatusAndThread !== undefined) { fediloveFunctions.updateStatusAndThread( fediloveApi.getCurrentInstance(), fediloveApi.getAccessToken(), window.location.pathname, fediloveApi.getChatMessageId() ); fediloveUI.scrollChatToLastItem(); } } } }; // this is our URL-based customizations made by JavaScript function fedilove_customization() { document.body.classList = ''; document.getElementById('sapper').style = ''; if (localStorage.store_currentInstance === "undefined") { $('body').addClass('not-logged-in'); if (window.location.pathname == '/') { $('body').addClass('home'); } return; } if (document.querySelector('#main-nav > div#dummy-nav') != null) { document.querySelector('#main-nav > div#dummy-nav').remove(); document.querySelector('#main-nav > ul.main-nav-ul').style = ''; } document.querySelector('.main-content').classList = "main-content"; document.querySelector('#meet-navigation').style = 'display: none'; document.querySelector('#meet-navigation > #profile-nav').style = 'display: none'; document.querySelector('#chat-compose-global').style = 'visibility: collapse'; document.querySelector('#chat-party-hide').style = 'display: none !important'; document.querySelector('nav#main-nav > ul.main-nav-ul').style = ''; fediloveUI.unregisterSwipeOnElement('main.infinite-scroll-page'); fediloveUI.unregisterSwipeOnElement('#meet-navigation #anim-swipe'); fediloveData.currentAccountIsEmpty = false; $('nav#main-nav li.main-nav-li svg').each(function() { $(this).removeClass('active'); }); // add some animations ( i can't with sass D: ) if (document.getElementById('fedilove-animations') === null) { var style = document.createElement('style'); style.id = 'fedilove-animations'; style.innerHTML = ` @keyframes spin2 { 100% { transform:rotate(-360deg); } } @keyframes fadeOut { 0% {opacity: 1;} 100% {opacity: 0;} } @keyframes dismissMeet { 0% {position: relative; left: 0} 100% {position: relative; left: -95%} } @keyframes acceptMeet { 0% {position: relative; left: 0} 100% {position: relative; left: 95%} } `; document.body.appendChild(style); } if (window.location.pathname === '/notifications') { $('nav#main-nav li.main-nav-li svg')[1].classList += ' active'; } else if (window.location.pathname.startsWith('/statuses/')) { $('div.main-content').addClass('chat'); $('body').addClass('chat'); document.querySelector('#chat-compose-global').style = ''; document.querySelector('#chat-party-hide').style = ''; document.querySelector('nav#main-nav > ul.main-nav-ul').style = 'display: none !important'; if (!fediloveData.composeTxtKeypressEvent) { $('div#chat-compose-global textarea').keypress(function(e) { const keycode = (e.keyCode ? e.keyCode : e.which); if (keycode == '13') { if (!e.ctrlKey && !e.shiftKey) { e.stopPropagation(); e.preventDefault(); setTimeout(function() { api_send_message() }, 100); return true; } else { if (!e.shiftKey && e.ctrlKey) { $(this).val($(this).val()+'\n'); } } } }); fediloveData.composeTxtKeypressEvent = true; } // ******* // load cached avatars or paint it empty (automated process after this will fill it) if (fediloveData.chatAvatarCache !== undefined) { fediloveUI.paintChatAvatarAndName( fediloveData.chatAvatarCache.id, fediloveData.chatAvatarCache.acct, fediloveData.chatAvatarCache.avatar, fediloveData.chatAvatarCache.name ); } else { fediloveUI.paintChatAvatarAndName(); } // this function changes the css class on articles (messages) // that match the given account_id var makeMessageUIModifications = function(account_id) { var theint = 150; const _this = setInterval(function() { if (!window.location.pathname.startsWith('/statuses/')) { clearInterval(_this); return; } // paint MY messages as mine $('div.main-content.chat article.status-article').each(function(i) { if ($(this).find('a.status-author-name').attr('href').endsWith(`/accounts/${account_id}`)) { $(this).addClass('mymsg'); theint = 250; } else { $(this).addClass('partymsg'); } }); // paint LIKES $('a.status-favs-reblogs.status-favs').each(function(i) { // easy, aria-label contains the times this status was fav // so, we use this as the input source to search for "0", // if no "0" found in text, we mark element as liked so we can apply styles and "undo like" function if ($(this).attr('aria-label').search(/0/) == -1) { $(this).addClass('liked-msg'); $(this).attr('data-liked', 1); } }); // remove liking functionality from our own messages $('div.the-list article.status-article.mymsg a.status-favs-reblogs').each(function(i) { $(this).attr('onclick', 'return false;'); }); // resize likes acording to the outer parent $('div.main-content.chat div.the-list article.status-article').each(function(e) { $(this).find('div.like-div').width($(this).width()); }); }, theint); }; // get the userAccountId to check against the href /account/NUM on the messages // so we can apply different style to my messages if (localStorage.store_userAccountId === undefined) { mastodon_get('/api/v1/accounts/verify_credentials', {}, function(data) { var json = JSON.parse(data); if (json !== undefined) { localStorage.store_userAccountId = json.id; makeMessageUIModifications(json.id); } }); } else { makeMessageUIModifications(localStorage.store_userAccountId); } } else if (window.location.pathname == '/direct') { $('div.main-content').addClass('direct'); $('body').addClass('direct'); $('nav#main-nav li.main-nav-li svg')[2].classList += ' active'; } else if (window.location.pathname == '/settings') { $('nav#main-nav li.main-nav-li svg')[3].classList += ' active'; } else if (window.location.pathname.startsWith('/accounts/')) { $('div.main-content').addClass('account'); $('body').addClass('account'); document.querySelector('nav#main-nav > ul.main-nav-ul').style = 'display: none !important'; document.querySelector('#meet-navigation').style = ''; document.querySelector('#meet-navigation > #profile-nav').style = ''; $('#meet-navigation > #anim-swipe').html(''); // prev and next navigation const cloneBackSvg = function(selector) { $(selector).html(''); $(selector).append( $('#meet-navigation > #back > svg').clone()); }; cloneBackSvg('#meet-navigation > #profile-nav > #prev'); cloneBackSvg('#meet-navigation > #profile-nav > #next'); fediloveUI.meetAccountImageDirection(null); // clone the first element and include the account header image var countmax = 0; const _applyImageTo = function(selector) { setTimeout(function() { // set image of first item to the account header (100ms after cloning the object) if (window.fediloveData.currentAccount != null) { if ($(selector)[0] !== undefined) $(selector)[0].src = window.fediloveData.currentAccount.header; } }, 300); }; const _this = setInterval(function() { if (!window.location.pathname.startsWith('/accounts')) { clearInterval(_this); return; } // max interval time: 10 seconds if (countmax > 50 || $('div.virtual-list > div.virtual-list-item').length > 0) { if ($('div.virtual-list > div.virtual-list-item').length > 0) { // clone the first element, insert it at the next position const _e = $('div.virtual-list > div.virtual-list-item:last').clone(); $(_e).insertAfter('div.virtual-list > div.virtual-list-item:last'); _applyImageTo('div.virtual-list > div.virtual-list-item:last div.status-media img'); fediloveUI.registerSwipeOnElement('main.infinite-scroll-page', function(isRightSwipe) { if (isRightSwipe) fediloveUI.meetAccountImageDirection('prev'); else fediloveUI.meetAccountImageDirection('next'); }); } clearInterval(_this); } else if (fediloveData.currentAccountIsEmpty && window.fediloveData.currentAccount != null) { if ($('div.virtual-list > div.no-images-account').length === 0) $('div.virtual-list').append('
'); _applyImageTo('div.virtual-list > div.no-images-account > img.fixed-size-img'); $('#meet-navigation > #profile-nav > #next')[0].style = 'display: none !important'; clearInterval(_this); } countmax++; }, 200); } else if (window.location.pathname == '/federated') { $('div.main-content').addClass('meet'); $('body').addClass('meet'); document.querySelector('nav#main-nav > ul.main-nav-ul').style = 'display: none !important'; document.querySelector('#meet-navigation').style = ''; // swipe left animation $('#meet-navigation > #anim-swipe').html(''); for (var i of [1,2,3]) { var elem = $('#meet-navigation > #back > svg').clone(); $('#meet-navigation > #anim-swipe').append(elem); setTimeout(function(it) { it.attr('style', 'animation: fadeOut .5s linear infinite'); }, (i*250), elem); } fediloveUI.registerSwipeOnElement('#meet-navigation #anim-swipe', function(swiperightBol) { fediloveUI.meetPageGoToCurrentAccount(swiperightBol) }); } else if (window.location.pathname.startsWith('/notifications/mentions')) { $('nav.notification-filters li > a.focus-fix').attr('onclick', 'return false;'); } if (!window.location.pathname.startsWith('/statuses/')) { fediloveData.chatAvatarCache = undefined; } } // we inject this script.js into the React framework at timelines.js // Watch for URL changes every 1/2 seconds (this is efficient, don't worry about it!) // and dispatch a call to "fedilove_customization" to load page customizations depending on URL context var __window_url = null; var __window_url_old = null; setInterval(function() { const newurl = window.location.pathname; if (newurl != __window_url) { __window_url_old = __window_url; __window_url = newurl; fedilove_customization(); } }, 100);