@@ -6,156 +6,213 @@ import Placeholder from "@tiptap/extension-placeholder";
6
6
import Starter from "@tiptap/starter-kit" ;
7
7
import { Mic , PlusIcon , Sticker , X } from "lucide-react" ;
8
8
import { AnimatePresence , motion , Variants } from "motion/react" ;
9
- import { useState } from "react" ;
9
+ import { useState , useRef } from "react" ;
10
10
11
- const transitionDebug = {
12
- type : "easeOut" ,
13
- duration : 0.2 ,
11
+ // Improved transition settings for smoother animations
12
+ const smoothTransition = {
13
+ type : "spring" ,
14
+ stiffness : 500 ,
15
+ damping : 40 ,
16
+ mass : 1 ,
17
+ } ;
18
+
19
+ // Animation variants
20
+ const messageVariants : Variants = {
21
+ initial : {
22
+ opacity : 0 ,
23
+ scale : 0.96 ,
24
+ y : 20 ,
25
+ } ,
26
+ animate : {
27
+ opacity : 1 ,
28
+ scale : 1 ,
29
+ y : 0 ,
30
+ transition : {
31
+ ...smoothTransition ,
32
+ delay : 0.05 ,
33
+ } ,
34
+ } ,
35
+ exit : {
36
+ opacity : 0 ,
37
+ scale : 0.96 ,
38
+ transition : { duration : 0.2 } ,
39
+ } ,
14
40
} ;
15
41
16
42
const selectedMessageVariants : Variants = {
17
43
initial : {
18
- y : 100 ,
44
+ y : 5 ,
45
+ opacity : 0 ,
19
46
} ,
20
47
animate : {
21
48
y : 0 ,
49
+ opacity : 1 ,
50
+ transition : {
51
+ ...smoothTransition ,
52
+ } ,
53
+ } ,
54
+ exit : {
55
+ y : - 5 ,
56
+ opacity : 0 ,
57
+ transition : { duration : 0.2 } ,
22
58
} ,
23
59
} ;
60
+
61
+ const containerVariants : Variants = {
62
+ collapsed : {
63
+ borderRadius : "28px" ,
64
+ } ,
65
+ expanded : {
66
+ borderRadius : "10px 10px 22px 22px" ,
67
+ transition : smoothTransition ,
68
+ } ,
69
+ } ;
70
+
71
+ interface Message {
72
+ id : number ;
73
+ text : string ;
74
+ }
75
+
24
76
const Imessage = ( ) => {
25
- const [ messages , setMessages ] = useState <
26
- {
27
- id : number ;
28
- text : string ;
29
- } [ ]
30
- > ( [ ] ) ;
77
+ const [ messages , setMessages ] = useState < Message [ ] > ( [ ] ) ;
31
78
const [ selectedMessage , setSelectedMessage ] = useState < string | null > ( null ) ;
79
+ const [ newMessage , setNewMessage ] = useState ( "" ) ;
80
+ const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
32
81
33
- const [ newMessage , setNewMessage ] = useState < string > ( "" ) ;
82
+ const scrollToBottom = ( ) => {
83
+ messagesEndRef . current ?. scrollIntoView ( { behavior : "smooth" } ) ;
84
+ } ;
34
85
35
86
const handleSubmit = ( ) => {
36
87
if ( newMessage . trim ( ) ) {
37
88
const timestamp = new Date ( ) . getTime ( ) ;
38
89
setMessages ( [ ...messages , { id : timestamp , text : newMessage } ] ) ;
39
90
setNewMessage ( "" ) ;
91
+
92
+ // Scroll to bottom on new message
93
+ setTimeout ( scrollToBottom , 100 ) ;
40
94
}
41
95
} ;
42
96
43
97
return (
44
98
< div className = "flex h-full flex-col items-end justify-end pb-6 mx-auto w-full gap-4" >
45
- < div className = "flex flex-col justify-end items-end gap-0.5 flex-1 h-full w-full overflow-hidden" >
46
- < AnimatePresence mode = "wait " >
99
+ < div className = "flex flex-col justify-end items-end gap-1 flex-1 h-full w-full overflow-y-auto overflow-x- hidden pr-1 pl-6 " >
100
+ < AnimatePresence mode = "popLayout " >
47
101
{ messages . map ( ( message ) => (
48
- < div
102
+ < motion . div
49
103
key = { message . id }
50
104
className = "flex flex-col items-end w-full"
51
105
onDoubleClick = { ( ) => setSelectedMessage ( message . text ) }
106
+ variants = { messageVariants }
107
+ initial = "initial"
108
+ animate = "animate"
109
+ exit = "exit"
110
+ layout
52
111
>
53
- < motion . div
54
- layoutId = { `container-[${ messages . length - 1 } ]` }
55
- transition = { transitionDebug }
56
- layout = "position"
57
- className = "px-3 py-2 text-base max-w-[80%] bg-background z-10 break-words rounded"
58
- >
112
+ < div className = "px-3 py-2 text-base max-w-[80%] bg-blue-500 text-white z-10 break-words rounded-2xl rounded-tr-sm" >
59
113
{ message . text }
60
- </ motion . div >
61
- </ div >
114
+ </ div >
115
+ </ motion . div >
62
116
) ) }
117
+ < div ref = { messagesEndRef } />
63
118
</ AnimatePresence >
64
119
</ div >
120
+
65
121
< motion . div
66
122
className = "w-full flex flex-col bg-background p-2 gap-2 relative overflow-hidden"
67
- animate = { {
68
- borderRadius : selectedMessage ? "10px 10px 22px 22px " : "28px" ,
69
- } }
123
+ variants = { containerVariants }
124
+ animate = { selectedMessage ? "expanded " : "collapsed" }
125
+ layout
70
126
>
71
127
< AnimatePresence >
72
128
{ selectedMessage && (
73
129
< motion . div
74
130
variants = { selectedMessageVariants }
75
131
initial = "initial"
76
132
animate = "animate"
77
- exit = "initial"
78
- className = "w-full bg-muted h-20 rounded-md relative p-2 z-0"
133
+ exit = "exit"
134
+ className = "w-full bg-muted h-20 rounded-md relative p-2 z-10"
135
+ layoutId = "selectedMessage"
79
136
>
80
137
< p className = "text-sm line-clamp-2 w-full text-muted-foreground" >
81
138
{ selectedMessage }
82
139
</ p >
83
140
< Button
84
- className = "absolute top-0 right-0 size-6 center"
141
+ className = "absolute top-1 right-1 size-6 center"
85
142
onClick = { ( ) => setSelectedMessage ( null ) }
86
143
>
87
144
< X className = "size-4" />
88
145
</ Button >
89
146
</ motion . div >
90
147
) }
91
148
</ AnimatePresence >
92
- < div className = "flex w-full items-end z-[9999999] bg-background" >
149
+
150
+ < motion . div
151
+ layoutId = "message-container-holder"
152
+ className = "flex w-full items-end z-20 bg-red-500 min-h-10 rounded-full "
153
+ >
93
154
< div className = "flex gap-0.5" >
94
- < Button >
155
+ < Button className = "" >
95
156
< PlusIcon className = "size-5" />
96
157
</ Button >
97
- < Button >
158
+ < Button className = "" >
98
159
< Sticker className = "size-5" />
99
160
</ Button >
100
161
</ div >
162
+
101
163
< Input setMessage = { setNewMessage } handleSubmit = { handleSubmit } />
102
- < motion . div
103
- key = { messages . length }
104
- layout = "position"
105
- className = "pointer-events-none absolute z-10 flex h-9 w-[80%] items-center overflow-hidden break-words rounded-full [word-break:break-word] "
106
- layoutId = { `container-[${ messages . length } ]` }
107
- transition = { transitionDebug }
108
- initial = { { opacity : 0.6 , zIndex : - 1 } }
109
- animate = { { opacity : 0.6 , zIndex : - 1 } }
110
- exit = { { opacity : 1 , zIndex : 1 } }
111
- >
112
- < div className = "" > { newMessage } </ div >
113
- </ motion . div >
114
- < Button className = "size-10 group hover:bg-[#1daa61] rounded-full center transition-all duration-300" >
115
- < Mic className = "" />
164
+
165
+ < Button className = "size-10 group hover:bg-[#1daa61] rounded-full flex items-center justify-center transition-all duration-300" >
166
+ < motion . div
167
+ whileTap = { { scale : 0.92 } }
168
+ transition = { smoothTransition }
169
+ >
170
+ < Mic />
171
+ </ motion . div >
116
172
</ Button >
117
- </ div >
173
+ </ motion . div >
118
174
</ motion . div >
119
175
</ div >
120
176
) ;
121
177
} ;
122
178
123
- const Button = ( {
124
- children,
125
- className,
126
- ...props
127
- } : React . ButtonHTMLAttributes < HTMLButtonElement > & {
179
+ interface ButtonProps {
128
180
children : React . ReactNode ;
129
- } ) => {
181
+ className ?: string ;
182
+ onClick ?: ( ) => void ;
183
+ }
184
+
185
+ const Button : React . FC < ButtonProps > = ( { children, className, ...props } ) => {
130
186
return (
131
- < button
132
- type = "submit "
187
+ < motion . button
188
+ type = "button "
133
189
className = { cn (
134
190
"flex size-10 items-center justify-center bg-background rounded-full hover:bg-muted transition-all duration-300" ,
135
191
className
136
192
) }
193
+ whileHover = { { backgroundColor : "hsl(var(--muted))" } }
194
+ whileTap = { { scale : 0.95 } }
195
+ transition = { smoothTransition }
137
196
{ ...props }
138
197
>
139
198
{ children }
140
- </ button >
199
+ </ motion . button >
141
200
) ;
142
201
} ;
143
202
144
- const Input = ( {
145
- setMessage,
146
- handleSubmit,
147
- } : {
203
+ interface InputProps {
148
204
setMessage : ( message : string ) => void ;
149
205
handleSubmit : ( ) => void ;
150
- } ) => {
206
+ }
207
+
208
+ const Input : React . FC < InputProps > = ( { setMessage, handleSubmit } ) => {
151
209
const editor = useEditor ( {
152
210
extensions : [
153
211
Starter ,
154
212
Placeholder . configure ( {
155
213
placeholder : "Type a message" ,
156
214
} ) ,
157
215
] ,
158
-
159
216
onUpdate : ( { editor } ) => {
160
217
setMessage ( editor . getText ( ) ) ;
161
218
} ,
@@ -168,11 +225,12 @@ const Input = ({
168
225
handleSubmit ( ) ;
169
226
}
170
227
} ;
228
+
171
229
return (
172
230
< div className = "flex flex-1 relative" >
173
231
< EditorContent
174
232
editor = { editor }
175
- className = "max-h-96 overflow-y-auto min-h-10 outline-none py-2 w-full px-1 bg-background"
233
+ className = "max-h-96 overflow-y-auto min-h-10 outline-none py-2 w-full px-3 bg-background rounded-2xl "
176
234
onKeyDown = { handleKeyDown }
177
235
/>
178
236
</ div >
0 commit comments