fix(mobile): clear technical debt — real SHA-256, feed pagination, search
source_hash: BAS JSON import now computes SHA-256 via crypto.subtle.digest
instead of the '${id}-${length}' stub. No extra package — Hermes supports
Web Crypto API natively.
Feed pagination: useActivities(query, limit) accepts a LIMIT parameter.
The feed screen starts at 50, calls loadMore() via FlatList onEndReached
(threshold 0.3) to increment by 50 each time. useActivityCount(query)
drives the hasMore guard so loadMore is a no-op at the end of the list.
Feed search: compact TextInput below the header filters by title via
SQLite json_extract LIKE. Changing the query resets limit to PAGE_SIZE
so stale paginated results don't linger.
Docs: close the three resolved debt items; keep only the accepted
background-polling limitation as a known gap.
This commit is contained in:
@@ -3,16 +3,20 @@ import { useFocusEffect } from 'expo-router';
|
||||
import { useSQLiteContext } from 'expo-sqlite';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Alert, FlatList, Pressable, RefreshControl, StyleSheet, Text, View } from 'react-native';
|
||||
import { deleteActivities, useActivities, type ActivitySummary } from '@/db/queries';
|
||||
import { Alert, FlatList, Pressable, RefreshControl, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||
import { deleteActivities, useActivities, useActivityCount, PAGE_SIZE, type ActivitySummary } from '@/db/queries';
|
||||
import { downloadFeed, uploadFeed } from '@/db/sync';
|
||||
import { useTheme } from '@/ThemeContext';
|
||||
|
||||
export default function FeedScreen() {
|
||||
const db = useSQLiteContext();
|
||||
const theme = useTheme();
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const activities = useActivities();
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [limit, setLimit] = useState(PAGE_SIZE);
|
||||
const activities = useActivities(searchQuery, limit);
|
||||
const totalCount = useActivityCount(searchQuery);
|
||||
const hasMore = activities.length < totalCount;
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [statusMsg, setStatusMsg] = useState<{ ok: boolean; text: string } | null>(null);
|
||||
@@ -73,6 +77,15 @@ export default function FeedScreen() {
|
||||
setRefreshKey(k => k + 1);
|
||||
}
|
||||
|
||||
function handleSearch(q: string) {
|
||||
setSearchQuery(q);
|
||||
setLimit(PAGE_SIZE); // reset pagination when search changes
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (hasMore) setLimit(l => l + PAGE_SIZE);
|
||||
}
|
||||
|
||||
function toggleSelect(id: string) {
|
||||
setSelected(prev => {
|
||||
const next = new Set(prev);
|
||||
@@ -158,6 +171,20 @@ export default function FeedScreen() {
|
||||
<Text style={statusMsg.ok ? styles.msgOk : styles.msgErr}>{statusMsg.text}</Text>
|
||||
)}
|
||||
|
||||
{!selecting && (
|
||||
<View style={styles.searchRow}>
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
value={searchQuery}
|
||||
onChangeText={handleSearch}
|
||||
placeholder="Search activities…"
|
||||
placeholderTextColor="#52525b"
|
||||
returnKeyType="search"
|
||||
clearButtonMode="while-editing"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activities.length === 0 && !busy ? (
|
||||
<View style={styles.empty}>
|
||||
<Text style={styles.emptyIcon}>🚴</Text>
|
||||
@@ -181,6 +208,8 @@ export default function FeedScreen() {
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={styles.list}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0.3}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={false}
|
||||
@@ -321,6 +350,12 @@ const styles = StyleSheet.create({
|
||||
cancelText: { color: '#a1a1aa', fontSize: 13, fontWeight: '600' },
|
||||
msgOk: { color: '#86efac', fontSize: 12, textAlign: 'center', paddingHorizontal: 16, paddingBottom: 8 },
|
||||
msgErr: { color: '#fca5a5', fontSize: 12, textAlign: 'center', paddingHorizontal: 16, paddingBottom: 8 },
|
||||
searchRow: { paddingHorizontal: 16, paddingBottom: 10 },
|
||||
searchInput: {
|
||||
backgroundColor: '#18181b', borderWidth: 1, borderColor: '#27272a',
|
||||
borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8,
|
||||
color: '#f4f4f5', fontSize: 14,
|
||||
},
|
||||
list: { padding: 16, gap: 12, paddingBottom: 80 },
|
||||
card: {
|
||||
backgroundColor: '#18181b', borderRadius: 12,
|
||||
|
||||
Reference in New Issue
Block a user