Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/components/search/SearchStates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,22 @@ export function SearchChainError({ show }: { show: boolean }) {
/>
);
}

export function SearchRedirecting({ show }: { show: boolean }) {
return (
<div className="absolute left-0 right-0 top-10">
<Fade show={show}>
<div className="my-10 flex justify-center">
<div className="flex max-w-md flex-col items-center justify-center px-3 py-5">
<div className="flex items-center justify-center">
<SpinnerIcon width={40} height={40} />
</div>
<div className="mt-4 text-center font-light leading-loose text-gray-700">
Found it! Redirecting...
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in practice this text never actually shows up and it just flashes me from the search page to the tx page

</div>
</div>
</div>
</Fade>
</div>
);
}
29 changes: 22 additions & 7 deletions src/features/messages/MessageDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { toTitleCase, trimToLength } from '@hyperlane-xyz/utils';
import { SpinnerIcon } from '@hyperlane-xyz/widgets';
import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useMemo } from 'react';
import { toast } from 'react-toastify';
import { Card } from '../../components/layout/Card';
Expand All @@ -21,7 +22,7 @@ import { WarpTransferDetailsCard } from './cards/WarpTransferDetailsCard';
import { useIsIcaMessage } from './ica';
import { usePiChainMessageQuery } from './pi-queries/usePiChainMessageQuery';
import { PLACEHOLDER_MESSAGE } from './placeholderMessages';
import { useMessageQuery } from './queries/useMessageQuery';
import { useMessageQuery, useTransactionMessageCount } from './queries/useMessageQuery';
import { parseWarpRouteMessageDetails } from './utils';

interface Props {
Expand Down Expand Up @@ -96,15 +97,29 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro
[message, warpRouteChainAddressMap, multiProvider],
);

// Check if there are multiple messages in this origin transaction
const txMessageCount = useTransactionMessageCount(origin?.hash);
const showTxLink = txMessageCount > 1;

