@stellar-expert/ui-framework 1.14.14 → 1.14.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/explorer-api-paginated-list-hooks.js +1 -1
- package/api/ledger-stream.js +101 -0
- package/index.js +2 -0
- package/ledger/ledger-info-parser.js +17 -0
- package/meta/page-meta-tags.js +78 -54
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {useRef} from 'react'
|
|
2
2
|
import isEqual from 'react-fast-compare'
|
|
3
3
|
import {parseQuery, stringifyQuery, navigation} from '@stellar-expert/navigation'
|
|
4
|
-
import {
|
|
4
|
+
import {useStellarNetwork} from '../state/stellar-network-hooks'
|
|
5
5
|
import {useDependantState} from '../state/state-hooks'
|
|
6
6
|
import {fetchExplorerApi} from './explorer-api-call'
|
|
7
7
|
import apiCache from './api-cache'
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {getCurrentStellarNetwork} from '../state/stellar-network-hooks'
|
|
2
|
+
import {fetchExplorerApi} from './explorer-api-call'
|
|
3
|
+
|
|
4
|
+
class LedgerStream {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.listeners = new Set()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @type {boolean}
|
|
11
|
+
* @private
|
|
12
|
+
*/
|
|
13
|
+
listening = false
|
|
14
|
+
/**
|
|
15
|
+
* @type {Set<function>}
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
listeners
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {function} listener
|
|
22
|
+
*/
|
|
23
|
+
on(listener) {
|
|
24
|
+
this.listeners.add(listener)
|
|
25
|
+
if (!this.listening) {
|
|
26
|
+
this.listening = true
|
|
27
|
+
setTimeout(() => this.waitForLedger(), 100)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {function} listener
|
|
33
|
+
*/
|
|
34
|
+
off(listener) {
|
|
35
|
+
this.listeners.delete(listener)
|
|
36
|
+
if (!this.listeners.size) {
|
|
37
|
+
this.listening = false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {number} ledger
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
notify(ledger) {
|
|
46
|
+
for (const listener of this.listeners) {
|
|
47
|
+
listener(ledger)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @return {Promise}
|
|
53
|
+
* @private
|
|
54
|
+
*/
|
|
55
|
+
async waitForLedger() {
|
|
56
|
+
const url = `${explorerApiOrigin}/explorer/${getCurrentStellarNetwork()}/ledger/stream`
|
|
57
|
+
try {
|
|
58
|
+
const resp = await fetch(url, {keepalive: true, cache: 'no-cache'})
|
|
59
|
+
if (!resp.ok) {
|
|
60
|
+
let errorExt
|
|
61
|
+
try {
|
|
62
|
+
errorExt = await resp.json()
|
|
63
|
+
} catch (parsingError) {
|
|
64
|
+
errorExt = {}
|
|
65
|
+
}
|
|
66
|
+
const err = new Error(errorExt?.error || resp.statusText || 'Failed to fetch data from the server')
|
|
67
|
+
err.status = resp.status
|
|
68
|
+
err.ext = errorExt
|
|
69
|
+
throw err
|
|
70
|
+
}
|
|
71
|
+
const {ledger} = await resp.json()
|
|
72
|
+
this.notify(ledger)
|
|
73
|
+
if (this.listening) {
|
|
74
|
+
setTimeout(() => this.waitForLedger(), 1000)
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error(e)
|
|
78
|
+
if (e instanceof Error) {
|
|
79
|
+
e = {
|
|
80
|
+
error: e.message,
|
|
81
|
+
status: e.status || 500,
|
|
82
|
+
ext: e.ext
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (e.ext && e.ext.status) {
|
|
86
|
+
e.status = e.ext.status
|
|
87
|
+
}
|
|
88
|
+
return e
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getLast() {
|
|
93
|
+
return fetchExplorerApi('ledger/last')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getLastSequence() {
|
|
97
|
+
return (await this.getLast()).sequence
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const ledgerStream = new LedgerStream()
|
package/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export * from './api/explorer-api-hooks'
|
|
|
19
19
|
export * from './api/explorer-api-paginated-list-hooks'
|
|
20
20
|
export * from './api/explorer-tx-api'
|
|
21
21
|
export * from './api/explorer-batch-info-loader'
|
|
22
|
+
export * from './api/ledger-stream'
|
|
22
23
|
//Horizon API binding and utils
|
|
23
24
|
export * from './stellar/ledger-generic-id'
|
|
24
25
|
export * from './horizon/horizon-client-helpers'
|
|
@@ -59,6 +60,7 @@ export * from './date/date-selector'
|
|
|
59
60
|
//ledger-entries-related components
|
|
60
61
|
export * from './ledger/ledger-entry-link'
|
|
61
62
|
export * from './ledger/ledger-entry-href-formatter'
|
|
63
|
+
export * from './ledger/ledger-info-parser'
|
|
62
64
|
//account-related components
|
|
63
65
|
export * from './account/identicon'
|
|
64
66
|
export * from './account/account-address'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {xdr} from '@stellar/stellar-base'
|
|
2
|
+
|
|
3
|
+
export function retrieveLedgerInfo(data) {
|
|
4
|
+
const parsed = xdr.LedgerHeader.fromXDR(data.xdr, 'base64')
|
|
5
|
+
return {
|
|
6
|
+
sequence: data.sequence,
|
|
7
|
+
ts: data.ts,
|
|
8
|
+
protocol: data.protocol,
|
|
9
|
+
operations: data.operations || 0,
|
|
10
|
+
txSuccess: data.txSuccess,
|
|
11
|
+
txFailed: data.txFailed,
|
|
12
|
+
xlm: parsed.totalCoins().toBigInt(),
|
|
13
|
+
feePool: parsed.feePool().toBigInt(),
|
|
14
|
+
baseFee: parsed.baseFee(),
|
|
15
|
+
baseReserve: parsed.baseReserve()
|
|
16
|
+
}
|
|
17
|
+
}
|
package/meta/page-meta-tags.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
import {useEffect} from 'react'
|
|
2
2
|
import isEqual from 'react-fast-compare'
|
|
3
3
|
|
|
4
|
-
const {origin} = window.location
|
|
5
|
-
const domain = origin.replace(/https?:\/\//, '')
|
|
6
|
-
|
|
7
4
|
const metaProps = {
|
|
8
5
|
serviceTitle: '',
|
|
9
6
|
description: '',
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
image: '',
|
|
8
|
+
imageEndpoint: '',
|
|
9
|
+
origin: '',
|
|
10
|
+
domain: ''
|
|
13
11
|
}
|
|
14
12
|
|
|
13
|
+
setOrigin(window.location.origin)
|
|
14
|
+
|
|
15
15
|
/**
|
|
16
16
|
* Initialize default meta properties for the application
|
|
17
|
-
* @param {{serviceTitle: String, description: String,
|
|
17
|
+
* @param {{serviceTitle: String, description: String, origin: String, [image]: String, [imageEndpoint]: String}} appMetaProps
|
|
18
18
|
*/
|
|
19
19
|
export function initMeta(appMetaProps) {
|
|
20
|
-
|
|
20
|
+
const {origin, ...props} = appMetaProps
|
|
21
|
+
Object.assign(metaProps, props)
|
|
22
|
+
if (origin) {
|
|
23
|
+
setOrigin(origin)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setOrigin(origin) {
|
|
28
|
+
metaProps.origin = origin
|
|
29
|
+
metaProps.domain = origin.replace(/https?:\/\//, '')
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
function formatPageTitle(title) {
|
|
@@ -47,48 +56,35 @@ function generateDescriptionMeta({description}) {
|
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
58
|
|
|
50
|
-
function
|
|
59
|
+
function generateOpenGraphMeta({description, title, image}, canonicalUrl) {
|
|
60
|
+
//imageEndpoint
|
|
51
61
|
return {
|
|
52
|
-
locator: '
|
|
62
|
+
locator: 'property',
|
|
53
63
|
tags: [
|
|
54
|
-
{name: '
|
|
55
|
-
{name: '
|
|
56
|
-
{name: '
|
|
57
|
-
{name: '
|
|
58
|
-
{name: '
|
|
64
|
+
{name: 'og:title', content: formatPageTitle(title)},
|
|
65
|
+
{name: 'og:url', content: canonicalUrl},
|
|
66
|
+
{name: 'og:site_name', content: formatPageTitle(metaProps.serviceTitle)},
|
|
67
|
+
{name: 'og:description', content: description || metaProps.description},
|
|
68
|
+
{name: 'og:type', content: 'website'},
|
|
69
|
+
{name: 'og:image:width', content: 1200},
|
|
70
|
+
{name: 'og:image:height', content: 630},
|
|
71
|
+
{name: 'og:image', content: formatPageImage(image, canonicalUrl)}
|
|
59
72
|
]
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
|
|
63
|
-
function generateOpenGraphMeta({description, title, facebookImage}, canonicalUrl) {
|
|
64
|
-
let tags = [
|
|
65
|
-
{name: 'og:title', content: formatPageTitle(title)},
|
|
66
|
-
{name: 'og:url', content: canonicalUrl},
|
|
67
|
-
{name: 'og:site_name', content: formatPageTitle(metaProps.serviceTitle)},
|
|
68
|
-
{name: 'og:description', content: description || metaProps.description},
|
|
69
|
-
{name: 'og:type', content: 'website'},
|
|
70
|
-
{name: 'og:image:width', content: 1200},
|
|
71
|
-
{name: 'og:image:height', content: 630},
|
|
72
|
-
{name: 'og:image', content: facebookImage || metaProps.facebookImage}
|
|
73
|
-
]
|
|
74
|
-
return {
|
|
75
|
-
locator: 'property',
|
|
76
|
-
tags
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
76
|
function generateItemPropSchema({description, title, image}) {
|
|
81
77
|
return {
|
|
82
78
|
locator: 'itemprop',
|
|
83
79
|
tags: [
|
|
84
80
|
{name: 'name', content: title},
|
|
85
81
|
{name: 'description', content: description || metaProps.description},
|
|
86
|
-
{name: 'image', content: image || metaProps.
|
|
82
|
+
{name: 'image', content: image || metaProps.image}
|
|
87
83
|
]
|
|
88
84
|
}
|
|
89
85
|
}
|
|
90
86
|
|
|
91
|
-
function generateLdJsonSchema({title}) {
|
|
87
|
+
function generateLdJsonSchema({title, description, image}, canonicalUrl) {
|
|
92
88
|
return {
|
|
93
89
|
tag: 'script',
|
|
94
90
|
locator: 'type',
|
|
@@ -97,10 +93,15 @@ function generateLdJsonSchema({title}) {
|
|
|
97
93
|
name: 'application/ld+json',
|
|
98
94
|
content: JSON.stringify({
|
|
99
95
|
'@context': 'http://schema.org',
|
|
100
|
-
'@type': '
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
'@type': 'WebPage',
|
|
97
|
+
name: formatPageTitle(title),
|
|
98
|
+
description: description || metaProps.description,
|
|
99
|
+
url: canonicalUrl,
|
|
100
|
+
thumbnailUrl: formatPageImage(image, canonicalUrl),
|
|
101
|
+
publisher: {
|
|
102
|
+
'@type': 'ProfilePage',
|
|
103
|
+
name: metaProps.serviceTitle
|
|
104
|
+
}
|
|
104
105
|
})
|
|
105
106
|
}
|
|
106
107
|
]
|
|
@@ -145,23 +146,37 @@ function removeTag(selector) {
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
function formatCanonicalUrl() {
|
|
150
|
+
return metaProps.origin + window.location.pathname// + location.search
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatPageImage(image, canonicalUrl) {
|
|
154
|
+
if (image)
|
|
155
|
+
return image
|
|
156
|
+
if (metaProps.imageEndpoint)
|
|
157
|
+
return metaProps.imageEndpoint + canonicalUrl.replace(metaProps.origin, '')
|
|
158
|
+
return metaProps.image
|
|
159
|
+
}
|
|
160
|
+
|
|
148
161
|
let pageMeta = {}
|
|
149
162
|
|
|
150
|
-
const tagReplacerPipeline = [
|
|
163
|
+
const tagReplacerPipeline = [
|
|
164
|
+
generateCanonicalLink,
|
|
165
|
+
generateDescriptionMeta,
|
|
166
|
+
generateOpenGraphMeta,
|
|
167
|
+
generateLdJsonSchema
|
|
168
|
+
] // generateItemPropSchema
|
|
151
169
|
|
|
152
170
|
/**
|
|
153
171
|
* Update page metadata tags
|
|
154
172
|
* @param {PageMeta} meta - Page metadata
|
|
155
173
|
*/
|
|
156
|
-
|
|
174
|
+
function setPageMetadata(meta) {
|
|
157
175
|
if (isEqual(pageMeta, meta))
|
|
158
176
|
return
|
|
159
|
-
|
|
177
|
+
//generate canonical URL
|
|
178
|
+
const canonicalUrl = formatCanonicalUrl()
|
|
160
179
|
document.title = formatPageTitle(meta.title)
|
|
161
|
-
if (meta.image) {
|
|
162
|
-
meta.facebookImage = meta.image
|
|
163
|
-
meta.twitterImage = meta.image
|
|
164
|
-
}
|
|
165
180
|
for (const replacer of tagReplacerPipeline) {
|
|
166
181
|
replaceMetaTags(replacer(meta, canonicalUrl))
|
|
167
182
|
}
|
|
@@ -175,12 +190,11 @@ export function setPageMetadata(meta) {
|
|
|
175
190
|
* Reset page metadata tags to their default values
|
|
176
191
|
* @param {PageMeta} meta - Page metadata
|
|
177
192
|
*/
|
|
178
|
-
|
|
193
|
+
function resetPageMetadata(meta) {
|
|
179
194
|
setPageMetadata({
|
|
180
195
|
title: metaProps.serviceTitle,
|
|
181
196
|
description: metaProps.description,
|
|
182
|
-
|
|
183
|
-
facebookImage: metaProps.facebookImage
|
|
197
|
+
image: metaProps.image
|
|
184
198
|
})
|
|
185
199
|
//TODO: add logic to cleanup custom page meta tags on page unload
|
|
186
200
|
}
|
|
@@ -188,22 +202,32 @@ export function resetPageMetadata(meta) {
|
|
|
188
202
|
/**
|
|
189
203
|
* React hook for setting page metadata
|
|
190
204
|
* @param {PageMeta} meta - Page metadata
|
|
191
|
-
* @param {*[]} dependencies
|
|
192
205
|
*/
|
|
193
|
-
export function usePageMetadata(meta
|
|
206
|
+
export function usePageMetadata(meta) {
|
|
194
207
|
useEffect(() => {
|
|
195
208
|
setPageMetadata(meta)
|
|
196
209
|
return () => resetPageMetadata(meta)
|
|
197
|
-
}, [JSON.stringify(meta),
|
|
210
|
+
}, [JSON.stringify(meta), [formatCanonicalUrl()]])
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function setPageNoIndex(noIndex) {
|
|
214
|
+
if (!noIndex) {
|
|
215
|
+
removeTag('meta[name=robots]')
|
|
216
|
+
} else {
|
|
217
|
+
replaceMetaTags({
|
|
218
|
+
locator: 'name',
|
|
219
|
+
tags: [
|
|
220
|
+
{name: 'robots', content: 'noindex,nofollow'}
|
|
221
|
+
]
|
|
222
|
+
})
|
|
223
|
+
}
|
|
198
224
|
}
|
|
199
225
|
|
|
200
226
|
/**
|
|
201
227
|
* @typedef {Object} PageMeta
|
|
202
228
|
* @property {String} title - Page title
|
|
203
229
|
* @property {String} description - Contents description
|
|
204
|
-
* @property {String} [
|
|
205
|
-
* @property {String} [facebookImage] - Facebook image url
|
|
206
|
-
* @property {String} [image] - Sets the image for both the Twitter image and the Facebook image
|
|
230
|
+
* @property {String} [image] - Page image url
|
|
207
231
|
* @property {MetaTagReplacement} [customMeta] - Custom metadata tags
|
|
208
232
|
*/
|
|
209
233
|
|