diff --git a/lib/selectors/keyserver-selectors.js b/lib/selectors/keyserver-selectors.js index 4811dd33b..b14e9c1ea 100644 --- a/lib/selectors/keyserver-selectors.js +++ b/lib/selectors/keyserver-selectors.js @@ -1,151 +1,156 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import type { PlatformDetails } from '../types/device-types'; import type { KeyserverInfo, KeyserverInfos, SelectedKeyserverInfo, } from '../types/keyserver-types'; import type { AppState } from '../types/redux-types.js'; import type { ConnectionInfo } from '../types/socket-types.js'; import type { UserInfos } from '../types/user-types.js'; const baseCookieSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.cookie; const cookieSelector: (keyserverID: string) => (state: AppState) => ?string = _memoize(baseCookieSelector); const cookiesSelector: (state: AppState) => { +[keyserverID: string]: string, } = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (infos: { +[key: string]: KeyserverInfo }) => { const cookies = {}; for (const keyserverID in infos) { cookies[keyserverID] = infos[keyserverID].cookie; } return cookies; }, ); const baseSessionIDSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.sessionID; const sessionIDSelector: (keyserverID: string) => (state: AppState) => ?string = _memoize(baseSessionIDSelector); const baseUpdatesCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.updatesCurrentAsOf ?? 0; const updatesCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = _memoize(baseUpdatesCurrentAsOfSelector); const baseCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = keyserverID => (state: AppState) => state.messageStore.currentAsOf[keyserverID] ?? 0; const currentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = _memoize(baseCurrentAsOfSelector); const baseUrlPrefixSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.urlPrefix; const urlPrefixSelector: (keyserverID: string) => (state: AppState) => ?string = _memoize(baseUrlPrefixSelector); const baseConnectionSelector: ( keyserverID: string, ) => (state: AppState) => ?ConnectionInfo = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.connection; const connectionSelector: ( keyserverID: string, ) => (state: AppState) => ?ConnectionInfo = _memoize(baseConnectionSelector); const baseLastCommunicatedPlatformDetailsSelector: ( keyserverID: string, ) => (state: AppState) => ?PlatformDetails = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID] ?.lastCommunicatedPlatformDetails; const lastCommunicatedPlatformDetailsSelector: ( keyserverID: string, ) => (state: AppState) => ?PlatformDetails = _memoize( baseLastCommunicatedPlatformDetailsSelector, ); const selectedKeyserversSelector: ( state: AppState, ) => $ReadOnlyArray = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (state: AppState) => state.userStore.userInfos, (keyserverInfos: KeyserverInfos, userInfos: UserInfos) => { const result = []; for (const key in keyserverInfos) { const keyserverInfo = keyserverInfos[key]; const keyserverAdminUsername = userInfos[key]?.username; if (!keyserverAdminUsername) { continue; } + const keyserverAdminUserInfo = { + id: userInfos[key].id, + username: keyserverAdminUsername, + }; + result.push({ - keyserverAdminUsername, + keyserverAdminUserInfo, keyserverInfo, }); } return result; }, ); const deviceTokensSelector: (state: AppState) => { +[keyserverID: string]: ?string, } = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (infos: { +[key: string]: KeyserverInfo }) => { const deviceTokens = {}; for (const keyserverID in infos) { deviceTokens[keyserverID] = infos[keyserverID].deviceToken; } return deviceTokens; }, ); const baseDeviceTokenSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.deviceToken; const deviceTokenSelector: ( keyserverID: string, ) => (state: AppState) => ?string = _memoize(baseDeviceTokenSelector); export { cookieSelector, cookiesSelector, sessionIDSelector, updatesCurrentAsOfSelector, currentAsOfSelector, urlPrefixSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, deviceTokensSelector, deviceTokenSelector, selectedKeyserversSelector, }; diff --git a/lib/types/keyserver-types.js b/lib/types/keyserver-types.js index be4647167..317681356 100644 --- a/lib/types/keyserver-types.js +++ b/lib/types/keyserver-types.js @@ -1,54 +1,55 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { PlatformDetails } from './device-types.js'; import { connectionInfoValidator } from './socket-types.js'; import type { ConnectionInfo } from './socket-types.js'; +import type { GlobalAccountUserInfo } from './user-types.js'; import { tShape, tPlatformDetails } from '../utils/validation-utils.js'; export type KeyserverInfo = { +cookie: ?string, +sessionID?: ?string, +updatesCurrentAsOf: number, // millisecond timestamp +urlPrefix: string, +connection: ConnectionInfo, +lastCommunicatedPlatformDetails: ?PlatformDetails, +deviceToken: ?string, }; export type KeyserverInfos = { +[key: string]: KeyserverInfo }; export type KeyserverStore = { +keyserverInfos: KeyserverInfos, }; export type SelectedKeyserverInfo = { - +keyserverAdminUsername: string, + +keyserverAdminUserInfo: GlobalAccountUserInfo, +keyserverInfo: KeyserverInfo, }; export type AddKeyserverPayload = { +keyserverAdminUserID: string, +newKeyserverInfo: KeyserverInfo, }; export type RemoveKeyserverPayload = { +keyserverAdminUserID: string, }; export const keyserverInfoValidator: TInterface = tShape({ cookie: t.maybe(t.String), sessionID: t.maybe(t.String), updatesCurrentAsOf: t.Number, urlPrefix: t.String, connection: connectionInfoValidator, lastCommunicatedPlatformDetails: t.maybe(tPlatformDetails), deviceToken: t.maybe(t.String), }); export const keyserverStoreValidator: TInterface = tShape({ keyserverInfos: t.dict(t.String, keyserverInfoValidator), }); diff --git a/native/profile/keyserver-selection-bottom-sheet.react.js b/native/profile/keyserver-selection-bottom-sheet.react.js index 8d05f38ef..d6bbaa752 100644 --- a/native/profile/keyserver-selection-bottom-sheet.react.js +++ b/native/profile/keyserver-selection-bottom-sheet.react.js @@ -1,199 +1,200 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; +import type { GlobalAccountUserInfo } from 'lib/types/user-types.js'; import { BottomSheetContext } from '../bottom-sheet/bottom-sheet-provider.react.js'; import BottomSheet from '../bottom-sheet/bottom-sheet.react.js'; import Button from '../components/button.react.js'; import CommIcon from '../components/comm-icon.react.js'; import Pill from '../components/pill.react.js'; import StatusIndicator from '../components/status-indicator.react.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useColors, useStyles } from '../themes/colors.js'; export type KeyserverSelectionBottomSheetParams = { - +keyserverAdminUsername: string, + +keyserverAdminUserInfo: GlobalAccountUserInfo, +keyserverInfo: KeyserverInfo, }; // header + paddingTop + paddingBottom + marginBottom const keyserverHeaderHeight = 84 + 16 + 16 + 24; type Props = { +navigation: RootNavigationProp<'KeyserverSelectionBottomSheet'>, +route: NavigationRoute<'KeyserverSelectionBottomSheet'>, }; function KeyserverSelectionBottomSheet(props: Props): React.Node { const { navigation, route: { - params: { keyserverAdminUsername, keyserverInfo }, + params: { keyserverAdminUserInfo, keyserverInfo }, }, } = props; const { goBack } = navigation; const bottomSheetContext = React.useContext(BottomSheetContext); invariant(bottomSheetContext, 'bottomSheetContext should be set'); const { setContentHeight } = bottomSheetContext; const removeKeyserverContainerRef = React.useRef(); const bottomSheetRef = React.useRef(); const colors = useColors(); const styles = useStyles(unboundStyles); const insets = useSafeAreaInsets(); const onLayout = React.useCallback(() => { removeKeyserverContainerRef.current?.measure( (x, y, width, height, pageX, pageY) => { if ( height === null || height === undefined || pageY === null || pageY === undefined ) { return; } setContentHeight(height + keyserverHeaderHeight + insets.bottom); }, ); }, [insets.bottom, setContentHeight]); const cloudIcon = React.useMemo( () => ( ), [colors.panelForegroundLabel], ); const onPressRemoveKeyserver = React.useCallback(() => { // TODO }, []); const removeKeyserver = React.useMemo(() => { if (keyserverInfo.connection.status !== 'connected') { return ( <> You may delete offline keyservers from your keyserver list. When you delete a keyserver, you will still remain in the associated communities. Any messages or content you have previously sent will remain on the keyserver’s communities after disconnecting or deleting. ); } return ( <> Disconnecting from this keyserver will remove you from its associated communities. Any messages or content you have previously sent will remain on the keyserver. ); }, [ keyserverInfo.connection.status, onPressRemoveKeyserver, styles.keyserverRemoveText, styles.removeButtonContainer, styles.removeButtonText, ]); return ( {keyserverInfo.urlPrefix} {removeKeyserver} ); } const unboundStyles = { container: { paddingHorizontal: 16, }, keyserverDetailsContainer: { alignItems: 'center', justifyContent: 'space-between', paddingVertical: 16, backgroundColor: 'modalAccentBackground', marginBottom: 24, borderRadius: 8, }, keyserverHeaderContainer: { flexDirection: 'row', alignItems: 'center', }, statusIndicatorContainer: { marginLeft: 8, }, keyserverURLText: { color: 'modalForegroundLabel', marginTop: 8, }, keyserverRemoveText: { color: 'modalForegroundLabel', marginBottom: 24, }, removeButtonContainer: { backgroundColor: 'vibrantRedButton', paddingVertical: 12, borderRadius: 8, alignItems: 'center', }, removeButtonText: { color: 'floatingButtonLabel', }, }; export default KeyserverSelectionBottomSheet; diff --git a/native/profile/keyserver-selection-list-item.react.js b/native/profile/keyserver-selection-list-item.react.js index 142ea746c..c9dd66b97 100644 --- a/native/profile/keyserver-selection-list-item.react.js +++ b/native/profile/keyserver-selection-list-item.react.js @@ -1,86 +1,87 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { TouchableOpacity } from 'react-native'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; +import type { GlobalAccountUserInfo } from 'lib/types/user-types.js'; import CommIcon from '../components/comm-icon.react.js'; import Pill from '../components/pill.react.js'; import StatusIndicator from '../components/status-indicator.react.js'; import { KeyserverSelectionBottomSheetRouteName } from '../navigation/route-names.js'; import { useStyles, useColors } from '../themes/colors.js'; type Props = { - +keyserverAdminUsername: string, + +keyserverAdminUserInfo: GlobalAccountUserInfo, +keyserverInfo: KeyserverInfo, }; function KeyserverSelectionListItem(props: Props): React.Node { - const { keyserverAdminUsername, keyserverInfo } = props; + const { keyserverAdminUserInfo, keyserverInfo } = props; const styles = useStyles(unboundStyles); const colors = useColors(); const { navigate } = useNavigation(); const onPress = React.useCallback(() => { navigate<'KeyserverSelectionBottomSheet'>({ name: KeyserverSelectionBottomSheetRouteName, params: { - keyserverAdminUsername, + keyserverAdminUserInfo, keyserverInfo, }, }); - }, [keyserverAdminUsername, keyserverInfo, navigate]); + }, [keyserverAdminUserInfo, keyserverInfo, navigate]); const cloudIcon = React.useMemo( () => ( ), [colors.panelForegroundLabel], ); const keyserverListItem = React.useMemo( () => ( ), [ cloudIcon, colors.codeBackground, - keyserverAdminUsername, + keyserverAdminUserInfo.username, keyserverInfo.connection, onPress, styles.keyserverListItemContainer, ], ); return keyserverListItem; } const unboundStyles = { keyserverListItemContainer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 24, paddingVertical: 10, }, }; export default KeyserverSelectionListItem; diff --git a/native/profile/keyserver-selection-list.react.js b/native/profile/keyserver-selection-list.react.js index 1b7ba1ffe..8ea6e35bc 100644 --- a/native/profile/keyserver-selection-list.react.js +++ b/native/profile/keyserver-selection-list.react.js @@ -1,121 +1,121 @@ // @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 { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; function keyExtractor(item: SelectedKeyserverInfo) { - return `${item.keyserverAdminUsername}${item.keyserverInfo.urlPrefix}`; + return `${item.keyserverAdminUserInfo.id}${item.keyserverInfo.urlPrefix}`; } function renderKeyserverListItem({ item }) { return ; } // eslint-disable-next-line no-unused-vars function KeyserverSelectionList(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;