Skip to content
76 linesCodeBlameRaw
1import { router } from "expo-router";
2import { X } from "lucide-react-native";
3import { useEffect, useRef } from "react";
4import { Animated, Pressable, StyleSheet, View } from "react-native";
5import { useSafeAreaInsets } from "react-native-safe-area-context";
6
7import { Avatar, Text } from "@/components/ui";
8import { useFeed } from "@/lib/feed";
9import { routeFor } from "@/lib/links";
10import { useSignedIn } from "@/lib/session";
11import { radius, space, useColors } from "@/theme";
12
13/**
14 * A notification while the app is open, as the site's toast: who, what,
15 * and a tap to go there. Not shown for the conversation on screen.
16 */
17export function Banner() {
18 const { banner, dismiss } = useFeed();
19 const { server } = useSignedIn();
20 const colors = useColors();
21 const insets = useSafeAreaInsets();
22 const offset = useRef(new Animated.Value(-160)).current;
23
24 useEffect(() => {
25 Animated.spring(offset, { toValue: banner ? 0 : -160, useNativeDriver: true, speed: 18, bounciness: 6 }).start();
26 }, [banner, offset]);
27
28 if (!banner) return null;
29 const go = () => {
30 dismiss();
31 if (banner.channel_id && banner.thread_root) router.push({ pathname: "/chat/thread", params: { channel: banner.channel_id, root: banner.thread_root } });
32 else if (banner.channel_id) router.push({ pathname: "/chat/[channel]", params: { channel: banner.channel_id } });
33 else {
34 const route = routeFor(banner.href);
35 router.push((route ?? `/open?path=${encodeURIComponent(banner.href)}`) as never);
36 }
37 };
38
39 return (
40 <Animated.View pointerEvents="box-none" style={[styles.wrap, { top: insets.top + space.sm, transform: [{ translateY: offset }] }]}>
41 <Pressable onPress={go} style={[styles.banner, { backgroundColor: colors.raised, borderColor: colors.lineStrong }]}>
42 <Avatar name={banner.actor.name} uri={banner.actor.avatar ? `${server.site}/avatars/${banner.actor.avatar}` : null} size={36} />
43 <View style={{ flex: 1 }}>
44 <Text style={{ fontWeight: "700" }} numberOfLines={1}>
45 {banner.title}
46 </Text>
47 {banner.body ? (
48 <Text size="small" tone="muted" numberOfLines={2}>
49 {banner.body}
50 </Text>
51 ) : null}
52 </View>
53 <Pressable accessibilityLabel="Dismiss" hitSlop={10} onPress={dismiss}>
54 <X size={18} color={colors.faint} />
55 </Pressable>
56 </Pressable>
57 </Animated.View>
58 );
59}
60
61const styles = StyleSheet.create({
62 wrap: { position: "absolute", left: space.md, right: space.md },
63 banner: {
64 flexDirection: "row",
65 alignItems: "center",
66 gap: space.md,
67 padding: space.md,
68 borderRadius: radius.lg,
69 borderWidth: StyleSheet.hairlineWidth,
70 shadowColor: "#000",
71 shadowOpacity: 0.3,
72 shadowRadius: 12,
73 shadowOffset: { width: 0, height: 4 },
74 elevation: 8,
75 },
76});