diff --git a/.changeset/plain-owls-speak.md b/.changeset/plain-owls-speak.md new file mode 100644 index 0000000000..4184903c26 --- /dev/null +++ b/.changeset/plain-owls-speak.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +💬 update asset section titles diff --git a/.changeset/slow-poems-sniff.md b/.changeset/slow-poems-sniff.md new file mode 100644 index 0000000000..ee08b05738 --- /dev/null +++ b/.changeset/slow-poems-sniff.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +⚡️ optimize activity list scroll diff --git a/src/components/activity/Activity.tsx b/src/components/activity/Activity.tsx index f6945a5b74..455cce02ef 100644 --- a/src/components/activity/Activity.tsx +++ b/src/components/activity/Activity.tsx @@ -1,6 +1,7 @@ -import React, { memo, useMemo, useRef } from "react"; +import React, { memo, useCallback, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { FlatList } from "react-native"; +import { FlatList, PixelRatio, useWindowDimensions } from "react-native"; +import type { LayoutChangeEvent } from "react-native"; import { useTheme } from "tamagui"; @@ -23,33 +24,44 @@ import View from "../shared/View"; export default function Activity() { const { data: activity } = useQuery({ queryKey: ["activity"] }); const { queryKey } = useAsset(); - const { t } = useTranslation(); + const { fontScale } = useWindowDimensions(); const theme = useTheme(); + const [headerHeight, setHeaderHeight] = useState(); + const [limit, setLimit] = useState(page); - const { data, stickyHeaderIndices } = useMemo(() => { - if (!activity?.length) return { data: [] as ActivityItemType[], stickyHeaderIndices: [] as number[] }; - + const layout = useMemo(() => { + const events = activity?.slice(0, limit) ?? []; const items: ActivityItemType[] = []; - const stickyIndices: number[] = []; - const totalEvents = activity.length; + const offsets: number[] = []; + const stickies: number[] = []; + const dateHeight = PixelRatio.roundToNearestPixel(21 * fontScale) + 16; let currentDate: string | undefined; - let eventPosition = 0; + let offset = 0; - for (const event of activity) { + for (const [index, event] of events.entries()) { const date = format(event.timestamp, "yyyy-MM-dd"); if (date !== currentDate) { - stickyIndices.push(items.length); - items.push({ type: "header", date }); + stickies.push(items.length + 1); + offsets.push(offset); + offset += dateHeight; + items.push({ type: "header", date, height: dateHeight }); currentDate = date; } - - const isLast = eventPosition === totalEvents - 1; - items.push({ type: "event", event, isLast }); - eventPosition += 1; + const isLast = index === events.length - 1; + const rowHeight = PixelRatio.roundToNearestPixel(Math.max(40, 38 * fontScale + 4)) + (isLast ? 24 : 16); + offsets.push(offset); + offset += rowHeight; + items.push({ type: "event", event, height: rowHeight, isLast }); } + offsets.push(offset); - return { data: items, stickyHeaderIndices: stickyIndices }; - }, [activity]); + return { items, offsets, stickies }; + }, [activity, fontScale, limit]); + + const onHeaderLayout = useCallback( + (event: LayoutChangeEvent) => setHeaderHeight(event.nativeEvent.layout.height), + [], + ); const listRef = useRef>(null); const refresh = () => @@ -58,13 +70,20 @@ export default function Activity() { queryClient.refetchQueries({ queryKey }), ]); useTabPress("activity", () => { - if (data.length > 0) listRef.current?.scrollToIndex({ index: 0, animated: true }); - refresh().catch(reportError); + if (layout.items.length > 0) listRef.current?.scrollToIndex({ index: 0, animated: true }); + refresh() + .then(() => setLimit(page)) + .catch(reportError); }); return ( - 0 ? "$backgroundMild" : "$backgroundSoft"}> + 0 ? "$backgroundMild" : "$backgroundSoft"} + > ref={listRef} @@ -72,40 +91,61 @@ export default function Activity() { onScrollToIndexFailed={() => undefined} contentContainerStyle={{ flexGrow: 1, - backgroundColor: data.length > 0 ? theme.backgroundMild.val : theme.backgroundSoft.val, + backgroundColor: layout.items.length > 0 ? theme.backgroundMild.val : theme.backgroundSoft.val, }} showsVerticalScrollIndicator={false} refreshControl={} - ListHeaderComponent={ - <> - - - - {t("All Activity")} - - - - - - - } + ListHeaderComponent={} ListEmptyComponent={} - data={data} + data={layout.items} renderItem={renderItem} keyExtractor={keyExtractor} - stickyHeaderIndices={stickyHeaderIndices.map((index) => index + 1)} + getItemLayout={ + headerHeight === undefined + ? undefined + : (_, index) => ({ + length: (layout.offsets[index + 1] ?? 0) - (layout.offsets[index] ?? 0), + offset: headerHeight + (layout.offsets[index] ?? 0), + index, + }) + } + initialNumToRender={14} + maxToRenderPerBatch={8} + windowSize={5} + onEndReachedThreshold={1} + onEndReached={() => setLimit((current) => (activity && current < activity.length ? current + page : current))} + stickyHeaderIndices={layout.stickies} /> ); } -type ActivityItemType = { date: string; type: "header" } | { event: ActivityEvent; isLast: boolean; type: "event" }; -type ActivityItemProperties = React.ComponentProps; +const page = 40; -const HeaderRow = memo(function HeaderRow({ date }: { date: string }) { +type ActivityItemType = + | { date: string; height: number; type: "header" } + | { event: ActivityEvent; height: number; isLast: boolean; type: "event" }; + +const ListHeader = memo(function ListHeader({ onLayout }: { onLayout: (event: LayoutChangeEvent) => void }) { + const { t } = useTranslation(); return ( - + + + + {t("All Activity")} + + + + + + ); +}); +ListHeader.displayName = "ListHeader"; + +const HeaderRow = memo(function HeaderRow({ date, height }: { date: string; height: number }) { + return ( + {date} @@ -115,17 +155,13 @@ const HeaderRow = memo(function HeaderRow({ date }: { date: string }) { HeaderRow.displayName = "HeaderRow"; function renderItem({ item }: { item: ActivityItemType }) { - if (item.type === "header") return ; - return ; -} - -function areActivityItemsEqual(previous: ActivityItemProperties, next: ActivityItemProperties) { - return previous.item === next.item && previous.isLast === next.isLast; + if (item.type === "header") return ; + return ; } function keyExtractor(item: ActivityItemType) { return item.type === "header" ? `header-${item.date}` : `event-${item.event.id}`; } -const MemoizedActivityItem = memo(ActivityItem, areActivityItemsEqual); +const MemoizedActivityItem = memo(ActivityItem); MemoizedActivityItem.displayName = "MemoizedActivityItem"; diff --git a/src/components/activity/ActivityItem.tsx b/src/components/activity/ActivityItem.tsx index b318be3043..677b989915 100644 --- a/src/components/activity/ActivityItem.tsx +++ b/src/components/activity/ActivityItem.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import { useRouter } from "expo-router"; +import { router } from "expo-router"; import { ArrowDownToLine, @@ -35,14 +35,15 @@ registerLocale(pt); export default function ActivityItem({ item, + height, isLast, stackProps, }: { + height?: number; isLast: boolean; item: Item; stackProps?: React.ComponentProps; }) { - const router = useRouter(); const { data: country } = useQuery({ queryKey: ["user", "country"] }); const declined = item.type === "panda" && item.status === "declined"; const processing = item.type === "panda" && !declined && country === "US" && isProcessing(item.timestamp); @@ -53,7 +54,7 @@ export default function ActivityItem({ } = useTranslation(); return ( { setSheetOpen(true); diff --git a/src/components/home/CollateralAssetsSheet.tsx b/src/components/home/CollateralAssetsSheet.tsx index 0750815616..79d0743804 100644 --- a/src/components/home/CollateralAssetsSheet.tsx +++ b/src/components/home/CollateralAssetsSheet.tsx @@ -21,7 +21,7 @@ export default function CollateralAssetsSheet({ onClose, open }: { onClose: () = > - {t("Collateral assets")} + {t("Assets backing your credit")} {t( diff --git a/src/components/home/ExternalAssets.tsx b/src/components/home/ExternalAssets.tsx index c4f61a3b6b..623da479b4 100644 --- a/src/components/home/ExternalAssets.tsx +++ b/src/components/home/ExternalAssets.tsx @@ -121,7 +121,7 @@ export default function ExternalAssets() { > setInfoSheetOpen(true)}> - {t("Non-collateral assets")} + {t("Other assets")} diff --git a/src/components/home/ExternalAssetsSheet.tsx b/src/components/home/ExternalAssetsSheet.tsx index 64780538cf..97fda7d0c4 100644 --- a/src/components/home/ExternalAssetsSheet.tsx +++ b/src/components/home/ExternalAssetsSheet.tsx @@ -23,7 +23,7 @@ export default function ExternalAssetsSheet({ onClose, open }: { onClose: () => > - {t("Non-collateral assets")} + {t("Other assets")} {t( diff --git a/src/components/shared/Text.tsx b/src/components/shared/Text.tsx index 6c7de0a2c9..227ea8e383 100644 --- a/src/components/shared/Text.tsx +++ b/src/components/shared/Text.tsx @@ -36,15 +36,14 @@ type TextProperties = ComponentPropsWithoutRef & { sensitive?: boolean; }; -const TextComponent = ({ ref: reference, children, sensitive, ...rest }: TextProperties) => { - const { data: hidden } = useQuery({ queryKey: ["settings", "sensitive"] }); - return ( - - {sensitive && hidden ? "***" : children} - - ); -}; +const TextComponent = ({ sensitive, ...rest }: TextProperties) => + sensitive ? : ; TextComponent.displayName = "Text"; export default TextComponent; + +function SensitiveText({ children, ...rest }: Omit) { + const { data: hidden } = useQuery({ queryKey: ["settings", "sensitive"] }); + return {hidden ? "***" : children}; +} diff --git a/src/i18n/es.json b/src/i18n/es.json index f8fc1f8863..051d6ae5e9 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -92,6 +92,7 @@ "Arrival time ≈ {{minutes}} min.": "Tiempo de llegada ≈ {{minutes}} min.", "Asset": "Activo", "Assets are added to your balance as collateral to increase your credit limit.": "Los activos se agregan a tu balance como garantía para aumentar tu límite de crédito.", + "Assets backing your credit": "Activos que respaldan tu crédito", "Assets from these networks need to be bridged to {{chain}}. Some may also require a swap to a supported asset to generate yield and increase your Exa Card credit limit. You can do both from your Portfolio.": "Los activos de estas redes necesitan un bridge a {{chain}}. Algunos también pueden requerir un swap a un activo soportado para generar rendimiento y aumentar el límite de crédito de tu Exa Card. Puedes hacer ambos desde tu Cartera.", "Assets you can hold, but they can't be used as backing. You can swap or bridge them to supported collateral assets on {{chain}} network to increase your credit limit.": "Activos que puedes mantener, pero que no pueden usarse como respaldo. Puedes intercambiarlos o transferirlos a activos de garantía compatibles en la red {{chain}} para aumentar tu límite de crédito.", "Assets you can use as backing to increase your credit limit and access features like Pay Later or funding.": "Activos que puedes usar como respaldo para aumentar tu límite de crédito y acceder a funciones como Pagar Después o financiamiento.", @@ -178,7 +179,6 @@ "CLABE": "CLABE", "Close": "Cerrar", "Collateral {{value}}": "Garantía {{value}}", - "Collateral assets": "Activos de garantía", "Collateral": "Garantía", "Complete a quick identity check to access more networks.": "Completa una verificación de identidad rápida para acceder a más redes.", "Complete identity verification to start swapping.": "Completa la verificación de identidad para empezar a intercambiar.", @@ -512,7 +512,6 @@ "No saved contacts.": "No hay contactos guardados.", "No tokens available": "No hay tokens disponibles", "No tokens found": "No se encontraron tokens", - "Non-collateral assets": "Activos sin garantía", "Non-supported network": "Red no compatible", "Nothing to see here for now. Once you add funds or make a payment, all your account activity will appear in this section.": "Nada que ver por ahora. Una vez que agregues fondos o realices un pago, toda la actividad de tu cuenta aparecerá en esta sección.", "Nothing to swap yet": "Aún no hay nada para intercambiar", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index ca3c6a063a..1d3445df27 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -92,6 +92,7 @@ "Arrival time ≈ {{minutes}} min.": "Tempo de chegada ≈ {{minutes}} min.", "Asset": "Ativo", "Assets are added to your balance as collateral to increase your credit limit.": "Os ativos são adicionados ao seu saldo como garantia para aumentar seu limite de crédito.", + "Assets backing your credit": "Ativos que respaldam seu crédito", "Assets from these networks need to be bridged to {{chain}}. Some may also require a swap to a supported asset to generate yield and increase your Exa Card credit limit. You can do both from your Portfolio.": "Os ativos dessas redes precisam de um bridge para {{chain}}. Alguns também podem exigir um swap para um ativo suportado para gerar rendimento e aumentar o limite de crédito do seu Exa Card. Você pode fazer ambos no seu Portfólio.", "Assets you can hold, but they can't be used as backing. You can swap or bridge them to supported collateral assets on {{chain}} network to increase your credit limit.": "Ativos que você pode manter, mas que não podem ser usados como garantia. Você pode trocar ou transferi-los para ativos de garantia compatíveis na rede {{chain}} para aumentar seu limite de crédito.", "Assets you can use as backing to increase your credit limit and access features like Pay Later or funding.": "Ativos que você pode usar como garantia para aumentar seu limite de crédito e acessar recursos como Pagar Depois ou financiamento.", @@ -178,7 +179,6 @@ "CLABE": "CLABE", "Close": "Fechar", "Collateral {{value}}": "Garantia {{value}}", - "Collateral assets": "Ativos de garantia", "Collateral": "Garantia", "Complete a quick identity check to access more networks.": "Complete uma verificação de identidade rápida para acessar mais redes.", "Complete identity verification to start swapping.": "Conclua a verificação de identidade para começar a trocar.", @@ -512,7 +512,6 @@ "No saved contacts.": "Nenhum contato salvo.", "No tokens available": "Nenhum token disponível", "No tokens found": "Nenhum token encontrado", - "Non-collateral assets": "Ativos sem garantia", "Non-supported network": "Rede não compatível", "Nothing to see here for now. Once you add funds or make a payment, all your account activity will appear in this section.": "Nada para ver por enquanto. Assim que você adicionar fundos ou fizer um pagamento, toda a atividade da sua conta aparecerá nesta seção.", "Nothing to swap yet": "Ainda não há nada para trocar",