as

Settings
Sign out
Notifications
Alexa
亚马逊应用商店
Ring
AWS
文档
Support
Contact Us
My Cases
新手入门
设计和开发
应用发布
参考
支持
感谢您的访问。此页面目前仅提供英语版本。我们正在开发中文版本。谢谢您的理解。

Implement Interactive Toast Notifications in Vega

In this article, you'll implement an engaging toast notification system that alerts users to available content and allows them to hold the play/pause button to start streaming directly. This solution demonstrates how to modify the Vega Video Sample App with a TV-optimized interactive toast that promotes in-app content discovery without disrupting the viewing experience.

Prerequisites

Install the required dependency

Add the toast message library to your project.

Copied to clipboard.


npm install react-native-toast-message@^2.2.0

Create the custom toast component

Create a new file for the TV-optimized toast component that displays a progress bar during user interaction.

Create src/components/ProgressToast.tsx with the following implementation.

Copied to clipboard.


import React from 'react';
import { View, Text, StyleSheet, Animated } from 'react-native';
import { BaseToast, ToastProps } from 'react-native-toast-message';

interface ProgressToastProps extends ToastProps {
  props?: {
    progress?: number;
    isHolding?: boolean;
  };
}

export const ProgressToast: React.FC<ProgressToastProps> = ({ props, text1, text2, ...rest }) => {
  const progress = props?.progress ?? 0;
  const isHolding = props?.isHolding ?? false;

  return (
    <View style={styles.toastContainer}>
      <Text style={styles.instructionText}>
        {text1 || 'Hold ⏯️ to start...'}
      </Text>
      
      {text2 && (
        <Text style={styles.secondaryText}>
          {text2}
        </Text>
      )}

      <View style={styles.progressBackground}>
        <Animated.View
          style={[
            styles.progressFill,
            {
              width: `${progress * 100}%`,
              backgroundColor: isHolding ? '#00ff00' : '#007bff',
            }
          ]}
        />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  toastContainer: {
    position: 'absolute',
    top: '20%',
    right: '2%',
    width: 480,
    backgroundColor: 'rgba(26, 26, 26, 0.95)',
    borderRadius: 8,
    padding: 16,
    shadowColor: '#000',
    shadowOffset: { width: 2, height: 2 },
    shadowOpacity: 0.8,
    shadowRadius: 6,
    elevation: 10,
    borderWidth: 1,
    borderColor: 'rgba(0, 123, 255, 0.3)',
  },
  instructionText: {
    fontSize: 22,
    fontWeight: '600',
    color: '#ffffff',
    textAlign: 'center',
    marginBottom: 8,
    textShadowColor: 'rgba(0, 0, 0, 0.8)',
    textShadowOffset: { width: 1, height: 1 },
    textShadowRadius: 2,
  },
  secondaryText: {
    fontSize: 20,
    fontWeight: '500',
    color: '#e0e0e0',
    textAlign: 'center',
    marginBottom: 12,
    textShadowColor: 'rgba(0, 0, 0, 0.8)',
    textShadowOffset: { width: 1, height: 1 },
    textShadowRadius: 2,
  },
  progressBackground: {
    width: '100%',
    height: 12,
    backgroundColor: '#333333',
    borderRadius: 6,
    overflow: 'hidden',
    marginTop: 4,
  },
  progressFill: {
    height: '100%',
    borderRadius: 6,
  },
});

export const toastConfig = {
  progress: (props: ProgressToastProps) => <ProgressToast {...props} />,
};

This component creates a rectangular toast positioned on the right side of the screen with real-time progress feedback. The progress bar changes from blue to green when the user actively holds the button.

Create the long press hook

Create a custom hook that manages the complete lifecycle of play/pause button interactions and toast visibility.

Create src/hooks/useLongPressToast.tsx with the following implementation.

Copied to clipboard.


import { useState, useRef, useCallback, useEffect } from 'react';
import { useTVEventHandler } from '@amazon-devices/react-native-kepler';
import Toast from 'react-native-toast-message';

export type TVButtonType = 'playpause' | 'select' | 'menu' | 'back' | 'up' | 'down' | 'left' | 'right';

