expo-popcore-app/app/(tabs)/message.tsx

443 lines
17 KiB
TypeScript

import { useState, useEffect } from 'react'
import {
View,
Text,
StyleSheet,
ScrollView,
StatusBar as RNStatusBar,
Pressable,
RefreshControl,
NativeScrollEvent,
NativeSyntheticEvent,
} from 'react-native'
import { LinearGradient } from 'expo-linear-gradient'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useTranslation } from 'react-i18next'
import { useMessages, type Message } from '@/hooks/use-messages'
import { useMessageActions } from '@/hooks/use-message-actions'
import LoadingState from '@/components/LoadingState'
import ErrorState from '@/components/ErrorState'
import PaginationLoader from '@/components/PaginationLoader'
const filterMessagesByTab = (messages: Message[], tab: 'all' | 'notice' | 'other') => {
if (tab === 'all') return messages
if (tab === 'notice') {
return messages.filter(m => m.type === 'SYSTEM' || m.type === 'ACTIVITY')
}
return messages.filter(m => m.type === 'BILLING' || m.type === 'MARKETING')
}
const formatTime = (date: Date) => {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
export default function MessageScreen() {
const { t } = useTranslation()
const [activeTab, setActiveTab] = useState<'all' | 'notice' | 'other'>('all')
const { markRead } = useMessageActions()
const { messages: allMessages, loading, loadingMore, error, refetch, loadMore, hasMore, execute } = useMessages({
limit: 20,
})
const messages = filterMessagesByTab(allMessages, activeTab)
useEffect(() => {
execute()
}, [])
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent
const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - 50
if (isCloseToBottom && hasMore && !loadingMore && !loading) {
loadMore()
}
}
const handleMessagePress = async (message: Message) => {
if (!message.isRead) {
await markRead(message.id)
refetch()
}
}
if (loading && messages.length === 0) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<View style={styles.segment}>
<Pressable onPress={() => setActiveTab('all')}>
{activeTab === 'all' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.all')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.all')}</Text>
)}
</Pressable>
<Pressable onPress={() => setActiveTab('notice')}>
{activeTab === 'notice' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.notice')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.notice')}</Text>
)}
</Pressable>
<Pressable onPress={() => setActiveTab('other')}>
{activeTab === 'other' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.other')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.other')}</Text>
)}
</Pressable>
</View>
<LoadingState color="#FFFFFF" />
</SafeAreaView>
)
}
if (error && messages.length === 0) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<View style={styles.segment}>
<Pressable onPress={() => setActiveTab('all')}>
{activeTab === 'all' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.all')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.all')}</Text>
)}
</Pressable>
<Pressable onPress={() => setActiveTab('notice')}>
{activeTab === 'notice' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.notice')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.notice')}</Text>
)}
</Pressable>
<Pressable onPress={() => setActiveTab('other')}>
{activeTab === 'other' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text style={[styles.segmentText, styles.segmentTextActiveText]}>
{t('message.other')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.other')}</Text>
)}
</Pressable>
</View>
<ErrorState message={error.message} onRetry={refetch} />
</SafeAreaView>
)
}
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<View style={styles.segment}>
<Pressable onPress={() => setActiveTab('all')}>
{activeTab === 'all' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text
style={[
styles.segmentText,
styles.segmentTextActiveText,
]}
>
{t('message.all')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.all')}</Text>
)}
</Pressable>
<Pressable onPress={() => setActiveTab('notice')}>
{activeTab === 'notice' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text
style={[
styles.segmentText,
styles.segmentTextActiveText,
]}
>
{t('message.notice')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.notice')}</Text>
)}
</Pressable>
<Pressable onPress={() => setActiveTab('other')}>
{activeTab === 'other' ? (
<View style={styles.segmentTextActiveWrapper}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.segmentTextActiveBg}
/>
<Text
style={[
styles.segmentText,
styles.segmentTextActiveText,
]}
>
{t('message.other')}
</Text>
</View>
) : (
<Text style={styles.segmentText}>{t('message.other')}</Text>
)}
</Pressable>
</View>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
onScroll={handleScroll}
scrollEventThrottle={400}
refreshControl={
<RefreshControl
refreshing={loading && messages.length > 0}
onRefresh={refetch}
tintColor="#FFFFFF"
/>
}
>
{messages.length > 0 ? (
<>
{messages.map((message) => (
<Pressable
key={message.id}
onPress={() => handleMessagePress(message)}
>
<View style={styles.cardContainer}>
{!message.isRead && (
<View style={styles.newMessageDotContainer}>
<View style={styles.newMessageDot} />
</View>
)}
<Text style={styles.cardTitle}>
{message.title}
</Text>
<Text style={styles.cardSubtitle} numberOfLines={2}>
{message.content}
</Text>
<View style={styles.cardBody}>
<Text style={styles.cardBodyText}>
{message.data || ''}
</Text>
</View>
<Text style={styles.cardTime}>
{formatTime(message.createdAt)}
</Text>
</View>
</Pressable>
))}
{loadingMore && <PaginationLoader color="#FFFFFF" />}
</>
) : (
<View style={styles.emptyContainer}>
<Text style={styles.emptyIcon}>💭</Text>
<Text style={styles.emptyText}>{t('message.noMessages')}</Text>
</View>
)}
</ScrollView>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#090A0B',
},
scrollView: {
flex: 1,
backgroundColor: '#090A0B',
},
scrollContent: {
paddingHorizontal: 4,
paddingTop: 12,
},
segment: {
flexDirection: 'row',
paddingHorizontal: 8,
paddingTop: 19,
paddingBottom: 12,
backgroundColor: '#090A0B',
gap: 16,
},
segmentText: {
fontSize: 14,
color: '#FFFFFF',
},
segmentTextActiveWrapper: {
position: 'relative',
paddingBottom: 2,
justifyContent: 'flex-end',
alignSelf: 'flex-start',
},
segmentTextActiveBg: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
height: 10,
backgroundColor: '#FF9966',
},
segmentTextActiveText: {
zIndex: 1,
},
cardContainer: {
backgroundColor: '#16181B',
borderRadius: 16,
padding: 16,
marginBottom: 12,
marginHorizontal: 4,
position: 'relative',
},
newMessageDotContainer: {
position: 'absolute',
top: -4,
right: -2,
width: 16,
height: 16,
borderWidth: 4,
borderColor: '#090A0B',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
},
newMessageDot: {
width: 6,
height: 6,
borderRadius: 4,
backgroundColor: '#00FF66',
zIndex: 1,
},
cardTitle: {
color: '#FFFFFF',
fontSize: 15,
fontWeight: '600',
marginBottom: 8,
},
cardSubtitle: {
color: '#ABABAB',
fontSize: 11,
marginBottom: 16,
},
cardBody: {
backgroundColor: '#26292E',
borderRadius: 12,
height: 100,
marginBottom: 12,
padding: 8,
},
cardBodyText: {
color: '#FFFFFF',
fontSize: 12,
opacity: 0.8,
},
cardTime: {
color: '#8A8A8A',
fontSize: 10,
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: 200,
},
emptyIcon: {
fontSize: 48,
},
emptyText: {
color: '#8A8A8A',
fontSize: 12,
marginTop: 16,
},
})