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;