return (
<>
<Card className="flex items-center justify-between rounded-full px-1">
<h2 className="font-medium text-blue-500">{`${
isIcaMsg ? 'ICA ' : ''
} Message ${trimToLength(msgId, 6)} to ${getChainDisplayName(
multiProvider,
destinationChainName,
)}`}</h2>
<div className="flex items-center gap-3">
<h2 className="font-medium text-blue-500">{`${
isIcaMsg ? 'ICA ' : ''
} Message ${trimToLength(msgId, 6)} to ${getChainDisplayName(
multiProvider,
destinationChainName,
)}`}</h2>
{showTxLink && (
<Link
href={`/tx/${origin.hash}`}
className="text-sm text-gray-500 transition-colors hover:text-gray-700"
>
View all {txMessageCount} messages in tx →
</Link>
)}
Comment on lines +118 to +121
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the msg count is inaccurate (probs need to increase the fetch size or something?)

Image

</div>
<StatusHeader
messageStatus={status}
isMessageFound={isMessageFound}
Expand Down
60 changes: 54 additions & 6 deletions src/features/messages/MessageSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';

import { Fade, IconButton, RefreshIcon, useDebounce } from '@hyperlane-xyz/widgets';
Expand All @@ -10,6 +11,7 @@ import {
SearchEmptyError,
SearchFetching,
SearchInvalidError,
SearchRedirecting,
SearchUnknownError,
} from '../../components/search/SearchStates';
import { useReadyMultiProvider } from '../../store';
Expand Down Expand Up @@ -130,6 +132,46 @@ export function MessageSearch() {
const isAnyMessageFound = isMessagesFound || isPiMessagesFound;
const messageListResult = isMessagesFound ? messageList : piMessageList;

// Compute redirect URL for direct message/tx lookups
const router = useRouter();
const redirectUrl = (() => {
// Wait for queries to complete
if (!hasAllRun || isAnyFetching) return null;

// Don't redirect without user input (prevents redirect on homepage with latest messages)
if (!hasInput) return null;

// Don't redirect if filters are applied
if (originChainFilter || destinationChainFilter || startTimeFilter || endTimeFilter)
return null;

// Need at least one result
if (!messageListResult.length) return null;

const firstMessage = messageListResult[0];

// Single result → always go to message page
if (messageListResult.length === 1) {
return `/message/${firstMessage.msgId}`;
}

// Multiple results + origin tx hash match → go to tx page
// Only redirect if GraphQL found results (tx page uses GraphQL only, not PI)
const inputLower = sanitizedInput.toLowerCase();
if (isMessagesFound && firstMessage.origin?.hash?.toLowerCase() === inputLower) {
return `/tx/${firstMessage.origin.hash}`;
}

return null;
})();
Comment on lines +137 to +166
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than using a IIFE, I'd prefer if we just wrap it in a memo hook, this ensure it only applies when state changes happen


// Perform the redirect
useEffect(() => {
if (redirectUrl) {
router.replace(redirectUrl);
}
}, [redirectUrl, router]);
Comment on lines +169 to +173
Copy link
Contributor

@Xaroz Xaroz Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use router.push, using replace will "replace" the current stack history and if I wanted to go back it wouldn't work, although with push we might run into an issue with the search looping itself 🤔, still if I press back I'd like to go back to the previous page


// Show message list if there are no errors and filters are valid
const showMessageTable =
!isAnyError &&
Expand Down Expand Up @@ -176,22 +218,28 @@ export function MessageSearch() {
<RefreshButton loading={isAnyFetching} onClick={refetch} />
</div>
</div>
<Fade show={showMessageTable}>
<SearchRedirecting show={!!redirectUrl} />
<Fade show={showMessageTable && !redirectUrl}>
<MessageTable messageList={messageListResult} isFetching={isAnyFetching} />
</Fade>
<SearchFetching
show={!isAnyError && isValidInput && !isAnyMessageFound && !hasAllRun}
show={!redirectUrl && !isAnyError && isValidInput && !isAnyMessageFound && !hasAllRun}
isPiFetching={isPiFetching}
/>
<SearchEmptyError
show={!isAnyError && isValidInput && !isAnyMessageFound && hasAllRun}
show={!redirectUrl && !isAnyError && isValidInput && !isAnyMessageFound && hasAllRun}
hasInput={hasInput}
allowAddress={true}
/>
<SearchUnknownError show={isAnyError && isValidInput} />
<SearchInvalidError show={!isValidInput} allowAddress={true} />
<SearchUnknownError show={!redirectUrl && isAnyError && isValidInput} />
<SearchInvalidError show={!redirectUrl && !isValidInput} allowAddress={true} />
<SearchChainError
show={(!isValidOrigin || !isValidDestination) && isValidInput && !!multiProvider}
show={
!redirectUrl &&
(!isValidOrigin || !isValidDestination) &&
isValidInput &&
!!multiProvider
}
/>
</Card>
</>
Expand Down
13 changes: 2 additions & 11 deletions src/features/messages/cards/TransactionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ function TransactionCard({
children,
}: PropsWithChildren<{ chainName: string; title: string; helpText: string }>) {
return (
<Card className="flex min-w-[400px] flex-1 basis-0 flex-col space-y-3">
<Card className="flex w-full flex-1 basis-0 flex-col space-y-3 sm:min-w-[400px]">
<div className="flex items-center justify-between">
<div className="relative -left-0.5 -top-px">
<ChainLogo chainName={chainName} />
Expand Down Expand Up @@ -266,6 +266,7 @@ function TransactionDetails({
displayWidth="w-60 sm:w-64"
showCopy={true}
blurValue={blur}
link={txExplorerLink}
/>
<KeyValueRow
label="From:"
Expand Down Expand Up @@ -311,16 +312,6 @@ function TransactionDetails({
blurValue={blur}
/>
)}
{txExplorerLink && (
<a
className={`block ${styles.textLink}`}
href={txExplorerLink}
target="_blank"
rel="noopener noreferrer"
>
View in block explorer
</a>
)}
</>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/features/messages/ica.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,7 @@ export function useRelatedIcaMessage(
true,
);
}
return buildMessageQuery(MessageIdentifierType.OriginTxHash, originTxHash, 10, true);
return buildMessageQuery(MessageIdentifierType.OriginTxHash, originTxHash, 1000, true);
}, [shouldSearch, originTxHash]);

// Execute the query
Expand Down
39 changes: 39 additions & 0 deletions src/features/messages/queries/useMessageQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,45 @@ export function useMessageQuery({ messageId, pause }: { messageId: string; pause
};
}

/**
* Hook to count messages in a given origin transaction.
* Used to determine if we should show the "View all messages in this transaction" link.
*/
export function useTransactionMessageCount(originTxHash: string | undefined) {
const { scrapedDomains: scrapedChains } = useScrapedDomains();
const multiProvider = useMultiProvider();

// Build query for origin tx hash
const { query, variables } = useMemo(() => {
if (!originTxHash) {
// Return a no-op query
return buildMessageQuery(
MessageIdentifierType.OriginTxHash,
'0x0000000000000000000000000000000000000000000000000000000000000000',
1,
true,
);
}
return buildMessageQuery(MessageIdentifierType.OriginTxHash, originTxHash, 1000, true);
}, [originTxHash]);

// Execute query
const [{ data }] = useQuery<MessagesStubQueryResult>({
query,
variables,
pause: !originTxHash,
});

// Parse results
const messageCount = useMemo(() => {
if (!data || !originTxHash) return 0;
const messages = parseMessageStubResult(multiProvider, scrapedChains, data);
return messages.length;
}, [data, multiProvider, scrapedChains, originTxHash]);

return messageCount;
}

function isWindowVisible() {
return document.visibilityState === 'visible';
}
Loading