···1010const postFormTitle = document.getElementById('postFormTitle')
1111let hasFileLimit = false;
1212let fileData = new Map();
1313+let waitingFiles = 0;
13141415/* Sections for handling UI changes and modifications */
1516const sectionRetweet = document.getElementById('section-retweet');
···2021function addOnUnloadBlocker() {
2122 window.onbeforeunload = function() {
2223 document.querySelectorAll(".fileDel").forEach((el) => {el.click();});
2424+ // only way to get the alert box to not show up
2325 return undefined;
2426 }
2527}
2628function clearOnUnloadBlocker() {
2729 window.onbeforeunload = null;
3030+}
3131+3232+function setFileData(fileName, data) {
3333+ fileData.set(fileName, data);
3434+ --waitingFiles;
2835}
29363037let fileDropzone = new Dropzone("#fileUploads", {
3138 url: "/post/upload",
3239 autoProcessQueue: true,
3333- /* We process this ourselves */
4040+ /* We add remove links ourselves */
3441 addRemoveLinks: false,
3542 maxFiles: FILE_DROP_MAX_FILES,
3643 dictMaxFilesExceeded: "max files",
···8087 fileDropzone.removeAllFiles();
8188 // Clear the file data map
8289 fileData.clear();
9090+ waitingFiles = 0;
8391});
84928593fileDropzone.on("reset", () => {
8694 hasFileLimit = false;
9595+ waitingFiles = 0;
8796 clearOnUnloadBlocker();
8897 showContentLabeler(false);
8998 setElementVisible(sectionLinkAttach, true);
···95104 pushToast("Maximum number of files reached", false);
96105 return;
97106 }
107107+ // Increase the number of waiting to be processed files.
108108+ ++waitingFiles;
98109 setElementVisible(sectionLinkAttach, false);
99110 const buttonHolder = Dropzone.createElement("<fieldset role='group' class='file-item-group'></fieldset>");
100111 const removeButton = Dropzone.createElement("<button class='fileDel outline btn-error' disabled><small>Remove file</small></button>");
···148159fileDropzone.on("success", function(file, response) {
149160 const deleteFileOnError = () => {
150161 const delButton = file.previewElement.querySelectorAll(".fileDel")[0];
162162+ --waitingFiles;
151163 delButton.setAttribute("bad", true);
152164 delButton.click();
153165 };
166166+ const deleteFileIfLengthOver = (length, max) => {
167167+ if (length > max) {
168168+ pushToast(`${file.name} is over the max duration by ${(length - max).toFixed(2)} seconds`, false);
169169+ deleteFileOnError();
170170+ return true;
171171+ }
172172+ return false;
173173+ }
154174 // show the labels
155175 showContentLabeler(true);
156176 const fileIsImage = imageTypes.includes(file.type);
···170190 videoTag.setAttribute("src", videoObjectURL);
171191 videoTag.addEventListener("loadeddata", () => {
172192 const videoDuration = videoTag.duration;
173173- if (videoDuration > MAX_VIDEO_LENGTH) {
174174- pushToast(`${file.name} is too long for bsky by ${(videoDuration - MAX_VIDEO_LENGTH).toFixed(2)} seconds`, false);
175175- deleteFileOnError();
176176- } else {
177177- fileData.set(file.name, {content: response.data, type: 3,
193193+ if (!deleteFileIfLengthOver(videoDuration, MAX_VIDEO_LENGTH)) {
194194+ setFileData(file.name, {content: response.data, type: 3,
178195 height: videoTag.videoHeight, width: videoTag.videoWidth, duration: videoDuration });
179196 hasFileLimit = true;
180197 }
···196213 let duration = 0;
197214 try {
198215 for (let i = 0, len = uint8.length; i < len; i++) {
199199- if (uint8[i] == 0x21
200200- && uint8[i + 1] == 0xF9
201201- && uint8[i + 2] == 0x04
202202- && uint8[i + 7] == 0x00)
216216+ if (uint8[i] == 0x21 && uint8[i + 1] == 0xF9
217217+ && uint8[i + 2] == 0x04 && uint8[i + 7] == 0x00)
203218 {
204219 const delay = (uint8[i + 5] << 8) | (uint8[i + 4] & 0xFF)
205220 duration += delay < 2 ? 10 : delay
···224239 if (videoDuration === null) {
225240 pushToast(`${file.name} duration could not be processed`, false);
226241 deleteFileOnError();
227227- } else if (videoDuration > MAX_VIDEO_LENGTH) {
228228- pushToast(`${file.name} is over the maximum video duration by ${(videoDuration - MAX_VIDEO_LENGTH).toFixed(2)} seconds`, false);
229229- deleteFileOnError();
230230- } else if (videoDuration >= MAX_GIF_LENGTH) {
231231- pushToast(`${file.name} is over the maximum length for a gif by ${(videoDuration - MAX_GIF_LENGTH).toFixed(2)} seconds`, false);
232232- deleteFileOnError();
242242+ } else if (deleteFileIfLengthOver(videoDuration, MAX_GIF_LENGTH)) {
243243+ return;
233244 } else {
234234- fileData.set(file.name, { content: response.data, type: 3, height: imageHeight, width: imageWidth, duration: videoDuration });
245245+ setFileData(file.name, { content: response.data, type: 3, height: imageHeight, width: imageWidth, duration: videoDuration });
235246 hasFileLimit = true;
236247 }
237248 };
238249 // Force the file to load.
239250 imgObj.src = gifImgURL;
240251 } else {
241241- fileData.set(file.name, {content: response.data, type: 1});
252252+ setFileData(file.name, {content: response.data, type: 1});
242253 }
243254244255 // Make the buttons pressable
···280291 pushToast(`Error: ${file.name} had an unexpected error`, false);
281292 }
282293294294+ // file failed to upload, so drop the value here too
295295+ --waitingFiles;
283296 fileDropzone.removeFile(file);
284297 if (fileData.length == 0) {
285298 setElementVisible(sectionLinkAttach, true);
···288301289302fileDropzone.on("uploadprogress", function(file, progress, bytesSent) {
290303 const progressObject = file.previewElement.querySelector(".dz-upload");
291291- progressObject.innerHTML = `${progress}%`;
304304+ progressObject.innerHTML = `${progress.toFixed(2)}%`;
292305 if ((progress === 100 || bytesSent == file.size) && progressObject) {
293306 progressObject.innerHTML = "Processing...please wait. This may take a bit!";
294307 }
···301314// Handle form submission
302315postForm.addEventListener('submit', async (e) => {
303316 e.preventDefault();
317317+ // prevent submissions if we're currently uploading files.
318318+ if (waitingFiles > 0) {
319319+ pushToast("Files are currently pending upload, please wait...", false);
320320+ return;
321321+ }
304322 showPostProgress(true);
305323 const contentVal = content.value;
306324 const postNow = postNowCheckbox.checked;
+12-5
src/layout/makePost.tsx
···2626 {type: "script", href: "/dep/tribute.min.js"}
2727];
28282929-export function PostCreation() {
2929+export function PostCreation({ctx}: any) {
3030+ const maxWidth: number|undefined = ctx.env.IMAGE_SETTINGS.max_width;
3031 const bskyImageLimits = `Max file size of ${BSKY_IMG_SIZE_LIMIT_IN_MB}MB`;
3132 return (
3233 <section>
···6364 <li><span data-tooltip={BSKY_IMG_FILE_EXTS}>Images</span>:
6465 <ul>
6566 <li>must be less than {CF_IMAGES_MAX_DIMENSION}x{CF_IMAGES_MAX_DIMENSION} pixels</li>
6666- <li>must have a file size smaller than {CF_IMAGES_FILE_SIZE_LIMIT_IN_MB}MB ({APP_NAME} will attempt to compress images to fit <span data-tooltip={bskyImageLimits}>BlueSky's requirements</span>)</li>
6767+ <li>must have a file size smaller than {CF_IMAGES_FILE_SIZE_LIMIT_IN_MB}MB ({APP_NAME} will attempt to compress images to fit <span data-tooltip={bskyImageLimits}>BlueSky's requirements</span>)
6868+ {maxWidth ?
6969+ <ol>
7070+ <li>images over {BSKY_IMG_SIZE_LIMIT_IN_MB}MB with a width greater than <b>{maxWidth}px</b> will also <u data-tooltip="will preserve aspect ratio">be resized</u> in addition to being compressed</li>
7171+ </ol> : null}
7272+ </li>
7373+6774 <li>thumbnails will only be shown here for images that are smaller than {MAX_THUMBNAIL_SIZE}MB</li>
6868- <li>don't upload and fail, it's recommended to use a lower resolution file instead</li>
7575+ <li>if an image fails to upload, you'll need to manually adjust the file to fit it properly</li>
6976 </ul></li>
7077 <li><span data-tooltip={BSKY_VIDEO_FILE_EXTS}>Videos</span>:
7178 <ul>
7279 <li>must be shorter than {BSKY_VIDEO_MAX_DURATION} minutes</li>
7380 <li>must be smaller than {R2_FILE_SIZE_LIMIT_IN_MB}MB</li>
7474- <li>will be processed on BSky after they're posted. This may show a temporary "Video not Found"/black screen for a bit after posting.</li>
8181+ <li>will be processed on your PDS after they're posted. This may show a temporary <i>"Video not Found"</i> message for a bit after posting.</li>
7582 </ul></li>
7683 </ul></small></div>
7784 </footer>
···95102 <section>
96103 <article>
97104 <header>Insert Post/Feed/List Link</header>
9898- <input id="recordBox" placeholder="https://" title="Must be a link to a ATProto powered record" />
105105+ <input id="recordBox" placeholder="https://" title="Must be a link to a ATProto based record" />
99106 <small>Posts must be quotable and all record types must exist upon the scheduled time. If it does not exist, it will not be attached to your post.</small>
100107 </article>
101108 </section>
+2-2
src/layout/settings.tsx
···3838 <label>
3939 BSky App Password:
4040 <BSkyAppPasswordField />
4141- <small>If you need to change your bsky application password, you can
4242- <a href="https://bsky.app/settings/app-passwords" target="_blank">get a new one here</a>.</small>
4141+ <small>If you need to change your bsky application password, you can
4242+ <a href="https://bsky.app/settings/app-passwords" target="_blank">get a new one here</a>.</small>
4343 </label>
4444 <label>
4545 BSky PDS:
···11// Change this value to break out of any caching that might be happening
22// for the runtime scripts (ex: main.js & postHelper.js)
33-export const CURRENT_SCRIPT_VERSION: string = "1.5.3";
33+export const CURRENT_SCRIPT_VERSION: string = "1.5.4";
4455export const getAppScriptStr = (scriptName: string) => `/js/${scriptName}.min.js?v=${CURRENT_SCRIPT_VERSION}`;
66
+19-7
src/utils/r2Query.ts
···108108 let failedToResize = true;
109109110110 if (c.env.IMAGE_SETTINGS.enabled) {
111111+ // Randomly generated id to be used during the resize process
111112 const resizeFilename = uuidv4();
112113 const resizeBucketPush = await c.env.R2RESIZE.put(resizeFilename, await file.bytes(), {
113114 customMetadata: {"user": userId },
···116117117118 if (!resizeBucketPush) {
118119 console.error(`Failed to push ${file.name} to the resizing bucket`);
119119- return {"success": false, "error": "unable to handle image for resize, please make it smaller"}
120120+ return {"success": false, "error": "resize process ran out of disk space, you'll need to resize the image or try again"};
120121 }
121122122122- // TODO: use the image wrangler binding
123123+ // Default image rules for resizing an image
124124+ const imageRules: RequestInitCfPropertiesImage = {
125125+ fit: "scale-down",
126126+ metadata: "copyright",
127127+ format: "jpeg",
128128+ };
129129+130130+ // if the application is to also resize the width automatically, do so here.
131131+ // this will preserve aspect ratio
132132+ if (c.env.IMAGE_SETTINGS.max_width) {
133133+ imageRules.width = c.env.IMAGE_SETTINGS.max_width;
134134+ }
135135+123136 for (var i = 0; i < c.env.IMAGE_SETTINGS.steps.length; ++i) {
124124- const qualityLevel = c.env.IMAGE_SETTINGS.steps[i];
137137+ const qualityLevel: number = c.env.IMAGE_SETTINGS.steps[i];
125138 const response = await fetch(new URL(resizeFilename, c.env.IMAGE_SETTINGS.bucket_url!), {
126139 headers: {
127140 "x-skyscheduler-helper": c.env.RESIZE_SECRET_HEADER
···129142 cf: {
130143 image: {
131144 quality: qualityLevel,
132132- metadata: "copyright",
133133- format: "jpeg"
145145+ ...imageRules
134146 }
135147 }
136148 });
···138150 const resizedHeader = response.headers.get("Cf-Resized");
139151 const returnType = response.headers.get("Content-Type") || "";
140152 const transformFileSize: number = Number(response.headers.get("Content-Length")) || 0;
141141- const resizeHadError = resizedHeader === null || resizedHeader.indexOf("err=") !== -1;
153153+ const resizeHadError: boolean = (resizedHeader === null || resizedHeader.indexOf("err=") !== -1);
142154143155 if (!resizeHadError && BSKY_IMG_MIME_TYPES.includes(returnType)) {
144156 console.log(`Attempting quality level ${qualityLevel}% for ${originalName}, size: ${transformFileSize}`);
···174186175187 if (failedToResize) {
176188 const fileSizeOverAmount: string = ((file.size - BSKY_IMG_SIZE_LIMIT)/MB_TO_BYTES).toFixed(2);
177177- return {"success": false, "originalName": originalName, "error": `Image is too large for bsky, over by ${fileSizeOverAmount}MB`};
189189+ return {"success": false, "originalName": originalName, "error": `Image is too large for BSky, size is over by ${fileSizeOverAmount}MB`};
178190 }
179191 }
180192
+2-1
wrangler.toml
···7979queue = "skyscheduler-repost-queue"
8080max_retries = 3
81818282+# used for resizing the thumbnails for link posts
8283[images]
8384binding = "IMAGES"
8485···9091BETTER_AUTH_URL="https://skyscheduler.work"
91929293# If we should use cf image transforms & how much of a quality percentage should we try and the public location of the resize bucket.
9393-IMAGE_SETTINGS={enabled=true, steps=[95, 85, 75], bucket_url="https://resize.skyscheduler.work/"}
9494+IMAGE_SETTINGS={enabled=true, steps=[95, 85, 75], bucket_url="https://resize.skyscheduler.work/", max_width=3000}
94959596# Signup options and if keys should be used
9697SIGNUP_SETTINGS = {use_captcha=true, invite_only=false, invite_thread="", invite_uses=10}