feat: implement new designs for tx notifs (#6232)

* feat: re-add transaction activity popups

* feat: implement new designs for tx notifs

* fix: address comments

* fix: remove color from alert icon

* fix: nits

* fix: remove null check

* fix: fix
This commit is contained in:
eddie 2023-03-27 13:56:52 -07:00 committed by GitHub
parent da79abbc0d
commit a0f20c54d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 169 additions and 154 deletions

@ -1,8 +1,8 @@
import { Trans } from '@lingui/macro'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { AlertTriangle } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import styled from 'styled-components/macro'
import { ThemedText } from '../../theme'
import { AutoColumn } from '../Column'
@ -12,26 +12,31 @@ const RowNoFlex = styled(AutoRow)`
flex-wrap: nowrap;
`
const ColumnContainer = styled(AutoColumn)`
margin: 0 12px;
`
export const PopupAlertTriangle = styled(AlertTriangleFilled)`
flex-shrink: 0;
width: 32px;
height: 32px;
`
export default function FailedNetworkSwitchPopup({ chainId }: { chainId: SupportedChainId }) {
const chainInfo = getChainInfo(chainId)
const theme = useTheme()
return (
<RowNoFlex>
<AutoColumn gap="sm">
<RowNoFlex style={{ alignItems: 'center' }}>
<div style={{ paddingRight: 13 }}>
<AlertTriangle color={theme.accentWarning} size={24} display="flex" />
</div>
<ThemedText.SubHeader>
<Trans>Failed to switch networks</Trans>
</ThemedText.SubHeader>
</RowNoFlex>
<RowNoFlex gap="12px">
<PopupAlertTriangle />
<ColumnContainer gap="sm">
<ThemedText.SubHeader color="textSecondary">
<Trans>Failed to switch networks</Trans>
</ThemedText.SubHeader>
<ThemedText.BodySmall>
<ThemedText.BodySmall color="textSecondary">
<Trans>To use Uniswap on {chainInfo.label}, switch the network in your wallets settings.</Trans>
</ThemedText.BodySmall>
</AutoColumn>
</ColumnContainer>
</RowNoFlex>
)
}

@ -1,7 +1,5 @@
import { useCallback, useEffect } from 'react'
import { X } from 'react-feather'
import { animated } from 'react-spring'
import { useSpring } from 'react-spring'
import styled, { useTheme } from 'styled-components/macro'
import { useRemovePopup } from '../../state/application/hooks'
@ -24,7 +22,7 @@ const Popup = styled.div`
padding: 1em;
background-color: ${({ theme }) => theme.backgroundSurface};
position: relative;
border-radius: 10px;
border-radius: 16px;
padding: 20px;
padding-right: 35px;
overflow: hidden;
@ -36,16 +34,6 @@ const Popup = styled.div`
}
`}
`
const Fader = styled.div`
position: absolute;
bottom: 0px;
left: 0px;
width: 100%;
height: 2px;
background-color: ${({ theme }) => theme.deprecated_bg3};
`
const AnimatedFader = animated(Fader)
export default function PopupItem({
removeAfterMs,
@ -71,11 +59,6 @@ export default function PopupItem({
}, [removeAfterMs, removeThisPopup])
const theme = useTheme()
const faderStyle = useSpring({
from: { width: '100%' },
to: { width: '0%' },
config: { duration: removeAfterMs ?? undefined },
})
let popupContent
if ('txn' in content) {
@ -88,7 +71,6 @@ export default function PopupItem({
<Popup>
<StyledClose color={theme.textSecondary} onClick={removeThisPopup} />
{popupContent}
{removeAfterMs !== null ? <AnimatedFader style={faderStyle} /> : null}
</Popup>
) : null
}

@ -1,46 +1,66 @@
import { useWeb3React } from '@web3-react/core'
import { TransactionSummary } from 'components/AccountDetails/TransactionSummary'
import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row'
import { useContext } from 'react'
import { AlertCircle, CheckCircle } from 'react-feather'
import Column from 'components/Column'
import { parseLocalActivity } from 'components/WalletDropdown/MiniPortfolio/Activity/parseLocal'
import { PortfolioLogo } from 'components/WalletDropdown/MiniPortfolio/PortfolioLogo'
import PortfolioRow from 'components/WalletDropdown/MiniPortfolio/PortfolioRow'
import useENSName from 'hooks/useENSName'
import { useCombinedActiveList } from 'state/lists/hooks'
import { useTransaction } from 'state/transactions/hooks'
import styled, { ThemeContext } from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import { TransactionDetails } from 'state/transactions/types'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
const RowNoFlex = styled(AutoRow)`
flex-wrap: nowrap;
import { PopupAlertTriangle } from './FailedNetworkSwitchPopup'
const Descriptor = styled(ThemedText.BodySmall)`
${EllipsisStyle}
`
function TransactionPopupContent({ tx, chainId }: { tx: TransactionDetails; chainId: number }) {
const success = tx.receipt?.status === 1
const tokens = useCombinedActiveList()
const activity = parseLocalActivity(tx, chainId, tokens)
const { ENSName } = useENSName(activity?.otherAccount)
if (!activity) return null
const explorerUrl = getExplorerLink(chainId, tx.hash, ExplorerDataType.TRANSACTION)
return (
<PortfolioRow
left={
success ? (
<Column>
<PortfolioLogo
chainId={chainId}
currencies={activity.currencies}
images={activity.logos}
accountAddress={activity.otherAccount}
/>
</Column>
) : (
<PopupAlertTriangle />
)
}
title={<ThemedText.SubHeader fontWeight={500}>{activity.title}</ThemedText.SubHeader>}
descriptor={
<Descriptor color="textSecondary">
{activity.descriptor}
{ENSName ?? activity.otherAccount}
</Descriptor>
}
onClick={() => window.open(explorerUrl, '_blank')}
/>
)
}
export default function TransactionPopup({ hash }: { hash: string }) {
const { chainId } = useWeb3React()
const tx = useTransaction(hash)
const theme = useContext(ThemeContext)
if (!tx) return null
const success = Boolean(tx.receipt && tx.receipt.status === 1)
if (!chainId || !tx) return null
return (
<RowNoFlex>
<div style={{ paddingRight: 16 }}>
{success ? (
<CheckCircle color={theme.accentSuccess} size={24} />
) : (
<AlertCircle color={theme.accentFailure} size={24} />
)}
</div>
<AutoColumn gap="8px">
<ThemedText.BodyPrimary fontWeight={500}>
<TransactionSummary info={tx.info} />
</ThemedText.BodyPrimary>
{chainId && (
<ExternalLink href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}>
View on Explorer
</ExternalLink>
)}
</AutoColumn>
</RowNoFlex>
)
return <TransactionPopupContent tx={tx} chainId={chainId} />
}

@ -41,7 +41,7 @@ const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding:
position: fixed;
top: ${({ extraPadding }) => (extraPadding ? '72px' : '64px')};
right: 1rem;
max-width: 355px !important;
max-width: 376px !important;
width: 100%;
z-index: 3;

@ -0,0 +1,71 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import Column from 'components/Column'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { LoaderV2 } from 'components/Icons/LoadingSpinner'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import useENSName from 'hooks/useENSName'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow from '../PortfolioRow'
import { useTimeSince } from './parseRemote'
import { Activity } from './types'
const ActivityRowDescriptor = styled(ThemedText.BodySmall)`
color: ${({ theme }) => theme.textSecondary};
${EllipsisStyle}
`
const StyledTimestamp = styled(ThemedText.Caption)`
color: ${({ theme }) => theme.textSecondary};
font-variant: small;
font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on;
`
export function ActivityRow({
activity: { chainId, status, title, descriptor, logos, otherAccount, currencies, timestamp, hash },
}: {
activity: Activity
}) {
const { ENSName } = useENSName(otherAccount)
const timeSince = useTimeSince(timestamp)
const explorerUrl = getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)
return (
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_ROW}
properties={{ hash, chain_id: chainId, explorer_url: explorerUrl }}
>
<PortfolioRow
left={
<Column>
<PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} />
</Column>
}
title={<ThemedText.SubHeader fontWeight={500}>{title}</ThemedText.SubHeader>}
descriptor={
<ActivityRowDescriptor color="textSecondary">
{descriptor}
{ENSName ?? otherAccount}
</ActivityRowDescriptor>
}
right={
status === TransactionStatus.Pending ? (
<LoaderV2 />
) : status === TransactionStatus.Confirmed ? (
<StyledTimestamp>{timeSince}</StyledTimestamp>
) : (
<AlertTriangleFilled />
)
}
onClick={() => window.open(explorerUrl, '_blank')}
/>
</TraceEvent>
)
}

@ -1,26 +1,20 @@
import { t } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import Column from 'components/Column'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { LoaderV2 } from 'components/Icons/LoadingSpinner'
import { LoadingBubble } from 'components/Tokens/loading'
import { useWalletDrawer } from 'components/WalletDropdown'
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns'
import { TransactionStatus, useTransactionListQuery } from 'graphql/data/__generated__/types-and-hooks'
import { PollingInterval } from 'graphql/data/util'
import useENSName from 'hooks/useENSName'
import { atom, useAtom } from 'jotai'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
import { useEffect, useMemo } from 'react'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { ThemedText } from 'theme'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
import { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
import { ActivityRow } from './ActivityRow'
import { useLocalActivities } from './parseLocal'
import { parseRemoteActivities, useTimeSince } from './parseRemote'
import { parseRemoteActivities } from './parseRemote'
import { Activity, ActivityMap } from './types'
interface ActivityGroup {
@ -103,7 +97,7 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
const lastFetchedAtom = atom<number | undefined>(0)
export default function ActivityTab({ account }: { account: string }) {
export function ActivityTab({ account }: { account: string }) {
const [drawerOpen, toggleWalletDrawer] = useWalletDrawer()
const [lastFetched, setLastFetched] = useAtom(lastFetchedAtom)
@ -160,56 +154,3 @@ export default function ActivityTab({ account }: { account: string }) {
)
}
}
const StyledDescriptor = styled(ThemedText.BodySmall)`
color: ${({ theme }) => theme.textSecondary};
${EllipsisStyle}
`
const StyledTimestamp = styled(ThemedText.Caption)`
color: ${({ theme }) => theme.textSecondary};
font-variant: small;
font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on;
`
function ActivityRow({ activity }: { activity: Activity }) {
const { chainId, status, title, descriptor, logos, otherAccount, currencies } = activity
const { ENSName } = useENSName(otherAccount)
const explorerUrl = getExplorerLink(activity.chainId, activity.hash, ExplorerDataType.TRANSACTION)
const timeSince = useTimeSince(activity.timestamp)
return (
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_ROW}
properties={{ hash: activity.hash, chain_id: chainId, explorer_url: explorerUrl }}
>
<PortfolioRow
left={
<Column>
<PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} />
</Column>
}
title={<ThemedText.SubHeader fontWeight={500}>{title}</ThemedText.SubHeader>}
descriptor={
<StyledDescriptor color="textSecondary">
{descriptor}
{ENSName ?? otherAccount}
</StyledDescriptor>
}
right={
status === TransactionStatus.Pending ? (
<LoaderV2 />
) : status === TransactionStatus.Confirmed ? (
<StyledTimestamp>{timeSince}</StyledTimestamp>
) : (
<AlertTriangleFilled />
)
}
onClick={() => window.open(explorerUrl, '_blank')}
/>
</TraceEvent>
)
}

@ -126,7 +126,7 @@ function parseMigrateCreateV3(
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
}
function parseLocalActivity(
export function parseLocalActivity(
details: TransactionDetails,
chainId: SupportedChainId,
tokens: TokenAddressMap

@ -1,10 +1,9 @@
import Column, { AutoColumn } from 'components/Column'
import Row from 'components/Row'
import { LoadingBubble } from 'components/Tokens/loading'
import { useMemo } from 'react'
import styled, { css, keyframes } from 'styled-components/macro'
const RowWrapper = styled(Row)<{ onClick?: any }>`
export const PortfolioRowWrapper = styled(Row)<{ onClick?: any }>`
gap: 12px;
height: 68px;
padding: 0 16px;
@ -14,7 +13,6 @@ const RowWrapper = styled(Row)<{ onClick?: any }>`
${({ onClick }) => onClick && 'cursor: pointer'};
&:hover {
background: ${({ theme }) => theme.hoverDefault};
cursor: pointer;
}
`
@ -28,39 +26,30 @@ export default function PortfolioRow({
title,
descriptor,
right,
setIsHover,
onClick,
}: {
left: React.ReactNode
title: React.ReactNode
descriptor?: React.ReactNode
right: React.ReactNode
right?: React.ReactNode
setIsHover?: (b: boolean) => void
onClick?: () => void
}) {
const onHover = useMemo(
() =>
setIsHover && {
onMouseEnter: () => setIsHover?.(true),
onMouseLeave: () => setIsHover?.(false),
},
[setIsHover]
)
return (
<RowWrapper {...onHover} onClick={onClick}>
<PortfolioRowWrapper onClick={onClick}>
{left}
<AutoColumn grow>
{title}
{descriptor}
</AutoColumn>
<EndColumn>{right}</EndColumn>
</RowWrapper>
{right && <EndColumn>{right}</EndColumn>}
</PortfolioRowWrapper>
)
}
function PortfolioSkeletonRow({ shrinkRight }: { shrinkRight?: boolean }) {
return (
<RowWrapper>
<PortfolioRowWrapper>
<LoadingBubble height="40px" width="40px" round />
<AutoColumn grow gap="4px">
<LoadingBubble height="16px" width="60px" delay="300ms" />
@ -76,7 +65,7 @@ function PortfolioSkeletonRow({ shrinkRight }: { shrinkRight?: boolean }) {
</>
)}
</EndColumn>
</RowWrapper>
</PortfolioRowWrapper>
)
}

@ -9,9 +9,10 @@ import { useState } from 'react'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import Activity from './Activity'
import { ActivityTab } from './Activity/ActivityTab'
import NFTs from './NFTs'
import Pools from './Pools'
import { PortfolioRowWrapper } from './PortfolioRow'
import Tokens from './Tokens'
const Wrapper = styled(Column)`
@ -20,6 +21,12 @@ const Wrapper = styled(Column)`
flex-direction: column;
height: 100%;
gap: 12px;
${PortfolioRowWrapper} {
&:hover {
background: ${({ theme }) => theme.hoverDefault};
}
}
`
const Nav = styled(AutoRow)`
@ -60,7 +67,7 @@ const Pages: Array<Page> = [
{ title: <Trans>Pools</Trans>, component: Pools, loggingElementName: InterfaceElementName.MINI_PORTFOLIO_POOLS_TAB },
{
title: <Trans>Activity</Trans>,
component: Activity,
component: ActivityTab,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_TAB,
},
]

@ -25,7 +25,7 @@ export default function useSelectChain() {
console.error('Failed to switch networks', error)
dispatch(updateConnectionError({ connectionType, error: error.message }))
dispatch(addPopup({ content: { failedSwitchNetwork: targetChain }, key: `failed-network-switch` }))
dispatch(addPopup({ content: { failedSwitchNetwork: targetChain }, key: 'failed-network-switch' }))
}
},
[connector, dispatch, getConnection]