diff --git a/lib/shared/threads/protocols/dm-thread-protocol.js b/lib/shared/threads/protocols/dm-thread-protocol.js --- a/lib/shared/threads/protocols/dm-thread-protocol.js +++ b/lib/shared/threads/protocols/dm-thread-protocol.js @@ -902,7 +902,6 @@ nativeChatThreadListIcon: 'lock', webChatThreadListIcon: 'lock', threadAncestorLabel: () => 'Local DM', - threadSearchHeaderShowsGenesis: false, protocolIcon: 'lock', description: 'Comm DMs are end-to-end encrypted and stored locally on your ' + diff --git a/lib/shared/threads/protocols/farcaster-thread-protocol.js b/lib/shared/threads/protocols/farcaster-thread-protocol.js --- a/lib/shared/threads/protocols/farcaster-thread-protocol.js +++ b/lib/shared/threads/protocols/farcaster-thread-protocol.js @@ -768,7 +768,6 @@ nativeChatThreadListIcon: 'lock', webChatThreadListIcon: 'lock', threadAncestorLabel: () => 'Farcaster DC', - threadSearchHeaderShowsGenesis: false, protocolIcon: 'farcaster', description: 'Farcaster Direct Casts are the native messaging protocol in ' + diff --git a/lib/shared/threads/protocols/keyserver-thread-protocol.js b/lib/shared/threads/protocols/keyserver-thread-protocol.js --- a/lib/shared/threads/protocols/keyserver-thread-protocol.js +++ b/lib/shared/threads/protocols/keyserver-thread-protocol.js @@ -704,7 +704,6 @@ nativeChatThreadListIcon: 'server', webChatThreadListIcon: 'server', threadAncestorLabel: (ancestorPath: React.Node) => ancestorPath, - threadSearchHeaderShowsGenesis: true, protocolIcon: 'server', description: "Genesis chats are a legacy chat type hosted on Ashoat's keyserver. " + diff --git a/lib/shared/threads/thread-spec.js b/lib/shared/threads/thread-spec.js --- a/lib/shared/threads/thread-spec.js +++ b/lib/shared/threads/thread-spec.js @@ -477,7 +477,6 @@ +nativeChatThreadListIcon: string, +webChatThreadListIcon: 'lock' | 'server', +threadAncestorLabel: (ancestorPath: React.Node) => React.Node, - +threadSearchHeaderShowsGenesis: boolean, +protocolIcon: 'lock' | 'server' | 'farcaster', +description: string, }, diff --git a/native/chat/chat.react.js b/native/chat/chat.react.js --- a/native/chat/chat.react.js +++ b/native/chat/chat.react.js @@ -25,7 +25,9 @@ import invariant from 'invariant'; import * as React from 'react'; import { + Alert, Platform, + TouchableOpacity, View, useWindowDimensions, type MeasureOnSuccessCallback, @@ -33,9 +35,13 @@ import MessageStorePruner from 'lib/components/message-store-pruner.react.js'; import ThreadDraftUpdater from 'lib/components/thread-draft-updater.react.js'; +import { useProtocolSelection } from 'lib/contexts/protocol-selection-context.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { threadSettingsNotificationsCopy } from 'lib/shared/thread-settings-notifications-utils.js'; import { threadIsPending, threadIsSidebar } from 'lib/shared/thread-utils.js'; +import { getProtocolByName } from 'lib/shared/threads/protocols/thread-protocols.js'; +import { threadSpecs } from 'lib/shared/threads/thread-specs.js'; +import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import BackgroundChatThreadList from './background-chat-thread-list.react.js'; import ChatHeader from './chat-header.react.js'; @@ -68,6 +74,7 @@ nuxTip, NUXTipsContext, } from '../components/nux-tips-context.react.js'; +import ProtocolIcon from '../components/protocol-icon.react.js'; import { ProtocolSelectionProvider } from '../components/protocol-selection-provider.react.js'; import { InputStateContext } from '../input/input-state.js'; import CommunityDrawerButton from '../navigation/community-drawer-button.react.js'; @@ -245,6 +252,67 @@ const headerRightStyle = { flexDirection: 'row' }; +function MessageListHeaderRight({ + threadInfo, + navigation, + areSettingsEnabled, + isSearching, + isSearchEmpty, +}: { + +threadInfo: ThreadInfo, + +navigation: ChatNavigationProp<'MessageList'>, + +areSettingsEnabled: boolean, + +isSearching: boolean, + +isSearchEmpty: boolean, +}) { + const { selectedProtocol } = useProtocolSelection(); + + const protocolIcon = React.useMemo(() => { + if (!isSearching || isSearchEmpty) { + return null; + } + + const protocol = selectedProtocol + ? getProtocolByName(selectedProtocol) + : threadSpecs[threadInfo.type].protocol(); + + if (!protocol) { + return null; + } + + const handleProtocolPress = () => { + Alert.alert( + protocol.protocolName, + protocol.presentationDetails.description, + ); + }; + + return ( + + + + ); + }, [isSearchEmpty, isSearching, selectedProtocol, threadInfo.type]); + + if (areSettingsEnabled) { + return ( + + + + {protocolIcon} + + ); + } + + return {protocolIcon}; +} + const messageListOptions = ({ navigation, route, @@ -252,8 +320,9 @@ +navigation: ChatNavigationProp<'MessageList'>, +route: NavigationRoute<'MessageList'>, }) => { + const isSearching = !!route.params.searching; const isSearchEmpty = - !!route.params.searching && route.params.threadInfo.members.length === 1; + isSearching && route.params.threadInfo.members.length === 1; const areSettingsEnabled = !threadIsPending(route.params.threadInfo.id) && !isSearchEmpty; @@ -268,20 +337,15 @@ {...props} /> ), - headerRight: areSettingsEnabled - ? () => ( - - - - - ) - : undefined, + headerRight: () => ( + + ), headerBackTitleVisible: false, headerTitleAlign: isSearchEmpty ? 'center' : 'left', headerLeftContainerStyle: { width: Platform.OS === 'ios' ? 32 : 40 }, diff --git a/native/chat/message-list-container.react.js b/native/chat/message-list-container.react.js --- a/native/chat/message-list-container.react.js +++ b/native/chat/message-list-container.react.js @@ -18,8 +18,8 @@ } from 'lib/shared/search-utils.js'; import { useExistingThreadInfoFinder } from 'lib/shared/thread-utils.js'; import { - threadSpecs, threadTypeIsPersonal, + threadSpecs, } from 'lib/shared/threads/thread-specs.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { AccountUserInfo, UserListItem } from 'lib/types/user-types.js'; @@ -36,9 +36,9 @@ import MessageListThreadSearch from './message-list-thread-search.react.js'; import { MessageListContextProvider } from './message-list-types.js'; import MessageList from './message-list.react.js'; -import ParentThreadHeader from './parent-thread-header.react.js'; import RelationshipPrompt from './relationship-prompt.react.js'; import ContentLoading from '../components/content-loading.react.js'; +import SelectProtocolDropdown from '../components/select-protocol-dropdown.react.js'; import { InputStateContext } from '../input/input-state.js'; import { OverlayContext, @@ -172,31 +172,10 @@ let searchComponent = null; if (searching) { - const { userInfoInputArray, genesisThreadInfo } = this.props; - let parentThreadHeader; - const protocol = threadSpecs[threadInfo.type].protocol(); - const childThreadType = protocol.pendingThreadType( - userInfoInputArray.length, - ); - const threadSearchHeaderShowsGenesis = - protocol.presentationDetails.threadSearchHeaderShowsGenesis; - if (!threadSearchHeaderShowsGenesis) { - parentThreadHeader = ( - - ); - } else if (genesisThreadInfo) { - // It's technically possible for the client to be missing the Genesis - // ThreadInfo when it first opens up (before the server delivers it) - parentThreadHeader = ( - - ); - } + const { userInfoInputArray } = this.props; searchComponent = ( <> - {parentThreadHeader} + { const newUserInfoInputArray = user.id === viewerID ? [] : [user]; + const resolvedThreadInfo = existingThreadInfoFinder({ searching: true, userInfoInputArray: newUserInfoInputArray, diff --git a/native/components/protocol-icon.react.js b/native/components/protocol-icon.react.js new file mode 100644 --- /dev/null +++ b/native/components/protocol-icon.react.js @@ -0,0 +1,72 @@ +// @flow + +import Icon from '@expo/vector-icons/FontAwesome.js'; +import * as React from 'react'; +import { View } from 'react-native'; + +import { getProtocolByName } from 'lib/shared/threads/protocols/thread-protocols.js'; +import type { ProtocolName } from 'lib/shared/threads/thread-spec.js'; + +import { useStyles } from '../themes/colors.js'; +import FarcasterLogo from '../vectors/farcaster-logo.react.js'; + +type Props = { + +icon?: React.Node, + +protocol?: ProtocolName, + +size: number, +}; + +function ProtocolIcon(props: Props): React.Node { + const styles = useStyles(unboundStyles); + + let iconComponent = null; + const protocolIcon = getProtocolByName(props.protocol)?.presentationDetails + ?.protocolIcon; + const iconSize = props.size * 0.65; + let containerStyle = styles.container; + if (props.icon) { + containerStyle = styles.container; + iconComponent = props.icon; + } else if (protocolIcon === 'lock') { + iconComponent = ; + } else if (protocolIcon === 'server') { + iconComponent = ; + } else if (protocolIcon === 'farcaster') { + iconComponent = ; + containerStyle = styles.farcasterContainer; + } + + const viewStyle = React.useMemo( + () => [ + containerStyle, + { + width: props.size, + height: props.size, + borderRadius: props.size, + }, + ], + [containerStyle, props.size], + ); + + return {iconComponent}; +} + +const unboundStyles = { + container: { + backgroundColor: 'panelBackground', + justifyContent: 'center', + alignItems: 'center', + marginHorizontal: 5, + }, + farcasterContainer: { + backgroundColor: '#855DCD', + justifyContent: 'center', + alignItems: 'center', + marginHorizontal: 5, + }, + icon: { + color: 'whiteText', + }, +}; + +export default ProtocolIcon; diff --git a/native/components/select-protocol-dropdown.react.js b/native/components/select-protocol-dropdown.react.js new file mode 100644 --- /dev/null +++ b/native/components/select-protocol-dropdown.react.js @@ -0,0 +1,183 @@ +// @flow + +import Icon from '@expo/vector-icons/FontAwesome5.js'; +import * as React from 'react'; +import { View, Text, TouchableOpacity, Alert } from 'react-native'; + +import { useProtocolSelection } from 'lib/contexts/protocol-selection-context.js'; +import { protocols } from 'lib/shared/threads/protocols/thread-protocols.js'; +import type { ProtocolName } from 'lib/shared/threads/thread-spec'; +import { protocolInfoAlert } from 'lib/utils/alert-utils.js'; + +import ProtocolIcon from './protocol-icon.react.js'; +import { useStyles } from '../themes/colors.js'; + +function SelectProtocolDropdown(): React.Node { + const { selectedProtocol, setSelectedProtocol, availableProtocols } = + useProtocolSelection(); + const styles = useStyles(unboundStyles); + + const [showOptions, setShowOptions] = React.useState(false); + + const onDropdownPress = React.useCallback(() => { + if (availableProtocols.length < 1) { + return; + } + setShowOptions(!showOptions); + }, [availableProtocols.length, showOptions]); + + const onInfoPress = React.useCallback(() => { + Alert.alert(protocolInfoAlert.title, protocolInfoAlert.message); + }, []); + + const onOptionSelection = React.useCallback( + (protocolIndex: ProtocolName) => { + setSelectedProtocol(protocolIndex); + setShowOptions(false); + }, + [setSelectedProtocol], + ); + + const options = React.useMemo( + () => + protocols() + .filter(protocol => availableProtocols.includes(protocol.protocolName)) + .map(protocol => ( + onOptionSelection(protocol.protocolName)} + key={protocol.protocolName} + > + + + {protocol.protocolName} + + + )), + [ + availableProtocols, + onOptionSelection, + styles.dropdownOption, + styles.protocolName, + ], + ); + + const dropdownHeader = React.useMemo(() => { + if (!selectedProtocol) { + return Select chat type; + } + return ( + + + {selectedProtocol} + + ); + }, [ + selectedProtocol, + styles.protocolName, + styles.selectedOption, + styles.text, + ]); + + const iconName = React.useMemo( + () => (availableProtocols.length > 0 ? 'chevron-down' : 'info-circle'), + [availableProtocols.length], + ); + + const optionsComponent = React.useMemo(() => { + if (!showOptions) { + return null; + } + return {options}; + }, [options, showOptions, styles.optionsContainer]); + + const containerStyle = React.useMemo( + () => [ + styles.container, + showOptions ? styles.bordersWithOptions : styles.bordersWithoutOptions, + ], + [ + styles.container, + styles.bordersWithOptions, + styles.bordersWithoutOptions, + showOptions, + ], + ); + + return ( + + + + {dropdownHeader} + + 0 ? onDropdownPress : onInfoPress + } + > + + + + {optionsComponent} + + ); +} + +const unboundStyles = { + container: { + backgroundColor: 'selectProtocolDropdownBackground', + zIndex: 4, + }, + button: { + flex: 1, + }, + bordersWithOptions: { + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + }, + bordersWithoutOptions: { + borderRadius: 10, + }, + dropdownHeader: { + paddingHorizontal: 12, + paddingVertical: 8, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + dropdownOption: { + paddingHorizontal: 12, + paddingVertical: 8, + flexDirection: 'row', + alignItems: 'center', + minHeight: 40, + }, + selectedOption: { + flexDirection: 'row', + alignItems: 'center', + height: 24, + }, + optionsContainer: { + backgroundColor: 'selectProtocolDropdownBackground', + borderBottomLeftRadius: 10, + borderBottomRightRadius: 10, + position: 'absolute', + top: 43, + width: '100%', + }, + text: { + fontSize: 16, + color: 'panelForegroundLabel', + height: 24, + textAlignVertical: 'center', + }, + protocolName: { + marginLeft: 8, + fontSize: 16, + color: 'panelForegroundLabel', + }, + icon: { + color: 'panelForegroundLabel', + }, +}; + +export default SelectProtocolDropdown; diff --git a/native/components/user-list-user.react.js b/native/components/user-list-user.react.js --- a/native/components/user-list-user.react.js +++ b/native/components/user-list-user.react.js @@ -3,14 +3,17 @@ import * as React from 'react'; import { Text, Platform } from 'react-native'; +import { protocolNames } from 'lib/shared/protocol-names.js'; import type { UserListItem, AccountUserInfo } from 'lib/types/user-types.js'; import Button from './button.react.js'; +import ProtocolIcon from './protocol-icon.react.js'; import SingleLine from './single-line.react.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import { type Colors, useColors, useStyles } from '../themes/colors.js'; import type { TextStyle } from '../types/styles.js'; import Alert from '../utils/alert.js'; +import CommLogo from '../vectors/comm-logo.react.js'; // eslint-disable-next-line no-unused-vars const getUserListItemHeight = (item: UserListItem): number => { @@ -57,6 +60,15 @@ } const { modalIosHighlightUnderlay: underlayColor } = this.props.colors; + let icon = null; + if (userInfo.supportedProtocols.includes(protocolNames.COMM_DM)) { + icon = } size={23} />; + } else if ( + userInfo.supportedProtocols.includes(protocolNames.FARCASTER_DC) + ) { + icon = ; + } + return ( ); } diff --git a/native/themes/colors.js b/native/themes/colors.js --- a/native/themes/colors.js +++ b/native/themes/colors.js @@ -160,6 +160,7 @@ redIndicatorOuter: string, deletedMessageText: string, deletedMessageBackground: string, + selectProtocolDropdownBackground: string, }>; const light: Colors = Object.freeze({ @@ -269,6 +270,7 @@ redIndicatorOuter: designSystemColors.errorDark50, deletedMessageText: designSystemColors.shadesBlack60, deletedMessageBackground: designSystemColors.shadesWhite90, + selectProtocolDropdownBackground: designSystemColors.shadesWhite60, }); const dark: Colors = Object.freeze({ @@ -378,6 +380,7 @@ redIndicatorOuter: designSystemColors.errorDark90, deletedMessageText: designSystemColors.shadesWhite60, deletedMessageBackground: designSystemColors.shadesBlack90, + selectProtocolDropdownBackground: designSystemColors.shadesBlack75, }); const colors = { light, dark }; diff --git a/native/vectors/comm-logo.react.js b/native/vectors/comm-logo.react.js new file mode 100644 --- /dev/null +++ b/native/vectors/comm-logo.react.js @@ -0,0 +1,32 @@ +// @flow + +import * as React from 'react'; +import Svg, { Path, Circle, Rect } from 'react-native-svg'; + +function CommLogo(): React.Node { + return ( + + + + + + ); +} + +export default CommLogo; diff --git a/native/vectors/farcaster-logo.react.js b/native/vectors/farcaster-logo.react.js --- a/native/vectors/farcaster-logo.react.js +++ b/native/vectors/farcaster-logo.react.js @@ -3,12 +3,16 @@ import * as React from 'react'; import Svg, { Path } from 'react-native-svg'; -function FarcasterLogo(): React.Node { - const farcasterLogo = React.useMemo( +type Props = { + +size?: number, +}; + +function FarcasterLogo({ size = 200 }: Props): React.Node { + return React.useMemo( () => ( ), - [], + [size], ); - - return farcasterLogo; } export default FarcasterLogo;