Repository: expo/expo
File: apps/notification-tester/src/Notifier.tsx (TypeScript)

Code before:
import { Platform, Button } from 'react-native';

import { View, Text } from './misc/Themed';
import { addItemToStorage, STORAGE_KEY } from './misc/addItemToStorage';
import { setupCategories } from './setupCategories';
import { usePushToken } from './usePushToken';

export const Notifier = () => {
  const [channels, setChannels] = useState<NotificationChannel[]>([]);
  const [notification, setNotification] = useState<Notification | undefined>(undefined);
  const [response, setResponse] = useState<NotificationResponse | undefined>(undefined);

  const [backgroundTaskString, setBackgroundTaskString] = useState<string | null>('');

  const loadAsyncStorage = () => {
    AsyncStorage.getItem(STORAGE_KEY)
      .then((value) => {
        console.log(`Retrieved value for STORAGE_KEY: ${value}`);
        setBackgroundTaskString(value);
      })
      .catch((reason) => {
        console.log(`Error retrieving value for STORAGE_KEY: ${reason}`);
        setBackgroundTaskString(reason);
      });
  };

  const lastResponse = useLastNotificationResponse();

  const { expoPushToken, devicePushToken } = usePushToken();

  useEffect(() => {
    setupCategories().catch(console.error);
    if (Platform.OS === 'android') {
      setNotificationChannelAsync('testAppWithSound2', {
        name: 'testAppWithSound',
        importance: AndroidImportance.HIGH,
        sound: 'pop_sound.wav',
        enableVibrate: true,
        showBadge: true,
        vibrationPattern: [0, 3000, 250, 250],
      })
        .then((value) => {
          console.log(`Set channel ${JSON.stringify(value, null, 2)}`);
          getNotificationChannelsAsync().then((value) => setChannels(value ?? []));
        })
        .catch((error) => {
          console.log(`Error in setting channel: ${error}`);
        });
    }

    const receivedListener = addNotificationReceivedListener((notification) => {

Review comment (suggestion):
This is probably fine for the tester app, although normally I'd use a fully tested library like Moment, Luxon, or day.js.org.

Code after:
import { Platform, Button } from 'react-native';

import { View, Text } from './misc/Themed';
import { addItemToStorage, STORAGE_KEY } from './misc/addItemToStorage';
import { setupCategories } from './setupCategories';
import { usePushToken } from './usePushToken';

export const Notifier = () => {
  const [channels, setChannels] = useState<NotificationChannel[]>([]);
  const [notification, setNotification] = useState<Notification | undefined>(undefined);
  const [response, setResponse] = useState<NotificationResponse | undefined>(undefined);

  const [backgroundTaskString, setBackgroundTaskString] = useState<string | null>('');
  const [displayPersisted, setDisplayPersisted] = useState<(Record<string, string> | undefined)[]>(
    []
  );

  useEffect(() => {
    const updateDisplay = () => {
      if (!backgroundTaskString) {
        setDisplayPersisted([]);
        return;
      }
      try {
        const parsed: Record<string, string>[] = JSON.parse(backgroundTaskString);
        const parsedProcessed = parsed.map((item) => {
          if (item && typeof item === 'object' && 'time' in item && typeof item.time === 'string') {
            const t = new Date(item.time);
            const now = new Date();
            const diffMs = now.getTime() - t.getTime();
            const diffMin = diffMs / 1000 / 60;
            if (diffMin > 15) {
              return item;
            } else {
              const diffSec = Math.max(0, Math.floor(diffMs / 1000));
              const minutes = Math.floor(diffSec / 60);
              const seconds = diffSec % 60;
              return {
                ...item,
                time: `(${minutes}m ${seconds}s ago)`,
              };
            }
          }
        });
        setDisplayPersisted(parsedProcessed);
      } catch (e) {
        setDisplayPersisted([{ error: `Failed to parse JSON: ${(e as Error).message}` }]);
      }
    };

    updateDisplay();