Schedule posts to Bluesky with Cloudflare workers.
skyscheduler.work
cf
tool
bsky-tool
cloudflare
bluesky
schedule
bsky
service
social-media
cloudflare-workers
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});