Actually hide NSFW content #1748
16 changed files with 690 additions and 676 deletions
|
@ -48,7 +48,7 @@
|
|||
"formik": "^0.10.4",
|
||||
"hast-util-sanitize": "^1.1.2",
|
||||
"keytar": "^4.2.1",
|
||||
"lbry-redux": "lbryio/lbry-redux#a0d2d1ac532ade639d39c92f79678ac26e904dfd",
|
||||
"lbry-redux": "lbryio/lbry-redux#177ef2c1916f9672e713267500e447d671ae1bc3",
|
||||
"localforage": "^1.7.1",
|
||||
"mime": "^2.3.1",
|
||||
"mixpanel-browser": "^2.17.1",
|
||||
|
|
|
@ -4,46 +4,20 @@ import classnames from 'classnames';
|
|||
|
||||
type Props = {
|
||||
thumbnail: ?string, // externally sourced image
|
||||
nsfw: ?boolean,
|
||||
};
|
||||
|
||||
const autoThumbColors = [
|
||||
'purple',
|
||||
'red',
|
||||
'pink',
|
||||
'indigo',
|
||||
'blue',
|
||||
'light-blue',
|
||||
'cyan',
|
||||
'teal',
|
||||
'green',
|
||||
'yellow',
|
||||
'orange',
|
||||
];
|
||||
|
||||
class CardMedia extends React.PureComponent<Props> {
|
||||
getAutoThumbClass = () => autoThumbColors[Math.floor(Math.random() * autoThumbColors.length)];
|
||||
|
||||
render() {
|
||||
const { thumbnail, nsfw } = this.props;
|
||||
|
||||
const generateAutothumb = !thumbnail && !nsfw;
|
||||
let autoThumbClass;
|
||||
if (generateAutothumb) {
|
||||
autoThumbClass = `card__media--autothumb.${this.getAutoThumbClass()}`;
|
||||
}
|
||||
const { thumbnail } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={thumbnail && !nsfw ? { backgroundImage: `url('${thumbnail}')` } : {}}
|
||||
className={classnames('card__media', autoThumbClass, {
|
||||
'card__media--no-img': !thumbnail || nsfw,
|
||||
'card__media--nsfw': nsfw,
|
||||
style={thumbnail ? { backgroundImage: `url('${thumbnail}')` } : {}}
|
||||
className={classnames('card__media', {
|
||||
'card__media--no-img': !thumbnail,
|
||||
})}
|
||||
>
|
||||
{(!thumbnail || nsfw) && (
|
||||
<span className="card__media-text">{nsfw ? __('NSFW') : 'LBRY'}</span>
|
||||
)}
|
||||
{!thumbnail && <span className="card__media-text">LBRY</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -249,7 +249,7 @@ class CategoryList extends React.PureComponent<Props, State> {
|
|||
)}
|
||||
</div>
|
||||
{obscureNsfw && isCommunityTopBids ? (
|
||||
<div className="card__content help">
|
||||
<div className="card-row__message help">
|
||||
{__(
|
||||
'The community top bids section is only visible if you allow mature content in the app. You can change your content viewing preferences'
|
||||
)}{' '}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { normalizeURI, convertToShareLink } from 'lbry-redux';
|
||||
import Button from 'component/button';
|
||||
import type { Claim, Metadata } from 'types/claim';
|
||||
import CardMedia from 'component/cardMedia';
|
||||
import TruncatedText from 'component/common/truncated-text';
|
||||
import Icon from 'component/common/icon';
|
||||
|
@ -14,9 +14,9 @@ import { openCopyLinkMenu } from '../../util/contextMenu';
|
|||
// TODO: iron these out
|
||||
type Props = {
|
||||
uri: string,
|
||||
claim: ?{ claim_id: string },
|
||||
claim: ?Claim,
|
||||
fileInfo: ?{},
|
||||
metadata: ?{ nsfw: boolean, title: string, thumbnail: ?string },
|
||||
metadata: ?Metadata,
|
||||
navigate: (string, ?{}) => void,
|
||||
rewardedContentClaimIds: Array<string>,
|
||||
obscureNsfw: boolean,
|
||||
|
@ -62,10 +62,15 @@ class FileCard extends React.PureComponent<Props> {
|
|||
showPrice,
|
||||
pending,
|
||||
} = this.props;
|
||||
|
||||
const shouldHide = !claimIsMine && !pending && obscureNsfw && metadata && metadata.nsfw;
|
||||
if (shouldHide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uri = !pending ? normalizeURI(this.props.uri) : this.props.uri;
|
||||
const title = metadata && metadata.title ? metadata.title : uri;
|
||||
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
|
||||
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw && !claimIsMine;
|
||||
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||
const handleContextMenu = event => {
|
||||
event.preventDefault();
|
||||
|
@ -86,46 +91,26 @@ class FileCard extends React.PureComponent<Props> {
|
|||
})}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<CardMedia nsfw={shouldObscureNsfw} thumbnail={thumbnail} />
|
||||
<CardMedia thumbnail={thumbnail} />
|
||||
<div className="card-media__internal-links">{showPrice && <FilePrice uri={uri} />}</div>
|
||||
|
||||
{shouldObscureNsfw ? (
|
||||
<div className="card__title-identity">
|
||||
<div className="card__title--small">
|
||||
<TruncatedText lines={3}>
|
||||
{__('This content is obscured because it is NSFW. You can change this in ')}
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Settings.')}
|
||||
onClick={e => {
|
||||
// Don't propagate to the onClick handler of parent element
|
||||
e.stopPropagation();
|
||||
navigate('/settings');
|
||||
}}
|
||||
/>
|
||||
</TruncatedText>
|
||||
</div>
|
||||
<div className="card__title-identity">
|
||||
<div className="card__title--small">
|
||||
<TruncatedText lines={3}>{title}</TruncatedText>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card__title-identity">
|
||||
<div className="card__title--small">
|
||||
<TruncatedText lines={3}>{title}</TruncatedText>
|
||||
</div>
|
||||
<div className="card__subtitle">
|
||||
{pending ? (
|
||||
<div>Pending...</div>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<UriIndicator uri={uri} link />
|
||||
<div>
|
||||
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
|
||||
{fileInfo && <Icon icon={icons.LOCAL} />}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
<div className="card__subtitle">
|
||||
{pending ? (
|
||||
<div>Pending...</div>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<UriIndicator uri={uri} link />
|
||||
<div>
|
||||
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
|
||||
{fileInfo && <Icon icon={icons.LOCAL} />}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/click-events-have-key-events */
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import FileTile from 'component/fileTile';
|
||||
import ChannelTile from 'component/channelTile';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
|
||||
|
||||
const NoResults = () => <div className="file-tile">{__('No results')}</div>;
|
||||
|
||||
|
@ -11,7 +12,6 @@ type Props = {
|
|||
isSearching: boolean,
|
||||
uris: ?Array<string>,
|
||||
downloadUris: ?Array<string>,
|
||||
resultCount: number,
|
||||
};
|
||||
|
||||
class FileListSearch extends React.PureComponent<Props> {
|
||||
|
@ -33,36 +33,37 @@ class FileListSearch extends React.PureComponent<Props> {
|
|||
|
||||
return (
|
||||
query && (
|
||||
<div className="search__results">
|
||||
<div className="search-result__row">
|
||||
<div className="file-list__header">{__('Content')}</div>
|
||||
{!isSearching &&
|
||||
(fileResults.length ? (
|
||||
<React.Fragment>
|
||||
<div className="search__results">
|
||||
<div className="search-result__row">
|
||||
<div className="file-list__header">{__('Content')}</div>
|
||||
<HiddenNsfwClaims uris={uris} />
|
||||
{!isSearching && fileResults.length ? (
|
||||
fileResults.map(uri => <FileTile key={uri} uri={uri} />)
|
||||
) : (
|
||||
<NoResults />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="search-result__row">
|
||||
<div className="file-list__header">{__('Channels')}</div>
|
||||
{!isSearching &&
|
||||
(channelResults.length ? (
|
||||
<div className="search-result__row">
|
||||
<div className="file-list__header">{__('Channels')}</div>
|
||||
{!isSearching && channelResults.length ? (
|
||||
channelResults.map(uri => <ChannelTile key={uri} uri={uri} />)
|
||||
) : (
|
||||
<NoResults />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="search-result__row">
|
||||
<div className="file-list__header">{__('Your downloads')}</div>
|
||||
{downloadUris && downloadUris.length ? (
|
||||
downloadUris.map(uri => <FileTile hideNoResult key={uri} uri={uri} />)
|
||||
) : (
|
||||
<NoResults />
|
||||
)}
|
||||
<div className="search-result__row">
|
||||
<div className="file-list__header">{__('Your downloads')}</div>
|
||||
{downloadUris && downloadUris.length ? (
|
||||
downloadUris.map(uri => <FileTile hideNoResult key={uri} uri={uri} />)
|
||||
) : (
|
||||
<NoResults />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,4 +30,7 @@ const perform = dispatch => ({
|
|||
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FileTile);
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileTile);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import * as icons from 'constants/icons';
|
||||
import type { Claim, Metadata } from 'types/claim';
|
||||
import { normalizeURI, parseURI } from 'lbry-redux';
|
||||
import CardMedia from 'component/cardMedia';
|
||||
import TruncatedText from 'component/common/truncated-text';
|
||||
|
@ -19,20 +20,14 @@ type Props = {
|
|||
uri: string,
|
||||
isResolvingUri: boolean,
|
||||
rewardedContentClaimIds: Array<string>,
|
||||
claim: ?{
|
||||
name: string,
|
||||
channel_name: string,
|
||||
claim_id: string,
|
||||
},
|
||||
metadata: ?{
|
||||
title: ?string,
|
||||
thumbnail: ?string,
|
||||
},
|
||||
claim: ?Claim,
|
||||
metadata: ?Metadata,
|
||||
resolveUri: string => void,
|
||||
navigate: (string, ?{}) => void,
|
||||
clearPublish: () => void,
|
||||
updatePublishForm: ({}) => void,
|
||||
hideNoResult: boolean, // don't show the tile if there is no claim at this uri
|
||||
displayHiddenMessage?: boolean,
|
||||
};
|
||||
|
||||
class FileTile extends React.PureComponent<Props> {
|
||||
|
@ -68,8 +63,20 @@ class FileTile extends React.PureComponent<Props> {
|
|||
clearPublish,
|
||||
updatePublishForm,
|
||||
hideNoResult,
|
||||
displayHiddenMessage,
|
||||
} = this.props;
|
||||
|
||||
const shouldHide = !claimIsMine && obscureNsfw && metadata && metadata.nsfw;
|
||||
if (shouldHide) {
|
||||
return displayHiddenMessage ? (
|
||||
<span className="help">
|
||||
{__('This file is hidden because it is marked NSFW. Update your')}{' '}
|
||||
<Button button="link" navigate="/settings" label={__('content viewing preferences')} />{' '}
|
||||
{__('to see it')}.
|
||||
</span>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const uri = normalizeURI(this.props.uri);
|
||||
const isClaimed = !!claim;
|
||||
const description = isClaimed && metadata && metadata.description ? metadata.description : '';
|
||||
|
@ -77,8 +84,6 @@ class FileTile extends React.PureComponent<Props> {
|
|||
isClaimed && metadata && metadata.title ? metadata.title : parseURI(uri).contentName;
|
||||
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
|
||||
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw && !claimIsMine;
|
||||
|
||||
const onClick = () => navigate('/show', { uri });
|
||||
|
||||
let name;
|
||||
|
@ -98,7 +103,7 @@ class FileTile extends React.PureComponent<Props> {
|
|||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<CardMedia title={title || name} thumbnail={thumbnail} nsfw={shouldObscureNsfw} />
|
||||
<CardMedia title={title || name} thumbnail={thumbnail} />
|
||||
<div className="file-tile__info">
|
||||
{isResolvingUri && <div className="card__title--small">{__('Loading...')}</div>}
|
||||
{!isResolvingUri && (
|
||||
|
|
30
src/renderer/component/hiddenNsfwClaims/index.js
Normal file
30
src/renderer/component/hiddenNsfwClaims/index.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectNsfwCountForChannel, makeSelectNsfwCountFromUris, parseURI } from 'lbry-redux';
|
||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||
import HiddenNsfwClaims from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { uri, uris } = props;
|
||||
|
||||
let numberOfNsfwClaims;
|
||||
if (uri) {
|
||||
const { isChannel } = parseURI(uri);
|
||||
numberOfNsfwClaims = isChannel
|
||||
? makeSelectNsfwCountForChannel(uri)(state)
|
||||
: makeSelectNsfwCountFromUris([uri])(state);
|
||||
} else if (uris) {
|
||||
numberOfNsfwClaims = makeSelectNsfwCountFromUris(uris)(state);
|
||||
}
|
||||
|
||||
return {
|
||||
numberOfNsfwClaims,
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = () => ({});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(HiddenNsfwClaims);
|
23
src/renderer/component/hiddenNsfwClaims/view.jsx
Normal file
23
src/renderer/component/hiddenNsfwClaims/view.jsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
|
||||
type Props = {
|
||||
numberOfNsfwClaims: number,
|
||||
obscureNsfw: boolean,
|
||||
className: ?string,
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const { numberOfNsfwClaims, obscureNsfw, className } = props;
|
||||
return (
|
||||
obscureNsfw &&
|
||||
Boolean(numberOfNsfwClaims) && (
|
||||
<div className={className || 'help'}>
|
||||
{numberOfNsfwClaims} {numberOfNsfwClaims > 1 ? __('files') : __('file')}{' '}
|
||||
{__('hidden due to your')}{' '}
|
||||
<Button button="link" navigate="/settings" label={__('content viewing preferences')} />.
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
|
@ -5,6 +5,7 @@ import {
|
|||
makeSelectClaimsInChannelForCurrentPage,
|
||||
makeSelectFetchingChannelClaims,
|
||||
makeSelectCurrentParam,
|
||||
makeSelectClaimIsMine,
|
||||
selectCurrentParams,
|
||||
} from 'lbry-redux';
|
||||
import { doNavigate } from 'redux/actions/navigation';
|
||||
|
@ -18,6 +19,7 @@ const select = (state, props) => ({
|
|||
page: makeSelectCurrentParam('page')(state),
|
||||
params: selectCurrentParams(state),
|
||||
totalPages: makeSelectTotalPagesForChannel(props.uri)(state),
|
||||
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
@ -26,4 +28,7 @@ const perform = dispatch => ({
|
|||
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ChannelPage);
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(ChannelPage);
|
||||
|
|
|
@ -7,6 +7,7 @@ import SubscribeButton from 'component/subscribeButton';
|
|||
import ViewOnWebButton from 'component/viewOnWebButton';
|
||||
import Page from 'component/page';
|
||||
import FileList from 'component/fileList';
|
||||
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
|
||||
import type { Claim } from 'types/claim';
|
||||
|
||||
type Props = {
|
||||
|
@ -16,7 +17,8 @@ type Props = {
|
|||
fetching: boolean,
|
||||
params: { page: number },
|
||||
claim: Claim,
|
||||
claimsInChannel: Array<{}>,
|
||||
claimsInChannel: Array<Claim>,
|
||||
channelIsMine: boolean,
|
||||
fetchClaims: (string, number) => void,
|
||||
fetchClaimCount: string => void,
|
||||
navigate: (string, {}) => void,
|
||||
|
@ -51,9 +53,10 @@ class ChannelPage extends React.PureComponent<Props> {
|
|||
paginate(e: SyntheticKeyboardEvent<*>, totalPages: number) {
|
||||
// Change page if enter was pressed, and the given page is between
|
||||
// the first and the last.
|
||||
const pageFromInput = Number(e.target.value);
|
||||
const pageFromInput = Number(e.currentTarget.value);
|
||||
|
||||
if (
|
||||
pageFromInput &&
|
||||
e.keyCode === 13 &&
|
||||
!Number.isNaN(pageFromInput) &&
|
||||
pageFromInput > 0 &&
|
||||
|
@ -64,7 +67,7 @@ class ChannelPage extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { fetching, claimsInChannel, claim, page, totalPages } = this.props;
|
||||
const { uri, fetching, claimsInChannel, claim, page, totalPages, channelIsMine } = this.props;
|
||||
const { name, permanent_url: permanentUrl, claim_id: claimId } = claim;
|
||||
const currentPage = parseInt((page || 1) - 1, 10);
|
||||
|
||||
|
@ -116,6 +119,7 @@ class ChannelPage extends React.PureComponent<Props> {
|
|||
/>
|
||||
</FormRow>
|
||||
)}
|
||||
{!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ class SearchPage extends React.PureComponent<Props> {
|
|||
<Icon icon={icons.HELP} />
|
||||
</ToolTip>
|
||||
</div>
|
||||
<FileTile fullWidth uri={normalizeURI(query)} showUri />
|
||||
<FileTile fullWidth showUri displayHiddenMessage uri={normalizeURI(query)} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
<FileListSearch query={query} />
|
||||
|
|
|
@ -7,6 +7,7 @@ import Button from 'component/button';
|
|||
import FileList from 'component/fileList';
|
||||
import type { Claim } from 'types/claim';
|
||||
import isDev from 'electron-is-dev';
|
||||
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
|
||||
|
||||
type Props = {
|
||||
doFetchClaimsByChannel: (string, number) => void,
|
||||
|
@ -51,9 +52,10 @@ export default class extends React.PureComponent<Props> {
|
|||
if (claim.claims.length) {
|
||||
subscriptionClaimMap[claim.uri] = 1;
|
||||
} else if (isDev) {
|
||||
console.error(
|
||||
`claim for ${claim.uri} was added to byId in redux but there are no loaded fetched claims`
|
||||
);
|
||||
console
|
||||
.error
|
||||
// `claim for ${claim.uri} was added to byId in redux but there are no loaded fetched claims`
|
||||
();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -72,8 +74,11 @@ export default class extends React.PureComponent<Props> {
|
|||
claimList = claimList.concat(claimData.claims);
|
||||
});
|
||||
|
||||
const subscriptionUris = claimList.map(claim => `lbry://${claim.name}#${claim.claim_id}`);
|
||||
|
||||
return (
|
||||
<Page notContained loading={isFetchingSubscriptions}>
|
||||
<HiddenNsfwClaims uris={subscriptionUris} />
|
||||
{!subscriptions.length && (
|
||||
<div className="page__empty">
|
||||
{__("It looks like you aren't subscribed to any channels yet.")}
|
||||
|
|
|
@ -281,14 +281,6 @@
|
|||
&:last-of-type {
|
||||
padding-bottom: $spacing-vertical * 2/3;
|
||||
}
|
||||
|
||||
// This is only for the text that is displayed when a user has nsfw hidden
|
||||
// It is not used anywhere else in the app and is needed due to the css related to
|
||||
// the content that scrolls off the edge of the screen
|
||||
.card__content.help {
|
||||
padding: 0 $spacing-width;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.card-row__header {
|
||||
|
@ -350,6 +342,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.card-row__message {
|
||||
padding: 0 $spacing-width;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/*
|
||||
How cards are displayed in lists
|
||||
*/
|
||||
|
@ -357,7 +354,7 @@
|
|||
.card {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: 60px;
|
||||
margin-bottom: $spacing-vertical * 3/2;
|
||||
|
||||
@media only screen and (max-width: $medium-breakpoint) {
|
||||
width: calc((100% / 4) - (60px / 4)); // 60px === 20px margin-right * three cards
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
// @flow
|
||||
|
||||
// Currently incomplete
|
||||
export type Metadata = {
|
||||
nsfw: boolean,
|
||||
title: string,
|
||||
thumbnail: ?string,
|
||||
description: ?string,
|
||||
};
|
||||
|
||||
// Actual claim type has more values than this
|
||||
// Add them as they are used
|
||||
export type Claim = {
|
||||
|
@ -22,9 +30,10 @@ export type Claim = {
|
|||
nout: number,
|
||||
signature_is_valid: boolean,
|
||||
valid_at_height: number,
|
||||
value: {
|
||||
value: ?{
|
||||
publisherSignature: ?{
|
||||
certificateId: ?string,
|
||||
},
|
||||
stream: ?Metadata,
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue