From 27ce6e2703254867960e63d57c294501d23fa981 Mon Sep 17 00:00:00 2001 From: Arjun Porwal <65214523+ap211unitech@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:15:58 +0530 Subject: [PATCH] Simplified Chain Endpoint Selection (#11742) * feat: added star icon for chain * feat: add favorite chains header and localization support * fix: star ux flow enhanced * feat: mark chains as favorite and store it * feat: added transitions and shown already favorite chains * fix: remove favorite chains key from localStorage on error * feat: integrate favorite chains functionality in Group * chore: fix spacing between group headers * feat: enhance favorite chains functionality with structured storage and utility functions * fix: prevent rendering empty component for groups with no favorite chains * chore: translations * chore: match favorite network with relay and paraId * chore: remove logging * chore: providers list removed * feat: created dropdown for choosing provider * feat: add alert for no available WebSocket providers in NetworkDisplay * fix: update setApiUrl to preserve groupIndex * feat: improved favorite chain header style * feat: add getContrastingColor utility and update GroupDisplay styling --- packages/apps/src/Endpoints/Group.tsx | 35 +++++++++++++++---- packages/apps/src/Endpoints/Network.tsx | 32 ++++++++++++------ packages/apps/src/Endpoints/Url.tsx | 45 ------------------------- packages/apps/src/Endpoints/index.tsx | 8 +++-- packages/apps/src/Endpoints/utils.ts | 28 +++++++++++++++ 5 files changed, 84 insertions(+), 64 deletions(-) delete mode 100644 packages/apps/src/Endpoints/Url.tsx diff --git a/packages/apps/src/Endpoints/Group.tsx b/packages/apps/src/Endpoints/Group.tsx index 8a59ad2c59..877bc00324 100644 --- a/packages/apps/src/Endpoints/Group.tsx +++ b/packages/apps/src/Endpoints/Group.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { Icon, styled } from '@polkadot/react-components'; import Network from './Network.js'; -import { isFavoriteChain } from './utils.js'; +import { getContrastingColor, isFavoriteChain } from './utils.js'; interface Props { affinities: Record; @@ -22,27 +22,33 @@ interface Props { setApiUrl: (network: string, apiUrl: string) => void; setGroup: (groupIndex: number) => void; value: Group; + highlightColor: string; } -function GroupDisplay ({ affinities, apiUrl, children, className = '', favoriteChains, index, isSelected, setApiUrl, setGroup, toggleFavoriteChain, value: { header, isSpaced, networks } }: Props): React.ReactElement { +function GroupDisplay ({ affinities, apiUrl, children, className = '', favoriteChains, highlightColor, index, isSelected, setApiUrl, setGroup, toggleFavoriteChain, value: { header, isSpaced, networks } }: Props): React.ReactElement { const _setGroup = useCallback( () => setGroup(isSelected ? -1 : index), [index, isSelected, setGroup] ); + const isFavoriteHeader = useMemo(() => header?.toString().includes('Favorite'), [header]); + const filtered = useMemo( () => networks.filter(({ isUnreachable }) => !isUnreachable), [networks] ); - if (header?.toString().includes('Favorite') && Object.keys(favoriteChains).length === 0) { + if (isFavoriteHeader && Object.keys(favoriteChains).length === 0) { return <>; } return ( - +
@@ -74,7 +80,7 @@ function GroupDisplay ({ affinities, apiUrl, children, className = '', favoriteC ); } -const StyledDiv = styled.div` +const StyledDiv = styled.div<{ highlightColor: string; }>` .groupHeader { border-radius: 0.25rem; cursor: pointer; @@ -94,6 +100,23 @@ const StyledDiv = styled.div` .ui--Icon { margin-right: 0.5rem; } + + &.isFavoriteHeader { + &:hover { + background: linear-gradient( + 135deg, + ${(props) => props.highlightColor}f2 0%, + ${(props) => props.highlightColor}99 100% + ); + color: ${(props) => getContrastingColor(props.highlightColor)}; + } + + &::after { + content: '⭐'; + margin-left: 8px; + font-size: 16px; + } + } } .groupNetworks { diff --git a/packages/apps/src/Endpoints/Network.tsx b/packages/apps/src/Endpoints/Network.tsx index cd7190a153..4e83ba5c99 100644 --- a/packages/apps/src/Endpoints/Network.tsx +++ b/packages/apps/src/Endpoints/Network.tsx @@ -5,10 +5,9 @@ import type { IFavoriteChainProps, Network } from './types.js'; import React, { useCallback, useMemo } from 'react'; -import { ChainImg, Icon, styled } from '@polkadot/react-components'; +import { ChainImg, Dropdown, Icon, styled } from '@polkadot/react-components'; import { useTranslation } from '../translate.js'; -import Url from './Url.js'; interface Props { affinity?: string; // unused - previous selection @@ -27,10 +26,23 @@ function NetworkDisplay ({ apiUrl, className = '', isFavorite, setApiUrl, toggle [apiUrl, providers] ); + const providersOptions = useMemo(() => { + return providers.map(({ name, url }) => ({ + text: name, + value: url + })); + }, [providers]); + const _selectUrl = useCallback( () => { const filteredProviders = providers.filter(({ url }) => !url.startsWith('light://')); + if (filteredProviders.length === 0) { + alert('No WebSocket (wss://) provider available'); + + return; + } + return setApiUrl(name, filteredProviders[Math.floor(Math.random() * filteredProviders.length)].url); }, [name, providers, setApiUrl] @@ -82,15 +94,15 @@ function NetworkDisplay ({ apiUrl, className = '', isFavorite, setApiUrl, toggle onClick={_toggleFavoriteChain} />
- {isSelected && providers.map(({ name, url }): React.ReactNode => ( - - ))} + }
); } diff --git a/packages/apps/src/Endpoints/Url.tsx b/packages/apps/src/Endpoints/Url.tsx deleted file mode 100644 index e32a3439aa..0000000000 --- a/packages/apps/src/Endpoints/Url.tsx +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2017-2025 @polkadot/apps authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import React, { useCallback } from 'react'; - -import { styled, Toggle } from '@polkadot/react-components'; - -interface Props { - apiUrl: string; - className?: string; - label: React.ReactNode; - setApiUrl: (apiUrl: string) => void; - url: string; -} - -function Url ({ apiUrl, className, label, setApiUrl, url }: Props): React.ReactElement { - const _setApiUrl = useCallback( - () => setApiUrl(url), - [setApiUrl, url] - ); - - return ( - - ); -} - -const StyledToggle = styled(Toggle)` - padding: 0.25rem; - text-align: right; - - > label { - max-width: 12.5rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -`; - -export default React.memo(Url); diff --git a/packages/apps/src/Endpoints/index.tsx b/packages/apps/src/Endpoints/index.tsx index 37a8d8e954..eeb46b9427 100644 --- a/packages/apps/src/Endpoints/index.tsx +++ b/packages/apps/src/Endpoints/index.tsx @@ -11,6 +11,7 @@ import store from 'store'; import { createWsEndpoints, CUSTOM_ENDPOINT_KEY } from '@polkadot/apps-config'; import { Button, Input, Sidebar, styled } from '@polkadot/react-components'; +import { defaultHighlight } from '@polkadot/react-components/styles'; import { useApi } from '@polkadot/react-hooks'; import { settings } from '@polkadot/ui-settings'; import { isAscii } from '@polkadot/util'; @@ -189,7 +190,7 @@ function isLocalForkDisabled (hasUrlChanged: boolean, apiUrl: string, isUrlValid function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElement { const { t } = useTranslation(); const linkOptions = createWsEndpoints(t); - const { isLocalFork } = useApi(); + const { apiEndpoint, isLocalFork } = useApi(); const [favoriteChains, setFavoriteChains] = useState(() => getFavoriteChains()); const [groups, setGroups] = useState(() => combineEndpoints(linkOptions)); const [{ apiUrl, groupIndex, hasUrlChanged, isUrlValid }, setApiUrl] = useState(() => extractUrlState(settings.get().apiUrl, groups)); @@ -269,7 +270,7 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem return newValue; }); - setApiUrl(extractUrlState(apiUrl, groups)); + setApiUrl((state) => ({ ...extractUrlState(apiUrl, groups), groupIndex: state.groupIndex })); }, [groups] ); @@ -280,7 +281,7 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem apiUrl = punycode.toASCII(apiUrl); } - setApiUrl(extractUrlState(apiUrl, groups)); + setApiUrl((state) => ({ ...extractUrlState(apiUrl, groups), groupIndex: state.groupIndex })); }, [groups] ); @@ -368,6 +369,7 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem affinities={affinities} apiUrl={apiUrl} favoriteChains={favoriteChains} + highlightColor={apiEndpoint?.ui.color || defaultHighlight} index={index} isSelected={groupIndex === index} key={index} diff --git a/packages/apps/src/Endpoints/utils.ts b/packages/apps/src/Endpoints/utils.ts index e2c81dcef6..c37ddeeaae 100644 --- a/packages/apps/src/Endpoints/utils.ts +++ b/packages/apps/src/Endpoints/utils.ts @@ -112,3 +112,31 @@ export const isFavoriteChain = ( return false; } }; + +export function getContrastingColor (hexColor: string): string { + if (typeof hexColor !== 'string') { + return '#000000'; + } + + let hex = hexColor.replace('#', '').trim(); + + if (hex.length === 3) { + hex = hex.split('').map((c) => c + c).join(''); + } + + if (hex.length !== 6 || /[^0-9a-f]/i.test(hex)) { + return '#000000'; + } + + try { + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + + return luminance > 0.5 ? '#000000' : '#FFFFFF'; + } catch { + return '#000000'; + } +}