Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plain-owls-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

💬 update asset section titles
5 changes: 5 additions & 0 deletions .changeset/slow-poems-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

âšĄïž optimize activity list scroll
132 changes: 84 additions & 48 deletions src/components/activity/Activity.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -23,33 +24,44 @@ import View from "../shared/View";
export default function Activity() {
const { data: activity } = useQuery<ActivityEvent[]>({ queryKey: ["activity"] });
const { queryKey } = useAsset();
const { t } = useTranslation();
const { fontScale } = useWindowDimensions();
const theme = useTheme();
const [headerHeight, setHeaderHeight] = useState<number>();
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);
Comment thread
dieguezguille marked this conversation as resolved.
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<FlatList<ActivityItemType>>(null);
const refresh = () =>
Expand All @@ -58,54 +70,82 @@ 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 (
<SafeView fullScreen tab backgroundColor="$backgroundSoft">
<View fullScreen gap="$s5" flex={1} backgroundColor={data.length > 0 ? "$backgroundMild" : "$backgroundSoft"}>
<View
fullScreen
gap="$s5"
flex={1}
backgroundColor={layout.items.length > 0 ? "$backgroundMild" : "$backgroundSoft"}
>
<View position="absolute" top={0} left={0} right={0} height="50%" backgroundColor="$backgroundSoft" />
<FlatList<ActivityItemType>
ref={listRef}
style={{ flex: 1 }}
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={<RefreshControl onRefresh={refresh} />}
ListHeaderComponent={
<>
<View padded gap="$s5" backgroundColor="$backgroundSoft">
<View flexDirection="row" gap="$s3_5" justifyContent="space-between" alignItems="center">
<Text fontSize={20} fontWeight="bold">
{t("All Activity")}
</Text>
</View>
</View>
<ProposalBanner />
<ProcessingBalanceBanner />
</>
}
ListHeaderComponent={<ListHeader onLayout={onHeaderLayout} />}
ListEmptyComponent={<Empty />}
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}
/>
</View>
</SafeView>
);
}

type ActivityItemType = { date: string; type: "header" } | { event: ActivityEvent; isLast: boolean; type: "event" };
type ActivityItemProperties = React.ComponentProps<typeof ActivityItem>;
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 (
<View paddingHorizontal="$s4" paddingVertical="$s3" backgroundColor="$backgroundSoft">
<View onLayout={onLayout}>
<View padded backgroundColor="$backgroundSoft">
<Text fontSize={20} fontWeight="bold">
{t("All Activity")}
</Text>
</View>
<ProposalBanner />
<ProcessingBalanceBanner />
</View>
);
});
ListHeader.displayName = "ListHeader";

const HeaderRow = memo(function HeaderRow({ date, height }: { date: string; height: number }) {
return (
<View height={height} paddingHorizontal="$s4" paddingVertical="$s3" backgroundColor="$backgroundSoft">
<Text subHeadline color="$uiNeutralSecondary">
{date}
</Text>
Expand All @@ -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 <HeaderRow date={item.date} />;
return <MemoizedActivityItem item={item.event} isLast={item.isLast} />;
}

function areActivityItemsEqual(previous: ActivityItemProperties, next: ActivityItemProperties) {
return previous.item === next.item && previous.isLast === next.isLast;
if (item.type === "header") return <HeaderRow date={item.date} height={item.height} />;
return <MemoizedActivityItem item={item.event} height={item.height} isLast={item.isLast} />;
}

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";
7 changes: 4 additions & 3 deletions src/components/activity/ActivityItem.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -35,14 +35,15 @@ registerLocale(pt);

export default function ActivityItem({
item,
height,
isLast,
stackProps,
}: {
height?: number;
isLast: boolean;
item: Item;
stackProps?: React.ComponentProps<typeof XStack>;
}) {
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);
Expand All @@ -53,7 +54,7 @@ export default function ActivityItem({
} = useTranslation();
return (
<XStack
key={item.id}
height={height}
gap="$s4"
alignItems="center"
paddingHorizontal="$s4"
Expand Down
2 changes: 1 addition & 1 deletion src/components/home/AssetList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function AssetList() {
return (
<>
<AssetSection
title={t("Collateral assets")}
title={t("Assets backing your credit")}
assets={collateralAssets}
onInfoPress={() => {
setSheetOpen(true);
Expand Down
2 changes: 1 addition & 1 deletion src/components/home/CollateralAssetsSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function CollateralAssetsSheet({ onClose, open }: { onClose: () =
>
<YStack gap="$s5" paddingTop="$s7" paddingHorizontal="$s5">
<Text emphasized headline>
{t("Collateral assets")}
{t("Assets backing your credit")}
</Text>
<Text subHeadline color="$uiNeutralSecondary">
{t(
Expand Down
2 changes: 1 addition & 1 deletion src/components/home/ExternalAssets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export default function ExternalAssets() {
>
<XStack alignItems="center" gap="$s2" onPress={() => setInfoSheetOpen(true)}>
<Text emphasized headline color="$uiNeutralPrimary">
{t("Non-collateral assets")}
{t("Other assets")}
</Text>
<Info size={16} color="$interactiveOnBaseBrandSoft" />
</XStack>
Expand Down
2 changes: 1 addition & 1 deletion src/components/home/ExternalAssetsSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function ExternalAssetsSheet({ onClose, open }: { onClose: () =>
>
<YStack gap="$s5" paddingTop="$s7" paddingHorizontal="$s5">
<Text emphasized headline>
{t("Non-collateral assets")}
{t("Other assets")}
</Text>
<Text subHeadline color="$uiNeutralSecondary">
{t(
Expand Down
15 changes: 7 additions & 8 deletions src/components/shared/Text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,14 @@ type TextProperties = ComponentPropsWithoutRef<typeof StyledText> & {
sensitive?: boolean;
};

const TextComponent = ({ ref: reference, children, sensitive, ...rest }: TextProperties) => {
const { data: hidden } = useQuery<boolean>({ queryKey: ["settings", "sensitive"] });
return (
<StyledText ref={reference} {...rest}>
{sensitive && hidden ? "***" : children}
</StyledText>
);
};
const TextComponent = ({ sensitive, ...rest }: TextProperties) =>
sensitive ? <SensitiveText {...rest} /> : <StyledText {...rest} />;

TextComponent.displayName = "Text";

export default TextComponent;

function SensitiveText({ children, ...rest }: Omit<TextProperties, "sensitive">) {
const { data: hidden } = useQuery<boolean>({ queryKey: ["settings", "sensitive"] });
return <StyledText {...rest}>{hidden ? "***" : children}</StyledText>;
}
3 changes: 1 addition & 2 deletions src/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 1 addition & 2 deletions src/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
Expand Down
Loading