diff --git a/native/apps/apps-directory.react.js b/native/apps/apps-directory.react.js
index ccce31f02..fb9190641 100644
--- a/native/apps/apps-directory.react.js
+++ b/native/apps/apps-directory.react.js
@@ -1,72 +1,78 @@
// @flow
import * as React from 'react';
import { Text, FlatList, View } from 'react-native';
import { useSelector } from 'react-redux';
import AppListing from './app-listing.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
+import type { TabNavigationProp } from '../navigation/tab-navigator.react.js';
import { useStyles } from '../themes/colors.js';
const APP_DIRECTORY_DATA = [
{
id: 'chat',
alwaysEnabled: true,
appName: 'Chat',
appIcon: 'message-square',
appCopy: 'Keep in touch with your community',
},
{
id: 'calendar',
alwaysEnabled: false,
appName: 'Calendar',
appIcon: 'calendar',
appCopy: 'Shared calendar for your community',
},
];
+type Props = {
+ +navigation: TabNavigationProp<'Apps'>,
+ +route: NavigationRoute<'Apps'>,
+};
// eslint-disable-next-line no-unused-vars
-function AppsDirectory(props: { ... }): React.Node {
+function AppsDirectory(props: Props): React.Node {
const styles = useStyles(unboundStyles);
const enabledApps = useSelector(state => state.enabledApps);
const renderAppCell = React.useCallback(
({ item }) => (
),
[enabledApps],
);
const getItemID = React.useCallback(item => item.id, []);
return (
Choose Apps
);
}
const unboundStyles = {
view: {
flex: 1,
backgroundColor: 'panelBackground',
padding: 18,
},
title: {
color: 'modalForegroundLabel',
fontSize: 28,
paddingVertical: 12,
},
};
export default AppsDirectory;
diff --git a/native/profile/add-keyserver.react.js b/native/profile/add-keyserver.react.js
index 6e346aeab..e15fad37c 100644
--- a/native/profile/add-keyserver.react.js
+++ b/native/profile/add-keyserver.react.js
@@ -1,119 +1,125 @@
// @flow
import { useNavigation } from '@react-navigation/native';
import * as React from 'react';
import { View, Text } from 'react-native';
import { useDispatch } from 'react-redux';
import { addKeyserverActionType } from 'lib/actions/keyserver-actions.js';
import type { KeyserverInfo } from 'lib/types/keyserver-types.js';
import { defaultConnectionInfo } from 'lib/types/socket-types.js';
+import type { ProfileNavigationProp } from './profile.react.js';
import TextInput from '../components/text-input.react.js';
import HeaderRightTextButton from '../navigation/header-right-text-button.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { useStyles, useColors } from '../themes/colors.js';
+type Props = {
+ +navigation: ProfileNavigationProp<'AddKeyserver'>,
+ +route: NavigationRoute<'AddKeyserver'>,
+};
// eslint-disable-next-line no-unused-vars
-function AddKeyserver(props: { ... }): React.Node {
+function AddKeyserver(props: Props): React.Node {
const { goBack, setOptions } = useNavigation();
const dispatch = useDispatch();
const currentUserID = useSelector(state => state.currentUserInfo?.id);
const { panelForegroundTertiaryLabel } = useColors();
const styles = useStyles(unboundStyles);
const [urlInput, setUrlInput] = React.useState('');
const onPressSave = React.useCallback(() => {
if (!currentUserID || !urlInput) {
return;
}
const newKeyserverInfo: KeyserverInfo = {
cookie: null,
updatesCurrentAsOf: 0,
urlPrefix: urlInput,
connection: defaultConnectionInfo,
lastCommunicatedPlatformDetails: null,
deviceToken: null,
};
dispatch({
type: addKeyserverActionType,
payload: {
keyserverAdminUserID: currentUserID,
newKeyserverInfo,
},
});
goBack();
}, [currentUserID, dispatch, goBack, urlInput]);
React.useEffect(() => {
setOptions({
// eslint-disable-next-line react/display-name
headerRight: () => (
),
});
}, [onPressSave, setOptions, styles.header]);
const onChangeText = React.useCallback(
(text: string) => setUrlInput(text),
[],
);
return (
KEYSERVER URL
);
}
const unboundStyles = {
container: {
paddingTop: 8,
},
header: {
color: 'panelBackgroundLabel',
fontSize: 12,
fontWeight: '400',
paddingBottom: 3,
paddingHorizontal: 24,
},
inputContainer: {
backgroundColor: 'panelForeground',
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 24,
paddingVertical: 12,
borderBottomWidth: 1,
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
},
input: {
color: 'panelForegroundLabel',
flex: 1,
fontFamily: 'Arial',
fontSize: 16,
paddingVertical: 0,
borderBottomColor: 'transparent',
},
};
export default AddKeyserver;
diff --git a/native/profile/appearance-preferences.react.js b/native/profile/appearance-preferences.react.js
index 70fd7f7e0..5651468c0 100644
--- a/native/profile/appearance-preferences.react.js
+++ b/native/profile/appearance-preferences.react.js
@@ -1,154 +1,163 @@
// @flow
import * as React from 'react';
import { View, Text, Platform } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import { useUpdateThemePreference } from 'lib/hooks/theme.js';
import type {
GlobalThemeInfo,
GlobalThemePreference,
} from 'lib/types/theme-types.js';
+import type { ProfileNavigationProp } from './profile.react.js';
import Button from '../components/button.react.js';
import SWMansionIcon from '../components/swmansion-icon.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { type Colors, useColors, useStyles } from '../themes/colors.js';
import { osCanTheme } from '../themes/theme-utils.js';
const CheckIcon = () => (
);
type OptionText = {
themePreference: GlobalThemePreference,
text: string,
};
const optionTexts: OptionText[] = [
{ themePreference: 'light', text: 'Light' },
{ themePreference: 'dark', text: 'Dark' },
];
if (osCanTheme) {
optionTexts.push({
themePreference: 'system',
text: 'Follow system preferences',
});
}
type Props = {
+ +navigation: ProfileNavigationProp<'AppearancePreferences'>,
+ +route: NavigationRoute<'AppearancePreferences'>,
+globalThemeInfo: GlobalThemeInfo,
+updateThemePreference: (themePreference: GlobalThemePreference) => mixed,
+styles: typeof unboundStyles,
+colors: Colors,
- ...
};
class AppearancePreferences extends React.PureComponent {
render() {
const { panelIosHighlightUnderlay: underlay } = this.props.colors;
const options = [];
for (let i = 0; i < optionTexts.length; i++) {
const { themePreference, text } = optionTexts[i];
const icon =
themePreference === this.props.globalThemeInfo.preference ? (
) : null;
options.push(
,
);
if (i + 1 < optionTexts.length) {
options.push(
,
);
}
}
return (
APP THEME
{options}
);
}
}
const unboundStyles = {
header: {
color: 'panelBackgroundLabel',
fontSize: 12,
fontWeight: '400',
paddingBottom: 3,
paddingHorizontal: 24,
},
hr: {
backgroundColor: 'panelForegroundBorder',
height: 1,
marginHorizontal: 15,
},
icon: {
lineHeight: Platform.OS === 'ios' ? 18 : 20,
},
option: {
color: 'panelForegroundLabel',
fontSize: 16,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 24,
paddingVertical: 10,
},
scrollView: {
backgroundColor: 'panelBackground',
},
scrollViewContentContainer: {
paddingTop: 24,
},
section: {
backgroundColor: 'panelForeground',
borderBottomWidth: 1,
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
marginBottom: 24,
paddingVertical: 2,
},
};
-const ConnectedAppearancePreferences: React.ComponentType<{ ... }> =
- React.memo<{ ... }>(function ConnectedAppearancePreferences(props: { ... }) {
+type BaseProps = {
+ +navigation: ProfileNavigationProp<'AppearancePreferences'>,
+ +route: NavigationRoute<'AppearancePreferences'>,
+};
+const ConnectedAppearancePreferences: React.ComponentType =
+ React.memo(function ConnectedAppearancePreferences(
+ props: BaseProps,
+ ) {
const globalThemeInfo = useSelector(state => state.globalThemeInfo);
const updateThemePreference = useUpdateThemePreference();
const styles = useStyles(unboundStyles);
const colors = useColors();
return (
);
});
export default ConnectedAppearancePreferences;
diff --git a/native/profile/backup-menu.react.js b/native/profile/backup-menu.react.js
index 0fe986cdc..316962aae 100644
--- a/native/profile/backup-menu.react.js
+++ b/native/profile/backup-menu.react.js
@@ -1,124 +1,130 @@
// @flow
import * as React from 'react';
import { Alert, Switch, Text, View } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import { useDispatch } from 'react-redux';
import { getMessageForException } from 'lib/utils/errors.js';
import { entries } from 'lib/utils/objects.js';
+import type { ProfileNavigationProp } from './profile.react.js';
import { useClientBackup } from '../backup/use-client-backup.js';
import Button from '../components/button.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
import { setLocalSettingsActionType } from '../redux/action-types.js';
import { useSelector } from '../redux/redux-utils.js';
import { useColors, useStyles } from '../themes/colors.js';
+type Props = {
+ +navigation: ProfileNavigationProp<'BackupMenu'>,
+ +route: NavigationRoute<'BackupMenu'>,
+};
// eslint-disable-next-line no-unused-vars
-function BackupMenu(props: { ... }): React.Node {
+function BackupMenu(props: Props): React.Node {
const styles = useStyles(unboundStyles);
const dispatch = useDispatch();
const colors = useColors();
const userStore = useSelector(state => state.userStore);
const isBackupEnabled = useSelector(
state => state.localSettings.isBackupEnabled,
);
const { restoreBackupProtocol } = useClientBackup();
const testRestore = React.useCallback(async () => {
let message;
try {
const result = await restoreBackupProtocol({ userStore });
message = entries(result)
.map(([key, value]) => `${key}: ${String(value)}`)
.join('\n');
} catch (e) {
console.error(`Backup uploading error: ${e}`);
message = `Backup restore error: ${String(getMessageForException(e))}`;
}
Alert.alert('Restore protocol result', message);
}, [restoreBackupProtocol, userStore]);
const onBackupToggled = React.useCallback(
value => {
dispatch({
type: setLocalSettingsActionType,
payload: { isBackupEnabled: value },
});
},
[dispatch],
);
return (
SETTINGS
Toggle automatic backup
ACTIONS
);
}
const unboundStyles = {
scrollViewContentContainer: {
paddingTop: 24,
},
scrollView: {
backgroundColor: 'panelBackground',
},
section: {
backgroundColor: 'panelForeground',
borderBottomWidth: 1,
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
marginBottom: 24,
marginVertical: 2,
},
header: {
color: 'panelBackgroundLabel',
fontSize: 12,
fontWeight: '400',
paddingBottom: 3,
paddingHorizontal: 24,
},
submenuButton: {
flexDirection: 'row',
paddingHorizontal: 24,
paddingVertical: 10,
alignItems: 'center',
},
submenuText: {
color: 'panelForegroundLabel',
flex: 1,
fontSize: 16,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 24,
paddingVertical: 14,
},
};
export default BackupMenu;
diff --git a/native/profile/build-info.react.js b/native/profile/build-info.react.js
index c7b4f8303..9ca6ea9b6 100644
--- a/native/profile/build-info.react.js
+++ b/native/profile/build-info.react.js
@@ -1,116 +1,122 @@
// @flow
import * as React from 'react';
import { View, Text } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import { useIsCurrentUserStaff } from 'lib/shared/staff-utils.js';
+import type { ProfileNavigationProp } from './profile.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
import { persistConfig, codeVersion } from '../redux/persist.js';
import { StaffContext } from '../staff/staff-context.js';
import { useStyles } from '../themes/colors.js';
import { isStaffRelease, useStaffCanSee } from '../utils/staff-utils.js';
+type Props = {
+ +navigation: ProfileNavigationProp<'BuildInfo'>,
+ +route: NavigationRoute<'BuildInfo'>,
+};
// eslint-disable-next-line no-unused-vars
-function BuildInfo(props: { ... }): React.Node {
+function BuildInfo(props: Props): React.Node {
const isCurrentUserStaff = useIsCurrentUserStaff();
const { staffUserHasBeenLoggedIn } = React.useContext(StaffContext);
const styles = useStyles(unboundStyles);
const staffCanSee = useStaffCanSee();
let staffCanSeeRows;
if (staffCanSee || staffUserHasBeenLoggedIn) {
staffCanSeeRows = (
<>
__DEV__
{__DEV__ ? 'TRUE' : 'FALSE'}
Staff Release
{isStaffRelease ? 'TRUE' : 'FALSE'}
isCurrentUserStaff
{isCurrentUserStaff ? 'TRUE' : 'FALSE'}
hasStaffUserLoggedIn
{staffUserHasBeenLoggedIn ? 'TRUE' : 'FALSE'}
>
);
}
return (
Code version
{codeVersion}
State version
{persistConfig.version}
{staffCanSeeRows}
Thank you for using Comm!
);
}
const unboundStyles = {
label: {
color: 'panelForegroundTertiaryLabel',
fontSize: 16,
paddingRight: 12,
},
releaseText: {
color: 'orange',
fontSize: 16,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 6,
},
scrollView: {
backgroundColor: 'panelBackground',
},
scrollViewContentContainer: {
paddingTop: 24,
},
section: {
backgroundColor: 'panelForeground',
borderBottomWidth: 1,
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
marginBottom: 24,
paddingHorizontal: 24,
paddingVertical: 6,
},
text: {
color: 'panelForegroundLabel',
fontSize: 16,
},
thanksText: {
color: 'panelForegroundLabel',
flex: 1,
fontSize: 16,
textAlign: 'center',
},
};
export default BuildInfo;
diff --git a/native/profile/default-notifications-preferences.react.js b/native/profile/default-notifications-preferences.react.js
index 482f3d4b1..75fe1c68a 100644
--- a/native/profile/default-notifications-preferences.react.js
+++ b/native/profile/default-notifications-preferences.react.js
@@ -1,213 +1,213 @@
// @flow
import * as React from 'react';
import { View, Text, Platform } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import {
useSetUserSettings,
setUserSettingsActionTypes,
} from 'lib/actions/user-actions.js';
import { registerFetchKey } from 'lib/reducers/loading-reducer.js';
import {
type UpdateUserSettingsRequest,
type NotificationTypes,
type DefaultNotificationPayload,
notificationTypes,
userSettingsTypes,
} from 'lib/types/account-types.js';
import {
type DispatchActionPromise,
useDispatchActionPromise,
} from 'lib/utils/action-utils.js';
import type { ProfileNavigationProp } from './profile.react.js';
import Action from '../components/action-row.react.js';
import SWMansionIcon from '../components/swmansion-icon.react.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { useStyles } from '../themes/colors.js';
import Alert from '../utils/alert.js';
const CheckIcon = () => (
);
type ProfileRowProps = {
+content: string,
+onPress: () => void,
+danger?: boolean,
+selected?: boolean,
};
function NotificationRow(props: ProfileRowProps): React.Node {
const { content, onPress, danger, selected } = props;
return (
{selected ? : null}
);
}
type BaseProps = {
- +navigation: ProfileNavigationProp<>,
+ +navigation: ProfileNavigationProp<'DefaultNotifications'>,
+route: NavigationRoute<'DefaultNotifications'>,
};
type Props = {
...BaseProps,
+styles: typeof unboundStyles,
+dispatchActionPromise: DispatchActionPromise,
+changeNotificationSettings: (
notificationSettingsRequest: UpdateUserSettingsRequest,
) => Promise,
+selectedDefaultNotification: NotificationTypes,
};
class DefaultNotificationsPreferences extends React.PureComponent {
async updatedDefaultNotifications(
data: NotificationTypes,
): Promise {
const { changeNotificationSettings } = this.props;
try {
await changeNotificationSettings({
name: userSettingsTypes.DEFAULT_NOTIFICATIONS,
data,
});
} catch (e) {
Alert.alert(
'Unknown error',
'Uhh... try again?',
[{ text: 'OK', onPress: () => {} }],
{ cancelable: false },
);
}
return {
[userSettingsTypes.DEFAULT_NOTIFICATIONS]: data,
};
}
selectNotificationSetting = (data: NotificationTypes) => {
const { dispatchActionPromise } = this.props;
dispatchActionPromise(
setUserSettingsActionTypes,
this.updatedDefaultNotifications(data),
);
};
selectAllNotifications = () => {
this.selectNotificationSetting(notificationTypes.FOCUSED);
};
selectBackgroundNotifications = () => {
this.selectNotificationSetting(notificationTypes.BACKGROUND);
};
selectNoneNotifications = () => {
this.selectNotificationSetting(notificationTypes.BADGE_ONLY);
};
render() {
const { styles, selectedDefaultNotification } = this.props;
return (
NOTIFICATIONS
);
}
}
const unboundStyles = {
scrollView: {
backgroundColor: 'panelBackground',
},
scrollViewContentContainer: {
paddingTop: 24,
},
section: {
backgroundColor: 'panelForeground',
borderBottomWidth: 1,
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
marginBottom: 24,
marginVertical: 2,
},
icon: {
lineHeight: Platform.OS === 'ios' ? 18 : 20,
},
header: {
color: 'panelBackgroundLabel',
fontSize: 12,
fontWeight: '400',
paddingBottom: 3,
paddingHorizontal: 24,
},
};
registerFetchKey(setUserSettingsActionTypes);
const ConnectedDefaultNotificationPreferences: React.ComponentType =
React.memo(function ConnectedDefaultNotificationPreferences(
props: BaseProps,
) {
const styles = useStyles(unboundStyles);
const dispatchActionPromise = useDispatchActionPromise();
const changeNotificationSettings = useSetUserSettings();
const defaultNotification = userSettingsTypes.DEFAULT_NOTIFICATIONS;
const selectedDefaultNotification = useSelector(
({ currentUserInfo }) => {
if (
currentUserInfo?.settings &&
currentUserInfo?.settings[defaultNotification]
) {
return currentUserInfo?.settings[defaultNotification];
}
return notificationTypes.FOCUSED;
},
);
return (
);
});
export default ConnectedDefaultNotificationPreferences;
diff --git a/native/profile/delete-account.react.js b/native/profile/delete-account.react.js
index 340268014..b5c3ff020 100644
--- a/native/profile/delete-account.react.js
+++ b/native/profile/delete-account.react.js
@@ -1,116 +1,122 @@
// @flow
import * as React from 'react';
import { Text, View, ActivityIndicator } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import {
deleteAccountActionTypes,
useDeleteAccount,
} from 'lib/actions/user-actions.js';
import { preRequestUserStateSelector } from 'lib/selectors/account-selectors.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import { useDispatchActionPromise } from 'lib/utils/action-utils.js';
+import type { ProfileNavigationProp } from './profile.react.js';
import { deleteNativeCredentialsFor } from '../account/native-credentials.js';
import Button from '../components/button.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { useStyles } from '../themes/colors.js';
import Alert from '../utils/alert.js';
const loadingStatusSelector = createLoadingStatusSelector(
deleteAccountActionTypes,
);
-const DeleteAccount: React.ComponentType<{ ... }> = React.memo<{ ... }>(
+type Props = {
+ +navigation: ProfileNavigationProp<'DeleteAccount'>,
+ +route: NavigationRoute<'DeleteAccount'>,
+};
+const DeleteAccount: React.ComponentType = React.memo(
function DeleteAccount() {
const loadingStatus = useSelector(loadingStatusSelector);
const preRequestUserState = useSelector(preRequestUserStateSelector);
const styles = useStyles(unboundStyles);
const dispatchActionPromise = useDispatchActionPromise();
const callDeleteAccount = useDeleteAccount();
const buttonContent =
loadingStatus === 'loading' ? (
) : (
Delete account
);
const noWayToReverseThisStyles = React.useMemo(
() => [styles.warningText, styles.lastWarningText],
[styles.warningText, styles.lastWarningText],
);
const deleteAction = React.useCallback(async () => {
try {
await deleteNativeCredentialsFor();
return await callDeleteAccount(preRequestUserState);
} catch (e) {
Alert.alert('Unknown error', 'Uhh... try again?', [{ text: 'OK' }], {
cancelable: false,
});
throw e;
}
}, [callDeleteAccount, preRequestUserState]);
const onDelete = React.useCallback(() => {
dispatchActionPromise(deleteAccountActionTypes, deleteAction());
}, [dispatchActionPromise, deleteAction]);
return (
Your account will be permanently deleted.
There is no way to reverse this.
);
},
);
const unboundStyles = {
deleteButton: {
backgroundColor: 'vibrantRedButton',
borderRadius: 5,
flex: 1,
marginHorizontal: 24,
marginVertical: 12,
padding: 12,
},
lastWarningText: {
marginBottom: 24,
},
saveText: {
color: 'white',
fontSize: 18,
textAlign: 'center',
},
scrollView: {
backgroundColor: 'panelBackground',
},
scrollViewContentContainer: {
paddingTop: 24,
},
warningText: {
color: 'panelForegroundLabel',
fontSize: 16,
marginHorizontal: 24,
textAlign: 'center',
},
};
export default DeleteAccount;
diff --git a/native/profile/emoji-user-avatar-creation.react.js b/native/profile/emoji-user-avatar-creation.react.js
index 0a7fe427e..fe1d13c59 100644
--- a/native/profile/emoji-user-avatar-creation.react.js
+++ b/native/profile/emoji-user-avatar-creation.react.js
@@ -1,45 +1,51 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { EditUserAvatarContext } from 'lib/components/edit-user-avatar-provider.react.js';
import { savedEmojiAvatarSelectorForCurrentUser } from 'lib/selectors/user-selectors.js';
+import type { ProfileNavigationProp } from './profile.react.js';
import { useNativeSetUserAvatar } from '../avatars/avatar-hooks.js';
import EmojiAvatarCreation from '../avatars/emoji-avatar-creation.react.js';
import { displayActionResultModal } from '../navigation/action-result-modal.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
+type Props = {
+ +navigation: ProfileNavigationProp<'EmojiUserAvatarCreation'>,
+ +route: NavigationRoute<'EmojiUserAvatarCreation'>,
+};
// eslint-disable-next-line no-unused-vars
-function EmojiUserAvatarCreation(props: { ... }): React.Node {
+function EmojiUserAvatarCreation(props: Props): React.Node {
const editUserAvatarContext = React.useContext(EditUserAvatarContext);
invariant(editUserAvatarContext, 'editUserAvatarContext should be set');
const { userAvatarSaveInProgress } = editUserAvatarContext;
const nativeSetUserAvatar = useNativeSetUserAvatar();
const setAvatar = React.useCallback(
async avatarRequest => {
const result = await nativeSetUserAvatar(avatarRequest);
displayActionResultModal('Avatar updated!');
return result;
},
[nativeSetUserAvatar],
);
const savedEmojiAvatarFunc = useSelector(
savedEmojiAvatarSelectorForCurrentUser,
);
return (
);
}
export default EmojiUserAvatarCreation;
diff --git a/native/profile/keyserver-selection-list.react.js b/native/profile/keyserver-selection-list.react.js
index 8ea6e35bc..b27bf253f 100644
--- a/native/profile/keyserver-selection-list.react.js
+++ b/native/profile/keyserver-selection-list.react.js
@@ -1,121 +1,127 @@
// @flow
import * as React from 'react';
import { Text, View, FlatList } from 'react-native';
import { selectedKeyserversSelector } from 'lib/selectors/keyserver-selectors.js';
import type { SelectedKeyserverInfo } from 'lib/types/keyserver-types.js';
import KeyserverSelectionListItem from './keyserver-selection-list-item.react.js';
+import type { ProfileNavigationProp } from './profile.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { useStyles } from '../themes/colors.js';
function keyExtractor(item: SelectedKeyserverInfo) {
return `${item.keyserverAdminUserInfo.id}${item.keyserverInfo.urlPrefix}`;
}
function renderKeyserverListItem({ item }) {
return ;
}
+type Props = {
+ +navigation: ProfileNavigationProp<'KeyserverSelectionList'>,
+ +route: NavigationRoute<'KeyserverSelectionList'>,
+};
// eslint-disable-next-line no-unused-vars
-function KeyserverSelectionList(props: { ... }): React.Node {
+function KeyserverSelectionList(props: Props): React.Node {
const styles = useStyles(unboundStyles);
const selectedKeyserverInfos: $ReadOnlyArray =
useSelector(selectedKeyserversSelector);
const keyserverListSeparatorComponent = React.useCallback(
() => ,
[styles.separator],
);
const keyserverSelectionList = React.useMemo(
() => (
CONNECTED KEYSERVERS
),
[
keyserverListSeparatorComponent,
selectedKeyserverInfos,
styles.container,
styles.header,
styles.keyserverListContentContainer,
],
);
return keyserverSelectionList;
}
const unboundStyles = {
container: {
flex: 1,
backgroundColor: 'panelBackground',
paddingTop: 24,
},
header: {
color: 'panelBackgroundLabel',
fontSize: 12,
fontWeight: '400',
paddingBottom: 3,
paddingHorizontal: 24,
},
keyserverListContentContainer: {
backgroundColor: 'panelForeground',
borderBottomWidth: 1,
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
marginBottom: 24,
paddingVertical: 2,
},
keyserverListItemContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 24,
paddingVertical: 10,
},
separator: {
backgroundColor: 'panelForegroundBorder',
height: 1,
marginHorizontal: 16,
},
onlineIndicatorOuter: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'greenIndicatorOuter',
width: 18,
height: 18,
borderRadius: 9,
},
onlineIndicatorInner: {
backgroundColor: 'greenIndicatorInner',
width: 9,
height: 9,
borderRadius: 4.5,
},
offlineIndicatorOuter: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'redIndicatorOuter',
width: 18,
height: 18,
borderRadius: 9,
},
offlineIndicatorInner: {
backgroundColor: 'redIndicatorInner',
width: 9,
height: 9,
borderRadius: 4.5,
},
};
export default KeyserverSelectionList;
diff --git a/native/profile/linked-devices.react.js b/native/profile/linked-devices.react.js
index 4613330e8..4e43464da 100644
--- a/native/profile/linked-devices.react.js
+++ b/native/profile/linked-devices.react.js
@@ -1,9 +1,17 @@
// @flow
+
import * as React from 'react';
+import type { ProfileNavigationProp } from './profile.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
+
+type Props = {
+ +navigation: ProfileNavigationProp<'LinkedDevices'>,
+ +route: NavigationRoute<'LinkedDevices'>,
+};
// eslint-disable-next-line no-unused-vars
-function LinkedDevices(props: { ... }): React.Node {
+function LinkedDevices(props: Props): React.Node {
return null;
}
export default LinkedDevices;
diff --git a/native/profile/privacy-preferences.react.js b/native/profile/privacy-preferences.react.js
index a1def1a81..854ace1e4 100644
--- a/native/profile/privacy-preferences.react.js
+++ b/native/profile/privacy-preferences.react.js
@@ -1,75 +1,81 @@
// @flow
import * as React from 'react';
import { View, Text } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
+import type { ProfileNavigationProp } from './profile.react.js';
import ToggleReport from './toggle-report.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
import { useStyles } from '../themes/colors.js';
+type Props = {
+ +navigation: ProfileNavigationProp<'PrivacyPreferences'>,
+ +route: NavigationRoute<'PrivacyPreferences'>,
+};
// eslint-disable-next-line no-unused-vars
-function PrivacyPreferences(props: { ... }): React.Node {
+function PrivacyPreferences(props: Props): React.Node {
const styles = useStyles(unboundStyles);
return (
REPORTS
Toggle crash reports
Toggle media reports
Toggle inconsistency reports
);
}
const unboundStyles = {
scrollView: {
backgroundColor: 'panelBackground',
},
scrollViewContentContainer: {
paddingTop: 24,
},
section: {
backgroundColor: 'panelForeground',
borderBottomWidth: 1,
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
marginBottom: 24,
marginVertical: 2,
},
header: {
color: 'panelBackgroundLabel',
fontSize: 12,
fontWeight: '400',
paddingBottom: 3,
paddingHorizontal: 24,
},
submenuButton: {
flexDirection: 'row',
paddingHorizontal: 24,
paddingVertical: 10,
alignItems: 'center',
},
submenuText: {
color: 'panelForegroundLabel',
flex: 1,
fontSize: 16,
},
};
export default PrivacyPreferences;
diff --git a/native/profile/relationship-list.react.js b/native/profile/relationship-list.react.js
index 917731753..cc8ebffbf 100644
--- a/native/profile/relationship-list.react.js
+++ b/native/profile/relationship-list.react.js
@@ -1,499 +1,499 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { View, Text, Platform } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import {
updateRelationshipsActionTypes,
updateRelationships,
} from 'lib/actions/relationship-actions.js';
import {
searchUsersActionTypes,
searchUsers,
} from 'lib/actions/user-actions.js';
import { useENSNames } from 'lib/hooks/ens-cache.js';
import { registerFetchKey } from 'lib/reducers/loading-reducer.js';
import { userRelationshipsSelector } from 'lib/selectors/relationship-selectors.js';
import { userStoreSearchIndex as userStoreSearchIndexSelector } from 'lib/selectors/user-selectors.js';
import {
userRelationshipStatus,
relationshipActions,
} from 'lib/types/relationship-types.js';
import type {
GlobalAccountUserInfo,
AccountUserInfo,
} from 'lib/types/user-types.js';
import {
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-utils.js';
import type { ProfileNavigationProp } from './profile.react.js';
import RelationshipListItem from './relationship-list-item.react.js';
import LinkButton from '../components/link-button.react.js';
import { createTagInput, BaseTagInput } from '../components/tag-input.react.js';
import { KeyboardContext } from '../keyboard/keyboard-state.js';
import { OverlayContext } from '../navigation/overlay-context.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import {
FriendListRouteName,
BlockListRouteName,
} from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { useStyles, useIndicatorStyle } from '../themes/colors.js';
import type { VerticalBounds } from '../types/layout-types.js';
import Alert from '../utils/alert.js';
const TagInput = createTagInput();
export type RelationshipListNavigate = $PropertyType<
ProfileNavigationProp<'FriendList' | 'BlockList'>,
'navigate',
>;
const tagInputProps = {
placeholder: 'username',
autoFocus: true,
returnKeyType: 'go',
};
type ListItem =
| { +type: 'empty', +because: 'no-relationships' | 'no-results' }
| { +type: 'header' }
| { +type: 'footer' }
| {
+type: 'user',
+userInfo: AccountUserInfo,
+lastListItem: boolean,
+verticalBounds: ?VerticalBounds,
};
// ESLint doesn't recognize that invariant always throws
// eslint-disable-next-line consistent-return
function keyExtractor(item: ListItem) {
if (item.userInfo) {
return item.userInfo.id;
} else if (item.type === 'empty') {
return 'empty';
} else if (item.type === 'header') {
return 'header';
} else if (item.type === 'footer') {
return 'footer';
}
invariant(false, 'keyExtractor conditions should be exhaustive');
}
const tagDataLabelExtractor = (userInfo: GlobalAccountUserInfo) =>
userInfo.username;
type Props = {
- +navigation: ProfileNavigationProp<>,
+ +navigation: ProfileNavigationProp<'FriendList' | 'BlockList'>,
+route: NavigationRoute<'FriendList' | 'BlockList'>,
};
function RelationshipList(props: Props): React.Node {
const callSearchUsers = useServerCall(searchUsers);
const userInfos = useSelector(state => state.userStore.userInfos);
const searchUsersOnServer = React.useCallback(
async (usernamePrefix: string) => {
if (usernamePrefix.length === 0) {
return [];
}
const userInfosResult = await callSearchUsers(usernamePrefix);
return userInfosResult.userInfos;
},
[callSearchUsers],
);
const [searchInputText, setSearchInputText] = React.useState('');
const [userStoreSearchResults, setUserStoreSearchResults] = React.useState<
$ReadOnlySet,
>(new Set());
const [serverSearchResults, setServerSearchResults] = React.useState<
$ReadOnlyArray,
>([]);
const { route } = props;
const routeName = route.name;
const userStoreSearchIndex = useSelector(userStoreSearchIndexSelector);
const onChangeSearchText = React.useCallback(
async (searchText: string) => {
setSearchInputText(searchText);
const excludeStatuses = {
[FriendListRouteName]: [
userRelationshipStatus.BLOCKED_VIEWER,
userRelationshipStatus.BOTH_BLOCKED,
],
[BlockListRouteName]: [],
}[routeName];
const results = userStoreSearchIndex
.getSearchResults(searchText)
.filter(userID => {
const relationship = userInfos[userID].relationshipStatus;
return !excludeStatuses.includes(relationship);
});
setUserStoreSearchResults(new Set(results));
const searchResultsFromServer = await searchUsersOnServer(searchText);
const filteredServerSearchResults = searchResultsFromServer.filter(
searchUserInfo => {
const userInfo = userInfos[searchUserInfo.id];
return (
!userInfo || !excludeStatuses.includes(userInfo.relationshipStatus)
);
},
);
setServerSearchResults(filteredServerSearchResults);
},
[routeName, userStoreSearchIndex, userInfos, searchUsersOnServer],
);
const overlayContext = React.useContext(OverlayContext);
invariant(overlayContext, 'RelationshipList should have OverlayContext');
const scrollEnabled = overlayContext.scrollBlockingModalStatus === 'closed';
const tagInputRef = React.useRef>();
const flatListContainerRef = React.useRef>();
const keyboardState = React.useContext(KeyboardContext);
const keyboardNotShowing = !!(
keyboardState && !keyboardState.keyboardShowing
);
const [verticalBounds, setVerticalBounds] =
React.useState(null);
const onFlatListContainerLayout = React.useCallback(() => {
if (!flatListContainerRef.current) {
return;
}
if (!keyboardNotShowing) {
return;
}
flatListContainerRef.current.measure(
(x, y, width, height, pageX, pageY) => {
if (
height === null ||
height === undefined ||
pageY === null ||
pageY === undefined
) {
return;
}
setVerticalBounds({ height, y: pageY });
},
);
}, [keyboardNotShowing]);
const [currentTags, setCurrentTags] = React.useState<
$ReadOnlyArray,
>([]);
const onSelect = React.useCallback(
(selectedUser: GlobalAccountUserInfo) => {
if (currentTags.find(o => o.id === selectedUser.id)) {
return;
}
setSearchInputText('');
setCurrentTags(prevCurrentTags => prevCurrentTags.concat(selectedUser));
},
[currentTags],
);
const onUnknownErrorAlertAcknowledged = React.useCallback(() => {
setCurrentTags([]);
setSearchInputText('');
tagInputRef.current?.focus();
}, []);
const callUpdateRelationships = useServerCall(updateRelationships);
const updateRelationshipsOnServer = React.useCallback(async () => {
const action = {
[FriendListRouteName]: relationshipActions.FRIEND,
[BlockListRouteName]: relationshipActions.BLOCK,
}[routeName];
const userIDs = currentTags.map(userInfo => userInfo.id);
try {
const result = await callUpdateRelationships({
action,
userIDs,
});
setCurrentTags([]);
setSearchInputText('');
return result;
} catch (e) {
Alert.alert(
'Unknown error',
'Uhh... try again?',
[{ text: 'OK', onPress: onUnknownErrorAlertAcknowledged }],
{ cancelable: true, onDismiss: onUnknownErrorAlertAcknowledged },
);
throw e;
}
}, [
routeName,
currentTags,
callUpdateRelationships,
onUnknownErrorAlertAcknowledged,
]);
const dispatchActionPromise = useDispatchActionPromise();
const noCurrentTags = currentTags.length === 0;
const onPressAdd = React.useCallback(() => {
if (noCurrentTags) {
return;
}
dispatchActionPromise(
updateRelationshipsActionTypes,
updateRelationshipsOnServer(),
);
}, [noCurrentTags, dispatchActionPromise, updateRelationshipsOnServer]);
const inputProps = React.useMemo(
() => ({
...tagInputProps,
onSubmitEditing: onPressAdd,
}),
[onPressAdd],
);
const { navigation } = props;
const { navigate } = navigation;
const styles = useStyles(unboundStyles);
const renderItem = React.useCallback(
// ESLint doesn't recognize that invariant always throws
// eslint-disable-next-line consistent-return
({ item }: { item: ListItem, ... }) => {
if (item.type === 'empty') {
const action = {
[FriendListRouteName]: 'added',
[BlockListRouteName]: 'blocked',
}[routeName];
const emptyMessage =
item.because === 'no-relationships'
? `You haven't ${action} any users yet`
: 'No results';
return {emptyMessage};
} else if (item.type === 'header' || item.type === 'footer') {
return ;
} else if (item.type === 'user') {
return (
);
} else {
invariant(false, `unexpected RelationshipList item type ${item.type}`);
}
},
[routeName, navigate, route, onSelect, styles.emptyText, styles.separator],
);
const { setOptions } = navigation;
const prevNoCurrentTags = React.useRef(noCurrentTags);
React.useEffect(() => {
let setSaveButtonDisabled;
if (!prevNoCurrentTags.current && noCurrentTags) {
setSaveButtonDisabled = true;
} else if (prevNoCurrentTags.current && !noCurrentTags) {
setSaveButtonDisabled = false;
}
prevNoCurrentTags.current = noCurrentTags;
if (setSaveButtonDisabled === undefined) {
return;
}
setOptions({
// eslint-disable-next-line react/display-name
headerRight: () => (
),
});
}, [setOptions, noCurrentTags, onPressAdd]);
const relationships = useSelector(userRelationshipsSelector);
const viewerID = useSelector(
state => state.currentUserInfo && state.currentUserInfo.id,
);
const usersWithoutENSNames = React.useMemo(() => {
if (searchInputText === '') {
return {
[FriendListRouteName]: relationships.friends,
[BlockListRouteName]: relationships.blocked,
}[routeName];
}
const mergedUserInfos: { [id: string]: AccountUserInfo } = {};
for (const userInfo of serverSearchResults) {
mergedUserInfos[userInfo.id] = userInfo;
}
for (const id of userStoreSearchResults) {
const { username, relationshipStatus } = userInfos[id];
if (username) {
mergedUserInfos[id] = { id, username, relationshipStatus };
}
}
const excludeUserIDsArray = currentTags
.map(userInfo => userInfo.id)
.concat(viewerID || []);
const excludeUserIDs = new Set(excludeUserIDsArray);
const sortToEnd = [];
const userSearchResults = [];
const sortRelationshipTypesToEnd = {
[FriendListRouteName]: [userRelationshipStatus.FRIEND],
[BlockListRouteName]: [
userRelationshipStatus.BLOCKED_BY_VIEWER,
userRelationshipStatus.BOTH_BLOCKED,
],
}[routeName];
for (const userID in mergedUserInfos) {
if (excludeUserIDs.has(userID)) {
continue;
}
const userInfo = mergedUserInfos[userID];
if (sortRelationshipTypesToEnd.includes(userInfo.relationshipStatus)) {
sortToEnd.push(userInfo);
} else {
userSearchResults.push(userInfo);
}
}
return userSearchResults.concat(sortToEnd);
}, [
searchInputText,
relationships,
routeName,
viewerID,
currentTags,
serverSearchResults,
userStoreSearchResults,
userInfos,
]);
const displayUsers = useENSNames(usersWithoutENSNames);
const listData = React.useMemo(() => {
let emptyItem;
if (displayUsers.length === 0 && searchInputText === '') {
emptyItem = { type: 'empty', because: 'no-relationships' };
} else if (displayUsers.length === 0) {
emptyItem = { type: 'empty', because: 'no-results' };
}
const mappedUsers = displayUsers.map((userInfo, index) => ({
type: 'user',
userInfo,
lastListItem: displayUsers.length - 1 === index,
verticalBounds,
}));
return []
.concat(emptyItem ? emptyItem : [])
.concat(emptyItem ? [] : { type: 'header' })
.concat(mappedUsers)
.concat(emptyItem ? [] : { type: 'footer' });
}, [displayUsers, verticalBounds, searchInputText]);
const indicatorStyle = useIndicatorStyle();
const currentTagsWithENSNames = useENSNames(currentTags);
return (
Search:
);
}
const unboundStyles = {
container: {
flex: 1,
backgroundColor: 'panelBackground',
},
contentContainer: {
paddingTop: 12,
paddingBottom: 24,
},
separator: {
backgroundColor: 'panelForegroundBorder',
height: Platform.OS === 'android' ? 1.5 : 1,
},
emptyText: {
color: 'panelForegroundSecondaryLabel',
flex: 1,
fontSize: 16,
lineHeight: 20,
textAlign: 'center',
paddingHorizontal: 12,
paddingVertical: 10,
marginHorizontal: 12,
},
tagInput: {
flex: 1,
marginLeft: 8,
paddingRight: 12,
},
tagInputLabel: {
color: 'panelForegroundTertiaryLabel',
fontSize: 16,
paddingLeft: 12,
},
tagInputContainer: {
alignItems: 'center',
backgroundColor: 'panelForeground',
borderBottomWidth: 1,
borderColor: 'panelForegroundBorder',
flexDirection: 'row',
paddingVertical: 6,
},
};
registerFetchKey(searchUsersActionTypes);
registerFetchKey(updateRelationshipsActionTypes);
const MemoizedRelationshipList: React.ComponentType =
React.memo(RelationshipList);
MemoizedRelationshipList.displayName = 'RelationshipList';
export default MemoizedRelationshipList;
diff --git a/native/profile/secondary-device-qr-code-scanner.react.js b/native/profile/secondary-device-qr-code-scanner.react.js
index bee7a8b14..bdb9b68e3 100644
--- a/native/profile/secondary-device-qr-code-scanner.react.js
+++ b/native/profile/secondary-device-qr-code-scanner.react.js
@@ -1,126 +1,132 @@
// @flow
import { useNavigation } from '@react-navigation/native';
import { BarCodeScanner, type BarCodeEvent } from 'expo-barcode-scanner';
import * as React from 'react';
import { View } from 'react-native';
import { parseDataFromDeepLink } from 'lib/facts/links.js';
+import type { ProfileNavigationProp } from './profile.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
import { useStyles } from '../themes/colors.js';
import Alert from '../utils/alert.js';
const barCodeTypes = [BarCodeScanner.Constants.BarCodeType.qr];
+type Props = {
+ +navigation: ProfileNavigationProp<'SecondaryDeviceQRCodeScanner'>,
+ +route: NavigationRoute<'SecondaryDeviceQRCodeScanner'>,
+};
// eslint-disable-next-line no-unused-vars
-function SecondaryDeviceQRCodeScanner(props: { ... }): React.Node {
+function SecondaryDeviceQRCodeScanner(props: Props): React.Node {
const [hasPermission, setHasPermission] = React.useState(null);
const [scanned, setScanned] = React.useState(false);
const styles = useStyles(unboundStyles);
const navigation = useNavigation();
React.useEffect(() => {
(async () => {
const { status } = await BarCodeScanner.requestPermissionsAsync();
setHasPermission(status === 'granted');
if (status !== 'granted') {
Alert.alert(
'No access to camera',
'Please allow Comm to access your camera in order to scan the QR code.',
[{ text: 'OK' }],
);
navigation.goBack();
}
})();
}, [navigation]);
const onConnect = React.useCallback((barCodeEvent: BarCodeEvent) => {
const { data } = barCodeEvent;
const parsedData = parseDataFromDeepLink(data);
const keysMatch = parsedData?.data?.keys;
if (!parsedData || !keysMatch) {
Alert.alert(
'Scan failed',
'QR code does not contain a valid pair of keys.',
[{ text: 'OK' }],
);
return;
}
const keys = JSON.parse(decodeURIComponent(keysMatch));
Alert.alert(
'Scan successful',
`QR code contains the following keys: ${JSON.stringify(keys)}`,
[{ text: 'OK' }],
);
}, []);
const onCancelScan = React.useCallback(() => setScanned(false), []);
const handleBarCodeScanned = React.useCallback(
(barCodeEvent: BarCodeEvent) => {
setScanned(true);
Alert.alert(
'Connect with this device?',
'Are you sure you want to allow this device to log in to your account?',
[
{
text: 'Cancel',
style: 'cancel',
onPress: onCancelScan,
},
{
text: 'Connect',
onPress: () => onConnect(barCodeEvent),
},
],
{ cancelable: false },
);
},
[onCancelScan, onConnect],
);
if (hasPermission === null) {
return ;
}
// Note: According to the BarCodeScanner Expo docs, we should adhere to two
// guidances when using the BarCodeScanner:
// 1. We should specify the potential barCodeTypes we want to scan for to
// minimize battery usage.
// 2. We should set the onBarCodeScanned callback to undefined if it scanned
// in order to 'pause' the scanner from continuing to scan while we
// process the data from the scan.
// See: https://docs.expo.io/versions/latest/sdk/bar-code-scanner
return (
);
}
const unboundStyles = {
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
},
scanner: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
};
export default SecondaryDeviceQRCodeScanner;
diff --git a/native/profile/tunnelbroker-menu.react.js b/native/profile/tunnelbroker-menu.react.js
index 0bc731545..8fbb83648 100644
--- a/native/profile/tunnelbroker-menu.react.js
+++ b/native/profile/tunnelbroker-menu.react.js
@@ -1,150 +1,156 @@
// @flow
import * as React from 'react';
import { useState } from 'react';
import { Text, View } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js';
import type { TunnelbrokerMessage } from 'lib/types/tunnelbroker/messages.js';
+import type { ProfileNavigationProp } from './profile.react.js';
import Button from '../components/button.react.js';
import TextInput from '../components/text-input.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
import { useColors, useStyles } from '../themes/colors.js';
+type Props = {
+ +navigation: ProfileNavigationProp<'TunnelbrokerMenu'>,
+ +route: NavigationRoute<'TunnelbrokerMenu'>,
+};
// eslint-disable-next-line no-unused-vars
-function TunnelbrokerMenu(props: { ... }): React.Node {
+function TunnelbrokerMenu(props: Props): React.Node {
const styles = useStyles(unboundStyles);
const colors = useColors();
const { connected, addListener, sendMessage, removeListener } =
useTunnelbroker();
const [messages, setMessages] = useState([]);
const [recipient, setRecipient] = useState('');
const [message, setMessage] = useState('');
const listener = React.useCallback((msg: TunnelbrokerMessage) => {
setMessages(prev => [...prev, msg]);
}, []);
React.useEffect(() => {
addListener(listener);
return () => removeListener(listener);
}, [addListener, listener, removeListener]);
const onSubmit = React.useCallback(async () => {
try {
await sendMessage({ deviceID: recipient, payload: message });
} catch (e) {
console.error(e.message);
}
}, [message, recipient, sendMessage]);
return (
INFO
Connected
{connected.toString()}
SEND MESSAGE
Recipient
Message
MESSAGES
{messages.map(msg => (
{JSON.stringify(msg)}
))}
);
}
const unboundStyles = {
scrollViewContentContainer: {
paddingTop: 24,
},
scrollView: {
backgroundColor: 'panelBackground',
},
section: {
backgroundColor: 'panelForeground',
borderBottomWidth: 1,
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
marginBottom: 24,
marginVertical: 2,
},
header: {
color: 'panelBackgroundLabel',
fontSize: 12,
fontWeight: '400',
paddingBottom: 3,
paddingHorizontal: 24,
},
submenuButton: {
flexDirection: 'row',
paddingHorizontal: 24,
paddingVertical: 10,
alignItems: 'center',
},
submenuText: {
color: 'panelForegroundLabel',
flex: 1,
fontSize: 16,
},
text: {
color: 'panelForegroundLabel',
fontSize: 16,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 24,
paddingVertical: 14,
},
textInput: {
color: 'modalBackgroundLabel',
flex: 1,
fontSize: 16,
margin: 0,
padding: 0,
borderBottomColor: 'transparent',
},
};
export default TunnelbrokerMenu;