222 lines
5.4 KiB
TypeScript
222 lines
5.4 KiB
TypeScript
import {AtUri} from '../third-party/uri'
|
|
import {Entity} from '../third-party/api/src/client/types/app/bsky/feed/post'
|
|
import {PROD_SERVICE} from '../state'
|
|
|
|
export const MAX_DISPLAY_NAME = 64
|
|
export const MAX_DESCRIPTION = 256
|
|
|
|
export function pluralize(n: number, base: string, plural?: string): string {
|
|
if (n === 1) {
|
|
return base
|
|
}
|
|
if (plural) {
|
|
return plural
|
|
}
|
|
return base + 's'
|
|
}
|
|
|
|
export function makeRecordUri(
|
|
didOrName: string,
|
|
collection: string,
|
|
rkey: string,
|
|
) {
|
|
const urip = new AtUri(`at://host/`)
|
|
urip.host = didOrName
|
|
urip.collection = collection
|
|
urip.rkey = rkey
|
|
return urip.toString()
|
|
}
|
|
|
|
const MINUTE = 60
|
|
const HOUR = MINUTE * 60
|
|
const DAY = HOUR * 24
|
|
const MONTH = DAY * 30
|
|
const YEAR = DAY * 365
|
|
export function ago(date: number | string | Date): string {
|
|
let ts: number
|
|
if (typeof date === 'string') {
|
|
ts = Number(new Date(date))
|
|
} else if (date instanceof Date) {
|
|
ts = Number(date)
|
|
} else {
|
|
ts = date
|
|
}
|
|
const diffSeconds = Math.floor((Date.now() - ts) / 1e3)
|
|
if (diffSeconds < MINUTE) {
|
|
return `${diffSeconds}s`
|
|
} else if (diffSeconds < HOUR) {
|
|
return `${Math.floor(diffSeconds / MINUTE)}m`
|
|
} else if (diffSeconds < DAY) {
|
|
return `${Math.floor(diffSeconds / HOUR)}h`
|
|
} else if (diffSeconds < MONTH) {
|
|
return `${Math.floor(diffSeconds / DAY)}d`
|
|
} else if (diffSeconds < YEAR) {
|
|
return `${Math.floor(diffSeconds / MONTH)}mo`
|
|
} else {
|
|
return new Date(ts).toLocaleDateString()
|
|
}
|
|
}
|
|
|
|
export function extractEntities(
|
|
text: string,
|
|
knownHandles?: Set<string>,
|
|
): Entity[] | undefined {
|
|
let match
|
|
let ents: Entity[] = []
|
|
{
|
|
// mentions
|
|
const re = /(^|\s)(@)([a-zA-Z0-9\.-]+)(\b)/dg
|
|
while ((match = re.exec(text))) {
|
|
if (knownHandles && !knownHandles.has(match[3])) {
|
|
continue // not a known handle
|
|
} else if (!match[3].includes('.')) {
|
|
continue // probably not a handle
|
|
}
|
|
ents.push({
|
|
type: 'mention',
|
|
value: match[3],
|
|
index: {
|
|
start: match.indices[2][0], // skip the (^|\s) but include the '@'
|
|
end: match.indices[3][1],
|
|
},
|
|
})
|
|
}
|
|
}
|
|
{
|
|
// links
|
|
const re =
|
|
/(^|\s)((https?:\/\/[\S]+)|([a-z][a-z0-9]*(\.[a-z0-9]+)+[\S]*))(\b)/dg
|
|
while ((match = re.exec(text))) {
|
|
let value = match[2]
|
|
if (!value.startsWith('http')) {
|
|
value = `https://${value}`
|
|
}
|
|
ents.push({
|
|
type: 'link',
|
|
value,
|
|
index: {
|
|
start: match.indices[2][0], // skip the (^|\s)
|
|
end: match.indices[2][1],
|
|
},
|
|
})
|
|
}
|
|
}
|
|
return ents.length > 0 ? ents : undefined
|
|
}
|
|
|
|
interface DetectedLink {
|
|
link: string
|
|
}
|
|
type DetectedLinkable = string | DetectedLink
|
|
export function detectLinkables(text: string): DetectedLinkable[] {
|
|
const re =
|
|
/((^|\s)@[a-z0-9\.-]*)|((^|\s)https?:\/\/[\S]+)|((^|\s)[a-z][a-z0-9]*(\.[a-z0-9]+)+[\S]*)/gi
|
|
const segments = []
|
|
let match
|
|
let start = 0
|
|
while ((match = re.exec(text))) {
|
|
let matchIndex = match.index
|
|
let matchValue = match[0]
|
|
|
|
if (/\s/.test(matchValue)) {
|
|
// HACK
|
|
// skip the starting space
|
|
// we have to do this because RN doesnt support negative lookaheads
|
|
// -prf
|
|
matchIndex++
|
|
matchValue = matchValue.slice(1)
|
|
}
|
|
|
|
if (start !== matchIndex) {
|
|
segments.push(text.slice(start, matchIndex))
|
|
}
|
|
segments.push({link: matchValue})
|
|
start = matchIndex + matchValue.length
|
|
}
|
|
if (start < text.length) {
|
|
segments.push(text.slice(start))
|
|
}
|
|
return segments
|
|
}
|
|
|
|
export function makeValidHandle(str: string): string {
|
|
if (str.length > 20) {
|
|
str = str.slice(0, 20)
|
|
}
|
|
str = str.toLowerCase()
|
|
return str.replace(/^[^a-z]+/g, '').replace(/[^a-z0-9-]/g, '')
|
|
}
|
|
|
|
export function createFullHandle(name: string, domain: string): string {
|
|
name = name.replace(/[\.]+$/, '')
|
|
domain = domain.replace(/^[\.]+/, '')
|
|
return `${name}.${domain}`
|
|
}
|
|
|
|
export function enforceLen(str: string, len: number): string {
|
|
str = str || ''
|
|
if (str.length > len) {
|
|
return str.slice(0, len)
|
|
}
|
|
return str
|
|
}
|
|
|
|
export function cleanError(str: string): string {
|
|
if (str.includes('Network request failed')) {
|
|
return 'Unable to connect. Please check your internet connection and try again.'
|
|
}
|
|
if (str.startsWith('Error: ')) {
|
|
return str.slice('Error: '.length)
|
|
}
|
|
return str
|
|
}
|
|
|
|
export function toNiceDomain(url: string): string {
|
|
try {
|
|
const urlp = new URL(url)
|
|
if (`https://${urlp.host}` === PROD_SERVICE) {
|
|
return 'Bluesky Social'
|
|
}
|
|
return urlp.host
|
|
} catch (e) {
|
|
return url
|
|
}
|
|
}
|
|
|
|
export function toShortUrl(url: string): string {
|
|
try {
|
|
const urlp = new URL(url)
|
|
const shortened =
|
|
urlp.host +
|
|
(urlp.pathname === '/' ? '' : urlp.pathname) +
|
|
urlp.search +
|
|
urlp.hash
|
|
if (shortened.length > 30) {
|
|
return shortened.slice(0, 27) + '...'
|
|
}
|
|
return shortened
|
|
} catch (e) {
|
|
return url
|
|
}
|
|
}
|
|
|
|
export function toShareUrl(url: string): string {
|
|
if (!url.startsWith('https')) {
|
|
const urlp = new URL('https://bsky.app')
|
|
urlp.pathname = url
|
|
url = urlp.toString()
|
|
}
|
|
return url
|
|
}
|
|
|
|
export function convertBskyAppUrlIfNeeded(url: string): string {
|
|
if (url.startsWith('https://bsky.app/')) {
|
|
try {
|
|
const urlp = new URL(url)
|
|
return urlp.pathname
|
|
} catch (e) {
|
|
console.log('Unexpected error in convertBskyAppUrlIfNeeded()', e)
|
|
}
|
|
}
|
|
return url
|
|
}
|