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