frontend for xcvr appview
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}