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',