interface UseLongPressToastProps {
  onLongPressComplete: () => void;
  targetButton?: TVButtonType;
  autoShowDelay?: number;
  longPressDuration?: number;
  toastTimeout?: number;
  initialText?: string;
  holdingText?: string;
  secondaryText?: string;
  completionText?: string;
  isScreenFocused?: boolean;
}

interface LongPressState {
  isHolding: boolean;
  progress: number;
  toastVisible: boolean;
  hasAutoShown: boolean;
  hasCompleted: boolean;
}

export const useLongPressToast = ({ 
  onLongPressComplete, 
  targetButton = 'playpause',
  autoShowDelay = 5000,
  longPressDuration = 2000,
  toastTimeout = 5000,
  initialText = 'Hold Button to Continue',
  holdingText = 'Keep Holding...',
  secondaryText,
  completionText = 'Action Completed',
  isScreenFocused = true
}: UseLongPressToastProps) => {
  const [state, setState] = useState<LongPressState>({
    isHolding: false,
    progress: 0,
    toastVisible: false,
    hasAutoShown: false,
    hasCompleted: false,
  });

  const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
  const autoShowTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const toastTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const holdStartTimeRef = useRef<number>(0);
  const isProgressCompleteRef = useRef<boolean>(false);
  const toastVisibleRef = useRef<boolean>(false);

  const showInitialToast = useCallback(() => {
    if (state.toastVisible || state.hasAutoShown || state.hasCompleted) return;

    Toast.show({
      type: 'progress',
      text1: initialText,
      text2: secondaryText,
      visibilityTime: toastTimeout,
      autoHide: false,
      props: {
        progress: 0,
        isHolding: false,
      },
    });

    setState(prev => ({
      ...prev,
      toastVisible: true,
      hasAutoShown: true,
    }));

    toastVisibleRef.current = true;

    toastTimeoutRef.current = setTimeout(() => {
      hideToast();
    }, toastTimeout);
  }, [state.toastVisible, state.hasAutoShown, state.hasCompleted, toastTimeout, initialText, secondaryText]);

  const hideToast = useCallback(() => {
    if (toastTimeoutRef.current) {
      clearTimeout(toastTimeoutRef.current);
      toastTimeoutRef.current = null;
    }

    Toast.hide();
    setState(prev => ({
      ...prev,
      toastVisible: false,
    }));

    toastVisibleRef.current = false;
  }, []);

  const startProgress = useCallback(() => {
    if (state.isHolding) return;

    if (toastTimeoutRef.current) {
      clearTimeout(toastTimeoutRef.current);
      toastTimeoutRef.current = null;
    }

    holdStartTimeRef.current = Date.now();
    isProgressCompleteRef.current = false;

    setState(prev => ({
      ...prev,
      isHolding: true,
      progress: 0,
      toastVisible: true,
    }));

    toastVisibleRef.current = true;

    progressIntervalRef.current = setInterval(() => {
      const elapsed = Date.now() - holdStartTimeRef.current;
      const newProgress = Math.min(elapsed / longPressDuration, 1);

      setState(prev => ({
        ...prev,
        progress: newProgress,
      }));

      Toast.show({
        type: 'progress',
        text1: holdingText,
        text2: secondaryText,
        autoHide: false,
        props: {
          progress: newProgress,
          isHolding: true,
        },
      });

      if (newProgress >= 1 && !isProgressCompleteRef.current) {
        isProgressCompleteRef.current = true;
        completeProgress();
      }
    }, 50);
  }, [state.isHolding, longPressDuration, holdingText, secondaryText]);

  const stopProgress = useCallback(() => {
    if (!state.isHolding) return;

    if (progressIntervalRef.current) {
      clearInterval(progressIntervalRef.current);
      progressIntervalRef.current = null;
    }

    if (!isProgressCompleteRef.current) {
      hideToast();
    }

    setState(prev => ({
      ...prev,
      isHolding: false,
      progress: 0,
    }));
  }, [state.isHolding, hideToast]);

  const completeProgress = useCallback(() => {
    if (progressIntervalRef.current) {
      clearInterval(progressIntervalRef.current);
      progressIntervalRef.current = null;
    }

    if (completionText) {
      Toast.show({
        type: 'progress',
        text1: completionText,
        visibilityTime: 2000,
        props: {
          progress: 1,
          isHolding: false,
        },
      });
    }

    setState(prev => ({
      ...prev,
      isHolding: false,
      progress: 1,
      toastVisible: false,
      hasCompleted: true,
    }));

    toastVisibleRef.current = false;

    setTimeout(() => {
      onLongPressComplete();
    }, 500);
  }, [onLongPressComplete, completionText]);

  const handleTVEvent = useCallback((evt: any) => {
    if (evt.eventType !== targetButton || !toastVisibleRef.current) {
      return;
    }

    if (evt.eventKeyAction === 0) {
      startProgress();
    } else if (evt.eventKeyAction === 1) {
      stopProgress();
    }
  }, [startProgress, stopProgress, targetButton]);

  useTVEventHandler(handleTVEvent);

  useEffect(() => {
    if (!isScreenFocused) {
      return;
    }

    setState(prev => ({ ...prev, hasAutoShown: false }));

    autoShowTimeoutRef.current = setTimeout(() => {
      showInitialToast();
    }, autoShowDelay);

    return () => {
      if (autoShowTimeoutRef.current) {
        clearTimeout(autoShowTimeoutRef.current);
      }
    };
  }, [autoShowDelay, showInitialToast, isScreenFocused]);

  useEffect(() => {
    return () => {
      if (progressIntervalRef.current) {
        clearInterval(progressIntervalRef.current);
      }
      if (autoShowTimeoutRef.current) {
        clearTimeout(autoShowTimeoutRef.current);
      }
      if (toastTimeoutRef.current) {
        clearTimeout(toastTimeoutRef.current);
      }
    };
  }, []);

  return {
    isHolding: state.isHolding,
    progress: state.progress,
    toastVisible: state.toastVisible,
    hasAutoShown: state.hasAutoShown,
    showInitialToast,
    hideToast,
  };
};

