/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
shared/teams/team/rows/index.tsx
367 строк
12 KB
chrisnojima
perf: cut the client-side RPC floods, and repair four desktop e2e tests (#29466)
04 авг 2026, 17:30
Не верифицирован
04 авг 2026, 17:30
f90db8b
Код
Авторство
О чём код?
import * as C from '@/constants' import * as T from '@/constants/types' import type * as Kb from '@/common-adapters' import * as React from 'react' import {useGeneralConvIDKey} from '@/teams/common/general-conv' import EmptyRow from './empty-row' import LoadingRow from './loading' import MemberRow from './member-row' import BotRow from './bot-row/bot' import AddBotRow from './bot-row/add-row' import ChannelRow from './channel-row/channel' import ChannelHeaderRow from './channel-row/add' import ChannelFooterRow from './channel-row/info' import EmojiItemRow from './emoji-row/item' import EmojiAddRow from './emoji-row/add' import EmojiHeader from './emoji-row/header' import RequestRow from './invite-row/request' import InviteRow from './invite-row/invite' import SubteamAddRow from './subteam-row/add' import SubteamInfoRow from './subteam-row/info' import SubteamTeamRow from './subteam-row/team' import {getOrderedMemberArray, sortInvites, getOrderedBotsArray} from './helpers' import {useEmojiState} from '../../emojis/use-emoji' import {useCurrentUserState} from '@/stores/current-user' import {useTeamsListMap} from '@/teams/use-teams-list' export type Item = | {type: 'members-loading'} | {type: 'members-none'} | {type: 'member-members'; mi: T.Teams.MemberInfo} | {type: 'emoji'; e: T.RPCChat.Emoji} | {type: 'add-bots'} | {type: 'invite-requests'; ctime: number; fullName: string; reset?: boolean; username: string} | {type: 'member-invites'; ii: T.Teams.InviteInfo} | {type: 'channel-empty'} | {type: 'channel-loading'} | {type: 'channel-add'} | { type: 'channel-channels' c: { channelname: string conversationIDKey: T.Chat.ConversationIDKey description: string } } | {type: 'channel-few'} | {type: 'channel-info'} | {type: 'subteam-add'} | {type: 'subteam-info'} | {type: 'subteam-none'} | {type: 'emoji-add'} | {type: 'emoji-header'} | {type: 'emoji-item'; e: T.RPCChat.Emoji} | {type: 'subteams'; id: string} | {type: 'header'} | {type: 'tabs'} | {type: 'settings'} export type Section = Kb.SectionType<Item> & { collapsed?: boolean onToggleCollapsed?: () => void title?: string } export const useMembersSections = ( teamID: T.Teams.TeamID, loading: boolean, meta: T.Teams.TeamMeta, details: T.Teams.TeamDetails, yourOperations: T.Teams.TeamOperations ): Array<Section> => { const yourUsername = useCurrentUserState(s => s.username) // TODO: figure out if this is bad for performance and if we should leave these functions early when we're not on that tab // TODO: consider moving this to the parent const stillLoading = loading || (meta.memberCount > 0 && !details.members.size) if (stillLoading) { return [{data: [{type: 'members-loading'}], renderItem: () => <LoadingRow />} as const] } const sections: Array<Section> = [ { data: getOrderedMemberArray(details.members, yourUsername, yourOperations).map(mi => ({ mi, type: 'member-members', })), renderItem: ({index, item}: {index: number; item: Item}) => item.type === 'member-members' ? ( <MemberRow teamID={teamID} username={item.mi.username} firstItem={index === 0} /> ) : null, title: `Already in team (${meta.memberCount})`, } as const, ] // When you're the only one in the team, still show the no-members row if (meta.memberCount === 0 || (meta.memberCount === 1 && meta.role !== 'none')) { sections.push({ data: [{type: 'members-none'}], renderItem: () => <EmptyRow teamID={teamID} type="members" />, } as const) } return sections } export const useBotSections = ( teamID: T.Teams.TeamID, loading: boolean, meta: T.Teams.TeamMeta, details: T.Teams.TeamDetails, yourOperations: T.Teams.TeamOperations ): Array<Section> => { const stillLoading = loading || (meta.memberCount > 0 && !details.members.size) if (stillLoading) { return [{data: [{type: 'members-loading'}], renderItem: () => <LoadingRow />} as const] } // TODO: is there an empty state here? return [ { data: getOrderedBotsArray(details.members).map(b => ({ mi: b, type: 'member-members', })), renderItem: ({item}: {item: Item}) => item.type === 'member-members' ? <BotRow teamID={teamID} username={item.mi.username} /> : null, } as const, ...(yourOperations.manageBots ? [{data: [{type: 'add-bots'}], renderItem: () => <AddBotRow teamID={teamID} />} as const] : []), ] } export const useInvitesSections = ( teamID: T.Teams.TeamID, details: T.Teams.TeamDetails, collapsed: boolean, setCollapsed: React.Dispatch<React.SetStateAction<boolean>> ): Array<Section> => { const onToggleCollapsed = () => setCollapsed(prev => !prev) const sections: Array<Section> = [] const resetMembers = [...details.members.values()].filter(m => m.status === 'reset') if (details.requests.size || resetMembers.length) { const requestsSection = { data: [ ...[...details.requests].map( req => ({ ctime: req.ctime, fullName: req.fullName, key: `invites-request:${req.username}`, type: 'invite-requests', username: req.username, }) as const ), ...resetMembers.map( memberInfo => ({ ctime: 0, fullName: memberInfo.fullName, key: `invites-reset:${memberInfo.username}`, reset: true, type: 'invite-requests', username: memberInfo.username, }) as const ), ], renderItem: ({index, item}: {index: number; item: Item}) => item.type === 'invite-requests' ? ( <RequestRow {...item} teamID={teamID} firstItem={index === 0} /> ) : null, title: isMobile ? `Requests (${details.requests.size})` : undefined, } satisfies Section sections.push(requestsSection) } if (details.invites.size) { sections.push({ collapsed, data: collapsed ? [] : [...details.invites].sort(sortInvites).map(i => ({ ii: i, type: 'member-invites', })), onToggleCollapsed, renderItem: ({index, item}: {index: number; item: Item}) => item.type === 'member-invites' ? ( <InviteRow teamID={teamID} id={item.ii.id} firstItem={index === 0} /> ) : null, title: `Invitations (${details.invites.size})`, } as const) } return sections } export const useChannelsSections = ( teamID: T.Teams.TeamID, yourOperations: T.Teams.TeamOperations, channels: ReadonlyMap<T.Chat.ConversationIDKey, T.Teams.TeamChannelInfo>, loading: boolean ): Array<Section> => { // Self-consistent: a big team has channels beyond #general. Derive from the // channels we loaded here rather than the chat inbox layout, which is empty // until the inbox has been visited. if (loading && channels.size === 0) { return [{data: [{type: 'channel-loading'}], renderItem: () => <LoadingRow />} as const] } const isBig = channels.size > 1 if (!isBig) { return [ { data: [{type: 'channel-empty'}], renderItem: () => <EmptyRow type="channelsEmpty" teamID={teamID} />, } as const, ] } const createRow = yourOperations.createChannel ? [{data: [{type: 'channel-add'}], renderItem: () => <ChannelHeaderRow teamID={teamID} />} as const] : [] const channelsValues = [...channels.values()] return [ ...createRow, { data: channelsValues .map(c => ({c, type: 'channel-channels'})) .sort((a, b) => a.c.channelname === 'general' ? -1 : b.c.channelname === 'general' ? 1 : a.c.channelname.localeCompare(b.c.channelname) ), renderItem: ({item}: {item: Item}) => item.type === 'channel-channels' ? ( <ChannelRow teamID={teamID} channel={item.c} /> ) : null, }, channels.size < 5 && yourOperations.createChannel ? ({ data: [{type: 'channel-few'}] as const, renderItem: ({item}: {item: Item}) => item.type === 'channel-few' ? <EmptyRow type="channelsFew" teamID={teamID} /> : null, } satisfies Section) : ({ data: [{type: 'channel-info'}] as const, renderItem: ({item}: {item: Item}) => (item.type === 'channel-info' ? <ChannelFooterRow /> : null), } satisfies Section), ] as Array<Section> } // When we delete the feature flag, clean this up a bit export const useSubteamsSections = ( teamID: T.Teams.TeamID, details: T.Teams.TeamDetails, yourOperations: T.Teams.TeamOperations, subteamFilter: string, setSubteamFilter: React.Dispatch<React.SetStateAction<string>> ): Array<Section> => { const teamMetaByID = useTeamsListMap() const filterLC = subteamFilter.toLowerCase().trim() const subteams = [...details.subteams] .filter(subteamID => !filterLC || teamMetaByID.get(subteamID)?.teamname.toLowerCase().includes(filterLC)) .sort() const sections: Array<Section> = [] if (yourOperations.manageSubteams && details.subteams.size) { sections.push({ data: [{type: 'subteam-add'}], renderItem: () => ( <SubteamAddRow teamID={teamID} subteamFilter={subteamFilter} setSubteamFilter={setSubteamFilter} /> ), } as const) } sections.push({ data: subteams.map(s => ({id: s, type: 'subteams'})), renderItem: ({item}: {item: Item}) => item.type === 'subteams' ? <SubteamTeamRow teamID={item.id} teamMeta={teamMetaByID.get(item.id)} /> : null, } as const) if (details.subteams.size) { sections.push({data: [{type: 'subteam-info'}], renderItem: () => <SubteamInfoRow />} as const) } else { sections.push({ data: [{type: 'subteam-none'}], renderItem: () => <EmptyRow teamID={teamID} type="subteams" />, } as const) } return sections } export const useEmojiSections = (teamID: T.Teams.TeamID, shouldActuallyLoad: boolean): Array<Section> => { const convID = useGeneralConvIDKey(teamID) const getUserEmoji = C.useRPC(T.RPCChat.localUserEmojisRpcPromise) const [customEmoji, setCustomEmoji] = React.useState<T.RPCChat.Emoji[]>([]) const [filter, setFilter] = React.useState('') const doGetUserEmoji = React.useEffectEvent(() => { if (!convID || convID === T.Chat.noConversationIDKey || !shouldActuallyLoad) { return } getUserEmoji( [ { convID: T.Chat.keyToConversationID(convID), opts: { getAliases: true, getCreationInfo: true, onlyInTeam: true, }, }, ], result => { let emojis: Array<T.RPCChat.Emoji> = [] result.emojis.emojis?.forEach(g => { emojis = emojis.concat(g.emojis ?? []) }) setCustomEmoji(emojis) }, _ => setCustomEmoji([]) ) }) const updatedTrigger = useEmojiState(s => s.emojiUpdatedTrigger) React.useEffect(() => { doGetUserEmoji() }, [convID, shouldActuallyLoad, updatedTrigger]) let filteredEmoji: T.RPCChat.Emoji[] = customEmoji if (filter !== '') { filteredEmoji = filteredEmoji.filter(e => e.alias.includes(filter.toLowerCase())) } const sections: Array<Section> = [] sections.push({ data: [{type: 'emoji-add'}], renderItem: () => ( <EmojiAddRow teamID={teamID} convID={convID ?? T.Chat.noConversationIDKey} filter={filter} setFilter={setFilter} /> ), } as const) if (customEmoji.length) { if (!isMobile) { sections.push({ data: [{type: 'emoji-header'}], renderItem: () => <EmojiHeader />, } as const) } sections.push({ data: filteredEmoji.map(e => ({e, type: 'emoji-item'})), renderItem: ({item, index}: {item: Item; index: number}) => item.type === 'emoji-item' ? ( <EmojiItemRow emoji={item.e} firstItem={index === 0} conversationIDKey={convID ?? T.Chat.noConversationIDKey} teamID={teamID} /> ) : null, } as const) } return sections }