Schedule posts to Bluesky with Cloudflare workers. skyscheduler.work
cf tool bsky-tool cloudflare bluesky schedule bsky service social-media cloudflare-workers
at main 272 lines 8.2 kB view raw
1/* Functions & vars that are mostly used on the application side of things */ 2var contentTabs = null; 3function getPostListElement(itemID) { 4 return document.getElementById(`post-${itemID}`); 5} 6 7function formatDate(date) { 8 return new Date(date).toLocaleString(undefined, { 9 year: "numeric", 10 month: "short", 11 day: "numeric", 12 hour: "numeric", 13 }); 14} 15 16function updateAllTimes() { 17 document.querySelectorAll(".timestamp").forEach(el => { 18 if (el.hasAttribute("corrected")) 19 return; 20 21 el.textContent = formatDate(el.innerText); 22 el.setAttribute("corrected", true); 23 }); 24 document.querySelectorAll(".repostTimesLeft").forEach(el => { 25 if (el.hasAttribute("data-tooltip")) 26 return; 27 28 if (el.firstElementChild.className == "repostInfoData") { 29 const repostInformation = el.firstElementChild.innerText; 30 if (repostInformation.length > 0) 31 el.setAttribute("data-tooltip", repostInformation); 32 } 33 }); 34} 35 36function scrollToPost(id) { 37 const postElement = getPostListElement(id); 38 if (postElement) { 39 postElement.scrollIntoView({behavior: "smooth", container: "nearest", block: "nearest", inline: "start" }); 40 } 41} 42 43function refreshPostTimer() { 44 console.log("ready to set post timer now"); 45 // Call refresh posts every hour 46 setInterval(refreshPosts, 3600000); 47 // refresh the post once 48 refreshPosts(); 49} 50 51function refreshPosts() { 52 htmx.trigger("body", "refreshPosts"); 53} 54 55document.addEventListener("scrollTop", function() { 56 scrollTop(); 57}); 58 59document.addEventListener("scrollListTop", function() { 60 scrollToObject(document.getElementById("posts")); 61}); 62 63document.addEventListener("scrollListToPost", function(ev) { 64 scrollToPost(ev.detail); 65}); 66 67document.addEventListener("postDeleted", function(ev) { 68 const type = ev.detail.value ? "Retweet" : "Post"; 69 pushToast(`${type} deleted`, true); 70}); 71 72document.addEventListener("postFailedDelete", function() { 73 pushToast("Post failed to delete, try again", false); 74 refreshPosts(); 75}); 76 77function sidebarButtonListener(className, eventName) { 78 document.querySelectorAll(`${className}[listen=false]`).forEach(el => { 79 addClickKeyboardListener(el, () => { 80 const buttonEvent = new CustomEvent(eventName, { 81 detail: { 82 target: el.parentElement 83 }}); 84 document.dispatchEvent(buttonEvent); 85 }); 86 el.setAttribute("listen", true); 87 }); 88} 89 90document.addEventListener("timeSidebar", function() { 91 updateAllTimes(); 92 sidebarButtonListener(".addThreadPost", "replyThreadCreate"); 93 sidebarButtonListener(".addRepostsButton", "addNewRepost"); 94}); 95 96document.addEventListener("postUpdatedNotice", function() { 97 pushToast("Post updated successfully!", true); 98}) 99 100document.addEventListener("accountUpdated", function(ev) { 101 closeModal(document.getElementById("changeInfo")); 102 document.getElementById("settingsData").reset(); 103 pushToast("Settings Updated!", true); 104}); 105 106function addCounter(textField, counter, maxLength) { 107 const textEl = document.getElementById(textField); 108 const counterEl = document.getElementById(counter); 109 if (counterEl) { 110 // escape out of adding a counter more than once 111 if (counterEl.hasAttribute("counting")) 112 return; 113 114 const handleCount = (counter) => { 115 counterEl.innerHTML = `${counter.all}/${maxLength}`; 116 // Show red color if the text field is too long, this will not be super accurate on items containing links, but w/e 117 if (counter.all > maxLength) { 118 counterEl.classList.add('tooLong'); 119 } else { 120 counterEl.classList.remove('tooLong'); 121 } 122 }; 123 124 Countable.on(textEl, handleCount); 125 counterEl.setAttribute("counting", true); 126 counterEl.addEventListener("reset", () => { 127 counterEl.innerHTML = `0/${maxLength}`; 128 counterEl.classList.remove('tooLong'); 129 }); 130 counterEl.addEventListener("recount", () => { 131 Countable.count(textEl, handleCount); 132 }); 133 } 134} 135 136function recountCounter(counter) { 137 const counterEl = document.getElementById(counter); 138 counterEl.dispatchEvent(new Event("recount")); 139} 140 141function resetCounter(counter) { 142 const counterEl = document.getElementById(counter); 143 counterEl.dispatchEvent(new Event("reset")); 144} 145 146function addEasyModalOpen(buttonID, modalEl, closeButtonID) { 147 addClickKeyboardListener(document.getElementById(buttonID), () => { 148 clearSettingsData(); 149 openModal(modalEl); 150 }); 151 addClickKeyboardListener(document.getElementById(closeButtonID), () => { 152 closeModal(modalEl); 153 }); 154} 155 156function setElementRequired(el, required) { 157 if (required) 158 el.setAttribute("required", true); 159 else 160 el.removeAttribute("required"); 161} 162 163function isElementVisible(el) { 164 return !el.classList.contains("hidden"); 165} 166 167function setElementVisible(el, shouldShow) { 168 if (shouldShow) 169 el.classList.remove("hidden"); 170 else 171 el.classList.add("hidden"); 172} 173 174function setElementDisabled(el, disabled) { 175 if (disabled) 176 el.setAttribute("disabled", true); 177 else 178 el.removeAttribute("disabled"); 179} 180 181function convertTimeValueLocally(number) { 182 const date = new Date(number); 183 date.setMinutes(0 - date.getTimezoneOffset()); 184 return date.toISOString().slice(0,16); 185} 186 187function getScheduleTimeForNextHour() { 188 // set current time to value of now + 1 hour 189 const curDate = new Date(); 190 curDate.setHours(curDate.getHours() + 1); 191 return convertTimeValueLocally(curDate); 192} 193 194function setupDashboard() { 195 const keys = ["Enter", " "]; 196 document.querySelectorAll(".autoRepostBox").forEach(el => { 197 addClickKeyboardListener(el, (e) => { 198 setSelectDisable(e.target.parentElement, !e.target.checked); 199 }, keys, false); 200 if (el.getAttribute("startchecked") == "true") { 201 setSelectDisable(el.parentElement, false); 202 } 203 }); 204 205 // find the post time scheduler object 206 document.querySelectorAll(".scheduledDateBlock").forEach(el => { 207 const dateScheduler = el.querySelector(".timeSelector"); 208 const scheduledPostNowBox = el.querySelector(".postNow"); 209 210 // rounddown minutes 211 dateScheduler.addEventListener('change', () => { 212 dateScheduler.value = convertTimeValueLocally(dateScheduler.value); 213 }); 214 215 // push a minimum date to make it easier (less chance of typing 2025 by accident) 216 dateScheduler.setAttribute("min", getScheduleTimeForNextHour()); 217 218 if (scheduledPostNowBox) { 219 addClickKeyboardListener(scheduledPostNowBox, () => { 220 const isChecked = scheduledPostNowBox.checked; 221 setElementRequired(dateScheduler, !isChecked); 222 setElementVisible(dateScheduler, !isChecked); 223 setElementVisible(dateScheduler.nextElementSibling, !isChecked); 224 }, keys, false); 225 } 226 }); 227 228 // look for the url card box and setup listeners 229 const urlCardBox = document.getElementById('urlCard'); 230 if (urlCardBox) { 231 urlCardBox.addEventListener("paste", () => { 232 showContentLabeler(true); 233 setElementVisible(sectionImageAttach, false); 234 }); 235 236 urlCardBox.addEventListener("input", (ev) => { 237 const isNotEmpty = ev.target.value.length > 0; 238 showContentLabeler(isNotEmpty); 239 setElementVisible(sectionImageAttach, !isNotEmpty); 240 }); 241 } else { 242 console.warn("Missing URLCard box"); 243 } 244 245 // Handle character counting 246 addCounter("content", "count", MAX_LENGTH); 247 addCounter("altTextField", "altTextCount", MAX_ALT_LENGTH); 248 249 // Add mentions to the main post field 250 tributeToElement(content); 251 // add event for the cancel thread button 252 if (cancelThreadBtn) { 253 addClickKeyboardListener(cancelThreadBtn, () => 254 {document.dispatchEvent(new Event("resetPost")) }); 255 } 256 // fire the events to keep our data nice and updated 257 document.dispatchEvent(new Event("timeSidebar")); 258 // Clean all pages to defaults 259 document.dispatchEvent(new Event("resetPost")); 260 document.dispatchEvent(new Event("resetRepost")); 261}; 262 263// go. 264document.addEventListener("DOMContentLoaded", () => { 265 setupDashboard(); 266 // set up timer to update the post list 267 const timeUntilNextHour = 3600000 - new Date().getTime() % 3600000; 268 console.log(`Will run refresh timer in ${timeUntilNextHour}ms`); 269 setTimeout(refreshPostTimer, timeUntilNextHour); 270 271 contentTabs = new PicoTabs('[role="tablist"]'); 272});