diff --git a/native/components/community-joiner-modal.react.js b/native/components/community-joiner-modal.react.js new file mode 100644 --- /dev/null +++ b/native/components/community-joiner-modal.react.js @@ -0,0 +1,208 @@ +// @flow + +import * as React from 'react'; +import { View, Text } from 'react-native'; +import { + TabView, + SceneMap, + TabBar, + type TabBarProps, +} from 'react-native-tab-view'; + +import { useThreadSearchIndex } from 'lib/selectors/nav-selectors.js'; +import { threadInfoFromRawThreadInfo } from 'lib/shared/thread-utils.js'; +import type { ClientCommunityInfoWithCommunityName } from 'lib/types/community-types.js'; + +import Modal from './modal.react.js'; +import CommunityList from '../components/community-list.react.js'; +import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; +import type { NavigationRoute } from '../navigation/route-names.js'; +import { useSelector } from '../redux/redux-utils.js'; +import { useColors, useStyles } from '../themes/colors.js'; + +export type CommunityJoinerModalParams = { + +communities: $ReadOnlyArray, +}; + +type Props = { + +navigation: RootNavigationProp<'CommunityJoinerModal'>, + +route: NavigationRoute<'CommunityJoinerModal'>, +}; + +const defaultCommunities: $ReadOnlyArray = + []; + +// This should be updated with the names of the crypto communities on Comm +const cryptoCommunityIDs: $ReadOnlyArray = []; + +function CommunityJoinerModal(props: Props): React.Node { + const { params } = props.route; + const communities = params?.communities ?? defaultCommunities; + + const viewerID = useSelector( + state => state.currentUserInfo && state.currentUserInfo.id, + ); + const userInfos = useSelector(state => state.userStore.userInfos); + const styles = useStyles(unboundStyles); + + const generalCommunities = React.useMemo( + () => + communities.filter( + community => !cryptoCommunityIDs.includes(community.id), + ), + [communities], + ); + const cryptoCommunities = React.useMemo( + () => + communities.filter(community => + cryptoCommunityIDs.includes(community.id), + ), + [communities], + ); + + const generateThreadInfos = React.useCallback( + (communityList: $ReadOnlyArray) => + communityList + .map(community => + community.threadInfo + ? threadInfoFromRawThreadInfo( + community.threadInfo, + viewerID, + userInfos, + ) + : null, + ) + .filter(Boolean), + [userInfos, viewerID], + ); + + const generalThreadInfos = React.useMemo( + () => generateThreadInfos(generalCommunities), + [generateThreadInfos, generalCommunities], + ); + + const cryptoThreadInfos = React.useMemo( + () => generateThreadInfos(cryptoCommunities), + [generateThreadInfos, cryptoCommunities], + ); + + const generalIndex = useThreadSearchIndex(generalThreadInfos); + const cryptoIndex = useThreadSearchIndex(cryptoThreadInfos); + + const renderGeneralTab = React.useCallback( + () => ( + + ), + [generalIndex, generalThreadInfos, styles.threadListItem], + ); + + const renderCryptoTab = React.useCallback( + () => ( + + ), + [cryptoIndex, cryptoThreadInfos, styles.threadListItem], + ); + + const [index, setIndex] = React.useState(0); + const [routes] = React.useState([ + { key: 'general', title: 'General' }, + { key: 'crypto', title: 'Crypto' }, + ]); + + const navigationState = React.useMemo( + () => ({ index, routes }), + [index, routes], + ); + + const renderScene = React.useMemo(() => { + return SceneMap({ + general: renderGeneralTab, + crypto: renderCryptoTab, + }); + }, [renderCryptoTab, renderGeneralTab]); + + const colors = useColors(); + const { tabBarBackground, tabBarAccent } = colors; + + const screenOptions = React.useMemo( + () => ({ + tabBarShowIcon: true, + tabBarStyle: { + backgroundColor: tabBarBackground, + }, + tabBarItemStyle: { + flexDirection: 'row', + }, + tabBarIndicatorStyle: { + borderColor: tabBarAccent, + borderBottomWidth: 2, + }, + }), + [tabBarAccent, tabBarBackground], + ); + + const initialLayout = React.useMemo(() => ({ width: 400 }), []); + + const renderTabBar = React.useCallback( + (tabBarProps: TabBarProps) => ( + + + + ), + [ + screenOptions.tabBarIndicatorStyle, + screenOptions.tabBarStyle, + styles.tabBarContainer, + ], + ); + + return ( + + + Discover communities + + + + ); +} + +const unboundStyles = { + headerContainer: { + padding: 10, + paddingBottom: 0, + }, + headerText: { + color: 'modalForegroundLabel', + fontSize: 20, + marginBottom: 8, + textAlign: 'center', + }, + threadListItem: { + paddingLeft: 10, + paddingRight: 10, + paddingVertical: 2, + }, + tabBarContainer: { + marginBottom: 15, + }, +}; + +export default CommunityJoinerModal; diff --git a/native/flow-typed/npm/react-native-tab-view_v3.x.x.js b/native/flow-typed/npm/react-native-tab-view_v3.x.x.js new file mode 100644 --- /dev/null +++ b/native/flow-typed/npm/react-native-tab-view_v3.x.x.js @@ -0,0 +1,61 @@ +// flow-typed signature: a7123e367e1cd3cb25dcd811f56eea57 +// flow-typed version: <>/react-native-tab-view_v3.3.0/flow_v0.202.1 + +declare module 'react-native-tab-view' { + import type { Route } from '@react-navigation/core'; + import type { + AnimatedValue, + ViewStyle, + } from 'react-native-gesture-handler/@react-native'; + + declare type Layout = { + +width: number, + +height: number, + }; + declare type SceneRendererProps = { + +layout: Layout, + +jumpTo: (key: string) => void, + ... + }; + + declare export var TabBarItem: React$ComponentType<{ + +position: AnimatedValue, + +route: Route<>, + +navigationState: { +index: number, ... }, + +onPress: () => void, + +onLongPress: () => void, + +style: ViewStyle, + +getLabelText?: () => string, + +getAccessible?: () => void, + +getAccessibilityLabel?: () => void, + +renderIcon?: ({ color: string, ... }) => React$Node, + +getTestID?: () => void, + +activeColor?: string, + ... + }>; + + declare export var TabView: React$ComponentType<{ + +navigationState: { +index: number, ... }, + +renderScene: (props: SceneRendererProps & { route: Route<> }) => React$Node, + +onIndexChange: (number) => void, + +initialLayout: Partial, + +renderTabBar: (TabBarProps) => React$Node, + ... + }>; + + declare export function SceneMap(scenes: { + [key: string]: () => React$Node, + }): (props: SceneRendererProps & { route: Route<> }) => React$Node; + + declare export var TabBar: React$ComponentType<{ + +style: ViewStyle, + +indicatorStyle: ViewStyle, + ... + }>; + + declare export type TabBarProps = { + +style: ViewStyle, + +indicatorStyle: ViewStyle, + ... + } +} diff --git a/native/navigation/root-navigator.react.js b/native/navigation/root-navigator.react.js --- a/native/navigation/root-navigator.react.js +++ b/native/navigation/root-navigator.react.js @@ -58,6 +58,7 @@ CreateMissingSIWEBackupMessageRouteName, RestoreSIWEBackupRouteName, LinkedDevicesBottomSheetRouteName, + CommunityJoinerModalRouteName, } from './route-names.js'; import LoggedOutModal from '../account/logged-out-modal.react.js'; import CreateMissingSIWEBackupMessage from '../account/registration/missing-registration-data/missing-siwe-backup-message.react.js'; @@ -75,6 +76,7 @@ import SubchannelsListModal from '../chat/subchannels-list-modal.react.js'; import CommunityCreationNavigator from '../community-creation/community-creation-navigator.react.js'; import TagFarcasterChannelNavigator from '../community-settings/tag-farcaster-channel/tag-farcaster-channel-navigator.react.js'; +import CommunityJoinerModal from '../components/community-joiner-modal.react.js'; import ConnectFarcasterBottomSheet from '../components/connect-farcaster-bottom-sheet.react.js'; import DirectoryPromptBottomSheet from '../components/directory-prompt-bottom-sheet.react.js'; import InviteLinksNavigator from '../invite-links/invite-links-navigator.react.js'; @@ -311,6 +313,11 @@ component={DirectoryPromptBottomSheet} options={modalOverlayScreenOptions} /> +