NFTs page (#7144)
* NFTs page * Chore * Fix no items condition * Extract ipfs data fetching logic into a separate hook * Update deps * @polkadot/app-assets -> @polkadot/app-nfts in headers * Update packages/page-nfts/src/AccountItems/Item.tsx Co-authored-by: Jaco <jacogr@gmail.com> * Update packages/page-nfts/src/translate.ts Co-authored-by: Jaco <jacogr@gmail.com> * Remove redundant component * Add TextEncoder/TextDecoder shims * Update version Co-authored-by: Jaco <jacogr@gmail.com>
This commit is contained in:
parent
89f6347b80
commit
14c0cb6f7a
@ -19,6 +19,7 @@ import files from './files';
|
||||
import gilt from './gilt';
|
||||
import js from './js';
|
||||
import membership from './membership';
|
||||
import nfts from './nfts';
|
||||
import parachains from './parachains';
|
||||
import poll from './poll';
|
||||
import rpc from './rpc';
|
||||
@ -52,6 +53,7 @@ export default function create (t: TFunction): Routes {
|
||||
parachains(t),
|
||||
gilt(t),
|
||||
assets(t),
|
||||
nfts(t),
|
||||
society(t),
|
||||
calendar(t),
|
||||
contracts(t),
|
||||
|
||||
22
packages/apps-routing/src/nfts.ts
Normal file
22
packages/apps-routing/src/nfts.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright 2017-2022 @polkadot/apps-routing authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { Route } from './types';
|
||||
|
||||
import Component from '@polkadot/app-nfts';
|
||||
|
||||
export default function create (t: TFunction): Route {
|
||||
return {
|
||||
Component,
|
||||
display: {
|
||||
needsApi: [
|
||||
'tx.uniques.create'
|
||||
]
|
||||
},
|
||||
group: 'network',
|
||||
icon: 'shopping-cart',
|
||||
name: 'nfts',
|
||||
text: t('nav.nfts', 'NFTs', { ns: 'apps-routing' })
|
||||
};
|
||||
}
|
||||
@ -14,6 +14,7 @@
|
||||
"nav.gilt": "Gilt",
|
||||
"nav.js": "JavaScript",
|
||||
"nav.membership": "Membership",
|
||||
"nav.nfts": "NFTs",
|
||||
"nav.parachains": "Parachains",
|
||||
"nav.poll": "Token poll",
|
||||
"nav.rpc": "RPC calls",
|
||||
@ -27,4 +28,4 @@
|
||||
"nav.teleport": "Teleport",
|
||||
"nav.transfer": "Transfer",
|
||||
"nav.treasury": "Treasury"
|
||||
}
|
||||
}
|
||||
|
||||
0
packages/page-nfts/.skip-build
Normal file
0
packages/page-nfts/.skip-build
Normal file
0
packages/page-nfts/.skip-npm
Normal file
0
packages/page-nfts/.skip-npm
Normal file
201
packages/page-nfts/LICENSE
Normal file
201
packages/page-nfts/LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
1
packages/page-nfts/README.md
Normal file
1
packages/page-nfts/README.md
Normal file
@ -0,0 +1 @@
|
||||
# @polkadot/app-nfts
|
||||
19
packages/page-nfts/package.json
Normal file
19
packages/page-nfts/package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"bugs": "https://github.com/polkadot-js/apps/issues",
|
||||
"homepage": "https://github.com/polkadot-js/apps/tree/master/packages/page-nfts#readme",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@polkadot/app-nfts",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"directory": "packages/page-nfts",
|
||||
"type": "git",
|
||||
"url": "https://github.com/polkadot-js/apps.git"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "0.109.2-3",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.2",
|
||||
"@polkadot/react-components": "^0.109.2-3"
|
||||
}
|
||||
}
|
||||
42
packages/page-nfts/src/AccountItems/Item.tsx
Normal file
42
packages/page-nfts/src/AccountItems/Item.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ItemInfo } from './types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressSmall, IconLink } from '@polkadot/react-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
collectionName: string;
|
||||
value: ItemInfo;
|
||||
}
|
||||
|
||||
function Item ({ className, collectionName, value: { account, id, ipfsData } }: Props): React.ReactElement<Props> {
|
||||
const name = ipfsData?.name || collectionName;
|
||||
const imageLink = ipfsData?.image ? `https://ipfs.io/ipfs/${ipfsData.image}` : '';
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<td className='number'><h1>{formatNumber(id)}</h1></td>
|
||||
<td className='together all'>
|
||||
{ name && imageLink
|
||||
? (
|
||||
<IconLink
|
||||
href={imageLink}
|
||||
icon='braille'
|
||||
label={name}
|
||||
rel='noopener'
|
||||
target='_blank'
|
||||
/>)
|
||||
: name
|
||||
}
|
||||
</td>
|
||||
<td className='address media--1000'><AddressSmall value={account} /></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Item);
|
||||
104
packages/page-nfts/src/AccountItems/index.tsx
Normal file
104
packages/page-nfts/src/AccountItems/index.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CollectionInfo, CollectionInfoComplete } from '../types';
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Dropdown, Table } from '@polkadot/react-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import Item from './Item';
|
||||
import useAccountItems from './useAccountItems';
|
||||
import useItemsInfos from './useItemsInfos';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
infos?: CollectionInfo[];
|
||||
}
|
||||
|
||||
function AccountItems ({ className, infos = [] }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const NO_NAME = ` - ${t('no name')} -`;
|
||||
|
||||
const [infoIndex, setInfoIndex] = useState(0);
|
||||
const [info, setInfo] = useState<CollectionInfoComplete | null>(null);
|
||||
const accountItems = useAccountItems();
|
||||
|
||||
const collectionItems = useMemo(
|
||||
() => !info || !accountItems
|
||||
? []
|
||||
: accountItems.filter(({ collectionId }) => collectionId.eq(info.id))
|
||||
, [info, accountItems]
|
||||
);
|
||||
|
||||
const itemsInfos = useItemsInfos(collectionItems);
|
||||
const collectionName = info?.ipfsData?.name || NO_NAME;
|
||||
|
||||
const completeInfos = useMemo(
|
||||
() => !accountItems
|
||||
? []
|
||||
: infos
|
||||
.filter((i): i is CollectionInfoComplete => !!(i.details && i.metadata) && accountItems.some(({ collectionId }) => i.id.eq(collectionId)))
|
||||
.sort((a, b) => a.id.cmp(b.id)),
|
||||
[infos, accountItems]
|
||||
);
|
||||
|
||||
const collectionOptions = useMemo(
|
||||
() => completeInfos.map(({ id, ipfsData }, index) => ({
|
||||
text: `${ipfsData?.name || NO_NAME} (ID: ${formatNumber(id)})`,
|
||||
value: index
|
||||
})),
|
||||
[completeInfos, NO_NAME]
|
||||
);
|
||||
|
||||
const headerRef = useRef([
|
||||
[t('items'), 'start', 2],
|
||||
[t('owner'), 'address media--1000']
|
||||
]);
|
||||
|
||||
useEffect((): void => {
|
||||
setInfo(() =>
|
||||
infoIndex >= 0 && infoIndex < completeInfos.length
|
||||
? completeInfos[infoIndex]
|
||||
: null
|
||||
);
|
||||
}, [completeInfos, infoIndex]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Table
|
||||
empty={!info && accountItems && t<string>('No accounts with items found for the collection')}
|
||||
filter={collectionOptions.length
|
||||
? (
|
||||
<Dropdown
|
||||
isFull
|
||||
label={t<string>('the collection to query for items')}
|
||||
onChange={setInfoIndex}
|
||||
options={collectionOptions}
|
||||
value={infoIndex}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{itemsInfos && itemsInfos.map((info) => (
|
||||
<Item
|
||||
collectionName={collectionName}
|
||||
key={info.key}
|
||||
value={info}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(AccountItems)`
|
||||
table {
|
||||
overflow: auto;
|
||||
}
|
||||
`);
|
||||
19
packages/page-nfts/src/AccountItems/types.ts
Normal file
19
packages/page-nfts/src/AccountItems/types.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId } from '@polkadot/types/interfaces';
|
||||
import type { PalletUniquesInstanceMetadata } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
|
||||
export interface ItemSupportedIpfsData {
|
||||
name: string | null;
|
||||
image: string | null;
|
||||
}
|
||||
|
||||
export interface ItemInfo {
|
||||
account: AccountId,
|
||||
id: BN;
|
||||
key: string;
|
||||
metadata: PalletUniquesInstanceMetadata | null;
|
||||
ipfsData: ItemSupportedIpfsData | null;
|
||||
}
|
||||
47
packages/page-nfts/src/AccountItems/useAccountItems.ts
Normal file
47
packages/page-nfts/src/AccountItems/useAccountItems.ts
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StorageKey, u32 } from '@polkadot/types';
|
||||
import type { AccountId32 } from '@polkadot/types/interfaces';
|
||||
import type { AccountItem } from '../types';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts, useApi, useIsMountedRef } from '@polkadot/react-hooks';
|
||||
|
||||
function transformResults (results: StorageKey<[AccountId32, u32, u32]>[][]): AccountItem[] {
|
||||
return results
|
||||
.filter((r) => !!r.length)
|
||||
.map((r) => r.map((item) => {
|
||||
const [accountId, collectionId, itemId] = item.args;
|
||||
|
||||
return {
|
||||
accountId,
|
||||
collectionId,
|
||||
itemId
|
||||
};
|
||||
}))
|
||||
.flat();
|
||||
}
|
||||
|
||||
function useAccountItemsImpl (): AccountItem[] | undefined {
|
||||
const mountedRef = useIsMountedRef();
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const [state, setState] = useState<AccountItem[] | undefined>();
|
||||
|
||||
useEffect((): void => {
|
||||
if (!allAccounts.length) return;
|
||||
|
||||
const promises = allAccounts.map((account) => api.query.uniques.account.keys(account));
|
||||
|
||||
Promise.all(promises)
|
||||
.then((results) => mountedRef.current && setState(transformResults(results)))
|
||||
.catch(console.error);
|
||||
}, [allAccounts, api.query.uniques.account, mountedRef]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default createNamedHook('useAccountItems', useAccountItemsImpl);
|
||||
95
packages/page-nfts/src/AccountItems/useItemsInfos.ts
Normal file
95
packages/page-nfts/src/AccountItems/useItemsInfos.ts
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletUniquesInstanceMetadata } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { AccountItem } from '../types';
|
||||
import type { ItemInfo, ItemSupportedIpfsData } from './types';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall, useIpfsFetch } from '@polkadot/react-hooks';
|
||||
|
||||
type IpfsData = Map<string, ItemSupportedIpfsData | null>;
|
||||
|
||||
const QUERY_OPTS = { withParams: true };
|
||||
|
||||
const IPFS_FETCH_OPTIONS = {
|
||||
transform: (data: string | undefined): ItemSupportedIpfsData | null => {
|
||||
if (!data) return null;
|
||||
|
||||
try {
|
||||
const result = JSON.parse(data) as {[key: string]: any};
|
||||
|
||||
if (result && typeof result === 'object') {
|
||||
return {
|
||||
image: typeof result.image === 'string' ? result.image.replace(/ipfs:\/\/|ipfs\//gi, '') : null,
|
||||
name: typeof result.name === 'string' ? result.name : null
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function extractInfo ([, itemId]: [BN, BN], metadata: PalletUniquesInstanceMetadata, accountItems: AccountItem[]): ItemInfo {
|
||||
const { accountId } = accountItems.find(({ itemId: _itemId }) => _itemId.eq(itemId)) as AccountItem;
|
||||
|
||||
return {
|
||||
account: accountId,
|
||||
id: itemId,
|
||||
ipfsData: null,
|
||||
key: itemId.toString(),
|
||||
metadata: metadata.isEmpty
|
||||
? null
|
||||
: metadata
|
||||
};
|
||||
}
|
||||
|
||||
const addIpfsData = (ipfsData: IpfsData) => (itemInfo: ItemInfo): ItemInfo => {
|
||||
const ipfsHash = itemInfo.metadata?.toHuman()?.data?.toString() || '';
|
||||
|
||||
return {
|
||||
...itemInfo,
|
||||
ipfsData: ipfsData.has(ipfsHash) ? ipfsData.get(ipfsHash) as ItemSupportedIpfsData | null : null
|
||||
};
|
||||
};
|
||||
|
||||
function useItemsInfosImpl (accountItems: AccountItem[]): ItemInfo[] | undefined {
|
||||
const { api } = useApi();
|
||||
const [state, setState] = useState<ItemInfo[] | undefined>();
|
||||
|
||||
const ids = useMemo(
|
||||
() => accountItems.map(({ collectionId, itemId }) => [collectionId, itemId]),
|
||||
[accountItems]
|
||||
);
|
||||
|
||||
const metadata = useCall<[[[BN, BN][]], PalletUniquesInstanceMetadata[]]>(api.query.uniques.instanceMetadataOf.multi, [ids], QUERY_OPTS);
|
||||
|
||||
const ipfsHashes = useMemo((): string[] | undefined => {
|
||||
if (metadata && metadata[1].length) {
|
||||
return metadata[1].map((metadataItem) => metadataItem.toHuman()?.data?.toString() || '');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [metadata]);
|
||||
|
||||
const ipfsData = useIpfsFetch<ItemSupportedIpfsData | null>(ipfsHashes, IPFS_FETCH_OPTIONS);
|
||||
|
||||
useEffect((): void => {
|
||||
if (ipfsData && accountItems.length && metadata && metadata[0][0].length) {
|
||||
const [collectionId] = metadata[0][0][0];
|
||||
|
||||
if (!collectionId.eq(ids[0][0])) return;
|
||||
|
||||
const itemsInfos = metadata[0][0].map((id, index) => extractInfo(id, metadata[1][index], accountItems));
|
||||
|
||||
setState(itemsInfos.map(addIpfsData(ipfsData)));
|
||||
}
|
||||
}, [accountItems, ids, ipfsData, metadata]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default createNamedHook('useItemsInfos', useItemsInfosImpl);
|
||||
46
packages/page-nfts/src/Overview/Collection.tsx
Normal file
46
packages/page-nfts/src/Overview/Collection.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CollectionInfo } from '../types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressSmall, IconLink } from '@polkadot/react-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: CollectionInfo;
|
||||
}
|
||||
|
||||
function Collection ({ className, value: { details, id, ipfsData } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const name = ipfsData?.name || '';
|
||||
const imageLink = ipfsData?.image ? `https://ipfs.io/ipfs/${ipfsData.image}` : '';
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<td className='number'><h1>{formatNumber(id)}</h1></td>
|
||||
<td className='together all'>
|
||||
{ name && imageLink
|
||||
? (
|
||||
<IconLink
|
||||
href={imageLink}
|
||||
icon='braille'
|
||||
label={name}
|
||||
rel='noopener'
|
||||
target='_blank'
|
||||
/>)
|
||||
: name
|
||||
}
|
||||
</td>
|
||||
<td className='address media--1000'>{details && <AddressSmall value={details.owner} />}</td>
|
||||
<td className='string'>{details?.toJSON()?.isFrozen && t('Frozen')}</td>
|
||||
<td className='number'>{details?.instances && formatNumber(details.instances)}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Collection);
|
||||
44
packages/page-nfts/src/Overview/Collections.tsx
Normal file
44
packages/page-nfts/src/Overview/Collections.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CollectionInfo } from '../types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import Collection from './Collection';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
infos?: CollectionInfo[];
|
||||
}
|
||||
|
||||
function Collections ({ className, infos }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerRef = useRef([
|
||||
[t('collections'), 'start', 2],
|
||||
[t('owner'), 'address media--1000'],
|
||||
[t('status')],
|
||||
[t('items')]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={infos && t<string>('No collections found')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{infos?.map((info) => (
|
||||
<Collection
|
||||
key={info.key}
|
||||
value={info}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Collections);
|
||||
28
packages/page-nfts/src/Overview/Summary.tsx
Normal file
28
packages/page-nfts/src/Overview/Summary.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { CardSummary, SummaryBox } from '@polkadot/react-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
numCollections?: number;
|
||||
}
|
||||
|
||||
function Summary ({ className, numCollections }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SummaryBox className={className}>
|
||||
<CardSummary label={t<string>('collections')}>
|
||||
{formatNumber(numCollections)}
|
||||
</CardSummary>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
27
packages/page-nfts/src/Overview/index.tsx
Normal file
27
packages/page-nfts/src/Overview/index.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { CollectionInfo } from '../types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Collections from './Collections';
|
||||
import Summary from './Summary';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
ids?: BN[];
|
||||
infos?: CollectionInfo[];
|
||||
}
|
||||
|
||||
function Overview ({ className, ids, infos }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Summary numCollections={ids?.length} />
|
||||
<Collections infos={infos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Overview);
|
||||
70
packages/page-nfts/src/index.tsx
Normal file
70
packages/page-nfts/src/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import '@polkadot/api-augment/substrate';
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { Route, Switch } from 'react-router';
|
||||
|
||||
import { Tabs } from '@polkadot/react-components';
|
||||
import { useAccounts } from '@polkadot/react-hooks';
|
||||
|
||||
import AccountItems from './AccountItems';
|
||||
import Overview from './Overview';
|
||||
import { useTranslation } from './translate';
|
||||
import useCollectionIds from './useCollectionIds';
|
||||
import useCollectionInfos from './useCollectionInfos';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function NftApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { hasAccounts } = useAccounts();
|
||||
const ids = useCollectionIds();
|
||||
const infos = useCollectionInfos(ids);
|
||||
|
||||
const tabsRef = useRef([
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t<string>('Overview')
|
||||
},
|
||||
{
|
||||
name: 'my-nfts',
|
||||
text: t<string>('My NFTs')
|
||||
}
|
||||
]);
|
||||
|
||||
const hidden = useMemo(
|
||||
() => (hasAccounts && infos && infos.some(({ details, metadata }) => !!(details && metadata)))
|
||||
? []
|
||||
: ['my-nfts'],
|
||||
[hasAccounts, infos]
|
||||
);
|
||||
|
||||
return (
|
||||
<main className={className}>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
hidden={hidden}
|
||||
items={tabsRef.current}
|
||||
/>
|
||||
<Switch>
|
||||
<Route path={`${basePath}/my-nfts`}>
|
||||
<AccountItems infos={infos} />
|
||||
</Route>
|
||||
<Route>
|
||||
<Overview
|
||||
ids={ids}
|
||||
infos={infos}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(NftApp);
|
||||
10
packages/page-nfts/src/translate.ts
Normal file
10
packages/page-nfts/src/translate.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { UseTranslationResponse } from 'react-i18next';
|
||||
|
||||
import { useTranslation as useTranslationBase } from 'react-i18next';
|
||||
|
||||
export function useTranslation (): UseTranslationResponse<'app-nfts', undefined> {
|
||||
return useTranslationBase('app-nfts');
|
||||
}
|
||||
34
packages/page-nfts/src/types.ts
Normal file
34
packages/page-nfts/src/types.ts
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId } from '@polkadot/types/interfaces';
|
||||
import type { PalletUniquesClassDetails, PalletUniquesClassMetadata } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
|
||||
export interface CollectionSupportedIpfsData {
|
||||
name: string | null;
|
||||
image: string | null;
|
||||
}
|
||||
|
||||
export interface CollectionInfo {
|
||||
details: PalletUniquesClassDetails | null;
|
||||
id: BN;
|
||||
isAdminMe: boolean;
|
||||
isIssuerMe: boolean;
|
||||
isFreezerMe: boolean;
|
||||
isOwnerMe: boolean;
|
||||
key: string;
|
||||
metadata: PalletUniquesClassMetadata | null;
|
||||
ipfsData: CollectionSupportedIpfsData | null;
|
||||
}
|
||||
|
||||
export interface CollectionInfoComplete extends CollectionInfo {
|
||||
details: PalletUniquesClassDetails;
|
||||
metadata: PalletUniquesClassMetadata;
|
||||
}
|
||||
|
||||
export interface AccountItem {
|
||||
accountId: AccountId;
|
||||
collectionId: BN;
|
||||
itemId: BN;
|
||||
}
|
||||
26
packages/page-nfts/src/useCollectionIds.ts
Normal file
26
packages/page-nfts/src/useCollectionIds.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StorageKey } from '@polkadot/types';
|
||||
import type { AssetId } from '@polkadot/types/interfaces';
|
||||
|
||||
import { createNamedHook, useApi, useEventTrigger, useMapKeys } from '@polkadot/react-hooks';
|
||||
|
||||
const options = {
|
||||
transform: (keys: StorageKey<[AssetId]>[]): AssetId[] =>
|
||||
keys
|
||||
.map(({ args: [assetId] }) => assetId)
|
||||
.sort((a, b) => a.cmp(b))
|
||||
};
|
||||
|
||||
function useCollectionIdsImpl (): AssetId[] | undefined {
|
||||
const { api } = useApi();
|
||||
const trigger = useEventTrigger([
|
||||
api.events.uniques.Created,
|
||||
api.events.uniques.Destroyed
|
||||
]);
|
||||
|
||||
return useMapKeys(api.query.uniques.class, options, trigger.blockHash);
|
||||
}
|
||||
|
||||
export default createNamedHook('useCollectionIds', useCollectionIdsImpl);
|
||||
114
packages/page-nfts/src/useCollectionInfos.ts
Normal file
114
packages/page-nfts/src/useCollectionInfos.ts
Normal file
@ -0,0 +1,114 @@
|
||||
// Copyright 2017-2022 @polkadot/app-nfts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@polkadot/types';
|
||||
import type { AccountId } from '@polkadot/types/interfaces';
|
||||
import type { PalletUniquesClassDetails, PalletUniquesClassMetadata } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { CollectionInfo } from './types';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts, useApi, useCall, useIpfsFetch } from '@polkadot/react-hooks';
|
||||
|
||||
import { CollectionSupportedIpfsData } from './types';
|
||||
|
||||
type IpfsData = Map<string, CollectionSupportedIpfsData | null>;
|
||||
|
||||
const EMPTY_FLAGS = {
|
||||
isAdminMe: false,
|
||||
isFreezerMe: false,
|
||||
isIssuerMe: false,
|
||||
isOwnerMe: false
|
||||
};
|
||||
|
||||
const QUERY_OPTS = { withParams: true };
|
||||
|
||||
const IPFS_FETCH_OPTIONS = {
|
||||
transform: (data: string | undefined): CollectionSupportedIpfsData | null => {
|
||||
if (!data) return null;
|
||||
|
||||
try {
|
||||
const result = JSON.parse(data) as {[key: string]: any};
|
||||
|
||||
if (result && typeof result === 'object') {
|
||||
return {
|
||||
image: typeof result.image === 'string' ? result.image.replace(/ipfs:\/\/|ipfs\//gi, '') : null,
|
||||
name: typeof result.name === 'string' ? result.name : null
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function isAccount (allAccounts: string[], accountId: AccountId): boolean {
|
||||
const address = accountId.toString();
|
||||
|
||||
return allAccounts.some((a) => a === address);
|
||||
}
|
||||
|
||||
function extractInfo (allAccounts: string[], id: BN, optDetails: Option<PalletUniquesClassDetails>, metadata: PalletUniquesClassMetadata): CollectionInfo {
|
||||
const details = optDetails.unwrapOr(null);
|
||||
|
||||
return {
|
||||
...(details
|
||||
? {
|
||||
isAdminMe: isAccount(allAccounts, details.admin),
|
||||
isFreezerMe: isAccount(allAccounts, details.freezer),
|
||||
isIssuerMe: isAccount(allAccounts, details.issuer),
|
||||
isOwnerMe: isAccount(allAccounts, details.owner)
|
||||
}
|
||||
: EMPTY_FLAGS
|
||||
),
|
||||
details,
|
||||
id,
|
||||
ipfsData: null,
|
||||
key: id.toString(),
|
||||
metadata: metadata.isEmpty
|
||||
? null
|
||||
: metadata
|
||||
};
|
||||
}
|
||||
|
||||
const addIpfsData = (ipfsData: IpfsData) => (collectionInfo: CollectionInfo): CollectionInfo => {
|
||||
const ipfsHash = collectionInfo.metadata?.toHuman()?.data?.toString() || '';
|
||||
|
||||
return {
|
||||
...collectionInfo,
|
||||
ipfsData: ipfsData.has(ipfsHash) ? ipfsData.get(ipfsHash) as CollectionSupportedIpfsData | null : null
|
||||
};
|
||||
};
|
||||
|
||||
function useCollectionInfosImpl (ids?: BN[]): CollectionInfo[] | undefined {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const metadata = useCall<[[BN[]], PalletUniquesClassMetadata[]]>(api.query.uniques.classMetadataOf.multi, [ids], QUERY_OPTS);
|
||||
const details = useCall<[[BN[]], Option<PalletUniquesClassDetails>[]]>(api.query.uniques.class.multi, [ids], QUERY_OPTS);
|
||||
const [state, setState] = useState<CollectionInfo[] | undefined>();
|
||||
|
||||
const ipfsHashes = useMemo((): string[] | undefined => {
|
||||
if (metadata && metadata[1].length) {
|
||||
return metadata[1].map((metadataItem) => metadataItem.toHuman()?.data?.toString() || '');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [metadata]);
|
||||
|
||||
const ipfsData = useIpfsFetch<CollectionSupportedIpfsData | null>(ipfsHashes, IPFS_FETCH_OPTIONS);
|
||||
|
||||
useEffect((): void => {
|
||||
if (ipfsData && details && metadata && (details[0][0].length === metadata[0][0].length)) {
|
||||
const collectionInfos = details[0][0].map((id, index) =>
|
||||
extractInfo(allAccounts, id, details[1][index], metadata[1][index])
|
||||
);
|
||||
|
||||
setState(collectionInfos.map(addIpfsData(ipfsData)));
|
||||
}
|
||||
}, [allAccounts, details, ids, ipfsData, metadata]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default createNamedHook('useCollectionInfos', useCollectionInfosImpl);
|
||||
9
packages/page-nfts/tsconfig.build.json
Normal file
9
packages/page-nfts/tsconfig.build.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "..",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"references": []
|
||||
}
|
||||
@ -14,6 +14,7 @@
|
||||
"version": "0.109.2-3",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.2",
|
||||
"@polkadot/hw-ledger": "^8.5.1"
|
||||
"@polkadot/hw-ledger": "^8.5.1",
|
||||
"is-ipfs": "6.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ export { useFormField } from './useFormField';
|
||||
export { useIncrement } from './useIncrement';
|
||||
export { useInflation } from './useInflation';
|
||||
export { useIpfs } from './useIpfs';
|
||||
export { useIpfsFetch } from './useIpfsFetch';
|
||||
export { useIsMountedRef } from './useIsMountedRef';
|
||||
export { useJudgements } from './useJudgements';
|
||||
export { useLedger } from './useLedger';
|
||||
|
||||
77
packages/react-hooks/src/useIpfsFetch.ts
Normal file
77
packages/react-hooks/src/useIpfsFetch.ts
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright 2017-2022 @polkadot/react-hooks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import '@polkadot/x-textencoder/shim';
|
||||
import '@polkadot/x-textdecoder/shim';
|
||||
|
||||
import type { CallOptions } from './types';
|
||||
|
||||
import isIPFS from 'is-ipfs';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { useIsMountedRef } from './useIsMountedRef';
|
||||
|
||||
function isCid (cid: string): boolean {
|
||||
return !!cid && (isIPFS.cid(cid) || isIPFS.base32cid(cid.toLowerCase()));
|
||||
}
|
||||
|
||||
const cache = new Map<string, any>();
|
||||
|
||||
async function fetchIpfsData <T> (ipfsHashes: string[]): Promise<Map<string, T>> {
|
||||
const result = new Map();
|
||||
|
||||
const promises = ipfsHashes.map((ipfsHash) => {
|
||||
if (cache.has(ipfsHash)) {
|
||||
result.set(ipfsHash, cache.get(ipfsHash));
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return fetch(`https://ipfs.io/ipfs/${ipfsHash}`)
|
||||
.then(async (res) => {
|
||||
const ipfsResponse = res.status >= 200 && res.status < 300 ? await res.text() : null;
|
||||
|
||||
cache.set(ipfsHash, ipfsResponse);
|
||||
result.set(ipfsHash, ipfsResponse);
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function postProcessData <T> (ipfsData: Map<string, T>, options?: CallOptions<T>) {
|
||||
if (!options?.transform) return ipfsData;
|
||||
|
||||
for (const [key, value] of ipfsData.entries()) {
|
||||
ipfsData.set(key, options?.transform(value));
|
||||
}
|
||||
|
||||
return ipfsData;
|
||||
}
|
||||
|
||||
// FIXME This is generic, we cannot really use createNamedHook
|
||||
export function useIpfsFetch <T> (hashes: string[] | undefined, options?: CallOptions<T>): Map<string, T> | undefined {
|
||||
const mountedRef = useIsMountedRef();
|
||||
const [value, setValue] = useState<Map<string, T> | undefined>();
|
||||
|
||||
const ipfsHashes = useMemo(() => {
|
||||
if (!hashes) return undefined;
|
||||
|
||||
return hashes
|
||||
.map((hash) => isCid(hash) ? hash : '')
|
||||
.filter((hash) => !!hash);
|
||||
}, [hashes]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (mountedRef.current && ipfsHashes) {
|
||||
fetchIpfsData<T>(ipfsHashes)
|
||||
.then((ipfsData) => setValue(postProcessData<T>(ipfsData, options)))
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
.catch(() => { });
|
||||
}
|
||||
}, [ipfsHashes, options, mountedRef]);
|
||||
|
||||
return value;
|
||||
}
|
||||
@ -42,6 +42,8 @@
|
||||
"@polkadot/app-js/*": ["page-js/src/*"],
|
||||
"@polkadot/app-membership": ["page-membership/src"],
|
||||
"@polkadot/app-membership/*": ["page-membership/src/*"],
|
||||
"@polkadot/app-nfts": ["page-nfts/src"],
|
||||
"@polkadot/app-nfts/*": ["page-nfts/src/*"],
|
||||
"@polkadot/app-parachains": ["page-parachains/src"],
|
||||
"@polkadot/app-parachains/*": ["page-parachains/src/*"],
|
||||
"@polkadot/app-poll": ["page-poll/src"],
|
||||
|
||||
112
yarn.lock
112
yarn.lock
@ -2813,6 +2813,15 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/app-nfts@workspace:packages/page-nfts":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/app-nfts@workspace:packages/page-nfts"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.17.2
|
||||
"@polkadot/react-components": ^0.109.2-3
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/app-parachains@workspace:packages/page-parachains":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/app-parachains@workspace:packages/page-parachains"
|
||||
@ -3308,6 +3317,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.17.2
|
||||
"@polkadot/hw-ledger": ^8.5.1
|
||||
is-ipfs: 6.0.2
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@ -8644,6 +8654,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dns-over-http-resolver@npm:^1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "dns-over-http-resolver@npm:1.2.3"
|
||||
dependencies:
|
||||
debug: ^4.3.1
|
||||
native-fetch: ^3.0.0
|
||||
receptacle: ^1.3.2
|
||||
checksum: 3cc1a1d77fc43e7a8a12453da987b80860ac96dc1031386c5eb1a39154775a87cfa1d50c0eaa5ea5e397e898791654608f6e2acf03f750f4098ab8822bb7d928
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dns-packet@npm:^1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "dns-packet@npm:1.3.1"
|
||||
@ -9230,6 +9251,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"err-code@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "err-code@npm:3.0.1"
|
||||
checksum: aede1f1d5ebe6d6b30b5e3175e3cc13e67de2e2e1ad99ce4917e957d7b59e8451ed10ee37dbc6493521920a47082c479b9097e5c39438d4aff4cc84438568a5a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"error-ex@npm:^1.3.1":
|
||||
version: 1.3.2
|
||||
resolution: "error-ex@npm:1.3.2"
|
||||
@ -12248,6 +12276,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-ipfs@npm:6.0.2":
|
||||
version: 6.0.2
|
||||
resolution: "is-ipfs@npm:6.0.2"
|
||||
dependencies:
|
||||
iso-url: ^1.1.3
|
||||
mafmt: ^10.0.0
|
||||
multiaddr: ^10.0.0
|
||||
multiformats: ^9.0.0
|
||||
uint8arrays: ^3.0.0
|
||||
checksum: 9397057264c61e381e3f24878b8d6d93aed68e5601a0a27ae998820beb92bb906c3ebba22449b0ec234db2d8e3429aabbc7372c692bbf55294e982413ab56c59
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-ipfs@npm:^0.6.0":
|
||||
version: 0.6.3
|
||||
resolution: "is-ipfs@npm:0.6.3"
|
||||
@ -12614,6 +12655,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iso-url@npm:^1.1.3":
|
||||
version: 1.2.1
|
||||
resolution: "iso-url@npm:1.2.1"
|
||||
checksum: 1af98c4ed6a39598407fd8c3c13e997c978985f477af2be3390d2aa3e422b4b5992ffbb0dac68656b165c71850fff748ac1309d29d4f2a728707d76bf0f98557
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"isobject@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "isobject@npm:3.0.1"
|
||||
@ -13951,6 +13999,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mafmt@npm:^10.0.0":
|
||||
version: 10.0.0
|
||||
resolution: "mafmt@npm:10.0.0"
|
||||
dependencies:
|
||||
multiaddr: ^10.0.0
|
||||
checksum: ab9aef1e429870f7ef7fd141d8dc139b1bca9d145196913a84e1acee7b4eb2b7c9e1865d6c928e3d84008a6e735666c6ae3d4b51a554bcb64d566ea12607bedb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mafmt@npm:^7.0.0":
|
||||
version: 7.1.0
|
||||
resolution: "mafmt@npm:7.1.0"
|
||||
@ -14690,6 +14747,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"multiaddr@npm:^10.0.0":
|
||||
version: 10.0.1
|
||||
resolution: "multiaddr@npm:10.0.1"
|
||||
dependencies:
|
||||
dns-over-http-resolver: ^1.2.3
|
||||
err-code: ^3.0.1
|
||||
is-ip: ^3.1.0
|
||||
multiformats: ^9.4.5
|
||||
uint8arrays: ^3.0.0
|
||||
varint: ^6.0.0
|
||||
checksum: d53aaf7efd52ee5e6413ef36ececd29239ceb5c1f048c1fa9b820442226dc232067312d25e509a2571a14047465fb934dd35029c7f3166f4d02d13e3c501925d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"multiaddr@npm:^7.2.1, multiaddr@npm:^7.3.0":
|
||||
version: 7.5.0
|
||||
resolution: "multiaddr@npm:7.5.0"
|
||||
@ -14772,6 +14843,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"multiformats@npm:^9.0.0, multiformats@npm:^9.4.2, multiformats@npm:^9.4.5":
|
||||
version: 9.6.4
|
||||
resolution: "multiformats@npm:9.6.4"
|
||||
checksum: b3b8e481112379d6f3aace199a06e6974dfe3ed4e2100a0effe19fef936ba31704382356df6278a3922b5cb47fcfefe3e5bc54e5bebef35272e3503bad3c62ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"multihashes@npm:^0.4.15, multihashes@npm:~0.4.13, multihashes@npm:~0.4.15":
|
||||
version: 0.4.21
|
||||
resolution: "multihashes@npm:0.4.21"
|
||||
@ -14833,6 +14911,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"native-fetch@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "native-fetch@npm:3.0.0"
|
||||
peerDependencies:
|
||||
node-fetch: "*"
|
||||
checksum: eec8cc78d6da4d0f3f56055e3e557473ac86dd35fd40053ea268d644af7b20babc891d2b53ef821b77ed2428265f60b85e49d754c555de89bfa071a743b853bb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"natural-compare-lite@npm:^1.4.0":
|
||||
version: 1.4.0
|
||||
resolution: "natural-compare-lite@npm:1.4.0"
|
||||
@ -16847,6 +16934,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"receptacle@npm:^1.3.2":
|
||||
version: 1.3.2
|
||||
resolution: "receptacle@npm:1.3.2"
|
||||
dependencies:
|
||||
ms: ^2.1.1
|
||||
checksum: 7c5011f19e6ddcb759c1e6756877cee3c9eb78fbd1278eca4572d75f74993f0ccdc1e5f7761de6e682dff5344ee94f7a69bc492e2e8eb81d8777774a2399ce9c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rechoir@npm:^0.6.2":
|
||||
version: 0.6.2
|
||||
resolution: "rechoir@npm:0.6.2"
|
||||
@ -19432,6 +19528,15 @@ resolve@^2.0.0-next.3:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uint8arrays@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "uint8arrays@npm:3.0.0"
|
||||
dependencies:
|
||||
multiformats: ^9.4.2
|
||||
checksum: 58470e687140e64a7fa08ab66b64777b75f105bf78180324448dc798436beacf0bd322cd2b58d20ca4cfa2e091f58e4b52d008e95f21d0ade16c1102b5d23ad3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ultron@npm:~1.1.0":
|
||||
version: 1.1.1
|
||||
resolution: "ultron@npm:1.1.1"
|
||||
@ -19876,6 +19981,13 @@ resolve@^2.0.0-next.3:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"varint@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "varint@npm:6.0.0"
|
||||
checksum: 7684113c9d497c01e40396e50169c502eb2176203219b96e1c5ac965a3e15b4892bd22b7e48d87148e10fffe638130516b6dbeedd0efde2b2d0395aa1772eea7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vary@npm:^1, vary@npm:~1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "vary@npm:1.1.2"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user