// 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; } }); } // We use the already polished compose system to trick it to send messages using our custom UI // instead of the reply compose box show in every message when you hit "reply" function api_send_message(dom) { // the status_id is the last part of our current URL const status_id = fediloveApi.getChatStatusId(); // search for the reply on status_id, dictated by URL $('button[id*=reply-]').each(function(i) { if ($(this).attr('id').includes('//'+status_id)) { // click the reply button on the current status, dictated by URL if ($('#the-compose-box-input-'+status_id).length == 0) { $(this).click(); } // wait for the button event to make the compose layer "visible" var _this = setInterval(function() { if ($('#the-compose-box-input-'+status_id).length > 0) { // search for the user ID in the title attribute $('a[id*=status-author-name-]').each(function(i2) { if ($(this).attr('id').includes('//'+status_id)) { // title contains the user ID on this server var text = $(this).attr('title')+' '+$('div#chat-compose-global textarea').val(); // set the text on thie invisible compose box of the status_id $('textarea#the-compose-box-input-'+status_id).val(text); // hit send :) $('div#list-item-'+status_id+' div.status-article-compose-box > div.compose-box-button-wrapper button.compose-box-button').click(); // empty our message box $('div#chat-compose-global textarea').val(''); fediloveUI.scrollChatToLastItem(); } }); clearInterval(_this); } }, 100); } }); } var fediloveUI = { 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); }, paintToolbarCompose: function() { if (fediloveData.toolbarElement != null) { console.log(fediloveData.toolbarElement); $('#chat-compose-global #tools-placeholder').html(fediloveData.toolbarElement); } } }; // objects to access from React code var fediloveApi = { getAccessToken: function() { var instance = fediloveApi.getCurrentInstance(); return JSON.parse(localStorage.store_loggedInInstances)[instance].access_token; }, getCurrentInstance: function() { return JSON.parse(localStorage.store_currentInstance); }, getChatStatusId: function() { var parts = window.location.pathname.split('/'); return parts[parts.length-1]; } }; var fediloveFunctions = {}; var fediloveData = { currentChat: null, toolbarFilled: false, toolbarElement: null }; var fediloveEvents = { onMessagesLoaded: function () { if (fediloveData.toolbarFilled) return true; fediloveData.toolbarFilled = true; // click on the reply button on the highlighted message (only once, save state) $('div.the-list > div.list-item > article.status-in-own-thread > div.status-toolbar > button.status-toolbar-reply-button').click(); // load the highlighted message compose tools on our custom compose bar to re-use functionality setTimeout(function() { fediloveData.toolbarElement = $('div.the-list > div.list-item > article.status-in-own-thread > div.status-article-compose-box div.compose-box-toolbar').clone(); fediloveUI.paintToolbarCompose(); }, 500); }, onNewNotification: function (dataItems) { if (window.location.pathname.startsWith('/statuses/')) { if (fediloveFunctions.updateStatusAndThread !== undefined) { fediloveFunctions.updateStatusAndThread( fediloveApi.getCurrentInstance(), fediloveApi.getAccessToken(), window.location.pathname, fediloveApi.getChatStatusId() ); fediloveUI.scrollChatToLastItem(); } } } }; // this is our URL-based customizations made by JavaScript var intervalChatCssChange = null; function fedilove_customization() { document.querySelector('.main-content').classList = "main-content"; document.querySelector('#chat-compose-global').style = 'visibility: collapse'; if (window.location.pathname.startsWith('/statuses/')) { $('div.main-content').addClass('chat'); document.querySelector('#chat-compose-global').style = ''; fediloveUI.paintToolbarCompose(); // add some animations var style = document.createElement('style'); style.innerHTML = '@keyframes spin2 { 100% { transform:rotate(-360deg); } }'; document.body.appendChild(style); // this function changes the css class on articles (messages) // that match the given account_id var makeMessageUIModifications = function(account_id) { if (intervalChatCssChange !== null) return; var theint = 150; setInterval(function() { intervalChatCssChange = this; // 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; } }); // 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); } }); }, 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.startsWith('/notifications/mentions')) { $('nav.notification-filters li > a.focus-fix').attr('onclick', 'return false;'); } else { fediloveData.toolbarFilled = false; } } // 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 theurl = null; setInterval(function() { var newurl = window.location.pathname; if (newurl != theurl) { fedilove_customization(); theurl = newurl; } }, 250);