diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..d99cd6af73b8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +# Repo Notes + +- This repo uses React Compiler. Assume React Compiler patterns are enabled when editing React code, and avoid adding `useMemo`/`useCallback` by default unless they are clearly needed for correctness or compatibility with existing code. diff --git a/shared/profile/user/actions/follow-button.tsx b/shared/profile/user/actions/follow-button.tsx index b528ed50c760..e1407b700d83 100644 --- a/shared/profile/user/actions/follow-button.tsx +++ b/shared/profile/user/actions/follow-button.tsx @@ -1,11 +1,5 @@ +import * as Kb from '@/common-adapters' import * as React from 'react' -import * as Styles from '@/styles' -import WaitingButton from '@/common-adapters/waiting-button' - -const Kb = { - Styles, - WaitingButton, -} type Props = { disabled?: boolean @@ -18,45 +12,58 @@ type Props = { onUnfollow?: () => void } -const FollowButton = (props: Props) => { - const [mouseOver, setMouseover] = React.useState(false) - const {following, followsYou, onFollow, onUnfollow, style, waitingKey, ...otherProps} = props +const getButtonStyle = (small: boolean | undefined, style: object | undefined) => + small ? style : {...styleButton, ...style} - if (following) { - const button = ( - - ) - if (Kb.Styles.isMobile) { - return button - } - return ( -
setMouseover(true)} - onMouseLeave={() => setMouseover(false)} - > - {button} -
- ) - } else { +const FollowButton = ({ + following, + followsYou, + onFollow, + onUnfollow, + small, + style, + waitingKey, + ...buttonProps +}: Props) => { + const [mouseOver, setMouseOver] = React.useState(false) + const sharedProps = { + ...buttonProps, + style: getButtonStyle(small, style), + waitingKey, + } + + if (!following) { return ( ) } + + const button = ( + + ) + + return Kb.Styles.isMobile ? ( + button + ) : ( + setMouseOver(true)} + onMouseLeave={() => setMouseOver(false)} + > + {button} + + ) } const styleButton = Kb.Styles.platformStyles({ diff --git a/shared/profile/user/actions/index.tsx b/shared/profile/user/actions/index.tsx index a7e7ef9bbea3..de91b1d63283 100644 --- a/shared/profile/user/actions/index.tsx +++ b/shared/profile/user/actions/index.tsx @@ -1,69 +1,163 @@ import * as C from '@/constants' -import * as T from '@/constants/types' +import * as FS from '@/stores/fs' import * as Kb from '@/common-adapters' import * as React from 'react' -import FollowButton from './follow-button' +import * as T from '@/constants/types' import ChatButton from '@/chat/chat-button' +import FollowButton from './follow-button' import {useBotsState} from '@/stores/bots' -import {useTrackerState} from '@/stores/tracker' -import * as FS from '@/stores/fs' -import {useFollowerState} from '@/stores/followers' import {useCurrentUserState} from '@/stores/current-user' +import {useFollowerState} from '@/stores/followers' +import {useTrackerState} from '@/stores/tracker' type OwnProps = {username: string} -const Container = (ownProps: OwnProps) => { - const username = ownProps.username - const d = useTrackerState(s => s.getDetails(username)) +type ActionState = { + chatButton: React.ReactNode + dropdown: React.ReactNode + followThem: boolean + followsYou: boolean + onAccept: () => void + onEditProfile?: () => void + onFollow: () => void + onOpenFolder: () => void + onReload: () => void + onUnfollow: () => void + state: T.Tracker.DetailsState +} + +const ActionRow = ({children}: {children: React.ReactNode}) => ( + + {children} + +) + +const getButtons = ({ + chatButton, + dropdown, + followThem, + followsYou, + onAccept, + onEditProfile, + onFollow, + onOpenFolder, + onReload, + onUnfollow, + state, +}: ActionState): Array => { + if (state === 'notAUserYet') { + return [ + chatButton, + , + ] + } + + if (onEditProfile) { + return [ + , + chatButton, + ] + } + + if (followThem) { + switch (state) { + case 'valid': + return [ + , + chatButton, + dropdown, + ] + case 'needsUpgrade': + return [ + chatButton, + , + dropdown, + ] + default: + return [ + , + , + dropdown, + ] + } + } + + if (state === 'error') { + return [ + , + chatButton, + dropdown, + ] + } + + return [ + , + chatButton, + dropdown, + ] +} + +const Container = ({username}: OwnProps) => { + const {blocked, guiID, hidFromFollowers, state} = useTrackerState(s => s.getDetails(username)) const followThem = useFollowerState(s => s.following.has(username)) const followsYou = useFollowerState(s => s.followers.has(username)) const isBot = useBotsState(s => s.featuredBotsMap.has(username)) + const currentUsername = useCurrentUserState(s => s.username) + const {changeFollow, showUser} = useTrackerState( + C.useShallow(s => ({ + changeFollow: s.dispatch.changeFollow, + showUser: s.dispatch.showUser, + })) + ) + const {navigateAppend} = C.useRouterState( + C.useShallow(s => ({ + navigateAppend: s.dispatch.navigateAppend, + })) + ) - const _guiID = d.guiID - const _you = useCurrentUserState(s => s.username) - const blocked = d.blocked - const hidFromFollowers = d.hidFromFollowers - const state = d.state - - const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend) - const _onAddToTeam = (username: string) => navigateAppend({name: 'profileAddToTeam', params: {username}}) - const _onBrowsePublicFolder = (username: string) => - FS.navToPath(T.FS.stringToPath(`/keybase/public/${username}`)) - const _onEditProfile = () => navigateAppend('profileEdit') - - const changeFollow = useTrackerState(s => s.dispatch.changeFollow) - const _onFollow = changeFollow - const _onInstallBot = (username: string) => { - navigateAppend({name: 'chatInstallBotPick', params: {botUsername: username}}) - } - const _onManageBlocking = (username: string) => - navigateAppend({name: 'chatBlockingModal', params: {username}}) - const _onOpenPrivateFolder = (myUsername: string, theirUsername: string) => - FS.navToPath(T.FS.stringToPath(`/keybase/private/${theirUsername},${myUsername}`)) - const showUser = useTrackerState(s => s.dispatch.showUser) - const _onReload = (username: string) => { - showUser(username, false) - } - const onAccept = () => _onFollow(_guiID, true) - const onAddToTeam = () => _onAddToTeam(username) - const onBrowsePublicFolder = () => _onBrowsePublicFolder(username) - const onEditProfile = _you === username ? _onEditProfile : undefined - const onFollow = () => _onFollow(_guiID, true) - const onInstallBot = () => _onInstallBot(username) - const onManageBlocking = () => _onManageBlocking(username) - const onOpenPrivateFolder = () => _onOpenPrivateFolder(_you, username) - const onReload = () => _onReload(username) - const onUnfollow = () => _onFollow(_guiID, false) + const onAddToTeam = () => navigateAppend({name: 'profileAddToTeam', params: {username}}) + const onBrowsePublicFolder = () => FS.navToPath(T.FS.stringToPath(`/keybase/public/${username}`)) + const onEditProfile = currentUsername === username ? () => navigateAppend('profileEdit') : undefined + const onFollow = () => changeFollow(guiID, true) + const onInstallBot = () => navigateAppend({name: 'chatInstallBotPick', params: {botUsername: username}}) + const onManageBlocking = () => navigateAppend({name: 'chatBlockingModal', params: {username}}) + const onOpenPrivateFolder = () => + FS.navToPath(T.FS.stringToPath(`/keybase/private/${username},${currentUsername}`)) + const onReload = () => showUser(username, false) + const onUnfollow = () => changeFollow(guiID, false) + const onAccept = onFollow const getFeaturedBots = useBotsState(s => s.dispatch.getFeaturedBots) - // load featured bots on first render React.useEffect(() => { - // TODO likely don't do this all the time, just once getFeaturedBots() }, [getFeaturedBots]) + if (blocked) { return ( - + { label="Manage blocking" onClick={onManageBlocking} /> - + ) } - let buttons: Array - const dropdown = ( ) - const chatButton = - - if (state === 'notAUserYet') { - buttons = [ - chatButton, - , - ] - } else if (onEditProfile) { - buttons = [ - , - chatButton, - ] - } else if (followThem) { - if (state === 'valid') { - buttons = [ - , - chatButton, - dropdown, - ] - } else if (state === 'needsUpgrade') { - buttons = [ - chatButton, - , - dropdown, - ] - } else { - buttons = [ - , - , - dropdown, - ] - } - } else { - if (state === 'error') { - buttons = [ - , - chatButton, - dropdown, - ] - } else { - buttons = [ - , - chatButton, - dropdown, - ] - } - } + const buttons = getButtons({ + chatButton: , + dropdown, + followThem, + followsYou, + onAccept, + onEditProfile, + onFollow, + onOpenFolder: onOpenPrivateFolder, + onReload, + onUnfollow, + state, + }) return ( - - {state === 'checking' ? : buttons} - + {state === 'checking' ? : buttons} ) } type DropdownProps = { - onManageBlocking: () => void - onInstallBot: () => void + blockedOrHidFromFollowers: boolean + isBot: boolean + onAddToTeam: () => void onBrowsePublicFolder: () => void + onInstallBot: () => void + onManageBlocking: () => void onOpenPrivateFolder: () => void - onAddToTeam: () => void - isBot: boolean - blockedOrHidFromFollowers: boolean onUnfollow?: () => void } -const DropdownButton = (p: DropdownProps) => { - const {onInstallBot, onAddToTeam, onBrowsePublicFolder, onUnfollow} = p - const {onManageBlocking, blockedOrHidFromFollowers, isBot, onOpenPrivateFolder} = p - const makePopup = (p: Kb.Popup2Parms) => { - const {attachTo, hidePopup} = p - const items: Kb.MenuItems = [ - isBot - ? {icon: 'iconfont-nav-2-robot', onClick: onInstallBot, title: 'Install bot in team or chat'} - : {icon: 'iconfont-people', onClick: onAddToTeam, title: 'Add to team...'}, - {icon: 'iconfont-folder-open', onClick: onOpenPrivateFolder, title: 'Open private folder'}, - {icon: 'iconfont-folder-public', onClick: onBrowsePublicFolder, title: 'Browse public folder'}, - onUnfollow && {icon: 'iconfont-wave', onClick: onUnfollow, title: 'Unfollow'}, - { - danger: true, - icon: 'iconfont-remove', - onClick: onManageBlocking, - title: blockedOrHidFromFollowers ? 'Manage blocking' : 'Block', - }, - ].reduce((arr, i) => { - i && arr.push(i as Kb.MenuItem) - return arr - }, []) - return ( - - ) - } +const makeMenuItems = ({ + blockedOrHidFromFollowers, + isBot, + onAddToTeam, + onBrowsePublicFolder, + onInstallBot, + onManageBlocking, + onOpenPrivateFolder, + onUnfollow, +}: DropdownProps): Kb.MenuItems => + [ + isBot + ? {icon: 'iconfont-nav-2-robot', onClick: onInstallBot, title: 'Install bot in team or chat'} + : {icon: 'iconfont-people', onClick: onAddToTeam, title: 'Add to team...'}, + {icon: 'iconfont-folder-open', onClick: onOpenPrivateFolder, title: 'Open private folder'}, + {icon: 'iconfont-folder-public', onClick: onBrowsePublicFolder, title: 'Browse public folder'}, + onUnfollow && {icon: 'iconfont-wave', onClick: onUnfollow, title: 'Unfollow'}, + { + danger: true, + icon: 'iconfont-remove', + onClick: onManageBlocking, + title: blockedOrHidFromFollowers ? 'Manage blocking' : 'Block', + }, + ].reduce((items, item) => { + if (item) { + items.push(item as Kb.MenuItem) + } + return items + }, []) + +const DropdownButton = (props: DropdownProps) => { + const makePopup = ({attachTo, hidePopup}: Kb.Popup2Parms) => ( + + ) const {showPopup, popup, popupAnchor} = Kb.usePopup2(makePopup) return ( diff --git a/shared/profile/user/friend.tsx b/shared/profile/user/friend.tsx index f8e386ae6faa..9dc163776bbc 100644 --- a/shared/profile/user/friend.tsx +++ b/shared/profile/user/friend.tsx @@ -1,7 +1,7 @@ -import {useProfileState} from '@/stores/profile' import * as Kb from '@/common-adapters' -import {useUsersState} from '@/stores/users' import {useFollowerState} from '@/stores/followers' +import {useProfileState} from '@/stores/profile' +import {useUsersState} from '@/stores/users' type OwnProps = { username: string @@ -12,25 +12,25 @@ const followSizeToStyle = { 64: {bottom: 0, left: 44, position: 'absolute'} as const, } -const Container = (ownProps: OwnProps) => { - const {username: _username, width} = ownProps - const _fullname = useUsersState(s => s.infoMap.get(ownProps.username)?.fullname ?? '') +const getFollowIconType = (following: boolean, followsYou: boolean) => { + if (following === followsYou) { + return followsYou ? ('icon-mutual-follow-21' as const) : undefined + } + return followsYou ? ('icon-follow-me-21' as const) : ('icon-following-21' as const) +} + +const Friend = ({username, width}: OwnProps) => { + const fullname = useUsersState(s => s.infoMap.get(username)?.fullname ?? '') const showUserProfile = useProfileState(s => s.dispatch.showUserProfile) - const _onClick = showUserProfile - const fullname = _fullname || '' - const onClick = () => _onClick(username) - const username = _username - const following = useFollowerState(s => (username ? s.following.has(username) : false)) - const followsYou = useFollowerState(s => (username ? s.followers.has(username) : false)) - const followIconType = followsYou === following - ? (followsYou ? ('icon-mutual-follow-21' as const) : undefined) - : followsYou ? ('icon-follow-me-21' as const) : ('icon-following-21' as const) + const following = useFollowerState(s => s.following.has(username)) + const followsYou = useFollowerState(s => s.followers.has(username)) + const followIconType = getFollowIconType(following, followsYou) return ( - + showUserProfile(username)} style={{width}}> @@ -69,4 +69,4 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ }), })) -export default Container +export default Friend diff --git a/shared/profile/user/hooks.tsx b/shared/profile/user/hooks.tsx index 7ce3ead47860..e840fab6c561 100644 --- a/shared/profile/user/hooks.tsx +++ b/shared/profile/user/hooks.tsx @@ -2,217 +2,234 @@ import * as C from '@/constants' import type * as T from '@/constants/types' import {type BackgroundColorType} from '.' import {useColorScheme} from 'react-native' -import {useTrackerState} from '@/stores/tracker' -import {useProfileState} from '@/stores/profile' -import {useFollowerState} from '@/stores/followers' import {useCurrentUserState} from '@/stores/current-user' +import {useFollowerState} from '@/stores/followers' +import {useProfileState} from '@/stores/profile' +import {useTrackerState} from '@/stores/tracker' -const headerBackgroundColorType = ( +const getHeaderBackgroundColorType = ( state: T.Tracker.DetailsState, followThem: boolean ): BackgroundColorType => { - if (['broken', 'error'].includes(state)) { - return 'red' - } else if (state === 'notAUserYet') { - return 'blue' - } else { - return followThem ? 'green' : 'blue' + switch (state) { + case 'broken': + case 'error': + return 'red' + case 'notAUserYet': + return 'blue' + default: + return followThem ? 'green' : 'blue' } } -// const filterWebOfTrustEntries = memoize( -// ( -// webOfTrustEntries: ReadonlyArray | undefined -// ): Array => -// webOfTrustEntries ? webOfTrustEntries.filter(C.Tracker.showableWotEntry) : [] -// ) +type BaseProfileState = { + blocked: boolean + hidFromFollowers: boolean + reason: string + state: T.Tracker.DetailsState + userIsYou: boolean + username: string +} -const useUserData = (username: string) => { - const myName = useCurrentUserState(s => s.username) - const userIsYou = username === myName - const trackerState = useTrackerState( - C.useShallow(s => { - const _suggestionKeys = userIsYou ? s.proofSuggestions : undefined - return { - _suggestionKeys, - d: s.getDetails(username), - getProofSuggestions: s.dispatch.getProofSuggestions, - loadNonUserProfile: s.dispatch.loadNonUserProfile, - nonUserDetails: s.getNonUserDetails(username), - showUser: s.dispatch.showUser, - } - }) - ) - const {d, getProofSuggestions, loadNonUserProfile, nonUserDetails, showUser, _suggestionKeys} = trackerState - const notAUser = d.state === 'notAUserYet' +type UserProfileState = BaseProfileState & { + assertions?: T.Tracker.Details['assertions'] + backgroundColorType: BackgroundColorType + followThem: boolean + followers?: T.Tracker.Details['followers'] + followersCount?: number + following?: T.Tracker.Details['following'] + followingCount?: number + fullName: string + name: string + sbsAvatarUrl?: string + service: string + serviceIcon?: ReadonlyArray + suggestionEntries?: ReadonlyArray + title: string +} + +const getBaseProfileState = ({ + details, + userIsYou, + username, +}: { + details: T.Tracker.Details + userIsYou: boolean + username: string +}): BaseProfileState => ({ + blocked: details.blocked, + hidFromFollowers: details.hidFromFollowers, + reason: details.reason, + state: details.state, + userIsYou, + username, +}) + +const getKeybaseProfileState = ({ + baseProfileState, + details, + followThem, + suggestionEntries, +}: { + baseProfileState: BaseProfileState + details: T.Tracker.Details + followThem: boolean + suggestionEntries?: ReadonlyArray +}): UserProfileState => ({ + ...baseProfileState, + assertions: details.assertions, + backgroundColorType: getHeaderBackgroundColorType(details.state, followThem), + followThem, + followers: details.followers, + followersCount: details.followersCount, + following: details.following, + followingCount: details.followingCount, + fullName: '', + name: '', + service: '', + suggestionEntries, + title: baseProfileState.username, +}) + +const getSbsProfileState = ({ + baseProfileState, + details, + isDarkMode, + nonUserDetails, +}: { + baseProfileState: BaseProfileState + details: T.Tracker.Details + isDarkMode: boolean + nonUserDetails: T.Tracker.NonUserDetails +}): UserProfileState => { + const name = nonUserDetails.assertionValue || baseProfileState.username + const service = nonUserDetails.assertionKey + const title = nonUserDetails.formattedName || name - const commonProps = { - _assertions: undefined, - _suggestionKeys: undefined, - blocked: d.blocked, + return { + ...baseProfileState, + backgroundColorType: getHeaderBackgroundColorType(details.state, false), followThem: false, - followers: undefined, followersCount: 0, - following: undefined, followingCount: 0, - fullName: '', - guiID: d.guiID, - hidFromFollowers: d.hidFromFollowers, - myName, - name: '', - reason: d.reason, - service: '', - state: d.state, - userIsYou, - username, + fullName: nonUserDetails.fullName, + name, + sbsAvatarUrl: nonUserDetails.pictureUrl || undefined, + service, + serviceIcon: isDarkMode ? nonUserDetails.siteIconFullDarkmode : nonUserDetails.siteIconFull, + title, } +} - const followThem = useFollowerState(s => s.following.has(username)) - // const followsYou = useFollowerState(s => s.followers.has(username)) - // const mutualFollow = followThem && followsYou +const getAssertionKeys = ({ + assertions, + notAUser, + service, + username, +}: { + assertions?: T.Tracker.Details['assertions'] + notAUser: boolean + service: string + username: string +}) => { + if (notAUser && (service === 'phone' || service === 'email')) { + return [] + } + if (notAUser && service) { + return [username] + } + + return assertions + ? [...assertions.entries()].sort((a, b) => a[1].priority - b[1].priority).map(([assertionKey]) => assertionKey) + : undefined +} + +const getSuggestionKeys = (suggestionEntries?: ReadonlyArray) => + suggestionEntries ? suggestionEntries.filter(s => !s.belowFold).map(s => s.assertionKey) : undefined + +const shouldAllowAddIdentity = (userIsYou: boolean, suggestionEntries?: ReadonlyArray) => + userIsYou && !!suggestionEntries?.some(s => s.belowFold) + +const useUserData = (username: string) => { + const myName = useCurrentUserState(s => s.username) + const userIsYou = username === myName + const trackerState = useTrackerState( + C.useShallow(s => ({ + details: s.getDetails(username), + getProofSuggestions: s.dispatch.getProofSuggestions, + loadNonUserProfile: s.dispatch.loadNonUserProfile, + nonUserDetails: s.getNonUserDetails(username), + showUser: s.dispatch.showUser, + suggestionEntries: userIsYou ? s.proofSuggestions : undefined, + })) + ) + const {details, getProofSuggestions, loadNonUserProfile, nonUserDetails, showUser, suggestionEntries} = + trackerState + const followThem = useFollowerState(s => s.following.has(username)) const isDarkMode = useColorScheme() === 'dark' - const stateProps = (() => { - if (!notAUser) { - // Keybase user - const {followersCount, followingCount, followers, following, reason /*, webOfTrustEntries = []*/} = d - - // const filteredWot = filterWebOfTrustEntries(webOfTrustEntries) - // const hasAlreadyVouched = filteredWot.some(entry => entry.attestingUser === myName) - // const vouchShowButton = mutualFollow && !hasAlreadyVouched - // const vouchDisableButton = !vouchShowButton || d.state !== 'valid' || d.resetBrokeTrack - - return { - ...commonProps, - _assertions: d.assertions, - _suggestionKeys, - backgroundColorType: headerBackgroundColorType(d.state, followThem), - followThem, - followers, - followersCount, - following, - followingCount, - reason, - sbsAvatarUrl: undefined, - serviceIcon: undefined, - title: username, - // vouchDisableButton, - // vouchShowButton, - // webOfTrustEntries: filteredWot, - } - } else { - // SBS profile. But `nonUserDetails` might not have arrived yet, - // make sure the screen does not appear broken until then. - const name = nonUserDetails.assertionValue || username - const service = nonUserDetails.assertionKey - // For SBS profiles, display service username as the "big username". Some - // profiles will have a special formatting for the name, e.g. phone numbers - // will be formatted. - const title = nonUserDetails.formattedName || name - - return { - ...commonProps, - backgroundColorType: headerBackgroundColorType(d.state, false), - fullName: nonUserDetails.fullName, - name, - sbsAvatarUrl: nonUserDetails.pictureUrl || undefined, - service, - serviceIcon: isDarkMode ? nonUserDetails.siteIconFullDarkmode : nonUserDetails.siteIconFull, - title, - vouchDisableButton: true, - vouchShowButton: false, - webOfTrustEntries: [], - } - } - })() + const notAUser = details.state === 'notAUserYet' - const editAvatar = useProfileState(s => s.dispatch.editAvatar) - const _onEditAvatar = editAvatar - // const _onIKnowThem = (username: string, guiID: string) => { - // dispatch( - // RouteTreeGen.createNavigateAppend({path: [{props: {guiID, username}, selected: 'profileWotAuthor'}]}) - // ) - // } - const _onReload = (username: string, isYou: boolean, state: T.Tracker.DetailsState) => { - if (state !== 'valid' && !isYou) { - // Might be a Keybase user or not, launch non-user profile fetch. - loadNonUserProfile(username) - } - if (state !== 'notAUserYet') { - showUser(username, false, true) + const baseProfileState = getBaseProfileState({details, userIsYou, username}) + const profileState = notAUser + ? getSbsProfileState({baseProfileState, details, isDarkMode, nonUserDetails}) + : getKeybaseProfileState({baseProfileState, details, followThem, suggestionEntries}) - if (isYou) { - getProofSuggestions() - } - } - } + const editAvatar = useProfileState(s => s.dispatch.editAvatar) const {navigateAppend, navigateUp} = C.useRouterState( C.useShallow(s => ({ navigateAppend: s.dispatch.navigateAppend, navigateUp: s.dispatch.navigateUp, })) ) - const onAddIdentity = () => { - navigateAppend('profileProofsList') - } - const onBack = () => { - navigateUp() - } - let allowOnAddIdentity = false - if (stateProps.userIsYou && stateProps._suggestionKeys?.some(s => s.belowFold)) { - allowOnAddIdentity = true + const onReload = () => { + if (details.state !== 'valid' && !userIsYou) { + loadNonUserProfile(username) + } + if (details.state !== 'notAUserYet') { + showUser(username, false, true) + if (userIsYou) { + getProofSuggestions() + } + } } - let assertionKeys = - notAUser && !!stateProps.service - ? [stateProps.username] - : stateProps._assertions - ? [...stateProps._assertions.entries()].sort((a, b) => a[1].priority - b[1].priority).map(e => e[0]) - : undefined - - // For 'phone' or 'email' profiles do not display placeholder assertions. - const service = stateProps.service - const impTofu = notAUser && (service === 'phone' || service === 'email') - if (impTofu) { - assertionKeys = [] - } + const onAddIdentity = shouldAllowAddIdentity(profileState.userIsYou, profileState.suggestionEntries) + ? () => navigateAppend('profileProofsList') + : undefined return { - assertionKeys, - backgroundColorType: stateProps.backgroundColorType, - blocked: stateProps.blocked, - followThem: stateProps.followThem, - followers: stateProps.followers ? [...stateProps.followers] : undefined, - followersCount: stateProps.followersCount, - following: stateProps.following ? [...stateProps.following] : undefined, - followingCount: stateProps.followingCount, - fullName: stateProps.fullName, - hidFromFollowers: stateProps.hidFromFollowers, - name: stateProps.name, + assertionKeys: getAssertionKeys({ + assertions: profileState.assertions, + notAUser, + service: profileState.service, + username: profileState.username, + }), + backgroundColorType: profileState.backgroundColorType, + blocked: profileState.blocked, + followThem: profileState.followThem, + followers: profileState.followers ? [...profileState.followers] : undefined, + followersCount: profileState.followersCount, + following: profileState.following ? [...profileState.following] : undefined, + followingCount: profileState.followingCount, + fullName: profileState.fullName, + hidFromFollowers: profileState.hidFromFollowers, + name: profileState.name, notAUser, - onAddIdentity: allowOnAddIdentity ? onAddIdentity : undefined, - onBack: onBack, - onEditAvatar: stateProps.userIsYou ? _onEditAvatar : undefined, - // onIKnowThem: - // stateProps.vouchShowButton && !stateProps.vouchDisableButton - // ? () => _onIKnowThem(stateProps.username, stateProps.guiID) - // : undefined, - onReload: () => _onReload(stateProps.username, stateProps.userIsYou, stateProps.state), - reason: stateProps.reason, - sbsAvatarUrl: stateProps.sbsAvatarUrl, - service: stateProps.service, - serviceIcon: stateProps.serviceIcon, - state: stateProps.state, - suggestionKeys: stateProps._suggestionKeys - ? stateProps._suggestionKeys.filter(s => !s.belowFold).map(s => s.assertionKey) - : undefined, - title: stateProps.title, - userIsYou: stateProps.userIsYou, - username: stateProps.username, - // vouchDisableButton: stateProps.vouchDisableButton, - // vouchShowButton: stateProps.vouchShowButton, - // webOfTrustEntries: stateProps.webOfTrustEntries, + onAddIdentity, + onBack: navigateUp, + onEditAvatar: profileState.userIsYou ? editAvatar : undefined, + onReload, + reason: profileState.reason, + sbsAvatarUrl: profileState.sbsAvatarUrl, + service: profileState.service, + serviceIcon: profileState.serviceIcon, + state: profileState.state, + suggestionKeys: getSuggestionKeys(profileState.suggestionEntries), + title: profileState.title, + userIsYou: profileState.userIsYou, + username: profileState.username, } } diff --git a/shared/profile/user/index.tsx b/shared/profile/user/index.tsx index c49d26f0bd25..1d876f6f5e16 100644 --- a/shared/profile/user/index.tsx +++ b/shared/profile/user/index.tsx @@ -1,14 +1,14 @@ import * as Kb from '@/common-adapters' import * as C from '@/constants' import * as React from 'react' +import * as T from '@/constants/types' +import type {RPCError} from '@/util/errors' import Actions from './actions' import Assertion from '@/tracker/assertion' import Bio from '@/tracker/bio' import Friend from './friend' import Teams from './teams' import chunk from 'lodash/chunk' -import * as T from '@/constants/types' -import type {RPCError} from '@/util/errors' import upperFirst from 'lodash/upperFirst' import {SiteIcon} from '../generic/shared' import useResizeObserver from '@/util/use-resize-observer' @@ -23,6 +23,7 @@ type Item = | {type: 'friend'; itemWidth: number; usernames: Array} type Section = Kb.SectionType +type Tab = 'followers' | 'following' export type Props = { assertionKeys?: ReadonlyArray @@ -52,16 +53,15 @@ export type Props = { title: string } -const colorTypeToStyle = (type: 'red' | 'green' | 'blue') => { +const colorTypeToStyle = (type: BackgroundColorType) => { switch (type) { case 'red': return styles.typedBackgroundRed case 'green': return styles.typedBackgroundGreen case 'blue': - return styles.typedBackgroundBlue default: - return styles.typedBackgroundRed + return styles.typedBackgroundBlue } } @@ -71,41 +71,62 @@ type SbsTitleProps = { serviceIcon?: ReadonlyArray sbsUsername: string } -const SbsTitle = (p: SbsTitleProps) => ( + +const SbsTitle = ({sbsUsername, serviceIcon}: SbsTitleProps) => ( - {p.serviceIcon && } - {p.sbsUsername} + {serviceIcon && } + {sbsUsername} ) -const BioLayout = (p: BioTeamProofsProps) => ( + +export type BioTeamProofsProps = { + onAddIdentity?: () => void + assertionKeys?: ReadonlyArray + backgroundColorType: BackgroundColorType + onEditAvatar?: (e?: React.BaseSyntheticEvent) => void + notAUser: boolean + suggestionKeys?: ReadonlyArray + username: string + reason: string + name: string + sbsAvatarUrl?: string + service: string + serviceIcon?: ReadonlyArray + fullName?: string + title: string +} + +const BioLayout = (props: BioTeamProofsProps) => ( : undefined + props.title !== props.username ? ( + + ) : undefined } - username={p.username} + username={props.username} underline={false} selectable={true} colorFollowing={true} - notFollowingColorOverride={p.notAUser ? Kb.Styles.globalColors.black_50 : Kb.Styles.globalColors.orange} - editableIcon={!!p.onEditAvatar} - onEditIcon={p.onEditAvatar || undefined} + notFollowingColorOverride={props.notAUser ? Kb.Styles.globalColors.black_50 : Kb.Styles.globalColors.orange} + editableIcon={!!props.onEditAvatar} + onEditIcon={props.onEditAvatar || undefined} avatarSize={avatarSize} size="huge" - avatarImageOverride={p.sbsAvatarUrl} + avatarImageOverride={props.sbsAvatarUrl} withProfileCardPopup={false} /> - - + + ) -const ProveIt = (p: BioTeamProofsProps) => { +const ProveIt = (props: BioTeamProofsProps) => { let doWhat: string - switch (p.service) { + switch (props.service) { case 'phone': doWhat = 'verify their phone number' break @@ -113,15 +134,16 @@ const ProveIt = (p: BioTeamProofsProps) => { doWhat = 'verify their e-mail address' break default: - doWhat = `prove their ${upperFirst(p.service)}` + doWhat = `prove their ${upperFirst(props.service)}` break } + const url = 'https://keybase.io/install' const installUrlProps = Kb.useClickURL(url) return ( <> - Tell {p.fullName || p.name} to join Keybase and {doWhat}. + Tell {props.fullName || props.name} to join Keybase and {doWhat}. Send them this link:{' '} @@ -133,23 +155,27 @@ const ProveIt = (p: BioTeamProofsProps) => { ) } -const Proofs = (p: BioTeamProofsProps) => { - let assertions: React.ReactNode - if (p.assertionKeys) { - assertions = [ - ...p.assertionKeys.map(a => ), - ...(p.suggestionKeys || []).map(s => ( - - )), - ] - } else { - assertions = null - } +const Proofs = (props: BioTeamProofsProps) => { + const assertions = props.assertionKeys + ? [ + ...props.assertionKeys.map(assertionKey => ( + + )), + ...(props.suggestionKeys || []).map(assertionKey => ( + + )), + ] + : null return ( {assertions} - {!!p.notAUser && !!p.service && } + {!!props.notAUser && !!props.service && } ) } @@ -158,39 +184,50 @@ type TabsProps = { loadingFollowers: boolean loadingFollowing: boolean onSelectTab: (tab: Tab) => void - selectedTab: string + selectedTab: Tab numFollowers: number | undefined numFollowing: number | undefined } -const Tabs = (p: TabsProps) => { - const onClickFollowing = () => p.onSelectTab('following') - const onClickFollowers = () => p.onSelectTab('followers') - const tab = (tab: Tab) => ( - - - - {tab === 'following' - ? `Following${!p.loadingFollowing ? ` (${p.numFollowing || 0})` : ''}` - : `Followers${!p.loadingFollowers ? ` (${p.numFollowers || 0})` : ''}`} - - {((tab === 'following' && p.loadingFollowing) || p.loadingFollowers) && ( - - )} - - - ) +const Tabs = ({ + loadingFollowers, + loadingFollowing, + numFollowers, + numFollowing, + onSelectTab, + selectedTab, +}: TabsProps) => { + const getTabLabel = (tab: Tab) => { + if (tab === 'following') { + return `Following${!loadingFollowing ? ` (${numFollowing || 0})` : ''}` + } + return `Followers${!loadingFollowers ? ` (${numFollowers || 0})` : ''}` + } + + const isLoading = (tab: Tab) => (tab === 'following' && loadingFollowing) || loadingFollowers + + const renderTab = (tab: Tab) => { + const selected = tab === selectedTab + return ( + onSelectTab(tab)} + style={Kb.Styles.collapseStyles([styles.followTab, selected && styles.followTabSelected])} + > + + + {getTabLabel(tab)} + + {isLoading(tab) && } + + + ) + } return ( - {tab('followers')} - {tab('following')} + {renderTab('followers')} + {renderTab('following')} ) } @@ -207,233 +244,320 @@ type FriendRowProps = { itemWidth: number } -function FriendRow(p: FriendRowProps) { - return ( - - {p.usernames.map(u => ( - - ))} - - ) -} +const FriendRow = ({itemWidth, usernames}: FriendRowProps) => ( + + {usernames.map(username => ( + + ))} + +) -export type BioTeamProofsProps = { - onAddIdentity?: () => void - assertionKeys?: ReadonlyArray - backgroundColorType: BackgroundColorType - onEditAvatar?: (e?: React.BaseSyntheticEvent) => void - notAUser: boolean - suggestionKeys?: ReadonlyArray - username: string - reason: string - name: string - sbsAvatarUrl?: string - service: string - serviceIcon?: ReadonlyArray - fullName?: string - title: string -} -const BioTeamProofs = (props: BioTeamProofsProps) => { - const addIdentity = props.onAddIdentity ? ( +const AddIdentityButton = ({onAddIdentity}: {onAddIdentity?: () => void}) => + onAddIdentity ? ( ) : null - return Kb.Styles.isMobile ? ( - - {!!props.reason && ( - - {props.reason} - - )} - - - + +const ProfileBackground = ({backgroundColorType}: {backgroundColorType: BackgroundColorType}) => ( + +) + +const ReasonBanner = ({ + backgroundColorType, + center, + reason, + withBackground, +}: { + backgroundColorType: BackgroundColorType + center: boolean + reason: string + withBackground: boolean +}) => + reason ? ( + + {reason} + + ) : null + +const ProofsPanel = ({ + addIdentity, + showReason, + ...props +}: BioTeamProofsProps & { + addIdentity: React.ReactNode + showReason: boolean +}) => ( + + {showReason && ( + + )} + + + {addIdentity} + +) + +const BioTeamProofsMobile = (props: BioTeamProofsProps & {addIdentity: React.ReactNode}) => ( + + + + + + + + +) + +const BioTeamProofsDesktop = (props: BioTeamProofsProps & {addIdentity: React.ReactNode}) => ( + <> + + - - - - {addIdentity} - + + +) + +const BioTeamProofs = (props: BioTeamProofsProps) => { + const addIdentity = + return Kb.Styles.isMobile ? ( + ) : ( - <> - - - - - - {props.reason} - - - - {addIdentity} - - - + ) } -type Tab = 'followers' | 'following' +const getSelectedFriends = ({ + followers, + following, + selectedTab, +}: { + followers?: ReadonlyArray + following?: ReadonlyArray + selectedTab: Tab +}) => (selectedTab === 'following' ? following : followers) + +const getEmptyFriendsText = ({ + selectedTab, + userIsYou, + username, +}: { + selectedTab: Tab + userIsYou: boolean + username: string +}) => + selectedTab === 'following' + ? `${userIsYou ? 'You are' : `${username} is`} not following anyone.` + : `${userIsYou ? 'You have' : `${username} has`} no followers.` + +const buildFriendItems = ({ + followers, + following, + itemWidth, + itemsInARow, + selectedTab, + userIsYou, + username, + width, +}: { + followers?: ReadonlyArray + following?: ReadonlyArray + itemWidth: number + itemsInARow: number + selectedTab: Tab + userIsYou: boolean + username: string + width: number +}): Array => { + const friends = getSelectedFriends({followers, following, selectedTab}) + const items = width + ? chunk(friends, itemsInARow).map(usernames => ({ + itemWidth, + type: 'friend' as const, + usernames, + })) + : [] + + if (items.length > 0) { + return items + } + + if (followers && following) { + return [ + { + text: getEmptyFriendsText({selectedTab, userIsYou, username}), + type: 'noFriends', + }, + ] + } + + return [{text: 'Loading...', type: 'loading'}] +} + +const EmptyFriendsState = ({text}: {text: string}) => ( + + {text} + +) + +const FriendsSectionItem = ({ + index, + item, + notAUser, +}: { + index: number + item: Item + notAUser: boolean +}) => { + switch (item.type) { + case 'bioTeamProofs': + return null + case 'friend': + return + case 'loading': + case 'noFriends': + return notAUser ? null : + } +} -const User = (props: {username: string}) => { - const p = useUserData(props.username) +const makeBioTeamProofsSection = (props: BioTeamProofsProps): Section => ({ + data: [{type: 'bioTeamProofs'}], + renderItem: () => , +}) + +const makeFriendsSection = (items: Array, notAUser: boolean): Section => ({ + data: items, + renderItem: ({item, index}: {item: Item; index: number}) => ( + + ), +}) + +const usernameSelectedTab = new Map() +const avatarSize = 128 + +const User = ({username: initialUsername}: {username: string}) => { + const userData = useUserData(initialUsername) const insetTop = Kb.useSafeAreaInsets().top - const {username, onReload} = p const [selectedTab, setSelectedTab] = React.useState( - usernameSelectedTab.get(p.username) ?? 'followers' + usernameSelectedTab.get(userData.username) ?? 'followers' ) const [width, setWidth] = React.useState(Kb.Styles.dimensionWidth) - const changeTab = React.useCallback( - (tab: Tab) => { - setSelectedTab(tab) - usernameSelectedTab.set(username, tab) - }, - [username] - ) + const changeTab = (tab: Tab) => { + setSelectedTab(tab) + usernameSelectedTab.set(userData.username, tab) + } - // desktop only const wrapperRef = React.useRef(null) - useResizeObserver(wrapperRef, e => setWidth(e.contentRect.width)) + useResizeObserver(wrapperRef, event => setWidth(event.contentRect.width)) - const lastUsernameRef = React.useRef(p.username) + const lastUsernameRef = React.useRef(userData.username) React.useEffect(() => { - if (username !== lastUsernameRef.current) { - lastUsernameRef.current = username - onReload() + if (userData.username !== lastUsernameRef.current) { + lastUsernameRef.current = userData.username + userData.onReload() } - }, [username, onReload]) - - const errorFilter = (e: RPCError) => e.code !== T.RPCGen.StatusCode.scresolutionfailed - - const {itemsInARow, itemWidth} = widthToDimensions(width) - const chunks: Array = React.useMemo(() => { - const friends = selectedTab === 'following' ? p.following : p.followers - const result: Array = width - ? chunk(friends, itemsInARow).map(c => { - return { - itemWidth, - type: 'friend', - usernames: c, - } as const - }) - : [] - if (result.length === 0) { - if (p.following && p.followers) { - result.push({ - text: - selectedTab === 'following' - ? `${p.userIsYou ? 'You are' : `${p.username} is`} not following anyone.` - : `${p.userIsYou ? 'You have' : `${p.username} has`} no followers.`, - type: 'noFriends', - }) - } else { - result.push({text: 'Loading...', type: 'loading'}) - } + }, [userData.username]) + + const errorFilter = (error: RPCError) => error.code !== T.RPCGen.StatusCode.scresolutionfailed + const {itemWidth, itemsInARow} = widthToDimensions(width) + const friendItems = buildFriendItems({ + followers: userData.followers, + following: userData.following, + itemWidth, + itemsInARow, + selectedTab, + userIsYou: userData.userIsYou, + username: userData.username, + width, + }) + + const bioTeamProofsProps = { + assertionKeys: userData.assertionKeys, + backgroundColorType: userData.backgroundColorType, + fullName: userData.fullName, + name: userData.name, + notAUser: userData.notAUser, + onAddIdentity: userData.onAddIdentity, + onEditAvatar: userData.onEditAvatar, + reason: userData.reason, + sbsAvatarUrl: userData.sbsAvatarUrl, + service: userData.service, + serviceIcon: userData.serviceIcon, + suggestionKeys: userData.suggestionKeys, + title: userData.title, + username: userData.username, + } + + const loadingFollowing = userData.following === undefined + const loadingFollowers = userData.followers === undefined + const renderSectionHeader = ({section}: {section: Section}) => { + if (section.data[0]?.type === 'bioTeamProofs' || userData.notAUser) { + return null } - return result - }, [selectedTab, p.following, p.followers, width, itemsInARow, itemWidth, p.userIsYou, p.username]) - const containerStyle = { - paddingTop: (Kb.Styles.isAndroid ? 56 : Kb.Styles.isTablet ? 80 : Kb.Styles.isIOS ? 46 : 80) + insetTop, + return ( + + ) } - const loadingFollowing = p.following === undefined - const loadingFollowers = p.followers === undefined - const renderSectionHeader = React.useCallback( - ({section}: {section: Section}) => { - if (section.data[0]?.type === 'bioTeamProofs') return null - if (p.notAUser) return null - return ( - - ) - }, - [p.notAUser, loadingFollowing, loadingFollowers, p.followersCount, p.followingCount, changeTab, selectedTab] - ) + const sections = [makeBioTeamProofsSection(bioTeamProofsProps), makeFriendsSection(friendItems, userData.notAUser)] - const sections: Array
= React.useMemo( - () => [ - { - data: [{type: 'bioTeamProofs'}], - renderItem: () => ( - - ), - } as const, - { - data: chunks, - renderItem: ({item, index}: {item: Item; index: number}) => { - if (item.type === 'bioTeamProofs') return null - if (item.type === 'friend') { - return - } - return p.notAUser ? null : ( - - {item.text} - - ) - }, - }, - ] as const, - [ - p.onAddIdentity, p.assertionKeys, p.backgroundColorType, p.username, p.name, - p.service, p.serviceIcon, p.reason, p.sbsAvatarUrl, p.suggestionKeys, - p.onEditAvatar, p.notAUser, p.fullName, p.title, chunks, - ] - ) + const containerStyle = { + paddingTop: + (Kb.Styles.isAndroid ? 56 : Kb.Styles.isTablet ? 80 : Kb.Styles.isIOS ? 46 : 80) + insetTop, + } return ( { direction="vertical" fullWidth={true} fullHeight={true} - style={Kb.Styles.collapseStyles([containerStyle, colorTypeToStyle(p.backgroundColorType)])} + style={Kb.Styles.collapseStyles([ + containerStyle, + colorTypeToStyle(userData.backgroundColorType), + ])} > { ) } -const usernameSelectedTab = new Map() - -const avatarSize = 128 - const styles = Kb.Styles.styleSheetCreate(() => ({ addIdentityButton: { marginBottom: Kb.Styles.globalMargins.xsmall, diff --git a/shared/profile/user/teams/index.tsx b/shared/profile/user/teams/index.tsx index 9a347e111416..14d288e1d80a 100644 --- a/shared/profile/user/teams/index.tsx +++ b/shared/profile/user/teams/index.tsx @@ -1,106 +1,105 @@ import * as C from '@/constants' -import {useTeamsState} from '@/stores/teams' -import * as T from '@/constants/types' import * as Kb from '@/common-adapters' +import * as T from '@/constants/types' import OpenMeta from './openmeta' -import {default as TeamInfo, type Props as TIProps} from './teaminfo' -import {useTrackerState} from '@/stores/tracker' +import {default as TeamInfo, type Props as TeamInfoProps} from './teaminfo' import {useCurrentUserState} from '@/stores/current-user' +import {useTeamsState} from '@/stores/teams' +import {useTrackerState} from '@/stores/tracker' type OwnProps = {username: string} -const noTeams = new Array() +const noTeams: Array = [] + +const getTeamMembershipByName = ({ + roles, + teamNameToID, + teamShowcase, +}: { + roles: ReadonlyMap + teamNameToID: ReadonlyMap + teamShowcase: ReadonlyArray +}) => + teamShowcase.reduce>((membershipByName, team) => { + const teamID = teamNameToID.get(team.name) || T.Teams.noTeamID + membershipByName[team.name] = (roles.get(teamID)?.role || 'none') !== 'none' + return membershipByName + }, {}) -const Container = (ownProps: OwnProps) => { - const d = useTrackerState(s => s.getDetails(ownProps.username)) - const _isYou = useCurrentUserState(s => s.username === ownProps.username) - const teamsState = useTeamsState( +const Container = ({username}: OwnProps) => { + const {teamShowcase = noTeams} = useTrackerState(s => s.getDetails(username)) + const isYou = useCurrentUserState(s => s.username === username) + const {joinTeam, roles, showTeamByName, teamNameToID, youAreInTeams} = useTeamsState( C.useShallow(s => ({ - _roles: s.teamRoleMap.roles, - _teamNameToID: s.teamNameToID, - _youAreInTeams: s.teamnames.size > 0, joinTeam: s.dispatch.joinTeam, + roles: s.teamRoleMap.roles, showTeamByName: s.dispatch.showTeamByName, + teamNameToID: s.teamNameToID, + youAreInTeams: s.teamnames.size > 0, })) ) - const {joinTeam, showTeamByName, _roles} = teamsState - const {_teamNameToID, _youAreInTeams} = teamsState - const teamShowcase = d.teamShowcase || noTeams const {clearModals, navigateAppend} = C.useRouterState( C.useShallow(s => ({ clearModals: s.dispatch.clearModals, navigateAppend: s.dispatch.navigateAppend, })) ) - const _onEdit = () => { - navigateAppend('profileShowcaseTeamOffer') - } - const onJoinTeam = joinTeam + + const canEditShowcase = isYou && youAreInTeams + const membershipByName = getTeamMembershipByName({roles, teamNameToID, teamShowcase}) + const onEdit = canEditShowcase ? () => navigateAppend('profileShowcaseTeamOffer') : undefined const onViewTeam = (teamname: string) => { clearModals() showTeamByName(teamname) } - const onEdit = _isYou && _youAreInTeams ? _onEdit : undefined - const teamMeta = teamShowcase.reduce<{ - [key: string]: { - inTeam: boolean - teamID: T.Teams.TeamID - } - }>((map, t) => { - const teamID = _teamNameToID.get(t.name) || T.Teams.noTeamID - map[t.name] = { - inTeam: !!((_roles.get(teamID)?.role || 'none') !== 'none'), - teamID, - } - return map - }, {}) - return onEdit || teamShowcase.length > 0 ? ( + if (!canEditShowcase && teamShowcase.length === 0) { + return null + } + + return ( Teams {!!onEdit && } - {!!onEdit && !teamShowcase.length && } - {teamShowcase.map(t => ( + {!!onEdit && teamShowcase.length === 0 && } + {teamShowcase.map(team => ( onViewTeam(t.name)} - inTeam={teamMeta[t.name]?.inTeam ?? false} + key={team.name} + {...team} + inTeam={membershipByName[team.name] ?? false} + onJoinTeam={joinTeam} + onViewTeam={() => onViewTeam(team.name)} /> ))} - ) : null + ) } -const TeamShowcase = (props: Omit) => { - const {name, isOpen} = props - const makePopup = (p: Kb.Popup2Parms) => { - const {attachTo, hidePopup} = p - return - } +const TeamShowcase = (props: Omit) => { + const makePopup = ({attachTo, hidePopup}: Kb.Popup2Parms) => ( + + ) const {showPopup, popup, popupAnchor} = Kb.usePopup2(makePopup) + return ( - <> - {popup} - - + {popup} + - {name} + {props.name} - + ) } -const ShowcaseTeamsOffer = (p: {onEdit: () => void}) => ( +const ShowcaseTeamsOffer = ({onEdit}: {onEdit: () => void}) => ( - + diff --git a/shared/profile/user/teams/teaminfo.tsx b/shared/profile/user/teams/teaminfo.tsx index ee585f51daa4..04638b52158f 100644 --- a/shared/profile/user/teams/teaminfo.tsx +++ b/shared/profile/user/teams/teaminfo.tsx @@ -1,14 +1,14 @@ +import * as C from '@/constants' import * as React from 'react' import * as Styles from '@/styles' -import * as C from '@/constants' -import OpenMeta from './openmeta' import FloatingMenu from '@/common-adapters/floating-menu' -import ConnectedUsernames from '@/common-adapters/usernames' import NameWithIcon from '@/common-adapters/name-with-icon' +import OpenMeta from './openmeta' import Text from '@/common-adapters/text' -import {Box2} from '@/common-adapters/box' import WaitingButton from '@/common-adapters/waiting-button' -import type {MeasureRef} from '@/common-adapters/measure-ref' +import {Box2} from '@/common-adapters/box' +import {type MeasureRef} from '@/common-adapters/measure-ref' +import ConnectedUsernames from '@/common-adapters/usernames' const Kb = { Box2, @@ -36,40 +36,49 @@ export type Props = { visible: boolean } -const TeamInfo = (props: Props) => { +const TeamInfo = ({ + attachTo, + description, + inTeam, + isOpen, + membersCount, + name, + onChat, + onHidden, + onJoinTeam, + onViewTeam, + position, + publicAdmins, + visible, +}: Props) => { const [requested, setRequested] = React.useState(false) + const isPrivate = membersCount === 0 && description.length === 0 + const memberText = isPrivate + ? 'This team is private. Admins will decide if they can let you in.' + : `${membersCount} member${membersCount > 1 ? 's' : ''}` + const joinLabel = requested ? 'Requested!' : isOpen ? 'Join team' : 'Request to join' - const _isPrivate = () => { - return props.membersCount === 0 && props.description.length === 0 - } - - const _onJoinTeam = () => { - props.onJoinTeam(props.name) + const handleJoinTeam = () => { + onJoinTeam(name) setRequested(true) } - const _onViewTeam = () => { - props.onViewTeam() - props.onHidden() + const handleViewTeam = () => { + onViewTeam() + onHidden() } - const _onChat = () => { - if (props.onChat) { - props.onChat() - props.onHidden() - } + const handleChat = () => { + onChat?.() + onHidden() } - const memberText = _isPrivate() - ? 'This team is private. Admins will decide if they can let you in.' - : `${props.membersCount} member${props.membersCount > 1 ? 's' : ''}` - return ( { > } + teamname={name} + title={name} + metaOne={} metaTwo={{memberText}} /> - {props.description} + {description} - {props.onChat && ( + {!!onChat && ( )} - {/* With teamsRedesign we have external team page, always show view team button */} - {!props.inTeam && ( + {!inTeam && ( )} - {!!props.publicAdmins.length && ( + {!!publicAdmins.length && ( Public admins:{' '} - { - - } + )} } - position={props.position ?? 'bottom left'} + position={position ?? 'bottom left'} items={[]} /> ) @@ -142,9 +148,7 @@ const styles = Kb.Styles.styleSheetCreate( () => ({ description: Kb.Styles.platformStyles({ - common: { - textAlign: 'center', - }, + common: {textAlign: 'center'}, isElectron: { width: '100%', wordWrap: 'break-word',