import React, { useCallback, useContext, useEffect, useState } from 'react'; import { Link, LinkOff } from '@mui/icons-material'; import { Box, Checkbox, CircularProgress, Grid, Rating, Typography, useTheme } from '@mui/material'; import { DataGrid, type GridColDef, type GridValidRowModel } from '@mui/x-data-grid'; import { useTranslation } from 'react-i18next'; import { AppContext, type UseAppStoreType } from '../../contexts/AppContext'; import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext'; import { type Coordinator } from '../../models'; import headerStyleFix from '../DataGrid/HeaderFix'; import RobotAvatar from '../RobotAvatar'; import { verifyCoordinatorToken } from '../../utils/nostr'; interface FederationTableProps { maxWidth?: number; maxHeight?: number; fillContainer?: boolean; } const FederationTable = ({ maxWidth = 90, maxHeight = 50, fillContainer = false, }: FederationTableProps): React.JSX.Element => { const { t } = useTranslation(); const { federation, federationUpdatedAt } = useContext(FederationContext); const { setOpen, windowSize, settings } = useContext(AppContext); const theme = useTheme(); const [pageSize, setPageSize] = useState(0); const [ratings, setRatings] = useState>>( federation.getCoordinators().reduce((acc, coord) => { if (coord.nostrHexPubkey) acc[coord.nostrHexPubkey] = {}; return acc; }, {}), ); const [useDefaultPageSize, setUseDefaultPageSize] = useState(true); // all sizes in 'em' const fontSize = theme.typography.fontSize; const verticalHeightFrame = 3.3; const verticalHeightRow = 3.27; const defaultPageSize = Math.max( Math.floor((maxHeight - verticalHeightFrame) / verticalHeightRow), 1, ); const height = defaultPageSize * verticalHeightRow + verticalHeightFrame; const mobile = windowSize.width < 44; useEffect(() => { loadRatings(); }, []); useEffect(() => { if (useDefaultPageSize) { setPageSize(defaultPageSize); } }, [federationUpdatedAt]); const loadRatings: () => void = () => { federation.roboPool.subscribeRatings({ onevent: (event) => { const verfied = verifyCoordinatorToken(event); const coordinatorPubKey = event.tags.find((t) => t[0] === 'p')?.[1]; if (verfied && coordinatorPubKey) { const rating = event.tags.find((t) => t[0] === 'rating')?.[1]; if (rating) { setRatings((prev) => { prev[coordinatorPubKey][event.pubkey] = parseFloat(rating); return prev; }); } } }, oneose: () => {}, }); }; const localeText = { noResultsOverlayLabel: t('No coordinators found.'), }; const onClickCoordinator = function (shortAlias: string): void { setOpen((open) => { return { ...open, coordinator: shortAlias }; }); }; const aliasObj = useCallback(() => { return { field: 'longAlias', headerName: mobile ? '' : t('Rating'), width: mobile ? 60 : 190, renderCell: (params: { row: Coordinator }) => { const coordinator = federation.getCoordinator(params.row.shortAlias); return ( { onClickCoordinator(params.row.shortAlias); }} alignItems='center' spacing={1} > {!mobile ? ( {params.row.longAlias} ) : ( <> )} ); }, }; }, []); const ratingObj = useCallback(() => { return { field: 'rating', headerName: t('Rating'), width: mobile ? 60 : 180, renderCell: (params: { row: Coordinator }) => { const coordinator = federation.getCoordinator(params.row.shortAlias); const coordinatorRating = ratings[coordinator.nostrHexPubkey]; if (!coordinatorRating) return <>; const totalRatings = Object.values(coordinatorRating); const total = totalRatings.length; const sum: number = Object.values(totalRatings).reduce((accumulator, currentValue) => { return accumulator + currentValue; }, 0); const average = total < 1 ? 0 : sum / total; return ( <> {mobile ? ( {`${parseFloat((average * 10).toFixed(1))}`} ) : ( <> { onClickCoordinator(params.row.shortAlias); }} /> {`(${total})`} )} ); }, }; }, [federationUpdatedAt]); const enabledObj = useCallback( (width: number) => { return { field: 'enabled', headerName: t('Enabled'), width: width * fontSize, renderCell: (params: { row: Coordinator }) => { return ( { onEnableChange(params.row.shortAlias); }} /> ); }, }; }, [federationUpdatedAt], ); const upObj = useCallback( (width: number) => { return { field: 'up', headerName: t('Up'), width: width * fontSize, renderCell: (params: { row: Coordinator }) => { return (
{ onClickCoordinator(params.row.shortAlias); }} > {Boolean(params.row.loadingLimits) && Boolean(params.row.enabled) ? ( ) : params.row.limits !== undefined ? ( ) : ( )}
); }, }; }, [federationUpdatedAt], ); const columnSpecs = { alias: { priority: 2, order: 1, normal: { width: 12.1, object: aliasObj, }, }, rating: { priority: 2, order: 2, normal: { width: 12.1, object: ratingObj, }, }, up: { priority: 3, order: 3, normal: { width: 3.5, object: upObj, }, }, enabled: { priority: 1, order: 4, normal: { width: 5, object: enabledObj, }, }, }; const filteredColumns = function (): { columns: Array>; width: number; } { const useSmall = maxWidth < 30; const selectedColumns: object[] = []; let width: number = 0; for (const value of Object.values(columnSpecs)) { const colWidth = Number( useSmall && Boolean(value.small) ? value.small.width : value.normal.width, ); const colObject = useSmall && Boolean(value.small) ? value.small.object : value.normal.object; if (width + colWidth < maxWidth || selectedColumns.length < 2) { width = width + colWidth; selectedColumns.push([colObject(colWidth, false), value.order]); } else { selectedColumns.push([colObject(colWidth, true), value.order]); } } // sort columns by column.order value selectedColumns.sort(function (first, second) { return first[1] - second[1]; }); const columns: Array> = selectedColumns.map(function (item) { return item[0]; }); return { columns, width: width * 0.9 }; }; const { columns, width } = filteredColumns(); const onEnableChange = function (shortAlias: string): void { if (federation.getCoordinator(shortAlias).enabled === true) { federation.disableCoordinator(shortAlias); } else { federation.enableCoordinator(shortAlias); } }; return ( params.shortAlias} columns={columns} checkboxSelection={false} pageSize={pageSize} rowsPerPageOptions={width < 22 ? [] : [0, pageSize, defaultPageSize * 2, 50, 100]} onPageSizeChange={(newPageSize) => { setPageSize(newPageSize); setUseDefaultPageSize(false); }} hideFooter={true} /> ); }; export default FederationTable;