@chakra-ui/react#useBoolean TypeScript Examples

The following examples show how to use @chakra-ui/react#useBoolean. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Lobby.tsx    From dope-monorepo with GNU General Public License v3.0 6 votes vote down vote up
MatchRow = () => {
  const [isExpanded, setIsExpanded] = useBoolean();

  const CELL_PROPS = {
    borderBottom: isExpanded ? 'none' : 'inherit',
  };

  const background = isExpanded ? '#434345' : 'inherit';

  return (
    <>
      <Tr background={background} cursor="pointer" onClick={() => setIsExpanded.toggle()}>
        <Td {...CELL_PROPS}>
          30 min
        </Td>
        <Td isNumeric {...CELL_PROPS}>
          16
        </Td>
      </Tr>
      {isExpanded && (
        <Tr background={background}>
          <Td colSpan={2}>
            <NavLink href={`/roll-your-own/1`}>
              <Button color="black" w="full">View</Button>
            </NavLink>
          </Td>
        </Tr>
      )}
    </>
  )
}
Example #2
Source File: PermissionRequirements.tsx    From bluebubbles-server with Apache License 2.0 5 votes vote down vote up
PermissionRequirements = (): JSX.Element => {
    const permissions: Array<RequirementsItem> = (useAppSelector(state => state.config.permissions) ?? []);
    const [showProgress, setShowProgress] = useBoolean();

    const refreshRequirements = () => {
        setShowProgress.on();
        checkPermissions().then(permissions => {
            // I like longer spinning
            setTimeout(() => {
                setShowProgress.off();
            }, 1000);

            if (!permissions) return;
            store.dispatch(setConfig({ name: 'permissions', value: permissions }));
        });
    };

    return (
        <Box border='1px solid' borderColor={useColorModeValue('gray.200', 'gray.700')} borderRadius='xl' p={3} width='325px'>
            <Stack direction='row' align='center'>
                <Text fontSize='lg' fontWeight='bold'>macOS Permissions</Text>
                <Box
                    _hover={{ cursor: 'pointer' }}
                    animation={showProgress ? `${spin} infinite 1s linear` : undefined}
                    onClick={refreshRequirements}
                >
                    <BiRefresh />
                </Box>
            </Stack>
            <UnorderedList mt={2} ml={8}>
                {permissions.map(e => (
                    <ListItem key={e.name}>
                        <Stack direction='row' align='center'>
                            <Text fontSize='md'><strong>{e.name}</strong>:&nbsp;
                                <Box as='span' color={e.pass ? 'green' : 'red'}>{e.pass ? 'Pass' : 'Fail'}</Box>
                            </Text>
                            {(e.pass) ? (
                                <Popover trigger='hover'>
                                    <PopoverTrigger>
                                        <Box ml={2} _hover={{ color: 'brand.primary', cursor: 'pointer' }}>
                                            <AiOutlineInfoCircle />
                                        </Box>
                                    </PopoverTrigger>
                                    <PopoverContent>
                                        <PopoverArrow />
                                        <PopoverCloseButton />
                                        <PopoverHeader>How to Fix</PopoverHeader>
                                        <PopoverBody>
                                            <Text>
                                                {e.solution}
                                            </Text>
                                        </PopoverBody>
                                    </PopoverContent>
                                </Popover>
                            ): null}
                        </Stack>
                    </ListItem>
                ))}
            </UnorderedList>
        </Box>
    );
}
Example #3
Source File: PrivateApiRequirements.tsx    From bluebubbles-server with Apache License 2.0 5 votes vote down vote up
PrivateApiRequirements = (): JSX.Element => {
    const requirements: Array<RequirementsItem> = (useAppSelector(state => state.config.private_api_requirements) ?? []);
    const [showProgress, setShowProgress] = useBoolean();

    const refreshRequirements = () => {
        setShowProgress.on();
        getPrivateApiRequirements().then(requirements => {
            // I like longer spinning
            setTimeout(() => {
                setShowProgress.off();
            }, 1000);
            
            if (!requirements) return;
            store.dispatch(setConfig({ name: 'private_api_requirements', value: requirements }));
        });
    };

    return (
        <Box border='1px solid' borderColor={useColorModeValue('gray.200', 'gray.700')} borderRadius='xl' p={3} width='325px'>
            <Stack direction='row' align='center'>
                <Text fontSize='lg' fontWeight='bold'>Private API Requirements</Text>
                <Box
                    _hover={{ cursor: 'pointer' }}
                    animation={showProgress ? `${spin} infinite 1s linear` : undefined}
                    onClick={refreshRequirements}
                >
                    <BiRefresh />
                </Box>
            </Stack>
            <UnorderedList mt={2} ml={8}>
                {requirements.map(e => (
                    <ListItem key={e.name}>
                        <Stack direction='row' align='center'>
                            <Text fontSize='md'><strong>{e.name}</strong>:&nbsp;
                                <Box as='span' color={e.pass ? 'green' : 'red'}>{e.pass ? 'Pass' : 'Fail'}</Box>
                            </Text>
                            {(!e.pass) ? (
                                <Popover trigger='hover'>
                                    <PopoverTrigger>
                                        <Box ml={2} _hover={{ color: 'brand.primary', cursor: 'pointer' }}>
                                            <AiOutlineInfoCircle />
                                        </Box>
                                    </PopoverTrigger>
                                    <PopoverContent>
                                        <PopoverArrow />
                                        <PopoverCloseButton />
                                        <PopoverHeader>How to Fix</PopoverHeader>
                                        <PopoverBody>
                                            <Text>
                                                {e.solution}
                                            </Text>
                                        </PopoverBody>
                                    </PopoverContent>
                                </Popover>
                            ): null}
                        </Stack>
                    </ListItem>
                ))}
            </UnorderedList>
        </Box>
    );
}
Example #4
Source File: ServerPasswordField.tsx    From bluebubbles-server with Apache License 2.0 5 votes vote down vote up
ServerPasswordField = ({ helpText }: ServerPasswordFieldProps): JSX.Element => {
    const dispatch = useAppDispatch();

    const password: string = (useAppSelector(state => state.config.password) ?? '');
    const [showPassword, setShowPassword] = useBoolean();
    const [newPassword, setNewPassword] = useState(password);
    const [passwordError, setPasswordError] = useState('');
    const hasPasswordError: boolean = (passwordError?? '').length > 0;

    useEffect(() => { setNewPassword(password); }, [password]);

    /**
     * A handler & validator for saving a new password.
     *
     * @param theNewPassword - The new password to save
     */
    const savePassword = (theNewPassword: string): void => {
        // Validate the port
        if (theNewPassword.length < 8) {
            setPasswordError('Your password must be at least 8 characters!');
            return;
        } else if (theNewPassword === password) {
            setPasswordError('You have not changed the password since your last save!');
            return;
        }

        dispatch(setConfig({ name: 'password', value: theNewPassword }));
        if (hasPasswordError) setPasswordError('');
        showSuccessToast({
            id: 'settings',
            description: 'Successfully saved new password!'
        });
    };

    return (
        <FormControl isInvalid={hasPasswordError}>
            <FormLabel htmlFor='password'>Server Password</FormLabel>
            <Input
                id='password'
                type={showPassword ? 'text' : 'password'}
                maxWidth="20em"
                value={newPassword}
                onChange={(e) => {
                    if (hasPasswordError) setPasswordError('');
                    setNewPassword(e.target.value);
                }}
            />
            <IconButton
                ml={3}
                verticalAlign='top'
                aria-label='View password'
                icon={showPassword ? <AiFillEye /> : <AiFillEyeInvisible />}
                onClick={() => setShowPassword.toggle()}
            />
            <IconButton
                ml={3}
                verticalAlign='top'
                aria-label='Save password'
                icon={<AiOutlineSave />}
                onClick={() => savePassword(newPassword)}
            />
            {!hasPasswordError ? (
                <FormHelperText>
                    {helpText ?? 'Enter a password to use for clients to authenticate with the server'}
                </FormHelperText>
            ) : (
                <FormErrorMessage>{passwordError}</FormErrorMessage>
            )}
        </FormControl>
    );
}
Example #5
Source File: Drugs.tsx    From dope-monorepo with GNU General Public License v3.0 5 votes vote down vote up
DrugRow = ({ drug }: { drug: Drug }) => {
  const router = useRouter();
  const { roundId, locationId } = router.query;
  const [isExpanded, setIsExpanded] = useBoolean();

  const CELL_PROPS = {
    borderBottom: isExpanded ? 'none' : 'inherit',
  };

  const background = isExpanded ? '#434345' : 'inherit';

  return (
    <>
      <Tr background={background} cursor="pointer" onClick={() => setIsExpanded.toggle()}>
        <Td {...CELL_PROPS}>
          {drug.rle && (
            <div
              css={css`
                width: 32px;
                height: 32px;
                overflow: hidden;
              }`}
              dangerouslySetInnerHTML={{ __html: buildIconSVG([drug.rle]) }}
            />
          )}
        </Td>
        <Td {...CELL_PROPS}>{drug.name}</Td>
        <Td isNumeric {...CELL_PROPS}>
          {drug.cost}
        </Td>
        <Td isNumeric {...CELL_PROPS}>
          {drug.quantity}
        </Td>
      </Tr>
      {isExpanded && (
        <Tr background={background}>
          <Td colSpan={2}>
            <NavLink href={`/roll-your-own/${roundId}/location/${locationId}/buy/${drug.id}`}>
              <Button color="black" w="full">Buy</Button>
            </NavLink>
          </Td>
          <Td colSpan={2}>
            <NavLink href={`/roll-your-own/${roundId}/location/${locationId}/sell/${drug.id}`}>
              <Button color="black" w="full">SELL</Button>
            </NavLink>
          </Td>
        </Tr>
      )}
    </>
  );
}
Example #6
Source File: NgrokAuthTokenField.tsx    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
NgrokAuthTokenField = ({ helpText }: NgrokAuthTokenFieldProps): JSX.Element => {
    const dispatch = useAppDispatch();
    const ngrokToken: string = (useAppSelector(state => state.config.ngrok_key) ?? '');
    const [showNgrokToken, setShowNgrokToken] = useBoolean();
    const [newNgrokToken, setNewNgrokToken] = useState(ngrokToken);
    const [ngrokTokenError, setNgrokTokenError] = useState('');
    const hasNgrokTokenError: boolean = (ngrokTokenError ?? '').length > 0;

    useEffect(() => { setNewNgrokToken(ngrokToken); }, [ngrokToken]);

    /**
     * A handler & validator for saving a new Ngrok auth token.
     *
     * @param theNewNgrokToken - The new auth token to save
     */
    const saveNgrokToken = (theNewNgrokToken: string): void => {
        theNewNgrokToken = theNewNgrokToken.trim();

        // Validate the port
        if (theNewNgrokToken === ngrokToken) {
            setNgrokTokenError('You have not changed the token since your last save!');
            return;
        } else if (theNewNgrokToken.includes(' ')) {
            setNgrokTokenError('Invalid Ngrok Auth Token! Please check that you have copied it correctly.');
            return;
        }

        dispatch(setConfig({ name: 'ngrok_key', value: theNewNgrokToken }));
        setNgrokTokenError('');
        showSuccessToast({
            id: 'settings',
            duration: 4000,
            description: 'Successfully saved new Ngrok Auth Token! Restarting Proxy service...'
        });
    };

    return (
        <FormControl isInvalid={hasNgrokTokenError}>
            <FormLabel htmlFor='ngrok_key'>Ngrok Auth Token (Optional)</FormLabel>
            <Input
                id='password'
                type={showNgrokToken ? 'text' : 'password'}
                maxWidth="20em"
                value={newNgrokToken}
                onChange={(e) => {
                    if (hasNgrokTokenError) setNgrokTokenError('');
                    setNewNgrokToken(e.target.value);
                }}
            />
            <IconButton
                ml={3}
                verticalAlign='top'
                aria-label='View Ngrok token'
                icon={showNgrokToken ? <AiFillEye /> : <AiFillEyeInvisible />}
                onClick={() => setShowNgrokToken.toggle()}
            />
            <IconButton
                ml={3}
                verticalAlign='top'
                aria-label='Save Ngrok token'
                icon={<AiOutlineSave />}
                onClick={() => saveNgrokToken(newNgrokToken)}
            />
            {!hasNgrokTokenError ? (
                <FormHelperText>
                    {helpText ?? (
                        <Text>
                            Using an Auth Token will allow you to use the benefits of the upgraded Ngrok
                            service. This can improve connection stability and reliability. If you do not have
                            an Ngrok Account, sign up for free here:&nbsp;
                            <Box as='span' color={baseTheme.colors.brand.primary}>
                                <Link href='https://dashboard.ngrok.com/get-started/your-authtoken' target='_blank'>ngrok.com</Link>
                            </Box>
                        </Text>
                    )}
                </FormHelperText>
            ) : (
                <FormErrorMessage>{ngrokTokenError}</FormErrorMessage>
            )}
        </FormControl>
    );
}
Example #7
Source File: ProxyServiceField.tsx    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
ProxyServiceField = ({ helpText, showAddress = true }: ProxyServiceFieldProps): JSX.Element => {
    const dispatch = useAppDispatch();
    const dnsRef = useRef(null);
    const alertRef = useRef(null);
    const proxyService: string = (useAppSelector(state => state.config.proxy_service) ?? '').toLowerCase().replace(' ', '-');
    const address: string = useAppSelector(state => state.config.server_address) ?? '';
    const port: number = useAppSelector(state => state.config.socket_port) ?? 1234;
    const [dnsModalOpen, setDnsModalOpen] = useBoolean();
    const [requiresConfirmation, confirm] = useState((): string | null => {
        return null;
    });
    return (
        <FormControl>
            <FormLabel htmlFor='proxy_service'>Proxy Service</FormLabel>
            <Flex flexDirection='row' justifyContent='flex-start' alignItems='center'>
                <Select
                    id='proxy_service'
                    placeholder='Select Proxy Service'
                    maxWidth="15em"
                    mr={3}
                    value={proxyService}
                    onChange={(e) => {
                        if (!e.target.value || e.target.value.length === 0) return;
                        onSelectChange(e);
                        if (e.target.value === 'dynamic-dns') {
                            setDnsModalOpen.on();
                        } else if (e.target.value === 'cloudflare') {
                            confirm('confirmation');
                        }
                    }}
                >
                    <option value='ngrok'>Ngrok</option>
                    <option value='cloudflare'>Cloudflare</option>
                    <option value='dynamic-dns'>Dynamic DNS</option>
                </Select>
                {(proxyService === 'dynamic-dns')
                    ? (
                        <IconButton
                            mr={3}
                            aria-label='Set address'
                            icon={<AiOutlineEdit />}
                            onClick={() => setDnsModalOpen.on()}
                        />
                    ) : null}
                {(showAddress) ? (
                    <>
                        <Text fontSize="md" color="grey">Address: {address}</Text>
                        <IconButton
                            ml={3}
                            aria-label='Copy address'
                            icon={<BiCopy />}
                            onClick={() => copyToClipboard(address)}
                        />
                    </>
                ) : null}
            </Flex>
            <FormHelperText>
                {helpText ?? 'Select a proxy service to use to make your server internet-accessible. Without one selected, your server will only be accessible on your local network'}
            </FormHelperText>

            <DynamicDnsDialog
                modalRef={dnsRef}
                onConfirm={(address) => dispatch(setConfig({ name: 'server_address', value: address }))}
                isOpen={dnsModalOpen}
                port={port as number}
                onClose={() => setDnsModalOpen.off()}
            />

            <ConfirmationDialog
                title="Notice"
                modalRef={alertRef}
                onClose={() => confirm(null)}
                body={confirmationActions[requiresConfirmation as string]?.message}
                acceptText="OK"
                declineText={null}
                onAccept={() => {
                    confirmationActions[requiresConfirmation as string].func();
                }}
                isOpen={requiresConfirmation !== null}
            />
        </FormControl>
    );
}
Example #8
Source File: ContactDialog.tsx    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
ContactDialog = ({
    onCancel,
    onDelete,
    onCreate,
    onUpdate,
    onClose,
    onAddressAdd,
    onAddressDelete,
    isOpen,
    modalRef,
    existingContact,
}: ContactDialogProps): JSX.Element => {
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [displayName, setDisplayName] = useState('');
    const [currentAddress, setCurrentAddress] = useState('');
    const [hasEdited, setHasEdited] = useBoolean(false);
    const [phones, setPhones] = useState([] as ContactAddress[]);
    const [emails, setEmails] = useState([] as ContactAddress[]);
    const [firstNameError, setFirstNameError] = useState('');
    const isNameValid = (firstNameError ?? '').length > 0;

    useEffect(() => {
        if (!existingContact) return;
        if (existingContact.firstName) setFirstName(existingContact.firstName);
        if (existingContact.lastName) setLastName(existingContact.lastName);
        if (existingContact.displayName) setDisplayName(existingContact.displayName);
        if (existingContact.phoneNumbers) setPhones(existingContact.phoneNumbers);
        if (existingContact.emails) setEmails(existingContact.emails);
    }, [existingContact]);

    const addAddress = (address: string) => {
        const existsPhone = phones.map((e: ContactAddress) => e.address).includes(address);
        const existsEmail = emails.map((e: ContactAddress) => e.address).includes(address);
        if (existsPhone || existsEmail) {
            return showErrorToast({
                id: 'contacts',
                description: 'Address already exists!'
            });
        }

        if (address.includes('@')) {
            setEmails([{ address }, ...emails]);
        } else {
            setPhones([{ address }, ...phones]);
        }

        if (onAddressAdd && existingContact) {
            onAddressAdd(existingContact.id, address);
        }
    };

    const removeAddress = (address: string, addressId: number | null) => {
        if (address.includes('@')) {
            setEmails(emails.filter((e: NodeJS.Dict<any>) => e.address !== address));
        } else {
            setPhones(phones.filter((e: NodeJS.Dict<any>) => e.address !== address));
        }

        if (onAddressDelete && addressId) {
            onAddressDelete(addressId);
        }
    };

    const _onClose = () => {
        setPhones([]);
        setEmails([]);
        setFirstName('');
        setLastName('');
        setDisplayName('');
        setCurrentAddress('');
        setHasEdited.off();

        if (onClose) onClose();
    };

    return (
        <AlertDialog
            isOpen={isOpen}
            leastDestructiveRef={modalRef}
            onClose={() => onClose()}
        >
            <AlertDialogOverlay>
                <AlertDialogContent>
                    <AlertDialogHeader fontSize='lg' fontWeight='bold'>
                        {(existingContact) ? 'Edit Contact' : 'Add a new Contact'}
                    </AlertDialogHeader>

                    <AlertDialogBody>
                        <Text>Add a custom contact to the server's database</Text>
                        <FormControl isInvalid={isNameValid} mt={5}>
                            <FormLabel htmlFor='firstName'>First Name</FormLabel>
                            <Input
                                id='firstName'
                                type='text'
                                value={firstName}
                                placeholder='Tim'
                                onChange={(e) => {
                                    setFirstNameError('');
                                    setFirstName(e.target.value);
                                    if (!hasEdited) {
                                        setDisplayName(`${e.target.value} ${lastName}`.trim());
                                    }
                                }}
                            />
                            {isNameValid ? (
                                <FormErrorMessage>{firstNameError}</FormErrorMessage>
                            ) : null}
                        </FormControl>
                        <FormControl mt={5}>
                            <FormLabel htmlFor='lastName'>Last Name</FormLabel>
                            <Input
                                id='lastName'
                                type='text'
                                value={lastName}
                                placeholder='Apple'
                                onChange={(e) => {
                                    setLastName(e.target.value);
                                    if (!hasEdited) {
                                        setDisplayName(`${firstName} ${e.target.value}`.trim());
                                    }
                                }}
                            />
                        </FormControl>
                        <FormControl mt={5}>
                            <FormLabel htmlFor='lastName'>Display Name</FormLabel>
                            <Input
                                id='displayName'
                                type='text'
                                value={displayName}
                                placeholder='Tim Apple'
                                onChange={(e) => {
                                    setHasEdited.on();
                                    setDisplayName(e.target.value);
                                }}
                            />
                        </FormControl>
                        <FormControl mt={5}>
                            <FormLabel htmlFor='address'>Addresses</FormLabel>
                            <HStack>
                                <Input
                                    id='address'
                                    type='text'
                                    value={currentAddress}
                                    placeholder='Add Address'
                                    onChange={(e) => {
                                        setCurrentAddress(e.target.value);
                                    }}
                                />
                                <IconButton
                                    onClick={() => {
                                        if (!currentAddress || currentAddress.length === 0) return;
                                        addAddress(currentAddress);
                                        setCurrentAddress('');
                                    }}
                                    aria-label='Add'
                                    icon={<AiOutlinePlus />}
                                />
                            </HStack>
                            <Flex flexDirection="row" alignItems="center" justifyContent="flex-start" flexWrap="wrap" mt={2}>
                                {[...phones, ...emails].map(((e: ContactAddress) => {
                                    return (
                                        <Tag
                                            mt={1}
                                            mx={1}
                                            size={'md'}
                                            key={e.address}
                                            borderRadius='full'
                                            variant='solid'
                                        >
                                            <TagLabel>{e.address}</TagLabel>
                                            <TagCloseButton
                                                onClick={() => {
                                                    removeAddress(e.address, (e.id) ? e.id : null);
                                                }}
                                            />
                                        </Tag>
                                    );
                                }))}
                            </Flex>
                        </FormControl>
                    </AlertDialogBody>

                    <AlertDialogFooter>
                        <Button
                            ref={modalRef as React.LegacyRef<HTMLButtonElement> | undefined}
                            onClick={() => {
                                if (!existingContact && onCancel) onCancel();
                                if (existingContact && onUpdate) {
                                    existingContact.firstName = firstName;
                                    existingContact.lastName = lastName;
                                    existingContact.displayName = displayName;
                                    onUpdate(existingContact);
                                }
                                _onClose();
                            }}
                        >
                            {(existingContact) ? 'Save & Close' : 'Cancel'}
                        </Button>
                        {(existingContact) ? (
                            <Button
                                ml={3}
                                bg='red'
                                ref={modalRef as React.LegacyRef<HTMLButtonElement> | undefined}
                                onClick={() => {
                                    if (onDelete) {
                                        onDelete(Number.parseInt(existingContact.id));
                                    }

                                    _onClose();
                                }}
                            >
                                Delete
                            </Button>
                        ) : null}
                        {(!existingContact) ? (
                            <Button
                                ml={3}
                                bg='brand.primary'
                                ref={modalRef as React.LegacyRef<HTMLButtonElement> | undefined}
                                onClick={() => {
                                    if (firstName.length === 0) {
                                        setFirstNameError('Please enter a first name for the contact!');
                                        return;
                                    }

                                    if (onCreate) {
                                        onCreate({
                                            firstName,
                                            lastName,
                                            phoneNumbers: phones,
                                            emails: emails,
                                            displayName,
                                            birthday: '',
                                            avatar: '',
                                            id: '',
                                            sourceType: 'db'
                                        });
                                    }

                                    _onClose();
                                }}
                            >
                                Create
                            </Button>
                        ) : null}
                    </AlertDialogFooter>
                </AlertDialogContent>
            </AlertDialogOverlay>
        </AlertDialog>
    );
}
Example #9
Source File: ContactsTable.tsx    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
ContactsTable = ({
    contacts,
    onCreate,
    onDelete,
    onUpdate,
    onAddressAdd,
    onAddressDelete
}: {
    contacts: Array<ContactItem>,
    onCreate?: (contact: ContactItem) => void,
    onDelete?: (contactId: number | string) => void,
    onUpdate?: (contact: Partial<ContactItem>) => void,
    onAddressAdd?: (contactId: number | string, address: string) => void;
    onAddressDelete?: (contactAddressId: number) => void;
}): JSX.Element => {
    const dialogRef = useRef(null);
    const [dialogOpen, setDialogOpen] = useBoolean();
    const [selectedContact, setSelectedContact] = useState(null as any | null);

    return (
        <Box>
            <Table variant="striped" colorScheme="blue" size='sm'>
                <Thead>
                    <Tr>
                        <Th>Edit</Th>
                        <Th>Display Name</Th>
                        <Th isNumeric>Addresses</Th>
                    </Tr>
                </Thead>
                <Tbody>
                    {contacts.map(item => {
                        const name = (item.displayName && item.displayName.length > 0)
                            ? item.displayName
                            : [item?.firstName, item?.lastName].filter((e) => e && e.length > 0).join(' ');
                        const addresses = [
                            ...(item.phoneNumbers ?? []).map(e => e.address),
                            ...(item.emails ?? []).map(e => e.address)
                        ];
                        return (
                            <Tr key={`${item.sourceType}-${item.id}-${name}-${addresses.join('_')}`}>
                                <Td _hover={{ cursor: (item?.sourceType === 'api') ? 'auto' : 'pointer' }} onClick={() => {
                                    if (item?.sourceType === 'api') return;
                                    setSelectedContact(item);
                                    setDialogOpen.on();
                                }}>
                                    {(item?.sourceType === 'api') ? (
                                        <Tooltip label="Not Editable" hasArrow aria-label='not editable tooltip'>
                                            <span>
                                                <Icon as={MdOutlineEditOff} />
                                            </span>
                                        </Tooltip>
                                    ): (
                                        <Tooltip label="Click to Edit" hasArrow aria-label='editable tooltip'>
                                            <span>
                                                <Icon as={AiOutlineEdit} />
                                            </span>
                                        </Tooltip>
                                    )}
                                </Td>
                                <Td>{name}</Td>
                                <Td isNumeric>{addresses.map((addr) => (
                                    <Badge ml={2} key={`${name}-${addr}-${addresses.length}`}>{addr}</Badge>
                                ))}</Td>
                            </Tr>
                        );
                    })}
                </Tbody>
            </Table>

            <ContactDialog
                modalRef={dialogRef}
                isOpen={dialogOpen}
                existingContact={selectedContact}
                onDelete={onDelete}
                onCreate={onCreate}
                onUpdate={onUpdate}
                onAddressAdd={onAddressAdd}
                onAddressDelete={onAddressDelete}
                onClose={() => {
                    setSelectedContact(null);
                    setDialogOpen.off();
                }}
            />
        </Box>
    );
}
Example #10
Source File: ApiLayout.tsx    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
ApiLayout = (): JSX.Element => {
    const dialogRef = useRef(null);
    const [dialogOpen, setDialogOpen] = useBoolean();
    const webhooks = useAppSelector(state => state.webhookStore.webhooks);

    return (
        <Box p={3} borderRadius={10}>
            <Flex flexDirection="column">
                <Stack direction='column' p={5}>
                    <Flex flexDirection='row' justifyContent='flex-start' alignItems='center'>
                        <Text fontSize='2xl'>API</Text>
                        <Popover trigger='hover'>
                            <PopoverTrigger>
                                <Box ml={2} _hover={{ color: 'brand.primary', cursor: 'pointer' }}>
                                    <AiOutlineInfoCircle />
                                </Box>
                            </PopoverTrigger>
                            <PopoverContent>
                                <PopoverArrow />
                                <PopoverCloseButton />
                                <PopoverHeader>Information</PopoverHeader>
                                <PopoverBody>
                                    <Text>
                                        Learn how you can interact with the API to automate and orchestrate iMessage-related
                                        actions. Our REST API gives you access to the underlying iMessage API in a
                                        more succinct and easy to digest way. We also offer webhooks so you can receive
                                        callbacks from the server.
                                    </Text>
                                </PopoverBody>
                            </PopoverContent>
                        </Popover>
                    </Flex>
                    <Divider orientation='horizontal' />
                    <Text>
                        BlueBubbles offers a high-level REST API to interact with the server, as well as iMessage itself.
                        With the API, you'll be able to send messages, fetch messages, filter chats, and more! To see what
                        else you can do in the API, please see the documentation below:
                    </Text>
                    <Spacer />
                    <LinkBox as='article' maxW='sm' px='5' pb={5} pt={2} borderWidth='1px' rounded='xl'>
                        <Text color='gray'>
                            https://documenter.getpostman.com
                        </Text>
                        <Heading size='sm' mt={2}>
                            <LinkOverlay href='https://documenter.getpostman.com/view/765844/UV5RnfwM' target='_blank'>
                                Click to view API documentation
                            </LinkOverlay>
                        </Heading>
                    </LinkBox>
                    
                </Stack>
                <Stack direction='column' p={5}>
                    <Flex flexDirection='row' justifyContent='flex-start' alignItems='center'>
                        <Text fontSize='2xl'>Webhooks</Text>
                        <Popover trigger='hover'>
                            <PopoverTrigger>
                                <Box ml={2} _hover={{ color: 'brand.primary', cursor: 'pointer' }}>
                                    <AiOutlineInfoCircle />
                                </Box>
                            </PopoverTrigger>
                            <PopoverContent>
                                <PopoverArrow />
                                <PopoverCloseButton />
                                <PopoverHeader>Information</PopoverHeader>
                                <PopoverBody>
                                    <Text>
                                        Any webhooks registered here will receive a POST request whenever an iMessage event
                                        occurs. The body of the POST request will be a JSON payload containing the type of
                                        event and the event data.
                                    </Text>
                                </PopoverBody>
                            </PopoverContent>
                        </Popover>
                    </Flex>
                    <Divider orientation='horizontal' />
                    <Spacer />
                    <Box>
                        <Menu>
                            <MenuButton
                                as={Button}
                                rightIcon={<BsChevronDown />}
                                width="12em"
                            >
                                Manage
                            </MenuButton>
                            <MenuList>
                                <MenuItem icon={<AiOutlinePlus />} onClick={setDialogOpen.on}>
                                    Add Webhook
                                </MenuItem>
                            </MenuList>
                        </Menu>
                    </Box>
                    <Spacer />
                    <WebhooksTable webhooks={webhooks} />
                </Stack>
            </Flex>

            <AddWebhookDialog
                modalRef={dialogRef}
                isOpen={dialogOpen}
                onClose={() => setDialogOpen.off()}
            />
        </Box>
    );
}
Example #11
Source File: ContactsLayout.tsx    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
ContactsLayout = (): JSX.Element => {
    const [search, setSearch] = useState('' as string);
    const [isLoading, setIsLoading] = useBoolean(true);
    const [contacts, setContacts] = useState([] as any[]);
    const [permission, setPermission] = useState((): string | null => {
        return null;
    });
    const dialogRef = useRef(null);
    const inputFile = useRef(null);
    const [dialogOpen, setDialogOpen] = useBoolean();
    const alertRef = useRef(null);
    const [requiresConfirmation, confirm] = useState((): string | null => {
        return null;
    });

    let filteredContacts = contacts;
    if (search && search.length > 0) {
        filteredContacts = filteredContacts.filter((c) => buildIdentifier(c).includes(search.toLowerCase()));
    }

    const {
        currentPage,
        setCurrentPage,
        pagesCount,
        pages
    } = usePagination({
        pagesCount: Math.ceil(filteredContacts.length / perPage),
        initialState: { currentPage: 1 },
    });

    const refreshPermissionStatus = async (): Promise<void> => {
        setPermission(null);
        await waitMs(500);
        ipcRenderer.invoke('contact-permission-status').then((status: string) => {
            setPermission(status);
        }).catch(() => {
            setPermission('Unknown');
        });
    };

    const requestContactPermission = async (): Promise<void> => {
        setPermission(null);
        ipcRenderer.invoke('request-contact-permission').then((status: string) => {
            setPermission(status);
        }).catch(() => {
            setPermission('Unknown');
        });
    };

    const loadContacts = (showToast = false) => {
        ipcRenderer.invoke('get-contacts').then((contactList: any[]) => {
            setContacts(contactList.map((e: any) => {
                // Patch the ID as a string
                e.id = String(e.id);
                return e;
            }));
            setIsLoading.off();
        }).catch(() => {
            setIsLoading.off();
        });

        if (showToast) {
            showSuccessToast({
                id: 'contacts',
                description: 'Successfully refreshed Contacts!'
            });
        }
    };

    useEffect(() => {
        loadContacts();
        refreshPermissionStatus();
    }, []);

    const getEmptyContent = () => {
        const wrap = (child: JSX.Element) => {
            return (
                <section style={{marginTop: 20}}>
                    {child}
                </section>
            );
        };

        if (isLoading) {
            return wrap(<CircularProgress isIndeterminate />);
        }

        if (contacts.length === 0) {
            return wrap(<Text fontSize="md">BlueBubbles found no contacts in your Mac's Address Book!</Text>);
        }

        return null;
    };

    const filterContacts = () => {
        return filteredContacts.slice((currentPage - 1) * perPage, currentPage * perPage);
    };

    const onCreate = async (contact: ContactItem) => {
        const newContact = await createContact(
            contact.firstName,
            contact.lastName,
            {
                emails: contact.emails.map((e: NodeJS.Dict<any>) => e.address),
                phoneNumbers: contact.phoneNumbers.map((e: NodeJS.Dict<any>) => e.address)
            }
        );

        if (newContact) {
            // Patch the contact using a string ID & source type
            newContact.id = String(newContact.id);
            newContact.sourceType = 'db';

            // Patch the addresses
            (newContact as any).phoneNumbers = (newContact as any).addresses.filter((e: any) => e.type === 'phone');
            (newContact as any).emails = (newContact as any).addresses.filter((e: any) => e.type === 'email');

            setContacts([newContact, ...contacts]);
        }
    };

    const onUpdate = async (contact: NodeJS.Dict<any>) => {
        const cId = typeof(contact.id) === 'string' ? Number.parseInt(contact.id) : contact.id as number;
        const newContact = await updateContact(
            cId,
            {
                firstName: contact.firstName,
                lastName: contact.lastName,
                displayName: contact.displayName
            }
        );

        const copiedContacts = [...contacts];
        let updated = false;
        for (let i = 0; i < copiedContacts.length; i++) {
            if (copiedContacts[i].id === String(cId)) {
                copiedContacts[i].firstName = newContact.firstName;
                copiedContacts[i].lastName = newContact.lastName;
                copiedContacts[i].displayName = newContact.displayName;
                updated = true;
            }
        }

        if (updated) {
            setContacts(copiedContacts);
        }
    };

    const onDelete = async (contactId: number | string) => {
        await deleteContact(typeof(contactId) === 'string' ? Number.parseInt(contactId as string) : contactId);
        setContacts(contacts.filter((e: ContactItem) => {
            return e.id !== String(contactId);
        }));
    };

    const onAddAddress = async (contactId: number | string, address: string) => {
        const cId = typeof(contactId) === 'string' ? Number.parseInt(contactId as string) : contactId;
        const addr = await addAddressToContact(cId, address, address.includes('@') ? 'email' : 'phone');
        if (addr) {
            setContacts(contacts.map((e: ContactItem) => {
                if (e.id !== String(contactId)) return e;
                if (address.includes('@')) {
                    e.emails = [...e.emails, addr];
                } else {
                    e.phoneNumbers = [...e.phoneNumbers, addr];
                }

                return e;
            }));
        }
    };

    const onDeleteAddress = async (contactAddressId: number) => {
        await deleteContactAddress(contactAddressId);
        setContacts(contacts.map((e: ContactItem) => {
            e.emails = e.emails.filter((e: ContactAddress) => e.id !== contactAddressId);
            e.phoneNumbers = e.phoneNumbers.filter((e: ContactAddress) => e.id !== contactAddressId);
            return e;
        }));
    };

    const clearLocalContacts = async () => {
        // Delete the contacts, then filter out the DB items
        await deleteLocalContacts();
        setContacts(contacts.filter(e => e.sourceType !== 'db'));
    };

    const confirmationActions: ConfirmationItems = {
        clearLocalContacts: {
            message: (
                'Are you sure you want to clear/delete all local Contacts?<br /><br />' +
                'This will remove any Contacts added manually, via the API, or via the import process'
            ),
            func: clearLocalContacts
        }
    };

    return (
        <Box p={3} borderRadius={10}>
            <Stack direction='column' p={5}>
                <Text fontSize='2xl'>Controls</Text>
                <Divider orientation='horizontal' />
                <Box>
                    <Menu>
                        <MenuButton
                            as={Button}
                            rightIcon={<BsChevronDown />}
                            width="12em"mr={5}
                        >
                            Manage
                        </MenuButton>
                        <MenuList>
                            <MenuItem icon={<BsPersonPlus />} onClick={() => setDialogOpen.on()}>
                                Add Contact
                            </MenuItem>
                            <MenuItem icon={<BiRefresh />} onClick={() => loadContacts(true)}>
                                Refresh Contacts
                            </MenuItem>
                            <MenuItem
                                icon={<BiImport />}
                                onClick={() => {
                                    if (inputFile && inputFile.current) {
                                        (inputFile.current as HTMLElement).click();
                                    }
                                }}
                            >
                                Import VCF
                                <input
                                    type='file'
                                    id='file'
                                    ref={inputFile}
                                    accept=".vcf"
                                    style={{display: 'none'}}
                                    onChange={(e) => {
                                        const files = e?.target?.files ?? [];
                                        for (const i of files) {
                                            ipcRenderer.invoke('import-vcf', i.webkitRelativePath);
                                        }
                                    }}
                                />
                            </MenuItem>
                            <MenuDivider />
                            <MenuItem icon={<FiTrash />} onClick={() => confirm('clearLocalContacts')}>
                                Clear Local Contacts
                            </MenuItem>
                        </MenuList>
                    </Menu>
                    <Menu>
                        <MenuButton
                            as={Button}
                            rightIcon={<BsChevronDown />}
                            width="12em"
                            mr={5}
                        >
                            Permissions
                        </MenuButton>
                        <MenuList>
                            <MenuItem icon={<BiRefresh />} onClick={() => refreshPermissionStatus()}>
                                Refresh Permission Status
                            </MenuItem>
                            {(permission !== null && permission !== 'Authorized') ? (
                                <MenuItem icon={<BsUnlockFill />} onClick={() => requestContactPermission()}>
                                    Request Permission
                                </MenuItem>
                            ) : null}
                        </MenuList>
                    </Menu>
                    <Text as="span" verticalAlign="middle">
                        Status: <Text as="span" color={getPermissionColor(permission)}>
                            {permission ? permission : 'Checking...'}
                        </Text>
                    </Text>
                </Box>
            </Stack>
            <Stack direction='column' p={5}>
                <Flex flexDirection='row' justifyContent='flex-start' alignItems='center'>
                    <Text fontSize='2xl'>Contacts ({filteredContacts.length})</Text>
                    <Popover trigger='hover'>
                        <PopoverTrigger>
                            <Box ml={2} _hover={{ color: 'brand.primary', cursor: 'pointer' }}>
                                <AiOutlineInfoCircle />
                            </Box>
                        </PopoverTrigger>
                        <PopoverContent>
                            <PopoverArrow />
                            <PopoverCloseButton />
                            <PopoverHeader>Information</PopoverHeader>
                            <PopoverBody>
                                <Text>
                                    Here are the contacts on your macOS device that BlueBubbles knows about,
                                    and will serve to any clients that want to know about them. These include
                                    contacts from this Mac's Address Book, as well as contacts from uploads/imports
                                    or manual entry.
                                </Text>
                            </PopoverBody>
                        </PopoverContent>
                    </Popover>
                </Flex>
                <Divider orientation='horizontal' />
                <Flex flexDirection='row' justifyContent='flex-end' alignItems='center' pt={3}>
                    <InputGroup width="xxs">
                        <InputLeftElement pointerEvents='none'>
                            <AiOutlineSearch color='gray.300' />
                        </InputLeftElement>
                        <Input
                            placeholder='Search Contacts'
                            onChange={(e) => {
                                if (currentPage > 1) {
                                    setCurrentPage(1);
                                }

                                setSearch(e.target.value);
                            }}
                            value={search}
                        />
                    </InputGroup>
                </Flex>
                <Flex justifyContent="center" alignItems="center">
                    {getEmptyContent()}
                </Flex>
                {(contacts.length > 0) ? (
                    <ContactsTable
                        contacts={filterContacts()}
                        onCreate={onCreate}
                        onDelete={onDelete}
                        onUpdate={onUpdate}
                        onAddressAdd={onAddAddress}
                        onAddressDelete={onDeleteAddress}
                    />
                ) : null}
                <Pagination
                    pagesCount={pagesCount}
                    currentPage={currentPage}
                    onPageChange={setCurrentPage}
                >
                    <PaginationContainer
                        align="center"
                        justify="space-between"
                        w="full"
                        pt={2}
                    >
                        <PaginationPrevious minWidth={'75px'}>Previous</PaginationPrevious>
                        <Box ml={1}></Box>
                        <PaginationPageGroup flexWrap="wrap" justifyContent="center">
                            {pages.map((page: number) => (
                                <PaginationPage 
                                    key={`pagination_page_${page}`} 
                                    page={page}
                                    my={1}
                                    px={3}
                                    fontSize={14}
                                />
                            ))}
                        </PaginationPageGroup>
                        <Box ml={1}></Box>
                        <PaginationNext minWidth={'50px'}>Next</PaginationNext>
                    </PaginationContainer>
                </Pagination>
            </Stack>

            <ContactDialog
                modalRef={dialogRef}
                isOpen={dialogOpen}
                onCreate={onCreate}
                onDelete={onDelete}
                onAddressAdd={onAddAddress}
                onAddressDelete={onDeleteAddress}
                onClose={() => setDialogOpen.off()}
            />

            <ConfirmationDialog
                modalRef={alertRef}
                onClose={() => confirm(null)}
                body={confirmationActions[requiresConfirmation as string]?.message}
                onAccept={() => {
                    confirmationActions[requiresConfirmation as string].func();
                }}
                isOpen={requiresConfirmation !== null}
            />
        </Box>
    );
}
Example #12
Source File: FcmLayout.tsx    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
FcmLayout = (): JSX.Element => {
    const dispatch = useAppDispatch();
    const alertRef = useRef(null);
    const confirmationActions: NodeJS.Dict<any> = {
        clearConfiguration: {
            message: (
                'Are you sure you want to clear your FCM Configuration?<br /><br />' +
                'Doing so will prevent notifications from being delivered until ' +
                'your configuration is re-loaded'
            ),
            func: async () => {
                const success = await clearFcmConfiguration();
                if (success) {
                    dispatch(setConfig({ name: 'fcm_client', 'value': null }));
                    dispatch(setConfig({ name: 'fcm_server', 'value': null }));
                }
            }
        }
    };

    const serverLoaded = (useAppSelector(state => state.config.fcm_server !== null) ?? false);
    const clientLoaded = (useAppSelector(state => state.config.fcm_client !== null) ?? false);
    const [isDragging, setDragging] = useBoolean();
    const [errors, setErrors] = useState([] as Array<ErrorItem>);
    const [requiresConfirmation, setRequiresConfirmation] = useState(null as string | null);
    const alertOpen = errors.length > 0;
    

    const onDrop = async (e: React.DragEvent<HTMLDivElement>) => {
        e.preventDefault();

        dragCounter = 0;
        setDragging.off();
        
        // I'm not sure why, but we need to copy the file data _before_ we read it using the file reader.
        // If we do not, the data transfer file list gets set to empty after reading the first file.
        const listCopy: Array<Blob> = [];
        for (let i = 0; i < e.dataTransfer.files.length; i++) {
            listCopy.push(e.dataTransfer.files.item(i) as Blob);
        }

        // Actually read the files
        const errors: Array<ErrorItem> = [];
        for (let i = 0; i < listCopy.length; i++) {
            try {
                const fileStr = await readFile(listCopy[i]);
                const validClient = isValidClientConfig(fileStr);
                const validServer = isValidServerConfig(fileStr);
                const jsonData = JSON.parse(fileStr);

                if (validClient) {
                    const test = isValidFirebaseUrl(jsonData);
                    if (test) {
                        await saveFcmClient(jsonData);
                        dispatch(setConfig({ name: 'fcm_client', 'value': jsonData }));
                    } else {
                        throw new Error(
                            'Your Firebase setup does not have a real-time database enabled. ' +
                            'Please enable the real-time database in your Firebase Console.'
                        );
                    }
                } else if (validServer) {
                    await saveFcmServer(jsonData);
                    dispatch(setConfig({ name: 'fcm_server', 'value': jsonData }));
                } else {
                    throw new Error('Invalid Google FCM File!');
                }
            } catch (ex: any) {
                errors.push({ id: String(i), message: ex?.message ?? String(ex) });
            }
        }

        if (errors.length > 0) {
            setErrors(errors);
        }
    };

    const onDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
        e.preventDefault();
        if (dragCounter === 0) {
            setDragging.on();
        }

        dragCounter += 1;
    };

    const onDragOver = (e: React.DragEvent<HTMLDivElement>) => {
        e.stopPropagation();
        e.preventDefault();
    };

    const onDragLeave = () => {
        dragCounter -= 1;
        if (dragCounter === 0) {
            setDragging.off();
        }
    };

    const closeAlert = () => {
        setErrors([]);
    };

    const confirm = (confirmationType: string | null) => {
        setRequiresConfirmation(confirmationType);
    };

    return (
        <Box
            p={3}
            borderRadius={10}
            onDragEnter={(e) => onDragEnter(e)}
            onDragLeave={() => onDragLeave()}
            onDragOver={(e) => onDragOver(e)}
            onDrop={(e) => onDrop(e)}
        >
            <Stack direction='column' p={5}>
                <Text fontSize='2xl'>Controls</Text>
                <Divider orientation='horizontal' />
                <Flex flexDirection="row" justifyContent="flex-start">
                    <Menu>
                        <MenuButton
                            as={Button}
                            rightIcon={<BsChevronDown />}
                            width="12em"
                            mr={5}
                        >
                            Manage
                        </MenuButton>
                        <MenuList>
                            <MenuItem icon={<FiTrash />} onClick={() => confirm('clearConfiguration')}>
                                Clear Configuration
                            </MenuItem>
                        </MenuList>
                    </Menu>
                </Flex>
            </Stack>
            <Stack direction='column' p={5}>
                <Flex flexDirection='row' justifyContent='flex-start' alignItems='center'>
                    <Text fontSize='2xl'>Configuration</Text>
                    <Popover trigger='hover'>
                        <PopoverTrigger>
                            <Box ml={2} _hover={{ color: 'brand.primary', cursor: 'pointer' }}>
                                <AiOutlineInfoCircle />
                            </Box>
                        </PopoverTrigger>
                        <PopoverContent>
                            <PopoverArrow />
                            <PopoverCloseButton />
                            <PopoverHeader>Information</PopoverHeader>
                            <PopoverBody>
                                <Text>
                                    Drag and drop your JSON configuration files from your Google Firebase Console. If you
                                    do not have these configuration files. Please go to
                                    <span style={{ color: baseTheme.colors.brand.primary }}>
                                        <Link href='https://bluebubbles.app/install' color='brand.primary' target='_blank'> Our Website </Link>
                                    </span>
                                    to learn how.
                                </Text>
                                <Text>
                                    These configurations enable the BlueBubbles server to send notifications and other
                                    messages to all of the clients via Google FCM. Google Play Services is required
                                    for Android Devices.
                                </Text>
                            </PopoverBody>
                        </PopoverContent>
                    </Popover>
                </Flex>
                <Divider orientation='horizontal' />
                <Spacer />

                <SimpleGrid columns={2} spacing={5}>
                    <DropZone
                        text="Drag n' Drop Google Services JSON"
                        loadedText="Google Services JSON Successfully Loaded!"
                        isDragging={isDragging}
                        isLoaded={clientLoaded}
                    />
                    <DropZone
                        text="Drag n' Drop Admin SDK JSON"
                        loadedText="Admin SDK JSON Successfully Loaded!"
                        isDragging={isDragging}
                        isLoaded={serverLoaded}
                    />
                </SimpleGrid>
            </Stack>

            <ErrorDialog
                errors={errors}
                modalRef={alertRef}
                onClose={() => closeAlert()}
                isOpen={alertOpen}
            />

            <ConfirmationDialog
                modalRef={alertRef}
                onClose={() => confirm(null)}
                body={confirmationActions[requiresConfirmation as string]?.message}
                onAccept={() => {
                    if (hasKey(confirmationActions, requiresConfirmation as string)) {
                        confirmationActions[requiresConfirmation as string].func();
                    }
                }}
                isOpen={requiresConfirmation !== null}
            />
        </Box>
    );
}
Example #13
Source File: NotificationsWalkthrough.tsx    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
NotificationsWalkthrough = (): JSX.Element => {
    const dispatch = useAppDispatch();
    const alertRef = useRef(null);

    const serverLoaded = (useAppSelector(state => state.config.fcm_server !== null) ?? false);
    const clientLoaded = (useAppSelector(state => state.config.fcm_client !== null) ?? false);
    const [isDragging, setDragging] = useBoolean();
    const [errors, setErrors] = useState([] as Array<ErrorItem>);
    const alertOpen = errors.length > 0;

    const onDrop = async (e: React.DragEvent<HTMLDivElement>) => {
        e.preventDefault();

        dragCounter = 0;
        setDragging.off();
        
        // I'm not sure why, but we need to copy the file data _before_ we read it using the file reader.
        // If we do not, the data transfer file list gets set to empty after reading the first file.
        const listCopy: Array<Blob> = [];
        for (let i = 0; i < e.dataTransfer.files.length; i++) {
            listCopy.push(e.dataTransfer.files.item(i) as Blob);
        }

        // Actually read the files
        const errors: Array<ErrorItem> = [];
        for (let i = 0; i < listCopy.length; i++) {
            try {
                const fileStr = await readFile(listCopy[i]);
                const validClient = isValidClientConfig(fileStr);
                const validServer = isValidServerConfig(fileStr);
                const jsonData = JSON.parse(fileStr);

                if (validClient) {
                    const test = isValidFirebaseUrl(jsonData);
                    if (test) {
                        await saveFcmClient(jsonData);
                        dispatch(setConfig({ name: 'fcm_client', 'value': jsonData }));
                    } else {
                        throw new Error(
                            'Your Firebase setup does not have a real-time database enabled. ' +
                            'Please enable the real-time database in your Firebase Console.'
                        );
                    }
                } else if (validServer) {
                    await saveFcmServer(jsonData);
                    dispatch(setConfig({ name: 'fcm_server', 'value': jsonData }));
                } else {
                    throw new Error('Invalid Google FCM File!');
                }
            } catch (ex: any) {
                errors.push({ id: String(i), message: ex?.message ?? String(ex) });
            }
        }

        if (errors.length > 0) {
            setErrors(errors);
        }
    };

    const onDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
        e.preventDefault();
        if (dragCounter === 0) {
            setDragging.on();
        }

        dragCounter += 1;
    };

    const onDragOver = (e: React.DragEvent<HTMLDivElement>) => {
        e.stopPropagation();
        e.preventDefault();
    };

    const onDragLeave = () => {
        dragCounter -= 1;
        if (dragCounter === 0) {
            setDragging.off();
        }
    };

    const closeAlert = () => {
        setErrors([]);
    };

    return (
        <SlideFade in={true} offsetY='150px'>
            <Box
                px={5}
                onDragEnter={(e) => onDragEnter(e)}
                onDragLeave={() => onDragLeave()}
                onDragOver={(e) => onDragOver(e)}
                onDrop={(e) => onDrop(e)}    
            >
                <Text fontSize='4xl'>Notifications &amp; Firebase</Text>
                <Text fontSize='md' mt={5}>
                    BlueBubbles utilizes Google FCM (Firebase Cloud Messaging) to deliver notifications to your devices.
                    We do this so the client do not need to hold a connection to the server at all times. As a result,
                    BlueBubbles can deliver notifications even when the app is running in the background. It also means
                    BlueBubbles will use less battery when running in the background.
                </Text>
                <Alert status='info' mt={5}>
                    <AlertIcon />
                    If you do not complete this setup, you will not receive notifications!
                </Alert>
                <Text fontSize='md' mt={5}>
                    The setup with Google FCM is a bit tedious, but it is a "set it and forget it" feature. Follow the
                    instructions here: <Link
                        as='span'
                        href='https://bluebubbles.app/install/'
                        color='brand.primary'
                        target='_blank'>https://bluebubbles.app/install</Link>
                </Text>
                <Text fontSize='3xl' mt={5}>Firebase Configurations</Text>
                
                <SimpleGrid columns={2} spacing={5} mt={5}>
                    <DropZone
                        text="Drag n' Drop google-services.json"
                        loadedText="google-services.json Successfully Loaded!"
                        isDragging={isDragging}
                        isLoaded={serverLoaded}
                    />
                    <DropZone
                        text="Drag n' Drop *-firebase-adminsdk-*.json"
                        loadedText="*-firebase-adminsdk-*.json Successfully Loaded!"
                        isDragging={isDragging}
                        isLoaded={clientLoaded}
                    />
                </SimpleGrid>
            </Box>

            <ErrorDialog
                errors={errors}
                modalRef={alertRef}
                onClose={() => closeAlert()}
                isOpen={alertOpen}
            />
        </SlideFade>
    );
}