Список последних событий, который обновляется сам: через заданный интервал сверху выезжает новая карточка с лёгким масштабом, остальные пружинно сдвигаются вниз, а нижняя растворяется. Низ списка мягко уходит в прозрачность через градиентную маску.
В каждой карточке иконка на цветной подложке, заголовок, описание и время. Пружины считаются на UI-потоке Reanimated. При «Уменьшении движения» лента не прокручивается и показывает статичный список. Работает на iOS, Android и в вебе.
Поставьте зависимости:
npm install react-native-reanimated expo-linear-gradient @react-native-masked-view/masked-viewpnpm add react-native-reanimated expo-linear-gradient @react-native-masked-view/masked-viewyarn add react-native-reanimated expo-linear-gradient @react-native-masked-view/masked-viewbun add react-native-reanimated expo-linear-gradient @react-native-masked-view/masked-viewСкопируйте эти файлы в src/shared/components/molecules/activity-feed/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import React, { useMemo } from "react";
import { StyleSheet, View } from "react-native";
import Svg, { Circle, Line, Path, Rect } from "react-native-svg";
import { ActivityFeed, type IActivityFeedItem } from "@/shared/components/molecules/activity-feed";
import { Showcase, useCustomize } from "~/showcase";
const ICON = { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" } as const;
const CreditCard = ({ color }: { color: string }) => (
<Svg {...ICON} stroke={color}>
<Rect width={20} height={14} x={2} y={5} rx={2} />
<Line x1={2} x2={22} y1={10} y2={10} />
</Svg>
);
const UserPlus = ({ color }: { color: string }) => (
<Svg {...ICON} stroke={color}>
<Path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<Circle cx={9} cy={7} r={4} />
<Line x1={19} x2={19} y1={8} y2={14} />
<Line x1={22} x2={16} y1={11} y2={11} />
</Svg>
);
const Rocket = ({ color }: { color: string }) => (
<Svg {...ICON} stroke={color}>
<Path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
<Path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09" />
<Path d="M9 12a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.4 22.4 0 0 1-4 2z" />
<Path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05" />
</Svg>
);
const ShieldCheck = ({ color }: { color: string }) => (
<Svg {...ICON} stroke={color}>
<Path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
<Path d="m9 12 2 2 4-4" />
</Svg>
);
const MessageSquare = ({ color }: { color: string }) => (
<Svg {...ICON} stroke={color}>
<Path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
</Svg>
);
const Download = ({ color }: { color: string }) => (
<Svg {...ICON} stroke={color}>
<Path d="M12 15V3" />
<Path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<Path d="m7 10 5 5 5-5" />
</Svg>
);
const ITEMS: IActivityFeedItem[] = [
{
icon: <CreditCard color="#34d399" />,
iconBackground: "rgba(16,185,129,0.15)",
title: "Платёж получен",
description: "4 900 ₽ от ООО «Вектор»",
time: "сейчас",
},
{
icon: <UserPlus color="#66d9e8" />,
iconBackground: "rgba(102,217,232,0.15)",
title: "Новый участник",
description: "Анна Смирнова в команде",
time: "1 мин",
},
{
icon: <Rocket color="#66d9e8" />,
iconBackground: "rgba(102,217,232,0.15)",
title: "Деплой завершён",
description: "astra-web · продакшен",
time: "2 мин",
},
{
icon: <ShieldCheck color="#fbbf24" />,
iconBackground: "rgba(245,158,11,0.15)",
title: "Проверка безопасности",
description: "Уязвимостей: 0",
time: "4 мин",
},
{
icon: <MessageSquare color="#fb7185" />,
iconBackground: "rgba(244,63,94,0.15)",
title: "Новое упоминание",
description: "#общий · «выкатываем 🚀»",
time: "6 мин",
},
{
icon: <Download color="#d4d4d8" />,
iconBackground: "rgba(113,113,122,0.15)",
title: "Отчёт выгружен",
description: "выручка-Q3.csv · 2,4 МБ",
time: "9 мин",
},
];
const CUSTOMIZE = [
{ key: "interval", label: "Интервал событий", type: "slider", min: 600, max: 5000, step: 100, unit: "мс", default: 1800 },
{ key: "visible", label: "Карточек на экране", type: "slider", min: 2, max: 6, step: 1, default: 5 },
{ key: "gap", label: "Отступ между карточками", type: "slider", min: 0, max: 28, step: 1, unit: "px", default: 12 },
{ key: "radius", label: "Скругление карточек", type: "slider", min: 0, max: 28, step: 1, unit: "px", default: 16 },
{ key: "title", label: "Заголовок первого события", type: "text", default: "Платёж получен", maxLength: 40 },
{ key: "description", label: "Описание первого события", type: "text", default: "4 900 ₽ от ООО «Вектор»", maxLength: 50 },
{ key: "showTime", label: "Показывать время", type: "toggle", default: true },
] as const;
export default function ActivityFeedScreen() {
const custom = useCustomize(CUSTOMIZE);
const items = useMemo(
() =>
ITEMS.map((item, index) => ({
...item,
title: index === 0 ? custom.title : item.title,
description: index === 0 ? custom.description : item.description,
time: custom.showTime ? item.time : undefined,
})),
[custom.title, custom.description, custom.showTime],
);
return (
<Showcase>
<View style={styles.page}>
<View style={styles.frame}>
<ActivityFeed
key={custom.visible}
items={items}
visible={custom.visible}
interval={custom.interval}
gap={custom.gap}
radius={custom.radius}
style={styles.feed}
/>
</View>
</View>
</Showcase>
);
}
const styles = StyleSheet.create({
page: { flex: 1, backgroundColor: "#0a0a0a" },
frame: { flex: 1, alignItems: "center", justifyContent: "center", paddingVertical: 24, paddingHorizontal: 16 },
feed: { maxWidth: 576 },
});Иконку события передайте в icon любым элементом, а цвет подложки — в iconBackground.
itemsТип readonly IActivityFeedItem[]
visible?Тип number
interval?Тип number
gap?Тип number
radius?Тип number
style?Тип StyleProp<ViewStyle>