refactor(frontend): update download functionality and improve time formatting utilities

This commit is contained in:
Ivan
2026-04-24 14:33:59 -05:00
parent 7a02ca3492
commit fcbea738f1
2 changed files with 102 additions and 24 deletions
+52 -11
View File
@@ -1,24 +1,65 @@
class DownloadUtils {
static downloadFile(filename, blob) {
// create object url for blob
const objectUrl = URL.createObjectURL(blob);
function isAndroidSaveBridge() {
return (
typeof window !== "undefined" &&
window.MeshChatXAndroid != null &&
typeof window.MeshChatXAndroid.saveDownload === "function"
);
}
// create hidden link element to download blob
class DownloadUtils {
static _blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const s = reader.result;
if (typeof s !== "string") {
reject(new Error("readAsDataURL failed"));
return;
}
const comma = s.indexOf(",");
resolve(comma >= 0 ? s.slice(comma + 1) : s);
};
reader.onerror = () => reject(reader.error || new Error("read failed"));
reader.readAsDataURL(blob);
});
}
static _triggerBrowserDownload(filename, objectUrl) {
const link = document.createElement("a");
link.href = objectUrl;
link.download = filename;
link.style.display = "none";
document.body.append(link);
// click link to download file in browser
link.click();
// link element is no longer needed
link.remove();
// revoke object url to clear memory
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
}
static downloadFromBase64(filename, fileBytesBase64) {
if (isAndroidSaveBridge()) {
window.MeshChatXAndroid.saveDownload(filename, fileBytesBase64);
return;
}
const byteCharacters = atob(fileBytesBase64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray]);
const objectUrl = URL.createObjectURL(blob);
DownloadUtils._triggerBrowserDownload(filename, objectUrl);
}
static async downloadFile(filename, blob) {
if (isAndroidSaveBridge()) {
const b64 = await DownloadUtils._blobToBase64(blob);
window.MeshChatXAndroid.saveDownload(filename, b64);
return;
}
const objectUrl = URL.createObjectURL(blob);
DownloadUtils._triggerBrowserDownload(filename, objectUrl);
}
}
export default DownloadUtils;
+50 -13
View File
@@ -46,38 +46,38 @@ class Utils {
};
}
static formatSeconds(seconds) {
static formatSecondsWithoutAgo(seconds) {
const parsedSeconds = this.parseSeconds(seconds);
if (parsedSeconds.days > 0) {
if (parsedSeconds.days === 1) {
return "1 day ago";
} else {
return parsedSeconds.days + " days ago";
return "1 day";
}
return parsedSeconds.days + " days";
}
if (parsedSeconds.hours > 0) {
if (parsedSeconds.hours === 1) {
return "1 hour ago";
} else {
return parsedSeconds.hours + " hours ago";
return "1 hour";
}
return parsedSeconds.hours + " hours";
}
if (parsedSeconds.minutes > 0) {
if (parsedSeconds.minutes === 1) {
return "1 min ago";
} else {
return parsedSeconds.minutes + " mins ago";
return "1 min";
}
return parsedSeconds.minutes + " mins";
}
if (parsedSeconds.seconds <= 1) {
return "a second ago";
} else {
return parsedSeconds.seconds + " seconds ago";
return "a second";
}
return parsedSeconds.seconds + " seconds";
}
static formatSeconds(seconds) {
return this.formatSecondsWithoutAgo(seconds) + " ago";
}
static formatTimeAgo(datetimeString) {
@@ -108,11 +108,48 @@ class Utils {
return this.formatSeconds(diffSec);
}
/**
* Relative age fragment without trailing " ago" / "ago" duplication when used inside i18n
* strings that already add a locale-specific suffix (e.g. "Last synced {time} ago.").
* Sub-minute uses a neutral phrase so English templates avoid "just now ago".
*/
static formatTimeAgoForI18n(datetimeString) {
if (!datetimeString) return "unknown";
let dateString = datetimeString;
if (typeof dateString === "string" && !dateString.includes("Z") && !dateString.includes("+")) {
dateString = dateString.replace(" ", "T") + "Z";
}
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffSec = Math.round(diffMs / 1000);
if (diffSec < 60) {
return "less than a minute";
}
if (diffSec > 86400) {
return dayjs(date).format("MMM D, h:mm A");
}
return this.formatSecondsWithoutAgo(diffSec);
}
static formatSecondsAgo(seconds) {
const secondsAgo = Math.round(Date.now() / 1000 - seconds);
return this.formatSeconds(secondsAgo);
}
static formatSecondsAgoForI18n(seconds) {
const secondsAgo = Math.round(Date.now() / 1000 - seconds);
if (secondsAgo < 60) {
return "less than a minute";
}
return this.formatSecondsWithoutAgo(secondsAgo);
}
static formatMinutesSeconds(seconds) {
const parsedSeconds = this.parseSeconds(seconds);
const paddedMinutes = parsedSeconds.minutes.toString().padStart(2, "0");