diff --git a/lib/shared/comm-icon-config.json b/lib/shared/comm-icon-config.json --- a/lib/shared/comm-icon-config.json +++ b/lib/shared/comm-icon-config.json @@ -447,6 +447,31 @@ "setIdx": 0, "setId": 2, "iconIdx": 17 + }, + { + "icon": { + "paths": [ + "M263.889 159.369H759.593V864.569H687.296V541.091H686.569C678.312 448.462 600.253 375.963 505.741 375.963C411.229 375.963 333.17 448.462 324.913 541.091H324.185V864.569H251.889V159.369H263.889Z", + "M131.852 259.481L162.074 359.737H187.593V763.815C174.791 763.815 164.444 774.483 164.444 787.63V815.408H159.741C146.938 815.408 136.593 826.074 136.593 839.222V866.998H391.111V839.222C391.111 826.074 380.766 815.408 367.963 815.408H363.259V787.63C363.259 774.483 352.914 763.815 340.111 763.815H312.593V259.481H131.852Z", + "M691.111 763.815C678.309 763.815 667.963 774.483 667.963 787.63V815.408H663.259C650.457 815.408 640.111 826.074 640.111 839.222V866.998H894.63V839.222C894.63 826.074 884.284 815.408 871.481 815.408H866.778V787.63C866.778 774.483 856.432 763.815 843.63 763.815V359.737H869.148L899.37 259.481H718.63V763.815H691.111Z" + ], + "attrs": [{}, {}, {}], + "isMulticolor": false, + "isMulticolor2": false, + "tags": ["farcaster"], + "grid": 0 + }, + "attrs": [{}, {}, {}], + "properties": { + "order": 408, + "id": 19, + "name": "farcaster", + "prevSize": 32, + "code": 59667 + }, + "setIdx": 0, + "setId": 2, + "iconIdx": 19 } ], "height": 1024, diff --git a/web/assets/comm-logo.react.js b/web/assets/comm-logo.react.js new file mode 100644 --- /dev/null +++ b/web/assets/comm-logo.react.js @@ -0,0 +1,31 @@ +// @flow + +import * as React from 'react'; + +function CommLogo(): React.Node { + return ( + + + + + + ); +} + +export default CommLogo; diff --git a/web/chat/chat-thread-composer.css b/web/chat/chat-thread-composer.css --- a/web/chat/chat-thread-composer.css +++ b/web/chat/chat-thread-composer.css @@ -32,7 +32,13 @@ div.searchField { flex-grow: 1; - padding: 1rem; + padding: 0.5rem 1rem 1rem 1rem; +} + +div.protocolRow { + padding: 1rem 1rem 0.5rem 1rem; + display: flex; + align-items: center; } .closeSearch { @@ -77,3 +83,17 @@ flex-direction: row; align-items: center; } + +div.rightContainer { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} + +div.protocolIcons { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; +} diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js --- a/web/chat/chat-thread-composer.react.js +++ b/web/chat/chat-thread-composer.react.js @@ -12,6 +12,7 @@ import { useResolvableNames } from 'lib/hooks/names-cache.js'; import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors.js'; import { extractFIDFromUserID } from 'lib/shared/id-utils.js'; +import { protocolNames } from 'lib/shared/protocol-names.js'; import { usePotentialMemberItems, useSearchUsers, @@ -29,10 +30,13 @@ import { supportsFarcasterDCs } from 'lib/utils/services-utils.js'; import css from './chat-thread-composer.css'; +import CommLogo from '../assets/comm-logo.react.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import Button from '../components/button.react.js'; import Label from '../components/label.react.js'; +import ProtocolIcon from '../components/protocol-icon.react.js'; import Search from '../components/search.react.js'; +import SelectProtocolDropdown from '../components/select-protocol-dropdown.react.js'; import type { InputState } from '../input/input-state.js'; import Alert from '../modals/alert.react.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; @@ -50,10 +54,10 @@ function ChatThreadComposer(props: Props): React.Node { const { userInfoInputArray, threadID, inputState } = props; + const { selectedProtocol } = useProtocolSelection(); const [usernameInputText, setUsernameInputText] = React.useState(''); - const { selectedProtocol } = useProtocolSelection(); const dispatch = useDispatch(); const loggedInUserInfo = useLoggedInUserInfo(); @@ -178,6 +182,18 @@ const userItems = userListItemsWithENSNames.map( (userSearchResult: UserListItem) => { + let icon = null; + if ( + userSearchResult.supportedProtocols.includes(protocolNames.COMM_DM) + ) { + icon = } size={23} />; + } else if ( + userSearchResult.supportedProtocols.includes( + protocolNames.FARCASTER_DC, + ) + ) { + icon = ; + } return (
  • ); @@ -198,10 +217,10 @@ return ; }, [ - onSelectUserFromSearch, - userInfoInputArray.length, userListItemsWithENSNames, usernameInputText, + userInfoInputArray.length, + onSelectUserFromSearch, ]); const hideSearch = React.useCallback( @@ -254,6 +273,9 @@ return (
    +
    + +
    state.navInfo.chatMode === 'create', + ); let threadMenu = null; if (!threadIsPending(threadInfo.id)) { @@ -42,6 +52,43 @@ const { uiName } = useResolvedThreadInfo(threadInfo); + const { selectedProtocol } = useProtocolSelection(); + + const protocolIcon = React.useMemo(() => { + if (!isThreadCreation) { + return null; + } + + const protocol = selectedProtocol + ? getProtocolByName(selectedProtocol) + : threadSpecs[threadInfo.type].protocol(); + if (!protocol) { + return null; + } + + const handleProtocolClick = () => { + pushModal( + +
    + {protocol.presentationDetails.description} +
    +
    , + ); + }; + + return ( + + ); + }, [ + isThreadCreation, + selectedProtocol, + threadInfo.type, + pushModal, + popModal, + ]); + return ( <>
    @@ -58,6 +105,7 @@ /> {threadMenu} + {protocolIcon}
    diff --git a/web/comm-icon.react.js b/web/comm-icon.react.js --- a/web/comm-icon.react.js +++ b/web/comm-icon.react.js @@ -26,7 +26,8 @@ | 'link_plus-outline' | 'user-filled' | 'user-edit' - | 'farcaster-outline'; + | 'farcaster-outline' + | 'farcaster'; type CommIconProps = { +icon: CommIcons, diff --git a/web/components/protocol-icon.react.js b/web/components/protocol-icon.react.js new file mode 100644 --- /dev/null +++ b/web/components/protocol-icon.react.js @@ -0,0 +1,67 @@ +// @flow + +import { + faServer as server, + faLock as lock, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import * as React from 'react'; + +import { getProtocolByName } from 'lib/shared/threads/protocols/thread-protocols.js'; +import type { ProtocolName } from 'lib/shared/threads/thread-spec.js'; + +import CommIcon from '../comm-icon.react.js'; + +type Props = { + +icon?: React.Node, + +protocol?: ProtocolName, + +size: number, +}; + +function ProtocolIcon(props: Props): React.Node { + let iconComponent = null; + const protocolIcon = getProtocolByName(props.protocol)?.presentationDetails + ?.protocolIcon; + const iconSize = props.size * 0.5; + let iconBackground = 'var(--bg)'; + if (props.icon) { + iconComponent = props.icon; + } else if (protocolIcon === 'lock') { + iconComponent = ( + + ); + } else if (protocolIcon === 'server') { + iconComponent = ( + + ); + } else if (protocolIcon === 'farcaster') { + iconComponent = ; + iconBackground = '#855DCD'; + } + + return ( +
    + {iconComponent} +
    + ); +} + +export default ProtocolIcon; diff --git a/web/components/select-protocol-dropdown.css b/web/components/select-protocol-dropdown.css new file mode 100644 --- /dev/null +++ b/web/components/select-protocol-dropdown.css @@ -0,0 +1,86 @@ +.container { + background-color: var(--inputField-background-secondary-default); + position: relative; + width: 100%; +} + +.bordersWithOptions { + border-top-left-radius: 10px; + border-top-right-radius: 10px; +} + +.bordersWithoutOptions { + border-radius: 10px; +} + +.dropdownHeader { + padding: 8px 12px; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.dropdownOption { + padding: 8px 12px; + display: flex; + align-items: center; + cursor: pointer; + list-style: none; +} + +.dropdownOption:hover { + background-color: var(--dropdown-option-hover-bg); + border-radius: 8px; +} + +.optionContent { + display: flex; + align-items: center; + height: 30px; +} + +.selectedOption { + display: flex; + align-items: center; + height: 30px; +} + +.optionsContainer { + background-color: var(--inputField-background-secondary-default); + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + position: absolute; + top: 47px; + width: 100%; + margin: 0; + padding: 0; + list-style: none; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.15); +} + +.text { + font-size: 16px; + color: var(--fg); + display: flex; + align-items: center; + height: 30px; +} + +.protocolName { + margin-left: 8px; + font-size: 16px; + color: var(--fg); +} + +.chevron { + color: var(--fg); +} + +.disabled { + cursor: not-allowed; +} + +.disabled .dropdownHeader { + cursor: not-allowed; +} diff --git a/web/components/select-protocol-dropdown.react.js b/web/components/select-protocol-dropdown.react.js new file mode 100644 --- /dev/null +++ b/web/components/select-protocol-dropdown.react.js @@ -0,0 +1,127 @@ +// @flow + +import { faChevronDown } from '@fortawesome/free-solid-svg-icons'; +import { faInfoCircle } from '@fortawesome/free-solid-svg-icons/faInfoCircle.js'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import classNames from 'classnames'; +import * as React from 'react'; + +import { useModalContext } from 'lib/components/modal-provider.react.js'; +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.js'; +import { protocolInfoAlert } from 'lib/utils/alert-utils.js'; + +import ProtocolIcon from './protocol-icon.react.js'; +import css from './select-protocol-dropdown.css'; +import Modal from '../modals/modal.react.js'; + +function SelectProtocolDropdown(): React.Node { + const { selectedProtocol, setSelectedProtocol, availableProtocols } = + useProtocolSelection(); + const { pushModal, popModal } = useModalContext(); + const [showOptions, setShowOptions] = React.useState(false); + + const onDropdownPress = React.useCallback(() => { + if (availableProtocols.length < 1) { + return; + } + setShowOptions(!showOptions); + }, [availableProtocols.length, showOptions]); + + const onInfoPress = React.useCallback(() => { + pushModal( + +
    +

    {protocolInfoAlert.message}

    +
    +
    , + ); + }, [pushModal, popModal]); + + const onOptionSelection = React.useCallback( + (protocolName: ProtocolName) => { + setSelectedProtocol(protocolName); + setShowOptions(false); + }, + [setSelectedProtocol], + ); + + const options = React.useMemo( + () => + protocols() + .filter(protocol => availableProtocols.includes(protocol.protocolName)) + .map(protocol => ( +
  • onOptionSelection(protocol.protocolName)} + > +
    + + {protocol.protocolName} +
    +
  • + )), + [availableProtocols, onOptionSelection], + ); + + const containerClassNames = classNames(css.container, { + [css.bordersWithOptions]: showOptions, + [css.bordersWithoutOptions]: !showOptions, + [css.disabled]: availableProtocols.length < 1, + }); + + const dropdownHeader = React.useMemo(() => { + if (!selectedProtocol) { + return Select chat type; + } + return ( +
    + + {selectedProtocol} +
    + ); + }, [selectedProtocol]); + + const optionsComponent = React.useMemo(() => { + if (!showOptions) { + return null; + } + return
      {options}
    ; + }, [options, showOptions]); + + const icon = React.useMemo( + () => (availableProtocols.length > 0 ? faChevronDown : faInfoCircle), + [availableProtocols.length], + ); + + return ( +
    +
    +
    + {dropdownHeader} +
    + 0 ? onDropdownPress : onInfoPress + } + style={{ cursor: 'pointer' }} + /> +
    + {optionsComponent} +
    + ); +} + +export default SelectProtocolDropdown;