fix: chat page styles for dark theme

This commit is contained in:
Mahyar Khanbolooki
2025-08-21 22:00:14 +03:30
parent cd89fdf90e
commit cacfb17ec6
3 changed files with 548 additions and 451 deletions
+139 -116
View File
@@ -1,137 +1,160 @@
'use client'; 'use client'
import { ef } from '@/lib/helpers/utfNumbers'; import { ef } from '@/lib/helpers/utfNumbers'
import { EmojiHappy, Microphone2, Paperclip2 } from 'iconsax-react'; import { EmojiHappy, Microphone2, Paperclip2 } from 'iconsax-react'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
type Props = object type Props = object
type MessageModel = { type MessageModel = {
type: "sender" | "receiver", type: 'sender' | 'receiver'
senderName: string, senderName: string
content: string, content: string
date: string, date: string
time: string, time: string
} }
function ChatIndex({ }: Props) { function ChatIndex ({}: Props) {
const { t } = useTranslation('chat')
const { t } = useTranslation('chat'); const [messages, setMessages] = useState<Array<MessageModel>>([
{
const [messages, setMessages] = useState<Array<MessageModel>>([ type: 'sender',
{ senderName: 'علی مصلحی',
type: "sender", content:
senderName: "علی مصلحی", 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است',
content: 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است', date: '1403/09/30',
date: '1403/09/30', time: '10:07'
time: '10:07' },
}, {
{ type: 'receiver',
type: 'receiver', senderName: 'سیما فرهادی',
senderName: 'سیما فرهادی', content:
content: 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است', 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است',
date: '1403/09/30', date: '1403/09/30',
time: '10:07' time: '10:07'
},
]);
const addMessageOutgoing = (model: MessageModel) => {
setMessages((previous) => {
return [...previous, model]
})
} }
])
const submitMessage = (e: React.FormEvent<HTMLFormElement>) => { const addMessageOutgoing = (model: MessageModel) => {
e.preventDefault(); setMessages(previous => {
const formData = new FormData(e.currentTarget); return [...previous, model]
const content = formData.get('textMessage'); })
if (!content) return; }
const model: MessageModel = {
type: 'sender',
senderName: 'علی مصلحی',
content: String(content),
date: '1403/09/30',
time: '10:07'
}
addMessageOutgoing(model);
};
const submitMessage = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const content = formData.get('textMessage')
if (!content) return
const model: MessageModel = {
type: 'sender',
senderName: 'علی مصلحی',
content: String(content),
date: '1403/09/30',
time: '10:07'
}
addMessageOutgoing(model)
}
const messagesMemo = useMemo(() => messages, [messages]); const messagesMemo = useMemo(() => messages, [messages])
return ( return (
<section className="flex flex-col h-full pt-6"> <section className='flex flex-col h-full pt-6'>
<h1 className="font-medium">علی مصلحی</h1> <h1 className='font-medium'>علی مصلحی</h1>
<div className="bg-container rounded-[30px] flex flex-col justify-between mt-6 flex-1 px-6 pt-7 pb-8 overflow-hidden"> <div className='bg-container rounded-[30px] flex flex-col justify-between mt-6 flex-1 px-6 pt-7 pb-8 overflow-hidden'>
<div className="overflow-y-auto flex flex-col-reverse"> <div className='overflow-y-auto flex flex-col-reverse'>
<ul className="grid gap-6 pb-6 "> <ul className='grid gap-6 pb-6 '>
{messagesMemo.map((v, i) => { {messagesMemo.map((v, i) => {
if (v.type === "sender") { if (v.type === 'sender') {
return ( return (
<li key={i} className='bg-[#F6F7FA] rounded-[30px] rounded-tr-none p-6'> <li
<article> key={i}
<p className='text-xs2'> className='bg-[#F6F7FA] dark:bg-gray-700 rounded-[30px] rounded-tr-none p-6'
{v.content} >
</p> <article>
<p className='text-xs2'>{v.content}</p>
<span dir='ltr' className='text-[11px] float-end text-disabled-text mt-2.5'> <span
{ef(`${v.time} | ${v.date}`)} dir='ltr'
</span> className='text-[11px] float-end text-disabled-text mt-2.5'
</article> >
</li> {ef(`${v.time} | ${v.date}`)}
) </span>
} </article>
else { </li>
return ( )
<li key={i} className='bg-[#EBEDF5] rounded-[30px] rounded-tl-none p-6'> } else {
<article> return (
<h6 className='text-xs font-medium'> <li
{v.senderName} key={i}
</h6> className='bg-[#EBEDF5] dark:bg-gray-600 rounded-[30px] rounded-tl-none p-6'
<p className='text-xs2 mt-2.5'> >
{v.content} <article>
</p> <h6 className='text-xs font-medium'>{v.senderName}</h6>
<p className='text-xs2 mt-2.5'>{v.content}</p>
<span dir='ltr' className='text-[11px] float-end text-disabled-text mt-2.5'> <span
{ef(`${v.time} | ${v.date}`)} dir='ltr'
</span> className='text-[11px] float-end text-disabled-text mt-2.5'
</article> >
</li> {ef(`${v.time} | ${v.date}`)}
) </span>
} </article>
})} </li>
</ul> )
</div> }
})}
</ul>
</div>
<section aria-labelledby={t("InputMessage.AriaLabelBy")} className="w-full"> <section
<form aria-labelledby={t('InputMessage.AriaLabelBy')}
onSubmit={submitMessage} className='w-full'
className="focus-within:outline-blue-400 outline outline-transparent transition-colors duration-100 flex items-center gap-0 bg-transparent border border-[#D0D0D0] h-12 rounded-xl py-2.5 px-2" >
> <form
<button type="button" className="active:bg-neutral-100 transition-colors duration-150 rounded-full p-1"> onSubmit={submitMessage}
<Microphone2 className="stroke-foreground" size={24} /> className='focus-within:outline-blue-400 outline outline-transparent transition-colors duration-100 flex items-center gap-0 bg-transparent border border-border h-12 rounded-xl py-2.5 px-2'
</button> >
<input <button
name="textMessage" type='button'
role="textbox" className='active:bg-neutral-100 dark:active:bg-neutral-600 transition-colors duration-150 rounded-full p-1'
autoComplete="off" >
className="w-full text-[11px] h-full outline-none ms-1" <Microphone2 className='stroke-foreground' size={24} />
type="text" </button>
placeholder={t("InputMessage.Placeholder")} <input
/> name='textMessage'
<button type="button" className="active:bg-neutral-100 transition-colors duration-150 rounded-full p-1.5"> role='textbox'
<EmojiHappy className="stroke-disabled2" size={20} /> autoComplete='off'
</button> className='w-full text-[11px] h-full outline-none ms-1'
<button type="button" className="active:bg-neutral-100 transition-colors duration-150 rounded-full p-1.5"> type='text'
<Paperclip2 className="stroke-disabled2" size={20} /> placeholder={t('InputMessage.Placeholder')}
</button> />
</form> <button
</section> type='button'
</div> className='active:bg-neutral-100 dark:active:bg-neutral-600 transition-colors duration-150 rounded-full p-1.5'
>
<EmojiHappy
className='stroke-disabled2 dark:stroke-neutral-400'
size={20}
/>
</button>
<button
type='button'
className='active:bg-neutral-100 dark:active:bg-neutral-600 transition-colors duration-150 rounded-full p-1.5'
>
<Paperclip2
className='stroke-disabled2 dark:stroke-neutral-400'
size={20}
/>
</button>
</form>
</section> </section>
) </div>
</section>
)
} }
export default ChatIndex export default ChatIndex
+162 -118
View File
@@ -1,140 +1,184 @@
'use client'; 'use client'
import { motion } from 'framer-motion'; import { motion } from 'framer-motion'
import Image from 'next/image'; import Image from 'next/image'
import Link from 'next/link'; import Link from 'next/link'
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react'
const users = [ const users = [
{ id: 0, name: '1 علی مصلحی', table: 12, avatar: '/assets/images/user-avatar.png' }, {
{ id: 1, name: '2 علی مصلحی', table: 10, avatar: '/assets/images/user-avatar.png' }, id: 0,
{ id: 2, name: '3 علی مصلحی', table: 11, avatar: '/assets/images/user-avatar.png' }, name: '1 علی مصلحی',
{ id: 3, name: '4 علی مصلحی', table: 5, avatar: '/assets/images/user-avatar.png' }, table: 12,
{ id: 4, name: '5 علی مصلحی', table: 2, avatar: '/assets/images/user-avatar.png' }, avatar: '/assets/images/user-avatar.png'
{ id: 5, name: '6 علی مصلحی', table: 1, avatar: '/assets/images/user-avatar.png' }, },
]; {
id: 1,
name: '2 علی مصلحی',
table: 10,
avatar: '/assets/images/user-avatar.png'
},
{
id: 2,
name: '3 علی مصلحی',
table: 11,
avatar: '/assets/images/user-avatar.png'
},
{
id: 3,
name: '4 علی مصلحی',
table: 5,
avatar: '/assets/images/user-avatar.png'
},
{
id: 4,
name: '5 علی مصلحی',
table: 2,
avatar: '/assets/images/user-avatar.png'
},
{
id: 5,
name: '6 علی مصلحی',
table: 1,
avatar: '/assets/images/user-avatar.png'
}
]
function ChatNearby() { function ChatNearby () {
const containerRef = useRef<HTMLElement>(null); const containerRef = useRef<HTMLElement>(null)
const [positions, setPositions] = useState<{ top: number; left: number }[]>([]); const [positions, setPositions] = useState<{ top: number; left: number }[]>(
[]
)
useEffect(() => { useEffect(() => {
if (containerRef.current) { if (containerRef.current) {
const maxW = containerRef.current.offsetWidth - 50; const maxW = containerRef.current.offsetWidth - 50
const maxH = containerRef.current.offsetHeight - 180; const maxH = containerRef.current.offsetHeight - 180
const elementSize = 110; // approximate size of each user element const elementSize = 110 // approximate size of each user element
const padding = 80; // minimum distance between elements const padding = 80 // minimum distance between elements
const positions: { top: number; left: number }[] = []; const positions: { top: number; left: number }[] = []
const isTooClose = (top: number, left: number) => { const isTooClose = (top: number, left: number) => {
return positions.some(p => { return positions.some(p => {
const dx = p.left - left; const dx = p.left - left
const dy = p.top - top; const dy = p.top - top
const distance = Math.sqrt(dx * dx + dy * dy); const distance = Math.sqrt(dx * dx + dy * dy)
return distance < padding; return distance < padding
}); })
}; }
users.forEach(() => { users.forEach(() => {
let top = 0; let top = 0
let left = 0; let left = 0
let tries = 0; let tries = 0
do { do {
top = Math.random() * Math.abs(maxH - elementSize); top = Math.random() * Math.abs(maxH - elementSize)
left = Math.random() * Math.abs(maxW - elementSize); left = Math.random() * Math.abs(maxW - elementSize)
tries++; tries++
} while (isTooClose(top, left) && tries < 100); } while (isTooClose(top, left) && tries < 100)
positions.push({ top, left }); positions.push({ top, left })
}); })
setPositions(positions); setPositions(positions)
} }
}, []); }, [])
return (
<section ref={containerRef} className='h-svh relative'>
<section className='relative h-full w-full mt-7'>
<motion.div
className='w-[1145px] h-[1145px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
bg-[linear-gradient(180deg,#EAECF0_4.52%,#DBDEE8_95.02%)]
dark:bg-[linear-gradient(180deg,#1F2937_4.52%,#374151_95.02%)]'
style={{ willChange: 'transform' }}
></motion.div>
return ( <motion.div
<section ref={containerRef} className="h-svh relative">s animate={{ scale: [1, 0.95, 1] }}
<section className="relative h-full w-full mt-7"> transition={{ repeat: Infinity, duration: 13 }}
<motion.div className='w-[824px] h-[824px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
className='size-[1145px] rounded-full fixed top-1/2 left-1/2 -translate-1/2' bg-[linear-gradient(180deg,#EBF0FC_4.52%,#EBF0FC_95.02%)]
style={{ background: 'linear-gradient(180deg, #EAECF0 4.52%, #DBDEE8 95.02%)', willChange: 'transform' }} dark:bg-[linear-gradient(180deg,#111827_4.52%,#1E293B_95.02%)]'
> style={{ willChange: 'transform' }}
</motion.div> ></motion.div>
<motion.div <motion.div
animate={{ scale: [1, 0.95, 1] }} animate={{ scale: [0.9, 1] }}
transition={{ repeat: Infinity, duration: 13 }} transition={{ repeat: Infinity, duration: 4, repeatType: 'reverse' }}
className='size-[824px] rounded-full fixed top-1/2 left-1/2 -translate-1/2' className='w-[617px] h-[617px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
style={{ background: 'linear-gradient(180deg, #EBF0FC 4.52%, #EBF0FC 95.02%)', willChange: 'transform' }} bg-[linear-gradient(180deg,#E6EBFA_0%,#DFE2EC_100%)]
> dark:bg-[linear-gradient(180deg,#1E293B_0%,#111827_100%)]'
</motion.div> style={{ willChange: 'transform' }}
></motion.div>
<motion.div <motion.div
animate={{ scale: [0.9, 1] }} animate={{ scale: [1.02, 0.95] }}
transition={{ repeat: Infinity, duration: 4, repeatType: 'reverse' }} transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }}
className='size-[617px] rounded-full fixed top-1/2 left-1/2 -translate-1/2' className='w-[425px] h-[425px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
style={{ background: 'linear-gradient(180deg, #E6EBFA 0%, #DFE2EC 100%)', willChange: 'transform' }} bg-[linear-gradient(180deg,#E1E5EF_0%,#D8DDE8_60.2%,#D2D7E3_100%)]
> dark:bg-[linear-gradient(180deg,#1F2937_0%,#374151_60.2%,#1E293B_100%)]'
</motion.div> style={{ willChange: 'transform' }}
></motion.div>
<motion.div <motion.div
animate={{ scale: [1.02, 0.95] }} animate={{ scale: [1.05, 1] }}
transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }} transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }}
className='size-[425px] rounded-full fixed top-1/2 left-1/2 -translate-1/2' className='w-[292px] h-[292px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
style={{ background: 'linear-gradient(180deg, #E1E5EF 0%, #D8DDE8 60.2%, #D2D7E3 100%)', willChange: 'transform' }} bg-[linear-gradient(180deg,#EAEDF5_0%,#E4E7F0_46.52%,#DDDFEB_100%)]
> dark:bg-[linear-gradient(180deg,#111827_0%,#1E293B_46.52%,#1F2937_100%)]'
</motion.div> style={{ willChange: 'transform' }}
></motion.div>
<motion.div <motion.div
animate={{ scale: [1.05, 1] }} animate={{ scale: [1, 1.05] }}
transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }} transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }}
className='size-[292px] rounded-full fixed top-1/2 left-1/2 -translate-1/2' className='w-[174px] h-[174px] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
style={{ background: 'linear-gradient(180deg, #EAEDF5 0%, #E4E7F0 46.52%, #DDDFEB 100%)', willChange: 'transform' }} bg-[linear-gradient(180deg,#F3F7FF_0%,#F2F5FC_100%)]
> dark:bg-[linear-gradient(180deg,#1E293B_0%,#111827_100%)]'
</motion.div> style={{ willChange: 'transform' }}
></motion.div>
<motion.div <motion.div
animate={{ scale: [1, 1.05] }} animate={{ scale: [0, 1.2], opacity: [1, 0] }}
transition={{ repeat: Infinity, duration: 2, repeatType: 'reverse' }} transition={{ repeat: Infinity, duration: 2 }}
className='h-full w-[100dvh] rounded-full fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
outline outline-solid outline-blue-400'
style={{ willChange: 'transform, opacity' }}
></motion.div>
className='size-[174px] rounded-full fixed top-1/2 left-1/2 -translate-1/2' <div>
style={{ background: 'linear-gradient(180deg, #F3F7FF 0%, #F2F5FC 100%)', willChange: 'transform' }} {positions.length === users.length &&
> users.map((v, i) => (
</motion.div> <Link
href={`${v.id}`}
<motion.div prefetch={false}
animate={{ scale: [0, 1.2], opacity: [1, 0] }} key={v.id}
transition={{ repeat: Infinity, duration: 2 }} style={{
position: 'absolute',
className='h-full w-dvh rounded-full fixed top-1/2 left-1/2 -translate-1/2 outline outline-solid outline-blue-400' top: positions[i].top,
style={{ willChange: 'transform, opacity' }} left: positions[i].left
> }}
</motion.div> className='justify-items-center'
>
<div> <Image
{positions.length === users.length && height={46}
users.map((v, i) => ( width={46}
<Link alt='user avatar'
href={`${v.id}`} src={v.avatar}
prefetch={false} />
key={v.id} <h6 className='mt-2 text-xs font-medium'>{v.name}</h6>
style={{ position: 'absolute', top: positions[i].top, left: positions[i].left }} <div className='mt-[3px] text-xs2 font-medium bg-disabled-text dark:bg-neutral-800 text-accent rounded-[5px] px-[5px] py-[3px]'>
className="justify-items-center" میز {v.table}
>
<Image height={46} width={46} alt="user avatar" src={v.avatar} />
<h6 className="mt-2 text-xs font-medium">{v.name}</h6>
<div className="mt-[3px] text-xs2 font-medium bg-disabled-text text-accent rounded-[5px] px-[5px] py-[3px]">
میز {v.table}
</div>
</Link>
))}
</div> </div>
</section> </Link>
</section> ))}
); </div>
</section>
</section>
)
} }
export default ChatNearby; export default ChatNearby
+247 -217
View File
@@ -1,235 +1,265 @@
'use client'; 'use client'
import SearchBox from '@/components/input/SearchBox'; import SearchBox from '@/components/input/SearchBox'
import { ScrollArea } from '@/components/ui/scrollarea'; import { ScrollArea } from '@/components/ui/scrollarea'
import { Radar2, Trash } from 'iconsax-react'; import { Radar2, Trash } from 'iconsax-react'
import { Check, CheckCheck } from 'lucide-react'; import { Check, CheckCheck } from 'lucide-react'
import Image from 'next/image'; import Image from 'next/image'
import Link from 'next/link'; import Link from 'next/link'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
import { LongPressCallbackMeta, LongPressReactEvents, useLongPress } from 'use-long-press' import {
import { useRouter } from 'next/navigation'; LongPressCallbackMeta,
import useToggle from '@/hooks/helpers/useToggle'; LongPressReactEvents,
import Prompt from '@/components/utils/Prompt'; useLongPress
} from 'use-long-press'
import { useRouter } from 'next/navigation'
import useToggle from '@/hooks/helpers/useToggle'
import Prompt from '@/components/utils/Prompt'
type Props = object type Props = object
type ChatEntryModel = { type ChatEntryModel = {
id: string, id: string
username: string, username: string
avatarUrl: string, avatarUrl: string
update?: { update?: {
time: string, time: string
content: string, content: string
count?: number, count?: number
seen?: boolean seen?: boolean
} }
} }
function ChatIndex({ }: Props) { function ChatIndex ({}: Props) {
const { t } = useTranslation('chat'); const { t } = useTranslation('chat')
const { t: tDeleteModal } = useTranslation('common', { const { t: tDeleteModal } = useTranslation('common', {
keyPrefix: 'DeleteChatModal' keyPrefix: 'DeleteChatModal'
}); })
const [search, setSearch] = useState(''); const [search, setSearch] = useState('')
const [selectedChats, setSelectedChats] = useState<Array<string>>([]); const [selectedChats, setSelectedChats] = useState<Array<string>>([])
const router = useRouter(); const router = useRouter()
const { state: deleteModal, toggle: toggleDeleteModal } = useToggle(); const { state: deleteModal, toggle: toggleDeleteModal } = useToggle()
const [chatsList, setChatList] = useState<Array<ChatEntryModel>>([ const [chatsList, setChatList] = useState<Array<ChatEntryModel>>([
{ {
id: '0', id: '0',
username: 'علی مصلحی', username: 'علی مصلحی',
avatarUrl: '/assets/images/user-avatar.png', avatarUrl: '/assets/images/user-avatar.png',
update: { update: {
time: '12:03', time: '12:03',
content: 'چه خبر', content: 'چه خبر',
count: 2, count: 2
} }
}, },
{ {
id: '1', id: '1',
username: 'علی مصلحی', username: 'علی مصلحی',
avatarUrl: '/assets/images/user-avatar.png', avatarUrl: '/assets/images/user-avatar.png',
update: { update: {
time: '12:03', time: '12:03',
content: 'چه خبر', content: 'چه خبر',
count: 2, count: 2
} }
}, },
{ {
id: '2', id: '2',
username: 'علی مصلحی', username: 'علی مصلحی',
avatarUrl: '/assets/images/user-avatar.png', avatarUrl: '/assets/images/user-avatar.png',
update: { update: {
time: '12:03', time: '12:03',
content: 'چه خبر', content: 'چه خبر',
seen: true seen: true
} }
}, },
{ {
id: '3', id: '3',
username: 'علی مصلحی', username: 'علی مصلحی',
avatarUrl: '/assets/images/user-avatar.png', avatarUrl: '/assets/images/user-avatar.png',
update: { update: {
time: '12:03', time: '12:03',
content: 'چه خبر', content: 'چه خبر',
seen: true seen: true
} }
}, },
{ {
id: '4', id: '4',
username: 'علی مصلحی', username: 'علی مصلحی',
avatarUrl: '/assets/images/user-avatar.png', avatarUrl: '/assets/images/user-avatar.png',
update: { update: {
time: '12:03', time: '12:03',
content: 'چه خبر', content: 'چه خبر',
seen: true seen: true
} }
}, },
{ {
id: '5', id: '5',
username: 'علی مصلحی', username: 'علی مصلحی',
avatarUrl: '/assets/images/user-avatar.png', avatarUrl: '/assets/images/user-avatar.png',
update: { update: {
time: '12:03', time: '12:03',
content: 'چه خبر', content: 'چه خبر',
seen: false seen: false
} }
},
]);
const toggleDeleteModalVisiblity = (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
if (deleteModal) {
setSelectedChats([]);
}
toggleDeleteModal()
};
const deleteSelectedChats = () => {
if (setSelectedChats.length <= 0) return;
setChatList((prev) => {
return prev.filter(x => !selectedChats.includes(x.id));
})
toggleDeleteModalVisiblity();
} }
])
const longPressCallback = useCallback((e: LongPressReactEvents<Element>, ctx: LongPressCallbackMeta<unknown>) => { const toggleDeleteModalVisiblity = (e?: React.MouseEvent) => {
e.preventDefault(); e?.preventDefault()
if (!ctx?.context) return; e?.stopPropagation()
const id = String(ctx.context); if (deleteModal) {
setSelectedChats((prev) => { setSelectedChats([])
if (prev.includes(id)) return prev.filter(x => x !== id); }
return [...prev, id] toggleDeleteModal()
}
const deleteSelectedChats = () => {
if (setSelectedChats.length <= 0) return
setChatList(prev => {
return prev.filter(x => !selectedChats.includes(x.id))
})
toggleDeleteModalVisiblity()
}
const longPressCallback = useCallback(
(e: LongPressReactEvents<Element>, ctx: LongPressCallbackMeta<unknown>) => {
e.preventDefault()
if (!ctx?.context) return
const id = String(ctx.context)
setSelectedChats(prev => {
if (prev.includes(id)) return prev.filter(x => x !== id)
return [...prev, id]
})
},
[]
)
const longPressCancelledCallback = useCallback(
(id: string, reason?: string) => {
if (reason && reason !== 'cancelled-by-release') return
if (selectedChats.length > 0) {
setSelectedChats(prev => {
if (prev.includes(id)) return prev.filter(x => x !== id)
return [...prev, id]
}) })
}, []) } else {
router.push(`chat/${id}`)
}
},
[router, selectedChats.length]
)
const longPressCancelledCallback = useCallback((id: string, reason?: string) => { const longPress = useLongPress(longPressCallback, {
if (reason && reason !== 'cancelled-by-release') return; onCancel: (event, ctx) =>
if (selectedChats.length > 0) { longPressCancelledCallback(String(ctx.context), String(ctx.reason)),
setSelectedChats((prev) => { threshold: 500, // In milliseconds
if (prev.includes(id)) return prev.filter(x => x !== id); captureEvent: true, // Event won't get cleared after React finish processing it
return [...prev, id] cancelOnMovement: 100, // Square side size (in pixels) inside which movement won't cancel long press
}) cancelOutsideElement: true // Cancel long press when moved mouse / pointer outside element while pressing
} else { })
router.push(`chat/${id}`)
}
}, [router, selectedChats.length])
const longPress = useLongPress(longPressCallback, { return (
onCancel: (event, ctx) => longPressCancelledCallback(String(ctx.context), String(ctx.reason)), <div className='pt-8 mb-8'>
threshold: 500, // In milliseconds <section className='flex justify-between items-center gap-x-4'>
captureEvent: true, // Event won't get cleared after React finish processing it <SearchBox
cancelOnMovement: 100, // Square side size (in pixels) inside which movement won't cancel long press placeholder={t('InputSearch.Placeholder')}
cancelOutsideElement: true, // Cancel long press when moved mouse / pointer outside element while pressing value={search}
}); onChange={e => setSearch(e.target.value)}
/>
return ( {selectedChats.length > 0 ? (
<div className='pt-8 mb-8'> <span
<section className='flex justify-between items-center gap-x-4'> onClick={toggleDeleteModal}
<SearchBox placeholder={t('InputSearch.Placeholder')} value={search} onChange={(e) => setSearch(e.target.value)} /> className='bg-container p-2 rounded-xl active:bg-gray-50 dark:active:bg-neutral-500'
>
{selectedChats.length > 0 ? <Trash
<span onClick={toggleDeleteModal} className='bg-container p-2 rounded-xl active:bg-gray-50'> size={24}
<Trash size={24} className='stroke-primary' /> className='stroke-primary dark:stroke-disabled-text'
</span>
:
<Link href={'chat/nearby'} className='bg-container p-2 rounded-xl active:bg-gray-50'>
<Radar2 size={24} className='stroke-primary' />
</Link>
}
</section>
<section>
<ScrollArea dir='rtl' className='w-full h-full'>
<ul className='pt-4'>
{chatsList.map((chat, i) => {
return (
<li
key={i}
{...longPress(chat.id)}
className='select-none flex items-center gap-x-2 hover:brightness-105 cursor-default active:brightness-102 p-2 bg-background rounded-xl'
>
<div className='size-[49px] min-w-[49px]'>
{
selectedChats.includes(chat.id) ?
<div className='size-[49px] relative bg-primary rounded-full'>
<Check className='absolute top-1/2 left-1/2 -translate-1/2 stroke-white' />
</div>
:
<Image
src={chat.avatarUrl}
height={49}
width={49}
alt={`${chat.username}'s avatar`}
unoptimized
/>
}
</div>
<div className='flex flex-col justify-between w-full gap-2'>
<h5 className='text-sm2 font-medium'>{chat.username}</h5>
<p className='text-xs'>{chat.update?.content}</p>
</div>
<div className='flex flex-col justify-between items-end gap-2'>
<span className='text-sm2 text-disabled2'>{chat.update?.time}</span>
{chat.update?.count !== undefined && chat.update?.count > 0 ?
<span className='text-xs w-5 h-5 relative bg-foreground text-white rounded-full'>
<span className='absolute top-1/2 left-1/2 -translate-1/2 pt-1'>{chat.update?.count}</span>
</span>
:
chat.update?.seen !== undefined &&
chat.update?.seen ?
<CheckCheck className='stroke-foreground' size={16} /> :
<Check className='stroke-foreground' size={16} />
}
</div>
</li>
)
})}
</ul>
</ScrollArea>
</section>
<Prompt
title={tDeleteModal('Heading')}
description={tDeleteModal('Description').replace('{count}', String(selectedChats.length))}
textConfirm={tDeleteModal('ButtonOk')}
textCancel={tDeleteModal('ButtonCancel')}
onConfirm={deleteSelectedChats}
onCancel={toggleDeleteModalVisiblity}
onClick={toggleDeleteModalVisiblity}
visible={deleteModal}
/> />
</div> </span>
) ) : (
<Link
href={'chat/nearby'}
className='bg-container p-2 rounded-xl active:bg-gray-50 dark:active:bg-neutral-500'
>
<Radar2
size={24}
className='stroke-primary dark:stroke-disabled-text'
/>
</Link>
)}
</section>
<section>
<ScrollArea dir='rtl' className='w-full h-full'>
<ul className='pt-4'>
{chatsList.map((chat, i) => {
return (
<li
key={i}
{...longPress(chat.id)}
className='select-none flex items-center gap-x-2 hover:brightness-105 cursor-default active:brightness-102 p-2 bg-background rounded-xl'
>
<div className='size-[49px] min-w-[49px]'>
{selectedChats.includes(chat.id) ? (
<div className='size-[49px] relative bg-primary rounded-full'>
<Check className='absolute top-1/2 left-1/2 -translate-1/2 stroke-white' />
</div>
) : (
<Image
src={chat.avatarUrl}
height={49}
width={49}
alt={`${chat.username}'s avatar`}
unoptimized
/>
)}
</div>
<div className='flex flex-col justify-between w-full gap-2'>
<h5 className='text-sm2 font-medium'>{chat.username}</h5>
<p className='text-xs'>{chat.update?.content}</p>
</div>
<div className='flex flex-col justify-between items-end gap-2'>
<span className='text-sm2 text-disabled2'>
{chat.update?.time}
</span>
{chat.update?.count !== undefined &&
chat.update?.count > 0 ? (
<span className='text-xs w-5 h-5 relative bg-foreground dark:bg-neutral-700 text-white rounded-full'>
<span className='absolute top-1/2 left-1/2 -translate-1/2 pt-1'>
{chat.update?.count}
</span>
</span>
) : chat.update?.seen !== undefined && chat.update?.seen ? (
<CheckCheck className='stroke-foreground' size={16} />
) : (
<Check className='stroke-foreground' size={16} />
)}
</div>
</li>
)
})}
</ul>
</ScrollArea>
</section>
<Prompt
title={tDeleteModal('Heading')}
description={tDeleteModal('Description').replace(
'{count}',
String(selectedChats.length)
)}
textConfirm={tDeleteModal('ButtonOk')}
textCancel={tDeleteModal('ButtonCancel')}
onConfirm={deleteSelectedChats}
onCancel={toggleDeleteModalVisiblity}
onClick={toggleDeleteModalVisiblity}
visible={deleteModal}
/>
</div>
)
} }
export default ChatIndex export default ChatIndex