add: chat page selection and delete chat functionalities
This commit is contained in:
Generated
+10
@@ -36,6 +36,7 @@
|
|||||||
"react-i18next": "^15.6.1",
|
"react-i18next": "^15.6.1",
|
||||||
"react-virtualized": "^9.22.6",
|
"react-virtualized": "^9.22.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"use-long-press": "^3.3.0",
|
||||||
"zustand": "^5.0.6"
|
"zustand": "^5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -6620,6 +6621,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-long-press": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-long-press/-/use-long-press-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-Yedz46ILxsb1BTS6kUzpV/wyEZPUlJDq+8Oat0LP1eOZQHbS887baJHJbIGENqCo8wTKNxmoTHLdY8lU/e+wvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-sidecar": {
|
"node_modules/use-sidecar": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"react-i18next": "^15.6.1",
|
"react-i18next": "^15.6.1",
|
||||||
"react-virtualized": "^9.22.6",
|
"react-virtualized": "^9.22.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"use-long-press": "^3.3.0",
|
||||||
"zustand": "^5.0.6"
|
"zustand": "^5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+138
-42
@@ -1,13 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import Button from '@/components/button/PrimaryButton';
|
||||||
import SearchBox from '@/components/input/SearchBox';
|
import SearchBox from '@/components/input/SearchBox';
|
||||||
|
import BlurredOverlayContainer from '@/components/overlays/BlurredOverlayContainer';
|
||||||
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, { useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { LongPressCallbackMeta, LongPressReactEvents, useLongPress } from 'use-long-press'
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
type Props = object
|
type Props = object
|
||||||
|
|
||||||
@@ -25,10 +30,15 @@ type ChatEntryModel = {
|
|||||||
|
|
||||||
function ChatIndex({ }: Props) {
|
function ChatIndex({ }: Props) {
|
||||||
const { t } = useTranslation('chat');
|
const { t } = useTranslation('chat');
|
||||||
|
const { t: tDeleteModal } = useTranslation('common', {
|
||||||
|
keyPrefix: 'DeleteChatModal'
|
||||||
|
});
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [selectedChats, setSelectedChats] = useState([]);
|
const [selectedChats, setSelectedChats] = useState<Array<string>>([]);
|
||||||
|
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const chatsList: Array<ChatEntryModel> = [
|
const [chatsList, setChatList] = useState<Array<ChatEntryModel>>([
|
||||||
{
|
{
|
||||||
id: '0',
|
id: '0',
|
||||||
username: 'علی مصلحی',
|
username: 'علی مصلحی',
|
||||||
@@ -89,66 +99,152 @@ function ChatIndex({ }: Props) {
|
|||||||
seen: false
|
seen: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]);
|
||||||
|
|
||||||
|
const toggleDeleteModalVisiblity = (e?: React.MouseEvent) => {
|
||||||
|
e?.preventDefault();
|
||||||
|
e?.stopPropagation();
|
||||||
|
setDeleteModalVisible((state) => {
|
||||||
|
if (state) {
|
||||||
|
setSelectedChats([]);
|
||||||
|
}
|
||||||
|
return !state;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 longPress = useLongPress(longPressCallback, {
|
||||||
|
onCancel: (event, ctx) => longPressCancelledCallback(String(ctx.context), String(ctx.reason)),
|
||||||
|
threshold: 500, // In milliseconds
|
||||||
|
captureEvent: true, // Event won't get cleared after React finish processing it
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='pt-8 mb-8'>
|
<div className='pt-8 mb-8'>
|
||||||
<section className='flex justify-between items-center gap-x-4'>
|
<section className='flex justify-between items-center gap-x-4'>
|
||||||
<SearchBox placeholder={t('InputSearch.Placeholder')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
<SearchBox placeholder={t('InputSearch.Placeholder')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||||
|
|
||||||
<button className='bg-container p-2 rounded-xl active:bg-gray-50'>
|
{selectedChats.length > 0 ?
|
||||||
{selectedChats.length <= 0 ?
|
<span onClick={toggleDeleteModalVisiblity} className='bg-container p-2 rounded-xl active:bg-gray-50'>
|
||||||
<Radar2 size={24} className='stroke-primary' />
|
|
||||||
:
|
|
||||||
<Trash size={24} className='stroke-primary' />
|
<Trash size={24} className='stroke-primary' />
|
||||||
}
|
</span>
|
||||||
</button>
|
:
|
||||||
|
<Link href={'chat/nearby'} className='bg-container p-2 rounded-xl active:bg-gray-50'>
|
||||||
|
<Radar2 size={24} className='stroke-primary' />
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section aria-labelledby={t("ChatsList.Aria-LabelBy")}>
|
<section>
|
||||||
<ScrollArea dir='rtl' className='w-full h-full'>
|
<ScrollArea dir='rtl' className='w-full h-full'>
|
||||||
<ul className='pt-4'>
|
<ul className='pt-4'>
|
||||||
{chatsList.map((chat, i) => {
|
{chatsList.map((chat, i) => {
|
||||||
return (
|
return (
|
||||||
<li key={i}>
|
<li
|
||||||
<Link
|
key={i}
|
||||||
className='flex items-center gap-x-2 hover:brightness-105 cursor-default active:brightness-102 p-2 bg-background rounded-xl'
|
{...longPress(chat.id)}
|
||||||
href={`chat/${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'
|
||||||
>
|
>
|
||||||
<Image
|
<div className='size-[49px] min-w-[49px]'>
|
||||||
src={chat.avatarUrl}
|
{
|
||||||
height={49}
|
selectedChats.includes(chat.id) ?
|
||||||
width={49}
|
<div className='size-[49px] relative bg-primary rounded-full'>
|
||||||
alt={`${chat.username}'s avatar`}
|
<Check className='absolute top-1/2 left-1/2 -translate-1/2 stroke-white' />
|
||||||
unoptimized
|
</div>
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='flex flex-col justify-between w-full gap-2'>
|
|
||||||
<span className='text-sm2 font-medium'>{chat.username}</span>
|
|
||||||
<span className='text-xs'>{chat.update?.content}</span>
|
|
||||||
</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 &&
|
<Image
|
||||||
chat.update?.seen ?
|
src={chat.avatarUrl}
|
||||||
<CheckCheck className='stroke-foreground' size={16} /> :
|
height={49}
|
||||||
<Check className='stroke-foreground' size={16} />
|
width={49}
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
alt={`${chat.username}'s avatar`}
|
||||||
</Link>
|
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>
|
</li>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<BlurredOverlayContainer visible={deleteModalVisible} onClick={toggleDeleteModalVisiblity} inDuration={100} outDuration={100} outDelay={75}>
|
||||||
|
<div className="absolute top-1/2 left-0 w-full px-6">
|
||||||
|
<motion.div
|
||||||
|
animate={{ y: deleteModalVisible ? [10, 0] : [0, 10] }}
|
||||||
|
transition={{ duration: 0.1, ease: 'easeInOut' }}
|
||||||
|
className="top-1/2 absolute left-1/2 min-w-xs w-full max-w-sm text-center -translate-1/2 px-6 pt-8 pb-9 drop-shadow-container bg-white/63 rounded-4xl"
|
||||||
|
>
|
||||||
|
<h2 className="text-base font-medium">
|
||||||
|
{tDeleteModal('Heading')}
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs font-medium mt-6">
|
||||||
|
{tDeleteModal('Description')}
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-5.5 mt-8">
|
||||||
|
<Button onClick={deleteSelectedChats} className="text-sm font-normal">
|
||||||
|
{tDeleteModal('ButtonOk')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={toggleDeleteModalVisiblity} className="bg-disabled! text-disabled2! text-sm font-normal">
|
||||||
|
{tDeleteModal('ButtonCancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</BlurredOverlayContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"InputSearch": {
|
||||||
|
"Placeholder": "جستجو"
|
||||||
|
},
|
||||||
"InputMessage": {
|
"InputMessage": {
|
||||||
"AriaLabelBy": "ارسال پیام",
|
"AriaLabelBy": "ارسال پیام",
|
||||||
"Placeholder": "پیام خود را بنویسید"
|
"Placeholder": "پیام خود را بنویسید"
|
||||||
|
|||||||
@@ -29,5 +29,11 @@
|
|||||||
"ButtonOk": "بله خارج میشوم",
|
"ButtonOk": "بله خارج میشوم",
|
||||||
"ButtonCancel": "منصرف شدم"
|
"ButtonCancel": "منصرف شدم"
|
||||||
},
|
},
|
||||||
|
"DeleteChatModal": {
|
||||||
|
"Heading": "پاک کردن چت",
|
||||||
|
"Description": "آیا از پاک کردن {count} چت اطمینان دارید؟",
|
||||||
|
"ButtonOk": "بله پاک میکنم",
|
||||||
|
"ButtonCancel": "منصرف شدم"
|
||||||
|
},
|
||||||
"SearchPlaceholder": "جستجو"
|
"SearchPlaceholder": "جستجو"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user