utils.ts raw

   1  import {
   2    EMAIL_REGEX,
   3    EMBEDDED_EVENT_REGEX,
   4    EMBEDDED_MENTION_REGEX,
   5    EMOJI_REGEX,
   6    HASHTAG_REGEX,
   7    URL_REGEX,
   8    WS_URL_REGEX
   9  } from '@/constants'
  10  import { TEmoji } from '@/types'
  11  import { clsx, type ClassValue } from 'clsx'
  12  import { parseNativeEmoji } from 'emoji-picker-react/src/dataUtils/parseNativeEmoji'
  13  import { franc } from 'franc-min'
  14  import { twMerge } from 'tailwind-merge'
  15  
  16  export function cn(...inputs: ClassValue[]) {
  17    return twMerge(clsx(inputs))
  18  }
  19  
  20  export function isSafari() {
  21    if (typeof window === 'undefined' || !window.navigator) return false
  22    const ua = window.navigator.userAgent
  23    const vendor = window.navigator.vendor
  24    return /Safari/.test(ua) && /Apple Computer/.test(vendor) && !/Chrome/.test(ua)
  25  }
  26  
  27  export function isAndroid() {
  28    if (typeof window === 'undefined' || !window.navigator) return false
  29    const ua = window.navigator.userAgent
  30    return /android/i.test(ua)
  31  }
  32  
  33  export function isTorBrowser() {
  34    if (typeof window === 'undefined' || !window.navigator) return false
  35    const ua = window.navigator.userAgent
  36    return /torbrowser/i.test(ua)
  37  }
  38  
  39  export function isTouchDevice() {
  40    if (typeof window === 'undefined' || !window.navigator) return false
  41    return 'ontouchstart' in window || navigator.maxTouchPoints > 0
  42  }
  43  
  44  export function isInViewport(el: HTMLElement) {
  45    const rect = el.getBoundingClientRect()
  46    return (
  47      rect.top >= 0 &&
  48      rect.left >= 0 &&
  49      rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
  50      rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  51    )
  52  }
  53  
  54  export function isPartiallyInViewport(el: HTMLElement) {
  55    const rect = el.getBoundingClientRect()
  56    return (
  57      rect.top < (window.innerHeight || document.documentElement.clientHeight) &&
  58      rect.bottom > 0 &&
  59      rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
  60      rect.right > 0
  61    )
  62  }
  63  
  64  export function isSupportCheckConnectionType() {
  65    if (typeof window === 'undefined' || !(navigator as any).connection) return false
  66    return typeof (navigator as any).connection.type === 'string'
  67  }
  68  
  69  export function isEmail(email: string) {
  70    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  71  }
  72  
  73  export function isDevEnv() {
  74    return process.env.NODE_ENV === 'development'
  75  }
  76  
  77  export function detectLanguage(text?: string): string | null {
  78    if (!text) {
  79      return null
  80    }
  81    const cleanText = text
  82      .replace(URL_REGEX, '')
  83      .replace(WS_URL_REGEX, '')
  84      .replace(EMAIL_REGEX, '')
  85      .replace(EMBEDDED_MENTION_REGEX, '')
  86      .replace(EMBEDDED_EVENT_REGEX, '')
  87      .replace(HASHTAG_REGEX, '')
  88      .replace(EMOJI_REGEX, '')
  89      .trim()
  90  
  91    if (!cleanText) {
  92      return null
  93    }
  94  
  95    if (/[\u3040-\u309f\u30a0-\u30ff]/.test(cleanText)) {
  96      return 'ja'
  97    }
  98    if (/[\u0e00-\u0e7f]/.test(cleanText)) {
  99      return 'th'
 100    }
 101    if (/[\u4e00-\u9fff]/.test(cleanText)) {
 102      return 'zh'
 103    }
 104    if (/[\u0600-\u06ff]/.test(cleanText)) {
 105      return 'ar'
 106    }
 107    if (/[\u0590-\u05FF]/.test(cleanText)) {
 108      return 'fa'
 109    }
 110    if (/[\u0400-\u04ff]/.test(cleanText)) {
 111      return 'ru'
 112    }
 113    if (/[\u0900-\u097f]/.test(cleanText)) {
 114      return 'hi'
 115    }
 116  
 117    try {
 118      const detectedLang = franc(cleanText)
 119      const langMap: { [key: string]: string } = {
 120        ara: 'ar', // Arabic
 121        deu: 'de', // German
 122        eng: 'en', // English
 123        spa: 'es', // Spanish
 124        fas: 'fa', // Persian (Farsi)
 125        pes: 'fa', // Persian (alternative code)
 126        fra: 'fr', // French
 127        hin: 'hi', // Hindi
 128        hun: 'hu', // Hungarian
 129        ita: 'it', // Italian
 130        jpn: 'ja', // Japanese
 131        pol: 'pl', // Polish
 132        por: 'pt', // Portuguese
 133        rus: 'ru', // Russian
 134        cmn: 'zh', // Chinese (Mandarin)
 135        zho: 'zh' // Chinese (alternative code)
 136      }
 137  
 138      const normalizedLang = langMap[detectedLang]
 139      if (!normalizedLang) {
 140        return 'und'
 141      }
 142  
 143      return normalizedLang
 144    } catch {
 145      return 'und'
 146    }
 147  }
 148  
 149  export function parseEmojiPickerUnified(unified: string): string | TEmoji | undefined {
 150    if (unified.startsWith(':')) {
 151      const secondColonIndex = unified.indexOf(':', 1)
 152      if (secondColonIndex < 0) return undefined
 153  
 154      const shortcode = unified.slice(1, secondColonIndex)
 155      const url = unified.slice(secondColonIndex + 1)
 156      return { shortcode, url }
 157    } else {
 158      return parseNativeEmoji(unified)
 159    }
 160  }
 161