{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "utils",
  "type": "registry:lib",
  "title": "Lens Utilities",
  "description": "Utility functions for interacting with Lens Social Protocol, including formatting and data manipulation.",
  "dependencies": ["@lens-protocol/react@0.0.0-canary-20250820102520", "@lens-chain/sdk@1.0.3"],
  "files": [
    {
      "path": "registry/new-york/lib/lens-utils.ts",
      "content": "import {\n  Account,\n  AnyPost,\n  EvmAddress,\n  MediaAudioType,\n  MediaVideoType,\n  Post,\n  ReferencedPost,\n  SigningError,\n  TransactionIndexingError,\n  UnauthenticatedError,\n  UnexpectedError,\n  ValidationError,\n} from \"@lens-protocol/react\";\nimport { chains } from \"@lens-chain/sdk/viem\";\nimport { PostMetadata } from \"@lens-protocol/metadata\";\n\n/**\n * The chain ID for the Lens Chain mainnet\n */\nexport const LensChainId = chains.mainnet.id;\n\n/**\n * The chain ID for the Lens Chain testnet\n */\nexport const LensChainTestnetId = chains.testnet.id;\n\n/**\n * A zero (or empty) address in EVM-compatible blockchains\n */\nexport const ZeroAddress = \"0x0000000000000000000000000000000000000000\";\n\n/**\n * The native token address in Lens Chain\n */\nexport const LensChainNativeToken = \"0x000000000000000000000000000000000000800A\";\n\n/**\n * Truncate a string to a maximum length, adding an ellipsis in the middle\n *\n * @param address The string to truncate\n * @param maxLength The maximum length of the truncated string, excluding the ellipsis and 0x prefix\n */\nexport const truncateAddress = (address: string, maxLength: number = 8): string => {\n  if (address.length <= maxLength) {\n    return address;\n  }\n  const ellipsis = \"…\";\n  const startLength = Math.ceil((maxLength + ellipsis.length) / 2) + 1;\n  const endLength = Math.floor((maxLength + ellipsis.length) / 2);\n  return address.slice(0, startLength) + ellipsis + address.slice(address.length - endLength);\n};\n\n/**\n * Truncates a given URL to a specified maximum length and removes common prefixes/suffixes.\n *\n * - Strips out `http://`, `https://`, and leading `www.` from the URL.\n * - Removes trailing `/` or `\\` characters.\n * - Truncates the cleaned URL to the given `maxLength`.\n * - Appends an ellipsis (`…`) if the original URL exceeds `maxLength`.\n *\n * @param url - The full URL string to truncate.\n * @param maxLength - The maximum allowed length of the resulting string (default: `30`).\n * @returns A cleaned and possibly truncated version of the URL.\n *\n * @example\n * ```ts\n * truncateUrl(\"https://www.example.com/some/long/path\", 20);\n * // \"example.com/some/lo…\"\n *\n * truncateUrl(\"http://my-site.io\");\n * // \"my-site.io\"\n * ```\n */\nexport const truncateUrl = (url: string, maxLength: number = 30): string => {\n  return (\n    url\n      .replace(/^https?:\\/\\//, \"\")\n      .replace(/\\/$/, \"\")\n      .replace(/\\\\$/, \"\")\n      .replace(/^www\\./, \"\")\n      .slice(0, maxLength) + (url.length > maxLength ? \"…\" : \"\")\n  );\n};\n\n/**\n * Extract the CID from an IPFS URL\n *\n * @example getCidFromIpfsUrl(\"ipfs://Qm...\") => \"Qm...\"\n * @example getCidFromIpfsUrl(\"ipfs://Qm.../path/to/file\") => \"Qm...\"\n *\n * @param ipfsUrl The IPFS URL to extract the CID from\n */\nexport const getCidFromIpfsUrl = (ipfsUrl: string): string => {\n  if (!ipfsUrl.startsWith(\"ipfs://\")) throw new Error(\"IPFS urls must begin with ipfs://\");\n  return ipfsUrl.replace(\"ipfs://\", \"\").replace(/^\\/+|\\/+$/g, \"\");\n};\n\n/**\n *  Convert an IPFS URL to a gateway URL\n *\n * @example ipfsUrlToGatewayUrl(\"ipfs://Qm...\") => \"https://ipfs.io/ipfs/Qm...\"\n * @example ipfsUrlToGatewayUrl(\"ipfs://Qm.../path/to/file\") => \"https://ipfs.io/ipfs/Qm.../path/to/file\"\n *\n * @param ipfsUrl The IPFS URL to convert\n * @param gatewayDomain The gateway domain to use (default: https://ipfs.io/ipfs/)\n */\nexport const ipfsUrlToGatewayUrl = (ipfsUrl: string, gatewayDomain: string = \"https://ipfs.io/ipfs/\"): string => {\n  if (ipfsUrl.length === 0 || !ipfsUrl.startsWith(\"ipfs://\")) return ipfsUrl;\n  const cid = getCidFromIpfsUrl(ipfsUrl);\n  const gatewayUrl = gatewayDomain + cid;\n  const path = ipfsUrl.split(cid)[1];\n  return path ? `${gatewayUrl}${path}` : gatewayUrl;\n};\n\n/**\n * Convert an Arweave URL to a gateway URL\n *\n * @example arweaveUrlToGatewayUrl(\"ar://TxId\") => \"https://arweave.net/TxId\"\n *\n * @param arUrl The Arweave URL to convert\n * @param gatewayDomain The gateway domain to use (default: https://arweave.net/)\n */\nexport const arweaveUrlToGatewayUrl = (arUrl: string, gatewayDomain: string = \"https://arweave.net/\"): string => {\n  if (arUrl.length === 0 || !arUrl.startsWith(\"ar://\")) return arUrl;\n  const txId = arUrl.replace(\"ar://\", \"\");\n  return `${gatewayDomain}${txId}`;\n};\n\n/**\n * Convert a Lens URL to a gateway URL\n *\n * @example lensUrlToGatewayUrl(\"lens://TxId\") => \"https://api.grove.storage/TxId\"\n *\n * @param lensUrl The Lens URL to convert\n * @param gatewayDomain The gateway domain to use (default: https://api.grove.storage/)\n */\nexport const lensUrlToGatewayUrl = (lensUrl: string, gatewayDomain: string = \"https://api.grove.storage/\"): string => {\n  if (lensUrl.length === 0 || !lensUrl.startsWith(\"lens://\")) return lensUrl;\n  const txId = lensUrl.replace(\"lens://\", \"\");\n  return `${gatewayDomain}${txId}`;\n};\n\n/**\n * Parse a URI and convert it to a gateway URL if it is an IPFS, Arweave, or Lens URL\n *\n * @param uri The URI to parse\n * @returns The parsed URI as a gateway URL, or null if the URI is invalid\n */\nexport const parseUri = (uri: string | undefined): string | undefined => {\n  if (!uri || uri.startsWith(\"data:\")) return uri; // Return data URIs as-is\n\n  if (uri.startsWith(\"https://gw.ipfs-lens.dev/ipfs/\")) {\n    const ipfs = uri.replace(\"https://gw.ipfs-lens.dev/ipfs/\", \"ipfs://\");\n    return ipfsUrlToGatewayUrl(ipfs);\n  }\n\n  try {\n    const { protocol } = new URL(uri);\n    switch (protocol) {\n      case \"ipfs:\":\n        return ipfsUrlToGatewayUrl(uri);\n      case \"ar:\":\n        return arweaveUrlToGatewayUrl(uri);\n      case \"lens:\":\n        return lensUrlToGatewayUrl(uri);\n      default:\n        return uri;\n    }\n  } catch {\n    return undefined;\n  }\n};\n\n/**\n * Get the file extension for a given MediaAudioType\n *\n * @param mediaAudioType The MediaAudioType to get the extension for\n */\nexport const getAudioExtension = (mediaAudioType: MediaAudioType): string => {\n  switch (mediaAudioType) {\n    case MediaAudioType.AudioWav:\n      return \"wav\";\n    case MediaAudioType.AudioVndWave:\n      return \"wave\";\n    case MediaAudioType.AudioMpeg:\n      return \"mp3\";\n    case MediaAudioType.AudioOgg:\n      return \"ogg\";\n    case MediaAudioType.AudioMp4:\n      return \"mp4\";\n    case MediaAudioType.AudioAac:\n      return \"aac\";\n    case MediaAudioType.AudioWebm:\n      return \"webm\";\n    case MediaAudioType.AudioFlac:\n      return \"flac\";\n  }\n};\n\n/**\n * Get the file extension for a given MediaVideoType\n *\n * @param mediaVideoType The MediaVideoType to get the extension for\n */\nexport const getVideoExtension = (mediaVideoType: MediaVideoType): string => {\n  switch (mediaVideoType) {\n    case MediaVideoType.VideoMp4:\n      return \"mp4\";\n    case MediaVideoType.VideoMpeg:\n      return \"mpeg\";\n    case MediaVideoType.VideoOgg:\n      return \"ogg\";\n    case MediaVideoType.VideoQuicktime:\n      return \"mov\";\n    case MediaVideoType.VideoWebm:\n      return \"webm\";\n    case MediaVideoType.VideoMov:\n      return \"mov\";\n    case MediaVideoType.VideoOgv:\n      return \"ogv\";\n    case MediaVideoType.VideoXm4v:\n      return \"xm4v\";\n    case MediaVideoType.ModelGltfJson:\n      return \"gltf\";\n    case MediaVideoType.ModelGltfBinary:\n      return \"glb\";\n  }\n};\n\n/**\n * Get the path for a Lens username, optionally including the namespace if it's not the default \"lens\" namespace.\n *\n * @example getUsernamePath(\"@lens/username\") => \"/u/username\"\n * @example getUsernamePath(\"@othernamespace/username\", \"0x1234...\") => \"/u/0x1234.../username\"\n *\n * @param username - The full username, including the namespace (e.g., \"@lens/username\")\n * @param namespace - The namespace address of the username. If not provided, the default namespace will be assumed.\n */\nexport const getUsernamePath = (username: string, namespace?: EvmAddress): string => {\n  let path = \"/u/\";\n  if (namespace && !username.startsWith(\"@lens\")) path += `${namespace}/`;\n  path += `${username.split(\"/\")[1]}`;\n  return path;\n};\n\n/**\n * Format a follower count as a string, using \"k\" for thousands and \"m\" for millions\n *\n * @example formatFollowerCount(123) => \"123\"\n * @example formatFollowerCount(12345) => \"12.3k\"\n * @example formatFollowerCount(1234567) => \"1.2m\"\n *\n * @param count The follower count to format\n */\nexport const formatFollowerCount = (count: number): string => {\n  if (count >= 1_000_000) {\n    return (count / 1_000_000).toFixed(1).replace(/\\.0$/, \"\") + \"m\";\n  } else if (count >= 10_000) {\n    return (count / 1_000).toFixed(1).replace(/\\.0$/, \"\") + \"k\";\n  }\n  return count.toString();\n};\n\n/**\n * Get the display name for an account, prioritizing the metadata name, then the username,\n * and finally truncating the address.\n *\n * @param account The account to get the display name for\n */\nexport const getDisplayName = (account: Account): string => {\n  if (account.metadata?.name) {\n    return account.metadata.name;\n  }\n  if (account.username) {\n    return `@${account.username.localName}`;\n  }\n  return truncateAddress(account.address);\n};\n\n/**\n *  Converts an unknown error to a Lens API `UnexpectedError`, or returns the original error if it's a known error.'\n */\nexport const toApiError = (\n  e: unknown,\n): SigningError | TransactionIndexingError | UnauthenticatedError | UnexpectedError | ValidationError => {\n  if (\n    e instanceof SigningError ||\n    e instanceof TransactionIndexingError ||\n    e instanceof UnauthenticatedError ||\n    e instanceof UnexpectedError ||\n    e instanceof ValidationError\n  )\n    return e;\n  return { name: \"UnexpectedError\", cause: e } as UnexpectedError;\n};\n\ntype AudioPost =\n  | (Post & { metadata: { __typename: \"AudioMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"AudioMetadata\" } });\n\ntype ArticlePost =\n  | (Post & { metadata: { __typename: \"ArticleMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"ArticleMetadata\" } });\n\ntype ImagePost =\n  | (Post & { metadata: { __typename: \"ImageMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"ImageMetadata\" } });\n\ntype LinkPost =\n  | (Post & { metadata: { __typename: \"LinkMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"LinkMetadata\" } });\n\ntype LiveStreamPost =\n  | (Post & { metadata: { __typename: \"LiveStreamMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"LiveStreamMetadata\" } });\n\ntype MintPost =\n  | (Post & { metadata: { __typename: \"MintMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"MintMetadata\" } });\n\ntype SpacePost =\n  | (Post & { metadata: { __typename: \"SpaceMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"SpaceMetadata\" } });\n\ntype StoryPost =\n  | (Post & { metadata: { __typename: \"StoryMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"StoryMetadata\" } });\n\ntype TextOnlyPost =\n  | (Post & { metadata: { __typename: \"TextOnlyMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"TextOnlyMetadata\" } });\n\ntype ThreeDPost =\n  | (Post & { metadata: { __typename: \"ThreeDMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"ThreeDMetadata\" } });\n\ntype TransactionPost =\n  | (Post & { metadata: { __typename: \"TransactionMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"TransactionMetadata\" } });\n\ntype VideoPost =\n  | (Post & { metadata: { __typename: \"VideoMetadata\" } })\n  | (ReferencedPost & { metadata: { __typename: \"VideoMetadata\" } });\n\n/**\n * Type guard that checks whether a post has a `metadata` field.\n *\n * Narrows `AnyPost` to a `Post`/`ReferencedPost` with a non-`undefined` `metadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if `post.metadata` exists and is not `undefined`; otherwise `false`.\n *\n * @example\n * if (hasMetadata(post)) {\n *   // post.metadata is now typed as PostMetadata\n *   console.log(post.metadata.__typename);\n * }\n */\nexport const hasMetadata = (\n  post: AnyPost,\n): post is (Post & { metadata: PostMetadata }) | (ReferencedPost & { metadata: PostMetadata }) =>\n  \"metadata\" in post && post.metadata !== undefined;\n\n/**\n * Internal helper that checks if a post has metadata of a specific GraphQL `__typename`.\n *\n * @param post - The post to inspect.\n * @param metadataType - The expected `__typename` of the post's metadata (e.g., `ImageMetadata`).\n * @returns `true` if the post has metadata and its `__typename` matches `metadataType`.\n *\n * @example\n * isPostWithMetadata(post, \"VideoMetadata\"); // -> boolean\n */\nconst isPostWithMetadata = (post: AnyPost, metadataType: string): boolean =>\n  hasMetadata(post) && post.metadata.__typename === metadataType;\n\n/**\n * Type guard for posts whose metadata is `ArticleMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `ArticleMetadata`.\n *\n * @example\n * if (isArticlePost(post)) {\n *   // post.metadata.__typename === \"ArticleMetadata\"\n * }\n */\nexport const isArticlePost = (post: AnyPost): post is ArticlePost => isPostWithMetadata(post, \"ArticleMetadata\");\n\n/**\n * Type guard for posts whose metadata is `AudioMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `AudioMetadata`.\n *\n * @example\n * if (isAudioPost(post)) {\n *   // Safe to treat as an audio post\n * }\n */\nexport const isAudioPost = (post: AnyPost): post is AudioPost => isPostWithMetadata(post, \"AudioMetadata\");\n\n/**\n * Type guard for posts whose metadata is `ImageMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `ImageMetadata`.\n */\nexport const isImagePost = (post: AnyPost): post is ImagePost => isPostWithMetadata(post, \"ImageMetadata\");\n\n/**\n * Type guard for posts whose metadata is `LinkMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `LinkMetadata`.\n */\nexport const isLinkPost = (post: AnyPost): post is LinkPost => isPostWithMetadata(post, \"LinkMetadata\");\n\n/**\n * Type guard for posts whose metadata is `LiveStreamMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `LiveStreamMetadata`.\n */\nexport const isLiveStreamPost = (post: AnyPost): post is LiveStreamPost =>\n  isPostWithMetadata(post, \"LiveStreamMetadata\");\n\n/**\n * Type guard for posts whose metadata is `MintMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `MintMetadata`.\n */\nexport const isMintPost = (post: AnyPost): post is MintPost => isPostWithMetadata(post, \"MintMetadata\");\n\n/**\n * Type guard for posts whose metadata is `SpaceMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `SpaceMetadata`.\n */\nexport const isSpacePost = (post: AnyPost): post is SpacePost => isPostWithMetadata(post, \"SpaceMetadata\");\n\n/**\n * Type guard for posts whose metadata is `StoryMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `StoryMetadata`.\n */\nexport const isStoryPost = (post: AnyPost): post is StoryPost => isPostWithMetadata(post, \"StoryMetadata\");\n\n/**\n * Type guard for posts whose metadata is `TextOnlyMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `TextOnlyMetadata`.\n */\nexport const isTextOnlyPost = (post: AnyPost): post is TextOnlyPost => isPostWithMetadata(post, \"TextOnlyMetadata\");\n\n/**\n * Type guard for posts whose metadata is `ThreeDMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `ThreeDMetadata`.\n */\nexport const isThreeDPost = (post: AnyPost): post is ThreeDPost => isPostWithMetadata(post, \"ThreeDMetadata\");\n\n/**\n * Type guard for posts whose metadata is `TransactionMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `TransactionMetadata`.\n */\nexport const isTransactionPost = (post: AnyPost): post is TransactionPost =>\n  isPostWithMetadata(post, \"TransactionMetadata\");\n\n/**\n * Type guard for posts whose metadata is `VideoMetadata`.\n *\n * @param post - The post to inspect.\n * @returns `true` if the post's metadata `__typename` is `VideoMetadata`.\n */\nexport const isVideoPost = (post: AnyPost): post is VideoPost => isPostWithMetadata(post, \"VideoMetadata\");\n\n/**\n * Safely extracts a textual `content` field from a post's metadata, if present.\n *\n * This is useful for metadata types that include a `content` property (e.g., text or article types).\n *\n * @param post - The post whose metadata content should be read.\n * @returns The `content` string if present on `post.metadata`; otherwise `undefined`.\n *\n * @example\n * const content = getMetadataContent(post);\n * if (content) {\n *   render(content);\n * }\n */\nexport const getMetadataContent = (post: AnyPost): string | undefined => {\n  if (hasMetadata(post) && \"content\" in post.metadata) {\n    return post.metadata.content;\n  }\n  return undefined;\n};\n",
      "type": "registry:lib"
    }
  ]
}
