diff options
author | polwex <polwex@sortug.com> | 2025-09-11 01:48:14 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-09-11 01:48:14 +0700 |
commit | b1d68ac307ed87d63e83820cbdf843fff0fd9f7f (patch) | |
tree | d6a684a70a80509e68ff667b842aa4e4c091906f /front/src/logic/utils.ts |
init
Diffstat (limited to 'front/src/logic/utils.ts')
-rw-r--r-- | front/src/logic/utils.ts | 459 |
1 files changed, 459 insertions, 0 deletions
diff --git a/front/src/logic/utils.ts b/front/src/logic/utils.ts new file mode 100644 index 0000000..dbd246e --- /dev/null +++ b/front/src/logic/utils.ts @@ -0,0 +1,459 @@ +import type { + Content, + Notification, + ID, + ExternalContent, + Poast, + Reference, + Inline, + PID, + SortugRef, +} from "@/types/trill"; +import type { Ship } from "@/types/urbit"; +import anyAscii from "any-ascii"; +import type { ReactGrouping, SPID } from "@/types/ui"; +import { openLock } from "./bunts"; +import { isValidPatp, patp2dec } from "urbit-ob"; +import { REF_REGEX } from "./constants"; + +export function parseSortugLink(link: string): SortugRef { + const s = link.replace("urbit://", "").split("/"); + const [type, ship, ...pat] = s; + const path = `/${pat.join("/")}`; + return { type, ship, path }; +} +export function sortugRefTolink(r: SortugRef): string { + return `urbit://${r.type}/${r.ship}${r.path}`; +} +// TODO + +export function createReference(ship: Ship, id: ID) { + return { + reference: { + feed: { id: id, ship: ship }, + }, + }; +} + +export function addScheme(url: string) { + if (url.includes("localhost")) { + return `http://${url.replace("http://", "")}`; + } else { + return `https://${url.replace("http://", "")}`; + } +} + +export function easyCode(code: string) { + const string = code.replace(/-/g, ""); + const matches = string.match(/.{1,6}/g); + if (matches) return matches.join("-"); +} + +export function tilde(patp: Ship) { + if (patp[0] == "~") { + return patp; + } else { + return "~" + patp; + } +} + +export function color_to_hex(color: string) { + let hex = "#" + color.replace(".", "").replace("0x", "").toUpperCase(); + if (hex == "#0") { + hex = "#000000"; + } + return hex; +} + +export function date_diff(date: number | Date, type: "short" | "long") { + const now = new Date().getTime(); + const diff = now - new Date(date).getTime(); + if (type == "short") { + return to_string(diff / 1000); + } else { + return to_string_long(diff / 1000); + } +} + +function to_string(s: number) { + if (s < 60) { + return "now"; + } else if (s < 3600) { + return `${Math.ceil(s / 60)}m`; + } else if (s < 86400) { + return `${Math.ceil(s / 60 / 60)}h`; + } else if (s < 2678400) { + return `${Math.ceil(s / 60 / 60 / 24)}d`; + } else if (s < 32140800) { + return `${Math.ceil(s / 60 / 60 / 24 / 30)}mo`; + } else { + return `${Math.ceil(s / 60 / 60 / 24 / 30 / 12)}y`; + } +} + +function to_string_long(s: number) { + if (s < 60) { + return "right now"; + } else if (s < 3600) { + return `${Math.ceil(s / 60)} minutes ago`; + } else if (s < 86400) { + return `${Math.ceil(s / 60 / 60)} hours ago`; + } else if (s < 2678400) { + return `${Math.ceil(s / 60 / 60 / 24)} days ago`; + } else if (s < 32140800) { + return `${Math.ceil(s / 60 / 60 / 24 / 30)} months ago`; + } else { + return `${Math.ceil(s / 60 / 60 / 24 / 30 / 12)} years ago`; + } +} + +export function regexes() { + const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i); + const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg)$/i); + const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i); + return { img: IMAGE_REGEX, aud: AUDIO_REGEX, vid: VIDEO_REGEX }; +} + +export function stringToSymbol(str: string) { + const ascii = anyAscii(str); + let result = ""; + for (let i = 0; i < ascii.length; i++) { + const n = ascii.charCodeAt(i); + if ((n >= 97 && n <= 122) || (n >= 48 && n <= 57)) { + result += ascii[i]; + } else if (n >= 65 && n <= 90) { + result += String.fromCharCode(n + 32); + } else { + result += "-"; + } + } + result = result.replace(/^[\-\d]+|\-+/g, "-"); + result = result.replace(/^\-+|\-+$/g, ""); + return result; +} +export function buildDM(author: Ship, recipient: Ship, contents: Content[]) { + const node: any = {}; + const point = patp2dec(recipient); + const index = `/${point}/${makeIndex()}`; + node[index] = { + children: null, + post: { + author: author, + contents: contents, + hash: null, + index: index, + signatures: [], + "time-sent": Date.now(), + }, + }; + return { + app: "dm-hook", + mark: "graph-update-3", + json: { + "add-nodes": { + resource: { name: "dm-inbox", ship: author }, + nodes: node, + }, + }, + }; +} + +export function makeIndex(): string { + const DA_UNIX_EPOCH = BigInt("170141184475152167957503069145530368000"); + const DA_SECOND = BigInt("18446744073709551616"); + const timeSinceEpoch = (BigInt(Date.now()) * DA_SECOND) / BigInt(1000); + return (DA_UNIX_EPOCH + timeSinceEpoch).toString(); +} +export function makeDottedIndex() { + const DA_UNIX_EPOCH = BigInt("170141184475152167957503069145530368000"); + const DA_SECOND = BigInt("18446744073709551616"); + const timeSinceEpoch = (BigInt(Date.now()) * DA_SECOND) / BigInt(1000); + const index = (DA_UNIX_EPOCH + timeSinceEpoch).toString(); + return index.replace(/\B(?=(\d{3})+(?!\d))/g, "."); +} + +export function repostData(p: Poast): PID | null { + if ( + p.contents.length === 1 && + "ref" in p.contents[0] && + p.contents[0].ref.type === "trill" + ) + return { + id: p.contents[0].ref.path.slice(1), + ship: p.contents[0].ref.ship, + }; + else return null; +} + +export function getNotificationTime(n: Notification): number { + if ("follow" in n) { + return n.follow.time; + } else if ("unfollow" in n) { + return n.unfollow.time; + } else if ("mention" in n) { + return n.mention.time; + } else if ("react" in n) { + return n.react.time; + } else if ("reply" in n) { + return n.reply.time; + } else if ("quote" in n) { + return n.quote.time; + } else if ("share" in n) { + return n.share.time; + } else { + return Date.now(); + } +} +export function abbreviateChat(s: string): string { + const plist = s.trim().split(" "); + if (isValidPatp(plist[0]) && plist.length > 1) { + return `${plist[0]} & ${plist.length - 1}+`; + } else if (s.length < 25) return s; + else return `${s.substring(0, 25)}...`; +} + +export function timestring(n: number): string { + const nn = new Date(n); + return nn.toTimeString().slice(0, 5); +} +export function wait(ms: number) { + return new Promise((resolve, _reject) => { + setTimeout(resolve, ms); + }); +} + +export function quoteToReference(d: SPID): Reference | ExternalContent { + if (d.service === "twatter") + return { + json: { + origin: "twatter", + content: JSON.stringify(d.post), + }, + }; + else + return { + ref: { + type: "trill", + ship: d.post.host, + path: `/${d.post.id}`, + }, + }; +} + +export function trillPermalink(t: Poast) { + return `urbit://trill/${t.host}/${t.id}`; +} +export function isFeedRef(c: Content): boolean { + return "ref" in c && (c as Reference).ref.type === "trill"; +} + +export function checkTilde(s: string) { + if (s[0] === "~") return s; + else return "~" + s; +} + +export function addDots(s: string, num: number): string { + const reversed = s.split("").reverse().join(""); + const reg = new RegExp(`.{${num}}`, "g"); + const withCommas = reversed.replace(reg, "$&."); + return withCommas.split("").reverse().join("").slice(1); +} +export function addDots5(s: string): string { + const reversed = s.split("").reverse().join(""); + const withCommas = reversed.replace(/.{5}/g, "$&."); + return withCommas.split("").reverse().join(""); +} +// TODO +export function getTrillText(c: Content): string { + if (!c) return ""; + const reducePara = (acc: string, item: Inline) => { + let t = ""; + if ("text" in item) t = item.text + " "; + if ("italic" in item) t = item.italic + " "; + if ("bold" in item) t = item.bold + " "; + if ("strike" in item) t = item.strike + " "; + if ("ship" in item) t = item.ship + " "; + if ("codespan" in item) t = item.codespan + " "; + if ("link" in item) t = item.link.href + " "; + if ("break" in item) t = "\n"; + return acc + t; + }; + return c.reduce((acc, item) => { + if ("paragraph" in item) { + const text = item.paragraph.reduce(reducePara, ""); + return acc + text + "\n"; + } else return acc; + }, ""); +} +export function isTwatterLink(s: string) { + const sp = s + .replace("https://", "") + .split("/") + .filter((s) => s); + return sp.length === 4 && sp[0] === "twitter.com" && sp[2] === "status"; +} +export const isSortugLink = (s: string) => !!s.match(REF_REGEX); +export function parseOutSortugLinks(s: string): [SortugRef[], string] { + const matches = s.match(REF_REGEX); + let refs = []; + let rest = s; + for (let m of matches || []) { + rest = rest.replace(m, ""); + refs.push(parseSortugLink(m)); + } + return [refs, rest]; +} + +export function isTrillLink(s: string): boolean { + if (!isSortugLink(s)) return false; + const r = parseSortugLink(s); + if (r.type !== "trill") return false; + return isValidPatp(r.ship) && !isNaN(Number(r.path.slice(1))); +} + +export function auraToHex(s: string): string { + if (s.startsWith("0x")) { + let numbers = s.replace("0x", "").replace(".", ""); + while (numbers.length < 6) { + numbers = "0" + numbers; + } + return "#" + numbers; + } else if (s.startsWith("#")) return s; + else { + // console.log(s, "weird hex"); + return "black"; + } +} + +export function buildPost( + author: Ship, + id: string, + time: number, + s: string, + content: string, +): Poast { + return { + host: author, + author: author, + thread: null, + parent: null, + contents: [{ paragraph: [{ text: s }] }], + read: openLock, + write: openLock, + tags: [], + id, + time, + children: [], + engagement: { reacts: {}, quoted: [], shared: [] }, + json: { origin: "rumors", content }, + }; +} + +// default cursors +export function makeNewestIndex() { + const DA_UNIX_EPOCH = BigInt("170141184475152167957503069145530368000"); + const DA_SECOND = BigInt("18446744073709551616"); + const timeSinceEpoch = (BigInt(Date.now()) * DA_SECOND) / BigInt(1000); + return (DA_UNIX_EPOCH + timeSinceEpoch).toString(); +} +export const startCursor = makeNewestIndex(); +export const endCursor = "0"; + +export function displayCount(c: number): string { + if (c <= 0) return ""; + if (c < 1_000) return `${c}`; + if (c >= 1_000 && c < 1_000_000) return `${Math.round(c / 1_00) / 10}K`; + if (c >= 1_000_000) return `${Math.round(c / 100_000) / 10}M`; + else return ""; +} +export function isWhiteish(hex: string): boolean { + if (hex.indexOf("#") === 0) hex = hex.slice(1); + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + return r > 200 && g > 200 && b > 200; +} + +export function localISOString(date: Date) { + const offset = new Date().getTimezoneOffset(); + const localts = date.getTime() - offset * 60_000; + return new Date(localts).toISOString().slice(0, 16); +} + +export function goback() { + window.history.back(); +} + +export function groupReacts(reacts: Record<Ship, string>): ReactGrouping { + const byReact = Object.entries(reacts).reduce( + (acc: Record<string, Ship[]>, item) => { + const shipList = acc[item[1]]; + if (!shipList) acc[item[1]] = [item[0]]; + else acc[item[1]] = [...shipList, item[0]]; + return acc; + }, + {}, + ); + return Object.entries(byReact) + .reduce((acc: ReactGrouping, item) => { + const pair = { react: item[0], ships: item[1] }; + return [...acc, pair]; + }, []) + .sort((a, b) => b.ships.length - a.ships.length); +} + +export function reverseRecord( + a: Record<string, string>, +): Record<string, string> { + return Object.entries(a).reduce((acc: Record<string, string>, [k, v]) => { + acc[v] = k; + return acc; + }, {}); +} + +export function getColorHex(color: string): string { + if (color.startsWith("0x")) + return `#${padString(stripFuckingDots(color), 6)}`; + else if (color.startsWith("#") && color.length === 7) return color; + else if (color.length === 6) return `#${color}`; + else { + console.log(color, "something weird with this color"); + return "#FFFFFF"; + } +} + +export function stripFuckingDots(hex: string) { + return hex.replace("0x", "").replaceAll(".", ""); +} +export function padString(s: string, size: number) { + if (s.length >= size) return s; + else return padString(`0${s}`, size); +} +export function isDark(hexColor: string): boolean { + const r = parseInt(hexColor.substring(1, 2), 16); + const g = parseInt(hexColor.substring(3, 5), 16); + const b = parseInt(hexColor.substring(5, 7), 16); + + const sr = r / 255; + const sg = g / 255; + const sb = b / 255; + const rSrgb = + sr <= 0.03928 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4); + const gSrgb = + sg <= 0.03928 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4); + const bSrgb = + sb <= 0.03928 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4); + + // Calculate luminance + const luminance = 0.2126 * rSrgb + 0.7152 * gSrgb + 0.0722 * bSrgb; + return luminance < 0.12; +} + +export function checkIfClickedOutside( + e: React.MouseEvent, + el: HTMLElement, + close: any, +) { + e.stopPropagation(); + if (el.contains(e.currentTarget)) close(); +} |