Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
A
AppUms_Lecturer
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
tungnq
AppUms_Lecturer
Commits
804f4936
Commit
804f4936
authored
Sep 09, 2025
by
tungnq
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
IMPORTANT: Xử lý cử chỉ vuốt trong swipe list của màn hộp thư đến
parent
00653950
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
214 additions
and
54 deletions
+214
-54
style.js
src/screens/email/style.js
+35
-0
view.js
src/screens/email/view.js
+179
-54
No files found.
src/screens/email/style.js
View file @
804f4936
...
...
@@ -110,6 +110,41 @@ const styles = StyleSheet.create({
fontWeight
:
'600'
,
fontFamily
:
R
.
fonts
.
fontMedium
,
fontSize
:
R
.
fontsize
.
fontSizeContent
,
},
swipeContainer
:
{
position
:
'relative'
,
overflow
:
'hidden'
,
},
deleteBackground
:
{
position
:
'absolute'
,
right
:
2.5
,
top
:
5
,
bottom
:
5
,
width
:
95
,
backgroundColor
:
R
.
colors
.
red
,
borderRadius
:
25
,
justifyContent
:
'center'
,
alignItems
:
'center'
,
flexDirection
:
'column'
,
},
deleteButton
:
{
width
:
100
,
height
:
'100%'
,
justifyContent
:
'center'
,
alignItems
:
'center'
,
flexDirection
:
'column'
,
},
deleteIcon
:
{
width
:
24
,
height
:
24
,
tintColor
:
R
.
colors
.
white
,
marginBottom
:
4
,
},
deleteText
:
{
color
:
R
.
colors
.
white
,
fontSize
:
R
.
fontsize
.
fontSizeContent
,
fontFamily
:
R
.
fonts
.
fontMedium
,
fontWeight
:
'600'
,
}
})
...
...
src/screens/email/view.js
View file @
804f4936
import
React
from
'react'
;
import
React
,
{
useState
,
useRef
}
from
'react'
;
import
{
Text
,
View
,
...
...
@@ -7,6 +7,9 @@ import {
Image
,
ActivityIndicator
,
RefreshControl
,
Animated
,
PanResponder
,
Dimensions
,
}
from
'react-native'
;
import
styles
from
'./style'
;
import
FAB
from
'../../components/FAB/fab'
;
...
...
@@ -19,6 +22,15 @@ const EmailHomeView = props => {
const
{
groupedEmails
,
loading
,
onEmailPress
,
onRefresh
,
formatEmailDate
}
=
props
;
const
navigation
=
useNavigation
();
const
screenWidth
=
Dimensions
.
get
(
'window'
).
width
;
const
[
localGroupedEmails
,
setLocalGroupedEmails
]
=
useState
(
groupedEmails
);
const
[
swipedEmailId
,
setSwipedEmailId
]
=
useState
(
null
);
// Update local state when props change
React
.
useEffect
(()
=>
{
setLocalGroupedEmails
(
groupedEmails
);
},
[
groupedEmails
]);
if
(
loading
)
{
return
(
...
...
@@ -29,60 +41,173 @@ const EmailHomeView = props => {
);
}
const
renderEmailItem
=
({
item
})
=>
(
<
TouchableOpacity
style
=
{[
styles
.
emailItem
,
!
item
.
isRead
&&
styles
.
unreadEmail
]}
onPress
=
{()
=>
navigation
.
navigate
(
SCREENNAME
.
DETAILEMAIL
)}
>
<
View
style
=
{
styles
.
avatarContainer
}
>
<
View
style
=
{
styles
.
avatar
}
>
<
Text
style
=
{
styles
.
avatarText
}
>
{
item
.
sender
.
charAt
(
0
).
toUpperCase
()}
<
/Text
>
<
/View
>
<
/View
>
const
SwipeableEmailItem
=
({
item
,
onDelete
})
=>
{
const
translateX
=
useRef
(
new
Animated
.
Value
(
0
)).
current
;
const
deleteThreshold
=
screenWidth
*
0.3
;
<
View
style
=
{
styles
.
emailContent
}
>
<
View
style
=
{
styles
.
emailHeader
}
>
<
Text
style
=
{[
styles
.
senderName
,
!
item
.
isRead
&&
styles
.
unreadText
]}
>
{
item
.
sender
}
<
/Text
>
<
/View
>
<
Text
style
=
{[
styles
.
emailSubject
,
!
item
.
isRead
&&
styles
.
unreadText
]}
numberOfLines
=
{
1
}
>
{
item
.
subject
}
<
/Text
>
<
Text
style
=
{[
styles
.
emailPreview
,
!
item
.
isRead
&&
styles
.
unreadEmail
]}
numberOfLines
=
{
2
}
>
{
item
.
preview
}
<
/Text
>
<
/View
>
// Reset position when another email is swiped
React
.
useEffect
(()
=>
{
if
(
swipedEmailId
!==
null
&&
swipedEmailId
!==
item
.
id
)
{
Animated
.
spring
(
translateX
,
{
toValue
:
0
,
useNativeDriver
:
true
,
}).
start
();
}
},
[
swipedEmailId
]);
const
panResponder
=
PanResponder
.
create
({
onMoveShouldSetPanResponder
:
(
evt
,
gestureState
)
=>
{
// console.log('gestureState', gestureState);
const
moveX
=
Math
.
abs
(
gestureState
.
dx
);
console
.
log
(
'moveX'
,
moveX
);
const
isSwipingLeft
=
moveX
<
deleteThreshold
;
console
.
log
(
'isSwipingLeft'
,
isSwipingLeft
);
return
isSwipingLeft
;
},
onPanResponderMove
:
(
evt
,
gestureState
)
=>
{
console
.
log
(
'gestureState'
,
gestureState
.
dx
);
if
(
gestureState
.
dx
>
-
100
&&
gestureState
.
dx
<
0
)
{
translateX
.
setValue
(
-
100
);
}
else
if
(
gestureState
.
dx
>
0
){
translateX
.
setValue
(
0
);
}
},
// onPanResponderRelease: (evt, gestureState) => {
// console.log('deleteThreshold', -deleteThreshold);
// if (gestureState.dx < -deleteThreshold) {
// // Show delete button
// Animated.timing(translateX, {
// toValue: -100,
// duration: 300,
// useNativeDriver: true,
// }).start();
// setSwipedEmailId(item.id);
// }
// else {
// Animated.spring(translateX, {
// toValue: 0,
// useNativeDriver: true,
// }).start(() => {
// setSwipedEmailId(null);
// });
// }
// },
});
const
deleteOpacity
=
translateX
.
interpolate
({
inputRange
:
[
-
deleteThreshold
,
0
],
outputRange
:
[
1
,
0
],
extrapolate
:
'clamp'
,
});
const
handleDeletePress
=
()
=>
{
// Delete immediately without animation
onDelete
(
item
.
id
);
setSwipedEmailId
(
null
);
};
const
handleCancelPress
=
()
=>
{
// Snap back to original position
Animated
.
spring
(
translateX
,
{
toValue
:
0
,
useNativeDriver
:
true
,
}).
start
(()
=>
{
setSwipedEmailId
(
null
);
});
};
return
(
<
View
style
=
{
styles
.
swipeContainer
}
>
{
/* Delete background */
}
<
Animated
.
View
style
=
{[
styles
.
deleteBackground
,
{
opacity
:
deleteOpacity
}]}
>
<
TouchableOpacity
style
=
{
styles
.
deleteButton
}
onPress
=
{
handleDeletePress
}
>
<
Image
source
=
{
R
.
images
.
icDelete
}
style
=
{
styles
.
deleteIcon
}
/
>
<
Text
style
=
{
styles
.
deleteText
}
>
X
ó
a
<
/Text
>
<
/TouchableOpacity
>
<
/Animated.View
>
{
/* Email item */
}
<
Animated
.
View
style
=
{[{
transform
:
[{
translateX
}]}]}
{...
panResponder
.
panHandlers
}
>
<
TouchableOpacity
style
=
{[
styles
.
emailItem
,
!
item
.
isRead
&&
styles
.
unreadEmail
]}
onPress
=
{()
=>
navigation
.
navigate
(
SCREENNAME
.
DETAILEMAIL
)}
>
<
View
style
=
{
styles
.
avatarContainer
}
>
<
View
style
=
{
styles
.
avatar
}
>
<
Text
style
=
{
styles
.
avatarText
}
>
{
item
.
sender
.
charAt
(
0
).
toUpperCase
()}
<
/Text
>
<
/View
>
<
/View
>
<
View
style
=
{
styles
.
emailContent
}
>
<
View
style
=
{
styles
.
emailHeader
}
>
<
Text
style
=
{[
styles
.
senderName
,
!
item
.
isRead
&&
styles
.
unreadText
]}
>
{
item
.
sender
}
<
/Text
>
<
/View
>
<
Text
style
=
{[
styles
.
emailSubject
,
!
item
.
isRead
&&
styles
.
unreadText
]}
numberOfLines
=
{
1
}
>
{
item
.
subject
}
<
/Text
>
<
Text
style
=
{[
styles
.
emailPreview
,
!
item
.
isRead
&&
styles
.
unreadEmail
]}
numberOfLines
=
{
2
}
>
{
item
.
preview
}
<
/Text
>
<
/View
>
<
View
style
=
{{
justifyContent
:
'center'
,
alignItems
:
'flex-end'
,
position
:
'absolute'
,
right
:
15
,
top
:
10
,
}}
>
<
Text
style
=
{
styles
.
emailDate
}
>
{
`
${
item
.
date
.
getDate
().
toString
().
padStart
(
2
,
'0'
)}
/
${(
item
.
date
.
getMonth
()
+
1
)
.
toString
()
.
padStart
(
2
,
'0'
)}
/
${
item
.
date
.
getFullYear
()}
`
}
<
/Text
>
{
item
.
hasAttachment
&&
(
<
View
style
=
{
styles
.
attachmentIndicator
}
>
<
Text
style
=
{
styles
.
attachmentCount
}
>
2
<
/Text
>
<
/View
>
)}
<
View
style
=
{{
justifyContent
:
'center'
,
alignItems
:
'flex-end'
,
position
:
'absolute'
,
right
:
15
,
top
:
10
,
}}
>
<
Text
style
=
{
styles
.
emailDate
}
>
{
`
${
item
.
date
.
getDate
().
toString
().
padStart
(
2
,
'0'
)}
/
${(
item
.
date
.
getMonth
()
+
1
)
.
toString
()
.
padStart
(
2
,
'0'
)}
/
${
item
.
date
.
getFullYear
()}
`
}
<
/Text
>
{
item
.
hasAttachment
&&
(
<
View
style
=
{
styles
.
attachmentIndicator
}
>
<
Text
style
=
{
styles
.
attachmentCount
}
>
2
<
/Text
>
<
/View
>
)}
<
/View
>
<
/TouchableOpacity
>
<
/Animated.View
>
<
/View
>
<
/TouchableOpacity
>
);
};
const
handleDeleteEmail
=
(
emailId
)
=>
{
// Remove email from local state temporarily
const
updatedGroupedEmails
=
{};
Object
.
keys
(
localGroupedEmails
).
forEach
(
dateKey
=>
{
const
filteredEmails
=
localGroupedEmails
[
dateKey
].
filter
(
email
=>
email
.
id
!==
emailId
);
if
(
filteredEmails
.
length
>
0
)
{
updatedGroupedEmails
[
dateKey
]
=
filteredEmails
;
}
});
setLocalGroupedEmails
(
updatedGroupedEmails
);
};
const
renderEmailItem
=
({
item
})
=>
(
<
SwipeableEmailItem
item
=
{
item
}
onDelete
=
{
handleDeleteEmail
}
/
>
);
const
renderDateSection
=
(
dateLabel
,
emailsForDate
)
=>
(
...
...
@@ -98,10 +223,10 @@ const EmailHomeView = props => {
<
View
style
=
{
styles
.
container
}
>
<
View
style
=
{
styles
.
body
}
>
<
FlatList
data
=
{
Object
.
keys
(
g
roupedEmails
)}
data
=
{
Object
.
keys
(
localG
roupedEmails
)}
keyExtractor
=
{
item
=>
item
}
renderItem
=
{({
item
:
dateLabel
})
=>
renderDateSection
(
dateLabel
,
g
roupedEmails
[
dateLabel
])
renderDateSection
(
dateLabel
,
localG
roupedEmails
[
dateLabel
])
}
refreshControl
=
{
<
RefreshControl
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment