mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at typescript-check 267 lines 6.7 kB view raw
1import {AtUri} from '../third-party/uri' 2import {AppBskyFeedPost} from '@atproto/api' 3type Entity = AppBskyFeedPost.Entity 4import {PROD_SERVICE} from '../state' 5import {isNetworkError} from './errors' 6import TLDs from 'tlds' 7 8export const MAX_DISPLAY_NAME = 64 9export const MAX_DESCRIPTION = 256 10 11export function pluralize(n: number, base: string, plural?: string): string { 12 if (n === 1) { 13 return base 14 } 15 if (plural) { 16 return plural 17 } 18 return base + 's' 19} 20 21export function makeRecordUri( 22 didOrName: string, 23 collection: string, 24 rkey: string, 25) { 26 const urip = new AtUri('at://host/') 27 urip.host = didOrName 28 urip.collection = collection 29 urip.rkey = rkey 30 return urip.toString() 31} 32 33const MINUTE = 60 34const HOUR = MINUTE * 60 35const DAY = HOUR * 24 36const MONTH = DAY * 30 37const YEAR = DAY * 365 38export function ago(date: number | string | Date): string { 39 let ts: number 40 if (typeof date === 'string') { 41 ts = Number(new Date(date)) 42 } else if (date instanceof Date) { 43 ts = Number(date) 44 } else { 45 ts = date 46 } 47 const diffSeconds = Math.floor((Date.now() - ts) / 1e3) 48 if (diffSeconds < MINUTE) { 49 return `${diffSeconds}s` 50 } else if (diffSeconds < HOUR) { 51 return `${Math.floor(diffSeconds / MINUTE)}m` 52 } else if (diffSeconds < DAY) { 53 return `${Math.floor(diffSeconds / HOUR)}h` 54 } else if (diffSeconds < MONTH) { 55 return `${Math.floor(diffSeconds / DAY)}d` 56 } else if (diffSeconds < YEAR) { 57 return `${Math.floor(diffSeconds / MONTH)}mo` 58 } else { 59 return new Date(ts).toLocaleDateString() 60 } 61} 62 63export function isValidDomain(str: string): boolean { 64 return !!TLDs.find(tld => { 65 let i = str.lastIndexOf(tld) 66 if (i === -1) { 67 return false 68 } 69 return str.charAt(i - 1) === '.' && i === str.length - tld.length 70 }) 71} 72 73export function extractEntities( 74 text: string, 75 knownHandles?: Set<string>, 76): Entity[] | undefined { 77 let match 78 let ents: Entity[] = [] 79 { 80 // mentions 81 const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g 82 while ((match = re.exec(text))) { 83 if (knownHandles && !knownHandles.has(match[3])) { 84 continue // not a known handle 85 } else if (!match[3].includes('.')) { 86 continue // probably not a handle 87 } 88 const start = text.indexOf(match[3], match.index) - 1 89 ents.push({ 90 type: 'mention', 91 value: match[3], 92 index: {start, end: start + match[3].length + 1}, 93 }) 94 } 95 } 96 { 97 // links 98 const re = 99 /(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim 100 while ((match = re.exec(text))) { 101 let value = match[2] 102 if (!value.startsWith('http')) { 103 const domain = match.groups?.domain 104 if (!domain || !isValidDomain(domain)) { 105 continue 106 } 107 value = `https://${value}` 108 } 109 const start = text.indexOf(match[2], match.index) 110 const index = {start, end: start + match[2].length} 111 // strip ending puncuation 112 if (/[.,;!?]$/.test(value)) { 113 value = value.slice(0, -1) 114 index.end-- 115 } 116 if (/[)]$/.test(value) && !value.includes('(')) { 117 value = value.slice(0, -1) 118 index.end-- 119 } 120 ents.push({ 121 type: 'link', 122 value, 123 index, 124 }) 125 } 126 } 127 return ents.length > 0 ? ents : undefined 128} 129 130interface DetectedLink { 131 link: string 132} 133type DetectedLinkable = string | DetectedLink 134export function detectLinkables(text: string): DetectedLinkable[] { 135 const re = 136 /((^|\s|\()@[a-z0-9.-]*)|((^|\s|\()https?:\/\/[\S]+)|((^|\s|\()(?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi 137 const segments = [] 138 let match 139 let start = 0 140 while ((match = re.exec(text))) { 141 let matchIndex = match.index 142 let matchValue = match[0] 143 144 if (match.groups?.domain && !isValidDomain(match.groups?.domain)) { 145 continue 146 } 147 148 if (/\s|\(/.test(matchValue)) { 149 // HACK 150 // skip the starting space 151 // we have to do this because RN doesnt support negative lookaheads 152 // -prf 153 matchIndex++ 154 matchValue = matchValue.slice(1) 155 } 156 157 // strip ending puncuation 158 if (/[.,;!?]$/.test(matchValue)) { 159 matchValue = matchValue.slice(0, -1) 160 } 161 if (/[)]$/.test(matchValue) && !matchValue.includes('(')) { 162 matchValue = matchValue.slice(0, -1) 163 } 164 165 if (start !== matchIndex) { 166 segments.push(text.slice(start, matchIndex)) 167 } 168 segments.push({link: matchValue}) 169 start = matchIndex + matchValue.length 170 } 171 if (start < text.length) { 172 segments.push(text.slice(start)) 173 } 174 return segments 175} 176 177export function makeValidHandle(str: string): string { 178 if (str.length > 20) { 179 str = str.slice(0, 20) 180 } 181 str = str.toLowerCase() 182 return str.replace(/^[^a-z]+/g, '').replace(/[^a-z0-9-]/g, '') 183} 184 185export function createFullHandle(name: string, domain: string): string { 186 name = (name || '').replace(/[.]+$/, '') 187 domain = (domain || '').replace(/^[.]+/, '') 188 return `${name}.${domain}` 189} 190 191export function enforceLen(str: string, len: number, ellipsis = false): string { 192 str = str || '' 193 if (str.length > len) { 194 return str.slice(0, len) + (ellipsis ? '...' : '') 195 } 196 return str 197} 198 199export function cleanError(str: any): string { 200 if (!str) { 201 return str 202 } 203 if (typeof str !== 'string') { 204 str = str.toString() 205 } 206 if (isNetworkError(str)) { 207 return 'Unable to connect. Please check your internet connection and try again.' 208 } 209 if (str.startsWith('Error: ')) { 210 return str.slice('Error: '.length) 211 } 212 return str 213} 214 215export function toNiceDomain(url: string): string { 216 try { 217 const urlp = new URL(url) 218 if (`https://${urlp.host}` === PROD_SERVICE) { 219 return 'Bluesky Social' 220 } 221 return urlp.host 222 } catch (e) { 223 return url 224 } 225} 226 227export function toShortUrl(url: string): string { 228 try { 229 const urlp = new URL(url) 230 const shortened = 231 urlp.host + 232 (urlp.pathname === '/' ? '' : urlp.pathname) + 233 urlp.search + 234 urlp.hash 235 if (shortened.length > 30) { 236 return shortened.slice(0, 27) + '...' 237 } 238 return shortened 239 } catch (e) { 240 return url 241 } 242} 243 244export function toShareUrl(url: string): string { 245 if (!url.startsWith('https')) { 246 const urlp = new URL('https://bsky.app') 247 urlp.pathname = url 248 url = urlp.toString() 249 } 250 return url 251} 252 253export function isBskyAppUrl(url: string): boolean { 254 return url.startsWith('https://bsky.app/') 255} 256 257export function convertBskyAppUrlIfNeeded(url: string): string { 258 if (isBskyAppUrl(url)) { 259 try { 260 const urlp = new URL(url) 261 return urlp.pathname 262 } catch (e) { 263 console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e) 264 } 265 } 266 return url 267}