frontend for xcvr appview
at main 4.7 kB view raw
1import type { ChannelView } from "$lib/types.ts" 2import type { SignedImageView, SignedMessageView, Message, Image } from "$lib/types.ts" 3export function getChannelWS(c: ChannelView): string | null { 4 const host = c.host 5 const uri = c.uri 6 let rkey = getRkeyFromUri(uri) 7 if (rkey == null) { 8 return null 9 } 10 return `wss://${host}/lrc/${rkey}/ws` 11 12} 13 14export function getChannelUrl(c: ChannelView): string | null { 15 const handle = c.creator.handle 16 const rkey = getRkeyFromUri(c.uri) 17 if (rkey == null) { 18 return null 19 } 20 return `/c/${handle}/${rkey}` 21} 22 23export function getChannelDeleteUrl(c: ChannelView): string | null { 24 const host = c.host 25 const uri = c.uri 26 let rkey = getRkeyFromUri(uri) 27 if (rkey == null) { 28 return null 29 } 30 return `https://${host}/lrc/${c.creator.did}/${rkey}/ws` 31} 32 33export function getRkeyFromUri(uri: string): string | null { 34 const matched = uri.match(/^at:\/\/[^/]+\/[^/]+\/([^/]+)$/) 35 return matched ? matched[1] : null 36} 37 38const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }) 39 40export function getNextCharBoundary(text: string, position: number) { 41 if (position >= text.length) return position; 42 const segments = Array.from(segmenter.segment(text.slice(position))); 43 return segments.length > 0 ? position + segments[0].segment.length : position; 44} 45 46export function getPrevCharBoundary(text: string, position: number) { 47 if (position <= 0) return 0; 48 const segments = Array.from(segmenter.segment(text.slice(0, position))); 49 return segments.length > 0 ? position - segments[segments.length - 1].segment.length : 0; 50} 51 52export function calculateMarginTop( 53 currentTime: number | null, 54 previousTime: number | null, 55) { 56 if (!previousTime || !currentTime) return 0; 57 const elapsedMs = currentTime - previousTime; 58 const elapsedMinutes = elapsedMs / (1000 * 60); 59 return Math.log(elapsedMinutes + 1); 60} 61 62export function signedImageViewToImage(sm: SignedImageView): Image { 63 return { 64 type: 'image', 65 id: sm.signet.lrcId, 66 lrcdata: { 67 muted: false, 68 mine: false 69 }, 70 signetView: sm.signet, 71 mediaView: { 72 $type: sm.$type, 73 uri: sm.uri, 74 author: sm.author, 75 imageView: sm.imageView, 76 ...(sm.nick && { nick: sm.nick }), 77 ...(sm.color && { color: sm.color }), 78 signetURI: sm.signet.uri, 79 postedAt: sm.postedAt 80 } 81 } 82} 83 84export function signedMessageViewToMessage(sm: SignedMessageView): Message { 85 return { 86 type: 'message', 87 id: sm.signet.lrcId, 88 lrcdata: { 89 body: "", 90 muted: false, 91 mine: false 92 }, 93 signetView: sm.signet, 94 messageView: { 95 $type: sm.$type, 96 uri: sm.uri, 97 author: sm.author, 98 body: sm.body, 99 ...(sm.nick && { nick: sm.nick }), 100 ...(sm.color && { color: sm.color }), 101 signetURI: sm.signet.uri, 102 postedAt: sm.postedAt 103 } 104 } 105} 106export function sanitizeHandle(input: string) { 107 return input 108 .normalize('NFKC') // Unicode normalization 109 .replace(/[\u0000-\u001F\u007F-\u009F]/g, '') // Control characters 110 .replace(/[\u200B-\u200F\u202A-\u202E\u2060-\u206F]/g, '') // Invisible/directional 111 .replace(/[\uFEFF]/g, '') // Byte order mark 112 .trim(); 113} 114 115export function smartAbsoluteTimestamp(then: number): string { 116 const now = Date.now() 117 try { 118 if (then > now) { 119 return "in the future" 120 } else if (now - then < 1000 * 60 * 60 * 18) { 121 const formatter = new Intl.DateTimeFormat("en-us", { hour: "numeric", minute: "numeric" }) 122 return formatter.format(then).toLocaleLowerCase() 123 } else if (now - then < 1000 * 60 * 60 * 24 * 6) { 124 const formatter = new Intl.DateTimeFormat("en-us", { weekday: "long", dayPeriod: "long" }) 125 return formatter.format(then).toLocaleLowerCase() 126 } else if (now - then < 1000 * 60 * 60 * 24 * 333) { 127 const formatter1 = new Intl.DateTimeFormat("en-us", { weekday: "long" }) 128 const formatter2 = new Intl.DateTimeFormat("en-us", { month: "long" }) 129 const formatter3 = new Intl.DateTimeFormat("en-us", { dayPeriod: "long" }) 130 return `a ${formatter1.format(then)} in ${formatter2.format(then)} ${formatter3.format(then)}`.toLocaleLowerCase() 131 } else { 132 const formatter1 = new Intl.DateTimeFormat("en-us", { weekday: "long" }) 133 const formatter2 = new Intl.DateTimeFormat("en-us", { month: "long" }) 134 const formatter3 = new Intl.DateTimeFormat("en-us", { year: "numeric", dayPeriod: "long" }) 135 return `a ${formatter1.format(then)} in ${formatter2.format(then)} ${formatter3.format(then)}`.toLocaleLowerCase() 136 } 137 } catch { 138 return `sometime who cares` 139 } 140} 141 142export function dumbAbsoluteTimestamp(then: number): string { 143 return (new Date(then)).toString() 144}