This hook manages automatic toast display timing, smooth progress animations, completion detection with callbacks, and proper cleanup of all timers and intervals using Vega's TVEventHandler.

Register the toast component

Update your main App component to register the custom toast configuration.

Add the following imports to src/App.tsx.

Copied to clipboard.


import Toast from 'react-native-toast-message';
import { toastConfig } from './components/ProgressToast';

Add the Toast component inside your App return statement.

Copied to clipboard.


const App = () => {
  return (
    <Provider store={store}>
      <ThemeProvider theme={theme}>
        <NavigationContainer>
          <AppStack />
        </NavigationContainer>
        <Toast config={toastConfig} />
      </ThemeProvider>
    </Provider>
  );
};

Integrate the toast in your screen

To add the long press toast hook to your HomeScreen component with screen focus tracking, add the following imports to src/screens/HomeScreen.tsx.

Copied to clipboard.


import { useLongPressToast } from '../hooks/useLongPressToast';
import { useIsFocused } from '@react-navigation/native';

Add the hook inside your HomeScreen component.

Copied to clipboard.


// Assumes tileData and Screens are defined in your app
const HomeScreen = ({ navigation }: AppStackScreenProps<Screens.HOME_SCREEN>) => {
  const isFocused = useIsFocused();

  const navigateToStream = useCallback(() => {
    const params = {
      data: tileData,
      sendDataOnBack: () => {},
    };

    try {
      navigation.navigate(Screens.PLAYER_SCREEN, params);
    } catch (error) {
      console.error('Failed to navigate to stream:', error);
      navigation.goBack();
    }
  }, [navigation]);

  useLongPressToast({
    onLongPressComplete: navigateToStream,
    initialText: tileData.title + ' starting soon!',
    holdingText: 'Keep Holding...',
    secondaryText: 'Hold ⏯️ to start',
    completionText: 'Starting stream',
    isScreenFocused: isFocused,
  });

  // ... rest of existing HomeScreen code
};

The isScreenFocused parameter makes sure the toast only appears when the HomeScreen is visible, preventing it from showing on other screens.

Test the implementation

Build and run your app on a Fire TV device or emulator:

  1. Navigate to the Home Screen.
  2. Wait five seconds for the toast to appear automatically.
  3. Press and hold the play/pause button on your remote.
  4. Observe the progress bar filling over two seconds.
  5. Release early to cancel, or hold for the full duration to trigger navigation.

Last updated: Mar 10, 2026