@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 |
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 |
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>:
<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 |
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>:
<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 |
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 |
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 |
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:
<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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 & 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>
);
}