From 359fea8d7e0f337c9447f37e56fcb16c84c58398 Mon Sep 17 00:00:00 2001 From: Yoginth Date: Fri, 6 Dec 2024 09:36:15 +0530 Subject: [PATCH] Migrate to Lens v3 --- .../src/components/Notification/Account.tsx | 10 +- apps/web/src/components/Post/PostAccount.tsx | 2 +- .../Settings/Handles/UnlinkHandle.tsx | 2 +- .../src/components/Settings/Handles/index.tsx | 2 +- .../src/components/Shared/Account/Follow.tsx | 14 +- .../components/Shared/Account/Unfollow.tsx | 14 +- .../src/components/Shared/AccountPreview.tsx | 4 +- .../Shared/Alert/BlockOrUnBlockAccount.tsx | 158 ++++++++++++------ .../src/components/Shared/SingleAccount.tsx | 2 +- .../Staff/Users/Overview/Tool/index.tsx | 2 +- .../store/persisted/useTransactionStore.ts | 3 + packages/helpers/getAccount.ts | 4 +- .../auth/AuthenticatedSessions.graphql | 1 + packages/indexer/generated.ts | 4 +- packages/types/enums.ts | 4 +- packages/types/misc.d.ts | 1 + 16 files changed, 138 insertions(+), 89 deletions(-) diff --git a/apps/web/src/components/Notification/Account.tsx b/apps/web/src/components/Notification/Account.tsx index f5e3648bb860..2c3cd52660eb 100644 --- a/apps/web/src/components/Notification/Account.tsx +++ b/apps/web/src/components/Notification/Account.tsx @@ -23,7 +23,10 @@ export const NotificationAccountAvatar: FC = ({ }; return ( - + = ({ const profileLink = getAccount(account).link; return ( - + = ({ href={getAccount(account).link} > diff --git a/apps/web/src/components/Settings/Handles/UnlinkHandle.tsx b/apps/web/src/components/Settings/Handles/UnlinkHandle.tsx index d429c0714dfd..c6ecbcf908da 100644 --- a/apps/web/src/components/Settings/Handles/UnlinkHandle.tsx +++ b/apps/web/src/components/Settings/Handles/UnlinkHandle.tsx @@ -30,7 +30,7 @@ const UnlinkHandle: FC = () => { try { setUnlinking(true); const request: UnlinkHandleFromProfileRequest = { - handle: currentAccount.username?.value + handle: currentAccount.username?.localName }; return await createUnlinkHandleFromProfileTypedData({ diff --git a/apps/web/src/components/Settings/Handles/index.tsx b/apps/web/src/components/Settings/Handles/index.tsx index dd9682f1851d..c78dadff3e2c 100644 --- a/apps/web/src/components/Settings/Handles/index.tsx +++ b/apps/web/src/components/Settings/Handles/index.tsx @@ -43,7 +43,7 @@ const HandlesSettings: NextPage = () => { is no longer publicly displayed or associated with your profile." title={ - Unlink from + Unlink from your profile } diff --git a/apps/web/src/components/Shared/Account/Follow.tsx b/apps/web/src/components/Shared/Account/Follow.tsx index 5835428e3c70..b93a4bb94748 100644 --- a/apps/web/src/components/Shared/Account/Follow.tsx +++ b/apps/web/src/components/Shared/Account/Follow.tsx @@ -5,11 +5,7 @@ import { Errors } from "@hey/data/errors"; import { ACCOUNT } from "@hey/data/tracking"; import selfFundedTransactionData from "@hey/helpers/selfFundedTransactionData"; import sponsoredTransactionData from "@hey/helpers/sponsoredTransactionData"; -import { - type Account, - type FollowResponse, - useFollowMutation -} from "@hey/indexer"; +import { type Account, useFollowMutation } from "@hey/indexer"; import { OptmisticPostType } from "@hey/types/enums"; import type { OptimisticTransaction } from "@hey/types/misc"; import { Button } from "@hey/ui"; @@ -66,11 +62,7 @@ const Follow: FC = ({ }); }; - const onCompleted = (hash: string, follow?: FollowResponse) => { - if (follow?.__typename !== "FollowResponse") { - return; - } - + const onCompleted = (hash: string) => { updateCache(); addTransaction(generateOptimisticFollow({ txHash: hash })); setIsLoading(false); @@ -89,7 +81,7 @@ const Follow: FC = ({ const [follow] = useFollowMutation({ onCompleted: async ({ follow }) => { if (follow.__typename === "FollowResponse") { - return onCompleted(follow.hash, follow); + return onCompleted(follow.hash); } if (walletClient) { diff --git a/apps/web/src/components/Shared/Account/Unfollow.tsx b/apps/web/src/components/Shared/Account/Unfollow.tsx index 2ce00ceef391..50063691fe66 100644 --- a/apps/web/src/components/Shared/Account/Unfollow.tsx +++ b/apps/web/src/components/Shared/Account/Unfollow.tsx @@ -5,11 +5,7 @@ import { Errors } from "@hey/data/errors"; import { ACCOUNT } from "@hey/data/tracking"; import selfFundedTransactionData from "@hey/helpers/selfFundedTransactionData"; import sponsoredTransactionData from "@hey/helpers/sponsoredTransactionData"; -import { - type Account, - type UnfollowResponse, - useUnfollowMutation -} from "@hey/indexer"; +import { type Account, useUnfollowMutation } from "@hey/indexer"; import { OptmisticPostType } from "@hey/types/enums"; import type { OptimisticTransaction } from "@hey/types/misc"; import { Button } from "@hey/ui"; @@ -66,11 +62,7 @@ const Unfollow: FC = ({ }); }; - const onCompleted = (hash: string, unfollow?: UnfollowResponse) => { - if (unfollow?.__typename !== "UnfollowResponse") { - return; - } - + const onCompleted = (hash: string) => { updateCache(); addTransaction(generateOptimisticUnfollow({ txHash: hash })); setIsLoading(false); @@ -89,7 +81,7 @@ const Unfollow: FC = ({ const [unfollow] = useUnfollowMutation({ onCompleted: async ({ unfollow }) => { if (unfollow.__typename === "UnfollowResponse") { - return onCompleted(unfollow.hash, unfollow); + return onCompleted(unfollow.hash); } if (walletClient) { diff --git a/apps/web/src/components/Shared/AccountPreview.tsx b/apps/web/src/components/Shared/AccountPreview.tsx index 54eefadd9263..a2a36cb465d8 100644 --- a/apps/web/src/components/Shared/AccountPreview.tsx +++ b/apps/web/src/components/Shared/AccountPreview.tsx @@ -41,9 +41,7 @@ const AccountPreview: FC = ({ showUserPreview = true }) => { const [loadAccount, { data, loading: networkLoading }] = - useFullAccountLazyQuery({ - fetchPolicy: "cache-and-network" - }); + useFullAccountLazyQuery({ fetchPolicy: "cache-and-network" }); const [syntheticLoading, setSyntheticLoading] = useState(networkLoading); const account = data?.account as Account; diff --git a/apps/web/src/components/Shared/Alert/BlockOrUnBlockAccount.tsx b/apps/web/src/components/Shared/Alert/BlockOrUnBlockAccount.tsx index ff4051c4508f..ceae6ad9dc12 100644 --- a/apps/web/src/components/Shared/Alert/BlockOrUnBlockAccount.tsx +++ b/apps/web/src/components/Shared/Alert/BlockOrUnBlockAccount.tsx @@ -1,10 +1,14 @@ -import type { ApolloCache } from "@apollo/client"; +import { useApolloClient } from "@apollo/client"; import errorToast from "@helpers/errorToast"; import { Leafwatch } from "@helpers/leafwatch"; import { Errors } from "@hey/data/errors"; import { ACCOUNT } from "@hey/data/tracking"; import getAccount from "@hey/helpers/getAccount"; +import selfFundedTransactionData from "@hey/helpers/selfFundedTransactionData"; +import sponsoredTransactionData from "@hey/helpers/sponsoredTransactionData"; import { useBlockMutation, useUnblockMutation } from "@hey/indexer"; +import { OptmisticPostType } from "@hey/types/enums"; +import type { OptimisticTransaction } from "@hey/types/misc"; import { Alert } from "@hey/ui"; import type { FC } from "react"; import { useState } from "react"; @@ -12,6 +16,9 @@ import { toast } from "react-hot-toast"; import { useAccountStatus } from "src/store/non-persisted/useAccountStatus"; import { useGlobalAlertStateStore } from "src/store/non-persisted/useGlobalAlertStateStore"; import { useAccountStore } from "src/store/persisted/useAccountStore"; +import { useTransactionStore } from "src/store/persisted/useTransactionStore"; +import { sendEip712Transaction, sendTransaction } from "viem/zksync"; +import { useWalletClient } from "wagmi"; const BlockOrUnBlockAccount: FC = () => { const { currentAccount } = useAccountStore(); @@ -20,40 +27,45 @@ const BlockOrUnBlockAccount: FC = () => { setShowBlockOrUnblockAlert, showBlockOrUnblockAlert } = useGlobalAlertStateStore(); + const { addTransaction, isBlockOrUnblockPending } = useTransactionStore(); + const [isLoading, setIsLoading] = useState(false); const [hasBlocked, setHasBlocked] = useState( - blockingorUnblockingProfile?.operations.isBlockedByMe.value + blockingorUnblockingProfile?.operations?.isBlockedByMe ); const { isSuspended } = useAccountStatus(); + const { cache } = useApolloClient(); + const { data: walletClient } = useWalletClient(); + + const generateOptimisticBlockOrUnblock = ({ + txHash + }: { + txHash: string; + }): OptimisticTransaction => { + return { + blockOrUnblockOn: blockingorUnblockingProfile?.address, + txHash, + type: hasBlocked ? OptmisticPostType.Unblock : OptmisticPostType.Block + }; + }; - const updateCache = (cache: ApolloCache) => { + const updateCache = () => { cache.modify({ - fields: { - isBlockedByMe: (existingValue) => { - return { ...existingValue, value: !hasBlocked }; - } - }, - id: `ProfileOperations:${blockingorUnblockingProfile?.id}` + fields: { isBlockedByMe: () => !hasBlocked }, + id: `ProfileOperations:${blockingorUnblockingProfile?.address}` }); - cache.evict({ id: `Profile:${blockingorUnblockingProfile?.id}` }); + cache.evict({ id: `Profile:${blockingorUnblockingProfile?.address}` }); }; - const onCompleted = ( - __typename?: "LensProfileManagerRelayError" | "RelayError" | "RelaySuccess" - ) => { - if ( - __typename === "RelayError" || - __typename === "LensProfileManagerRelayError" - ) { - return; - } - + const onCompleted = (hash: string) => { + updateCache(); + addTransaction(generateOptimisticBlockOrUnblock({ txHash: hash })); setIsLoading(false); setHasBlocked(!hasBlocked); setShowBlockOrUnblockAlert(false, null); toast.success(hasBlocked ? "Unblocked" : "Blocked"); Leafwatch.track(hasBlocked ? ACCOUNT.BLOCK : ACCOUNT.UNBLOCK, { - address: blockingorUnblockingProfile?.id + address: blockingorUnblockingProfile?.address }); }; @@ -62,33 +74,71 @@ const BlockOrUnBlockAccount: FC = () => { errorToast(error); }; - const [blockProfile] = useBlockMutation({ - onCompleted: ({ block }) => onCompleted(block.__typename), - onError, - update: updateCache - }); + const [block] = useBlockMutation({ + onCompleted: async ({ block }) => { + if (block.__typename === "BlockResponse") { + return onCompleted(block.hash); + } - const [unBlockProfile] = useUnblockMutation({ - onCompleted: ({ unblock }) => onCompleted(unblock.__typename), - onError, - update: updateCache + if (walletClient) { + if (block.__typename === "SponsoredTransactionRequest") { + const hash = await sendEip712Transaction(walletClient, { + account: walletClient.account, + ...sponsoredTransactionData(block.raw) + }); + + return onCompleted(hash); + } + + if (block.__typename === "SelfFundedTransactionRequest") { + const hash = await sendTransaction(walletClient, { + account: walletClient.account, + ...selfFundedTransactionData(block.raw) + }); + + return onCompleted(hash); + } + } + + if (block.__typename === "BlockError") { + return toast.error(block.error); + } + }, + onError }); - const blockViaLensManager = async (request: BlockRequest) => { - const { data } = await blockProfile({ variables: { request } }); + const [unblock] = useUnblockMutation({ + onCompleted: async ({ unblock }) => { + if (unblock.__typename === "UnblockResponse") { + return onCompleted(unblock.hash); + } - if (data?.block.__typename === "LensProfileManagerRelayError") { - return await createBlockProfilesTypedData({ variables: { request } }); - } - }; + if (walletClient) { + if (unblock.__typename === "SponsoredTransactionRequest") { + const hash = await sendEip712Transaction(walletClient, { + account: walletClient.account, + ...sponsoredTransactionData(unblock.raw) + }); - const unBlockViaLensManager = async (request: UnblockRequest) => { - const { data } = await unBlockProfile({ variables: { request } }); + return onCompleted(hash); + } - if (data?.unblock.__typename === "LensProfileManagerRelayError") { - return await createUnblockProfilesTypedData({ variables: { request } }); - } - }; + if (unblock.__typename === "SelfFundedTransactionRequest") { + const hash = await sendTransaction(walletClient, { + account: walletClient.account, + ...selfFundedTransactionData(unblock.raw) + }); + + return onCompleted(hash); + } + } + + if (unblock.__typename === "UnblockError") { + return toast.error(unblock.error); + } + }, + onError + }); const blockOrUnblock = async () => { if (!currentAccount) { @@ -101,20 +151,21 @@ const BlockOrUnBlockAccount: FC = () => { try { setIsLoading(true); - const request: BlockRequest | UnblockRequest = { - profiles: [blockingorUnblockingProfile?.id] - }; - // Block + // Unblock if (hasBlocked) { - return await createUnblockProfilesTypedData({ - variables: { request } + return await unblock({ + variables: { + request: { account: blockingorUnblockingProfile?.address } + } }); } - // Unblock - return await createBlockProfilesTypedData({ - variables: { request } + // Block + return await block({ + variables: { + request: { account: blockingorUnblockingProfile?.address } + } }); } catch (error) { onError(error); @@ -128,7 +179,10 @@ const BlockOrUnBlockAccount: FC = () => { hasBlocked ? "un-block" : "block" } ${getAccount(blockingorUnblockingProfile).slugWithPrefix}?`} isDestructive - isPerformingAction={isLoading} + isPerformingAction={ + isLoading || + isBlockOrUnblockPending(blockingorUnblockingProfile?.address) + } onClose={() => setShowBlockOrUnblockAlert(false, null)} onConfirm={blockOrUnblock} show={showBlockOrUnblockAlert} diff --git a/apps/web/src/components/Shared/SingleAccount.tsx b/apps/web/src/components/Shared/SingleAccount.tsx index 729b1d01fdd4..5a9cc3f6c13b 100644 --- a/apps/web/src/components/Shared/SingleAccount.tsx +++ b/apps/web/src/components/Shared/SingleAccount.tsx @@ -83,7 +83,7 @@ const SingleAccount: FC = ({ const AccountInfo: FC = () => ( diff --git a/apps/web/src/components/Staff/Users/Overview/Tool/index.tsx b/apps/web/src/components/Staff/Users/Overview/Tool/index.tsx index e14d8465e2eb..eaf55cba8e47 100644 --- a/apps/web/src/components/Staff/Users/Overview/Tool/index.tsx +++ b/apps/web/src/components/Staff/Users/Overview/Tool/index.tsx @@ -42,7 +42,7 @@ const AccountStaffTool: FC = ({ account }) => {
diff --git a/apps/web/src/store/persisted/useTransactionStore.ts b/apps/web/src/store/persisted/useTransactionStore.ts index 1b9aaf2e1018..ead8c4446226 100644 --- a/apps/web/src/store/persisted/useTransactionStore.ts +++ b/apps/web/src/store/persisted/useTransactionStore.ts @@ -9,6 +9,7 @@ interface State { indexedPostHash: null | string; isFollowPending: (profileAddress: string) => boolean; isUnfollowPending: (profileAddress: string) => boolean; + isBlockOrUnblockPending: (profileAddress: string) => boolean; removeTransaction: (hash: string) => void; reset: () => void; setIndexedPostHash: (hash: string) => void; @@ -28,6 +29,8 @@ const store = create( get().txnQueue.some((txn) => txn.followOn === profileAddress), isUnfollowPending: (profileAddress) => get().txnQueue.some((txn) => txn.unfollowOn === profileAddress), + isBlockOrUnblockPending: (profileAddress) => + get().txnQueue.some((txn) => txn.blockOrUnblockOn === profileAddress), removeTransaction: (hash) => set((state) => ({ txnQueue: state.txnQueue.filter((txn) => txn.txHash !== hash) diff --git a/packages/helpers/getAccount.ts b/packages/helpers/getAccount.ts index ec7bdd1811d6..810a39155008 100644 --- a/packages/helpers/getAccount.ts +++ b/packages/helpers/getAccount.ts @@ -24,9 +24,9 @@ const getAccount = ( } const prefix = account.username ? "@" : "#"; - const slug = account.username?.value || account.address; + const slug = account.username?.localName || account.address; const link = account.username - ? `/u/${account.username.value}` + ? `/u/${account.username.localName}` : `/account/${account.address}`; return { diff --git a/packages/indexer/documents/queries/auth/AuthenticatedSessions.graphql b/packages/indexer/documents/queries/auth/AuthenticatedSessions.graphql index 899ab707da3f..e3befbb68585 100644 --- a/packages/indexer/documents/queries/auth/AuthenticatedSessions.graphql +++ b/packages/indexer/documents/queries/auth/AuthenticatedSessions.graphql @@ -8,6 +8,7 @@ query AuthenticatedSessions($request: AuthenticatedSessionsRequest!) { os origin signer + expiresAt createdAt updatedAt } diff --git a/packages/indexer/generated.ts b/packages/indexer/generated.ts index 27721421c469..61a4c90c527c 100644 --- a/packages/indexer/generated.ts +++ b/packages/indexer/generated.ts @@ -6664,7 +6664,7 @@ export type AuthenticatedSessionsQueryVariables = Exact<{ }>; -export type AuthenticatedSessionsQuery = { __typename?: 'Query', authenticatedSessions: { __typename?: 'PaginatedActiveAuthenticationsResult', items: Array<{ __typename?: 'AuthenticatedSession', authenticationId: any, app: any, browser?: string | null, device?: string | null, os?: string | null, origin?: any | null, signer: any, createdAt: any, updatedAt: any }>, pageInfo: ( +export type AuthenticatedSessionsQuery = { __typename?: 'Query', authenticatedSessions: { __typename?: 'PaginatedActiveAuthenticationsResult', items: Array<{ __typename?: 'AuthenticatedSession', authenticationId: any, app: any, browser?: string | null, device?: string | null, os?: string | null, origin?: any | null, signer: any, expiresAt: any, createdAt: any, updatedAt: any }>, pageInfo: ( { __typename?: 'PaginatedResultInfo' } & PaginatedResultInfoFieldsFragment ) } }; @@ -8334,7 +8334,7 @@ export type StaffPicksQueryHookResult = ReturnType; export type StaffPicksLazyQueryHookResult = ReturnType; export type StaffPicksSuspenseQueryHookResult = ReturnType; export type StaffPicksQueryResult = Apollo.QueryResult; -export const AuthenticatedSessionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AuthenticatedSessions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"request"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AuthenticatedSessionsRequest"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authenticatedSessions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"request"},"value":{"kind":"Variable","name":{"kind":"Name","value":"request"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authenticationId"}},{"kind":"Field","name":{"kind":"Name","value":"app"}},{"kind":"Field","name":{"kind":"Name","value":"browser"}},{"kind":"Field","name":{"kind":"Name","value":"device"}},{"kind":"Field","name":{"kind":"Name","value":"os"}},{"kind":"Field","name":{"kind":"Name","value":"origin"}},{"kind":"Field","name":{"kind":"Name","value":"signer"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PaginatedResultInfoFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PaginatedResultInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PaginatedResultInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"prev"}},{"kind":"Field","name":{"kind":"Name","value":"next"}}]}}]} as unknown as DocumentNode; +export const AuthenticatedSessionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AuthenticatedSessions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"request"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AuthenticatedSessionsRequest"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authenticatedSessions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"request"},"value":{"kind":"Variable","name":{"kind":"Name","value":"request"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authenticationId"}},{"kind":"Field","name":{"kind":"Name","value":"app"}},{"kind":"Field","name":{"kind":"Name","value":"browser"}},{"kind":"Field","name":{"kind":"Name","value":"device"}},{"kind":"Field","name":{"kind":"Name","value":"os"}},{"kind":"Field","name":{"kind":"Name","value":"origin"}},{"kind":"Field","name":{"kind":"Name","value":"signer"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PaginatedResultInfoFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PaginatedResultInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PaginatedResultInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"prev"}},{"kind":"Field","name":{"kind":"Name","value":"next"}}]}}]} as unknown as DocumentNode; /** * __useAuthenticatedSessionsQuery__ diff --git a/packages/types/enums.ts b/packages/types/enums.ts index 06ef7dea5d71..f94d1afe6bdb 100644 --- a/packages/types/enums.ts +++ b/packages/types/enums.ts @@ -5,5 +5,7 @@ export enum OptmisticPostType { Mirror = "Mirror", Post = "Post", Quote = "Quote", - Unfollow = "Unfollow" + Unfollow = "Unfollow", + Block = "Block", + Unblock = "Unblock" } diff --git a/packages/types/misc.d.ts b/packages/types/misc.d.ts index 05e62e6f381b..c11788be6921 100644 --- a/packages/types/misc.d.ts +++ b/packages/types/misc.d.ts @@ -90,6 +90,7 @@ export interface Emoji { } export interface OptimisticTransaction { + blockOrUnblockOn?: string; collectOn?: string; commentOn?: string; content?: string;