hyperchats
This commit is contained in:
parent
bd62a55608
commit
6d89f0df7e
66 changed files with 1594 additions and 433 deletions
35
flow-typed/Comment.js
vendored
35
flow-typed/Comment.js
vendored
|
@ -12,11 +12,13 @@ declare type Comment = {
|
||||||
is_channel_signature_valid?: boolean, // whether or not the signature could be validated
|
is_channel_signature_valid?: boolean, // whether or not the signature could be validated
|
||||||
parent_id?: number, // comment_id of comment this is in reply to
|
parent_id?: number, // comment_id of comment this is in reply to
|
||||||
is_pinned: boolean,
|
is_pinned: boolean,
|
||||||
|
support_amount: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
// todo: relate individual comments to their commentId
|
// todo: relate individual comments to their commentId
|
||||||
declare type CommentsState = {
|
declare type CommentsState = {
|
||||||
commentsByUri: { [string]: string },
|
commentsByUri: { [string]: string },
|
||||||
|
superChatsByUri: { [string]: { totalAmount: number, comments: Array<Comment> } },
|
||||||
byId: { [string]: Array<string> },
|
byId: { [string]: Array<string> },
|
||||||
repliesByParentId: { [string]: Array<string> }, // ParentCommentID -> list of reply comments
|
repliesByParentId: { [string]: Array<string> }, // ParentCommentID -> list of reply comments
|
||||||
topLevelCommentsById: { [string]: Array<string> }, // ClaimID -> list of top level comments
|
topLevelCommentsById: { [string]: Array<string> }, // ClaimID -> list of top level comments
|
||||||
|
@ -41,3 +43,36 @@ declare type CommentReactParams = {
|
||||||
clear_types?: string,
|
clear_types?: string,
|
||||||
remove?: boolean,
|
remove?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @flow
|
||||||
|
declare type CommentListParams = {
|
||||||
|
page: number,
|
||||||
|
page_size: number,
|
||||||
|
claim_id: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type CommentListResponse = {
|
||||||
|
items: Array<Comment>,
|
||||||
|
total_amount: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type CommentAbandonParams = {
|
||||||
|
comment_id: string,
|
||||||
|
creator_channel_id?: string,
|
||||||
|
creator_channel_name?: string,
|
||||||
|
channel_id?: string,
|
||||||
|
hexdata?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type CommentCreateParams = {
|
||||||
|
comment: string,
|
||||||
|
claim_id: string,
|
||||||
|
parent_id?: string,
|
||||||
|
signature: string,
|
||||||
|
signing_ts: number,
|
||||||
|
support_tx_id?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type SuperListParams = {};
|
||||||
|
|
||||||
|
declare type ModerationBlockParams = {};
|
||||||
|
|
16
flow-typed/comments.js
vendored
16
flow-typed/comments.js
vendored
|
@ -1,16 +0,0 @@
|
||||||
// @flow
|
|
||||||
declare type CommentListParams = {
|
|
||||||
page: number,
|
|
||||||
page_size: number,
|
|
||||||
claim_id: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
declare type CommentAbandonParams = {
|
|
||||||
comment_id: string,
|
|
||||||
creator_channel_id?: string,
|
|
||||||
creator_channel_name?: string,
|
|
||||||
channel_id?: string,
|
|
||||||
hexdata?: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
declare type ModerationBlockParams = {};
|
|
1
flow-typed/user.js
vendored
1
flow-typed/user.js
vendored
|
@ -30,4 +30,5 @@ declare type User = {
|
||||||
experimental_ui: boolean,
|
experimental_ui: boolean,
|
||||||
odysee_live_enabled: boolean,
|
odysee_live_enabled: boolean,
|
||||||
odysee_live_disabled: boolean,
|
odysee_live_disabled: boolean,
|
||||||
|
global_mod: boolean,
|
||||||
};
|
};
|
||||||
|
|
|
@ -142,7 +142,7 @@
|
||||||
"imagesloaded": "^4.1.4",
|
"imagesloaded": "^4.1.4",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||||
"lbry-redux": "lbryio/lbry-redux#eb37009a987410a60e9f2ba79708049c9904687c",
|
"lbry-redux": "lbryio/lbry-redux#7e173446838b381491492526ff29ca8312819879",
|
||||||
"lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
|
"lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
|
||||||
"lint-staged": "^7.0.2",
|
"lint-staged": "^7.0.2",
|
||||||
"localforage": "^1.7.1",
|
"localforage": "^1.7.1",
|
||||||
|
|
|
@ -10,6 +10,8 @@ const Comments = {
|
||||||
moderation_block_list: (params: ModerationBlockParams) => fetchCommentsApi('moderation.BlockedList', params),
|
moderation_block_list: (params: ModerationBlockParams) => fetchCommentsApi('moderation.BlockedList', params),
|
||||||
comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params),
|
comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params),
|
||||||
comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
|
comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
|
||||||
|
comment_create: (params: CommentCreateParams) => fetchCommentsApi('comment.Create', params),
|
||||||
|
super_list: (params: SuperListParams) => fetchCommentsApi('comment.SuperChatList', params),
|
||||||
};
|
};
|
||||||
|
|
||||||
function fetchCommentsApi(method: string, params: {}) {
|
function fetchCommentsApi(method: string, params: {}) {
|
||||||
|
|
|
@ -19,6 +19,7 @@ type Props = {
|
||||||
isResolving: boolean,
|
isResolving: boolean,
|
||||||
showDelayedMessage?: boolean,
|
showDelayedMessage?: boolean,
|
||||||
hideStakedIndicator?: boolean,
|
hideStakedIndicator?: boolean,
|
||||||
|
xsmall?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChannelThumbnail(props: Props) {
|
function ChannelThumbnail(props: Props) {
|
||||||
|
@ -29,6 +30,7 @@ function ChannelThumbnail(props: Props) {
|
||||||
thumbnailPreview: rawThumbnailPreview,
|
thumbnailPreview: rawThumbnailPreview,
|
||||||
obscure,
|
obscure,
|
||||||
small = false,
|
small = false,
|
||||||
|
xsmall = false,
|
||||||
allowGifs = false,
|
allowGifs = false,
|
||||||
claim,
|
claim,
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
|
@ -72,6 +74,7 @@ function ChannelThumbnail(props: Props) {
|
||||||
className={classnames('channel-thumbnail', className, {
|
className={classnames('channel-thumbnail', className, {
|
||||||
[colorClassName]: !showThumb,
|
[colorClassName]: !showThumb,
|
||||||
'channel-thumbnail--small': small,
|
'channel-thumbnail--small': small,
|
||||||
|
'channel-thumbnail--xsmall': xsmall,
|
||||||
'channel-thumbnail--resolving': isResolving,
|
'channel-thumbnail--resolving': isResolving,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
@ -30,12 +30,12 @@ type Props = {
|
||||||
persistedStorageKey?: string,
|
persistedStorageKey?: string,
|
||||||
showHiddenByUser: boolean,
|
showHiddenByUser: boolean,
|
||||||
showUnresolvedClaims?: boolean,
|
showUnresolvedClaims?: boolean,
|
||||||
renderProperties: ?(Claim) => Node,
|
renderActions?: (Claim) => ?Node,
|
||||||
|
renderProperties?: (Claim) => ?Node,
|
||||||
includeSupportAction?: boolean,
|
includeSupportAction?: boolean,
|
||||||
injectedItem: ?Node,
|
injectedItem: ?Node,
|
||||||
timedOutMessage?: Node,
|
timedOutMessage?: Node,
|
||||||
tileLayout?: boolean,
|
tileLayout?: boolean,
|
||||||
renderActions?: (Claim) => ?Node,
|
|
||||||
searchInLanguage: boolean,
|
searchInLanguage: boolean,
|
||||||
hideMenu?: boolean,
|
hideMenu?: boolean,
|
||||||
};
|
};
|
||||||
|
@ -55,12 +55,12 @@ export default function ClaimList(props: Props) {
|
||||||
page,
|
page,
|
||||||
showHiddenByUser,
|
showHiddenByUser,
|
||||||
showUnresolvedClaims,
|
showUnresolvedClaims,
|
||||||
renderProperties,
|
|
||||||
includeSupportAction,
|
includeSupportAction,
|
||||||
injectedItem,
|
injectedItem,
|
||||||
timedOutMessage,
|
timedOutMessage,
|
||||||
tileLayout = false,
|
tileLayout = false,
|
||||||
renderActions,
|
renderActions,
|
||||||
|
renderProperties,
|
||||||
searchInLanguage,
|
searchInLanguage,
|
||||||
hideMenu,
|
hideMenu,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -101,7 +101,9 @@ export default function ClaimList(props: Props) {
|
||||||
return tileLayout && !header ? (
|
return tileLayout && !header ? (
|
||||||
<section className="claim-grid">
|
<section className="claim-grid">
|
||||||
{urisLength > 0 &&
|
{urisLength > 0 &&
|
||||||
uris.map((uri) => <ClaimPreviewTile key={uri} uri={uri} showHiddenByUser={showHiddenByUser} />)}
|
uris.map((uri) => (
|
||||||
|
<ClaimPreviewTile key={uri} uri={uri} showHiddenByUser={showHiddenByUser} properties={renderProperties} />
|
||||||
|
))}
|
||||||
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
|
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
|
||||||
{timedOut && timedOutMessage && <div className="empty main--empty">{timedOutMessage}</div>}
|
{timedOut && timedOutMessage && <div className="empty main--empty">{timedOutMessage}</div>}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -301,10 +301,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
<ClaimPreviewTitle uri={uri} />
|
<ClaimPreviewTitle uri={uri} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* {type !== 'small' && !isChannelUri && signingChannel && SIMPLE_SITE && (
|
|
||||||
<ChannelThumbnail uri={signingChannel.permanent_url} />
|
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
<ClaimPreviewSubtitle uri={uri} type={type} />
|
<ClaimPreviewSubtitle uri={uri} type={type} />
|
||||||
{(pending || !!reflectingProgress) && <PublishPending uri={uri} />}
|
{(pending || !!reflectingProgress) && <PublishPending uri={uri} />}
|
||||||
|
|
|
@ -40,6 +40,7 @@ type Props = {
|
||||||
isMature: boolean,
|
isMature: boolean,
|
||||||
showMature: boolean,
|
showMature: boolean,
|
||||||
showHiddenByUser?: boolean,
|
showHiddenByUser?: boolean,
|
||||||
|
properties?: (Claim) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimPreviewTile(props: Props) {
|
function ClaimPreviewTile(props: Props) {
|
||||||
|
@ -60,6 +61,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
isMature,
|
isMature,
|
||||||
showMature,
|
showMature,
|
||||||
showHiddenByUser,
|
showHiddenByUser,
|
||||||
|
properties,
|
||||||
} = props;
|
} = props;
|
||||||
const isRepost = claim && claim.repost_channel_url;
|
const isRepost = claim && claim.repost_channel_url;
|
||||||
const shouldFetch = claim === undefined;
|
const shouldFetch = claim === undefined;
|
||||||
|
@ -171,7 +173,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
<div className="claim-preview__file-property-overlay">
|
<div className="claim-preview__file-property-overlay">
|
||||||
<FileProperties uri={uri} small />
|
<FileProperties uri={uri} small properties={properties} />
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config';
|
import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config';
|
||||||
import * as CS from 'constants/claim_search';
|
import * as CS from 'constants/claim_search';
|
||||||
|
import type { Node } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
|
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
|
||||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||||
|
@ -34,6 +35,8 @@ type Props = {
|
||||||
timestamp?: string,
|
timestamp?: string,
|
||||||
feeAmount?: string,
|
feeAmount?: string,
|
||||||
limitClaimsPerChannel?: number,
|
limitClaimsPerChannel?: number,
|
||||||
|
hasNoSource?: boolean,
|
||||||
|
renderProperties?: (Claim) => ?Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimTilesDiscover(props: Props) {
|
function ClaimTilesDiscover(props: Props) {
|
||||||
|
@ -57,6 +60,8 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
feeAmount,
|
feeAmount,
|
||||||
limitClaimsPerChannel,
|
limitClaimsPerChannel,
|
||||||
fetchingClaimSearchByQuery,
|
fetchingClaimSearchByQuery,
|
||||||
|
hasNoSource,
|
||||||
|
renderProperties,
|
||||||
} = props;
|
} = props;
|
||||||
const { location } = useHistory();
|
const { location } = useHistory();
|
||||||
const urlParams = new URLSearchParams(location.search);
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
@ -95,7 +100,9 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
stream_types: streamTypes === null ? undefined : SIMPLE_SITE ? [CS.FILE_VIDEO, CS.FILE_AUDIO] : undefined,
|
stream_types: streamTypes === null ? undefined : SIMPLE_SITE ? [CS.FILE_VIDEO, CS.FILE_AUDIO] : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!ENABLE_NO_SOURCE_CLAIMS && (!claimType || claimType === 'stream')) {
|
if (ENABLE_NO_SOURCE_CLAIMS && hasNoSource) {
|
||||||
|
options.has_no_source = true;
|
||||||
|
} else if (!ENABLE_NO_SOURCE_CLAIMS && (!claimType || claimType === 'stream')) {
|
||||||
options.has_source = true;
|
options.has_source = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,7 +156,7 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
return (
|
return (
|
||||||
<ul className="claim-grid">
|
<ul className="claim-grid">
|
||||||
{uris && uris.length
|
{uris && uris.length
|
||||||
? uris.map((uri) => <ClaimPreviewTile key={uri} uri={uri} />)
|
? uris.map((uri) => <ClaimPreviewTile key={uri} uri={uri} properties={renderProperties} />)
|
||||||
: new Array(pageSize).fill(1).map((x, i) => <ClaimPreviewTile key={i} placeholder />)}
|
: new Array(pageSize).fill(1).map((x, i) => <ClaimPreviewTile key={i} placeholder />)}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { useHistory } from 'react-router';
|
||||||
import CommentCreate from 'component/commentCreate';
|
import CommentCreate from 'component/commentCreate';
|
||||||
import CommentMenuList from 'component/commentMenuList';
|
import CommentMenuList from 'component/commentMenuList';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
clearPlayingUri: () => void,
|
clearPlayingUri: () => void,
|
||||||
|
@ -51,7 +52,7 @@ type Props = {
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
playingUri: ?PlayingUri,
|
playingUri: ?PlayingUri,
|
||||||
stakedLevel: number,
|
stakedLevel: number,
|
||||||
livestream?: boolean,
|
supportAmount: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LENGTH_TO_COLLAPSE = 300;
|
const LENGTH_TO_COLLAPSE = 300;
|
||||||
|
@ -80,7 +81,7 @@ function Comment(props: Props) {
|
||||||
othersReacts,
|
othersReacts,
|
||||||
playingUri,
|
playingUri,
|
||||||
stakedLevel,
|
stakedLevel,
|
||||||
livestream,
|
supportAmount,
|
||||||
} = props;
|
} = props;
|
||||||
const {
|
const {
|
||||||
push,
|
push,
|
||||||
|
@ -167,7 +168,7 @@ function Comment(props: Props) {
|
||||||
className={classnames('comment', {
|
className={classnames('comment', {
|
||||||
'comment--top-level': isTopLevel,
|
'comment--top-level': isTopLevel,
|
||||||
'comment--reply': !isTopLevel,
|
'comment--reply': !isTopLevel,
|
||||||
'comment--livestream': livestream,
|
'comment--superchat': supportAmount > 0,
|
||||||
})}
|
})}
|
||||||
id={commentId}
|
id={commentId}
|
||||||
onMouseOver={() => setMouseHover(true)}
|
onMouseOver={() => setMouseHover(true)}
|
||||||
|
@ -179,22 +180,15 @@ function Comment(props: Props) {
|
||||||
'comment--slimed': slimedToDeath && !displayDeadComment,
|
'comment--slimed': slimedToDeath && !displayDeadComment,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!livestream && (
|
|
||||||
<div className="comment__thumbnail-wrapper">
|
<div className="comment__thumbnail-wrapper">
|
||||||
{authorUri ? (
|
{authorUri ? (
|
||||||
<ChannelThumbnail
|
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} small className="comment__author-thumbnail" />
|
||||||
uri={authorUri}
|
|
||||||
obscure={channelIsBlocked}
|
|
||||||
small
|
|
||||||
className="comment__author-thumbnail"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<ChannelThumbnail small className="comment__author-thumbnail" />
|
<ChannelThumbnail small className="comment__author-thumbnail" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="comment__body_container">
|
<div className="comment__body-container">
|
||||||
<div className="comment__meta">
|
<div className="comment__meta">
|
||||||
<div className="comment__meta-information">
|
<div className="comment__meta-information">
|
||||||
{!author ? (
|
{!author ? (
|
||||||
|
@ -205,17 +199,16 @@ function Comment(props: Props) {
|
||||||
'comment__author--creator': commentByOwnerOfContent,
|
'comment__author--creator': commentByOwnerOfContent,
|
||||||
})}
|
})}
|
||||||
link
|
link
|
||||||
external={livestream}
|
|
||||||
uri={authorUri}
|
uri={authorUri}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!livestream && (
|
|
||||||
<Button
|
<Button
|
||||||
className="comment__time"
|
className="comment__time"
|
||||||
onClick={handleTimeClick}
|
onClick={handleTimeClick}
|
||||||
label={<DateTime date={timePosted} timeAgo />}
|
label={<DateTime date={timePosted} timeAgo />}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
{supportAmount > 0 && <CreditAmount amount={supportAmount} superChatLight size={12} />}
|
||||||
|
|
||||||
{isPinned && (
|
{isPinned && (
|
||||||
<span className="comment__pin">
|
<span className="comment__pin">
|
||||||
|
@ -251,6 +244,7 @@ function Comment(props: Props) {
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<FormField
|
<FormField
|
||||||
|
className="comment__edit-input"
|
||||||
type={!SIMPLE_SITE && advancedEditor ? 'markdown' : 'textarea'}
|
type={!SIMPLE_SITE && advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="editing_comment"
|
name="editing_comment"
|
||||||
value={editedMessage}
|
value={editedMessage}
|
||||||
|
@ -258,7 +252,7 @@ function Comment(props: Props) {
|
||||||
onChange={handleEditMessageChanged}
|
onChange={handleEditMessageChanged}
|
||||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||||
/>
|
/>
|
||||||
<div className="section__actions">
|
<div className="section__actions section__actions--no-margin">
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -295,7 +289,6 @@ function Comment(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!livestream && (
|
|
||||||
<div className="comment__actions">
|
<div className="comment__actions">
|
||||||
{threadDepth !== 0 && (
|
{threadDepth !== 0 && (
|
||||||
<Button
|
<Button
|
||||||
|
@ -308,7 +301,6 @@ function Comment(props: Props) {
|
||||||
)}
|
)}
|
||||||
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
|
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{isReplying && (
|
{isReplying && (
|
||||||
<CommentCreate
|
<CommentCreate
|
||||||
|
|
|
@ -4,8 +4,8 @@ import {
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
selectMyChannelClaims,
|
selectMyChannelClaims,
|
||||||
selectFetchingMyChannels,
|
selectFetchingMyChannels,
|
||||||
|
doSendTip,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectIsPostingComment } from 'redux/selectors/comments';
|
|
||||||
import { doOpenModal, doSetActiveChannel } from 'redux/actions/app';
|
import { doOpenModal, doSetActiveChannel } from 'redux/actions/app';
|
||||||
import { doCommentCreate } from 'redux/actions/comments';
|
import { doCommentCreate } from 'redux/actions/comments';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
@ -18,17 +18,17 @@ const select = (state, props) => ({
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
channels: selectMyChannelClaims(state),
|
channels: selectMyChannelClaims(state),
|
||||||
isFetchingChannels: selectFetchingMyChannels(state),
|
isFetchingChannels: selectFetchingMyChannels(state),
|
||||||
isPostingComment: selectIsPostingComment(state),
|
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch, ownProps) => ({
|
const perform = (dispatch, ownProps) => ({
|
||||||
createComment: (comment, claimId, parentId) =>
|
createComment: (comment, claimId, parentId, txid) =>
|
||||||
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream)),
|
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream, txid)),
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
||||||
toast: (message) => dispatch(doToast({ message, isError: true })),
|
toast: (message) => dispatch(doToast({ message, isError: true })),
|
||||||
|
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(CommentCreate);
|
export default connect(select, perform)(CommentCreate);
|
||||||
|
|
|
@ -1,25 +1,22 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import type { ElementRef } from 'react';
|
||||||
import { SIMPLE_SITE } from 'config';
|
import { SIMPLE_SITE } from 'config';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import React, { useEffect, useState } from 'react';
|
import * as ICONS from 'constants/icons';
|
||||||
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import { FormField, Form } from 'component/common/form';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import SelectChannel from 'component/selectChannel';
|
import SelectChannel from 'component/selectChannel';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_COMMENT, FF_MAX_CHARS_IN_LIVESTREAM_COMMENT } from 'constants/form-field';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import type { ElementRef } from 'react';
|
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||||
import emoji from 'emoji-dictionary';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
import UriIndicator from 'component/uriIndicator';
|
||||||
|
|
||||||
const COMMENT_SLOW_MODE_SECONDS = 5;
|
const COMMENT_SLOW_MODE_SECONDS = 5;
|
||||||
const LIVESTREAM_EMOJIS = [
|
|
||||||
emoji.getUnicode('rocket'),
|
|
||||||
emoji.getUnicode('jeans'),
|
|
||||||
emoji.getUnicode('fire'),
|
|
||||||
emoji.getUnicode('heart'),
|
|
||||||
emoji.getUnicode('open_mouth'),
|
|
||||||
];
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -32,12 +29,12 @@ type Props = {
|
||||||
isFetchingChannels: boolean,
|
isFetchingChannels: boolean,
|
||||||
parentId: string,
|
parentId: string,
|
||||||
isReply: boolean,
|
isReply: boolean,
|
||||||
isPostingComment: boolean,
|
|
||||||
activeChannel: string,
|
activeChannel: string,
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
livestream?: boolean,
|
livestream?: boolean,
|
||||||
toast: (string) => void,
|
toast: (string) => void,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
|
sendTip: ({}, (any) => void, (any) => void) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CommentCreate(props: Props) {
|
export function CommentCreate(props: Props) {
|
||||||
|
@ -51,24 +48,28 @@ export function CommentCreate(props: Props) {
|
||||||
isFetchingChannels,
|
isFetchingChannels,
|
||||||
isReply,
|
isReply,
|
||||||
parentId,
|
parentId,
|
||||||
isPostingComment,
|
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
livestream,
|
livestream,
|
||||||
toast,
|
toast,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
|
sendTip,
|
||||||
} = props;
|
} = props;
|
||||||
const buttonref: ElementRef<any> = React.useRef();
|
const buttonref: ElementRef<any> = React.useRef();
|
||||||
const {
|
const {
|
||||||
push,
|
push,
|
||||||
location: { pathname },
|
location: { pathname },
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||||
const { claim_id: claimId } = claim;
|
const { claim_id: claimId } = claim;
|
||||||
|
const [isSupportComment, setIsSupportComment] = React.useState();
|
||||||
|
const [isReviewingSupportComment, setIsReviewingSupportComment] = React.useState();
|
||||||
|
const [tipAmount, setTipAmount] = React.useState(1);
|
||||||
const [commentValue, setCommentValue] = React.useState('');
|
const [commentValue, setCommentValue] = React.useState('');
|
||||||
const [lastCommentTime, setLastCommentTime] = React.useState();
|
const [lastCommentTime, setLastCommentTime] = React.useState();
|
||||||
const [charCount, setCharCount] = useState(commentValue.length);
|
|
||||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||||
const hasChannels = channels && channels.length;
|
const hasChannels = channels && channels.length;
|
||||||
const disabled = isPostingComment || !activeChannelClaim || !commentValue.length;
|
const disabled = isSubmitting || !activeChannelClaim || !commentValue.length;
|
||||||
|
const charCount = commentValue.length;
|
||||||
|
|
||||||
function handleCommentChange(event) {
|
function handleCommentChange(event) {
|
||||||
let commentValue;
|
let commentValue;
|
||||||
|
@ -108,25 +109,63 @@ export function CommentCreate(props: Props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createComment(commentValue, claimId, parentId).then((res) => {
|
handleCreateComment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSupportComment() {
|
||||||
|
if (!activeChannelClaim) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
amount: tipAmount,
|
||||||
|
claim_id: claimId,
|
||||||
|
channel_id: activeChannelClaim.claim_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
sendTip(
|
||||||
|
params,
|
||||||
|
(response) => {
|
||||||
|
const { txid } = response;
|
||||||
|
setTimeout(() => {
|
||||||
|
handleCreateComment(txid);
|
||||||
|
}, 1500);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateComment(txid) {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
createComment(commentValue, claimId, parentId, txid)
|
||||||
|
.then((res) => {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
|
||||||
if (res && res.signature) {
|
if (res && res.signature) {
|
||||||
setCommentValue('');
|
setCommentValue('');
|
||||||
setLastCommentTime(Date.now());
|
setLastCommentTime(Date.now());
|
||||||
|
setIsReviewingSupportComment(false);
|
||||||
|
setIsSupportComment(false);
|
||||||
|
|
||||||
if (onDoneReplying) {
|
if (onDoneReplying) {
|
||||||
onDoneReplying();
|
onDoneReplying();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setIsSubmitting(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function toggleEditorMode() {
|
function toggleEditorMode() {
|
||||||
setAdvancedEditor(!advancedEditor);
|
setAdvancedEditor(!advancedEditor);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => setCharCount(commentValue.length), [commentValue]);
|
|
||||||
|
|
||||||
if (!hasChannels) {
|
if (!hasChannels) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -146,13 +185,39 @@ export function CommentCreate(props: Props) {
|
||||||
placeholder={__('Say something about this...')}
|
placeholder={__('Say something about this...')}
|
||||||
label={isFetchingChannels ? __('Comment') : undefined}
|
label={isFetchingChannels ? __('Comment') : undefined}
|
||||||
/>
|
/>
|
||||||
<div className="section__actions">
|
<div className="section__actions--no-margin">
|
||||||
<Button disabled button="primary" label={__('Post --[button to submit something]--')} requiresAuth={IS_WEB} />
|
<Button disabled button="primary" label={__('Post --[button to submit something]--')} requiresAuth={IS_WEB} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isReviewingSupportComment && activeChannelClaim) {
|
||||||
|
return (
|
||||||
|
<div className="comment__create">
|
||||||
|
<div className="comment__sc-preview">
|
||||||
|
<CreditAmount className="comment__scpreview-amount" amount={tipAmount} size={18} />
|
||||||
|
|
||||||
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
|
<div>
|
||||||
|
<UriIndicator uri={activeChannelClaim.name} link />
|
||||||
|
<div>{commentValue}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="section__actions--no-margin">
|
||||||
|
<Button
|
||||||
|
autoFocus
|
||||||
|
button="primary"
|
||||||
|
disabled={disabled}
|
||||||
|
label={isSubmitting ? __('Sending...') : __('Send')}
|
||||||
|
onClick={handleSupportComment}
|
||||||
|
/>
|
||||||
|
<Button button="link" label={__('Cancel')} onClick={() => setIsReviewingSupportComment(false)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
@ -167,7 +232,9 @@ export function CommentCreate(props: Props) {
|
||||||
name={isReply ? 'content_reply' : 'content_description'}
|
name={isReply ? 'content_reply' : 'content_description'}
|
||||||
label={
|
label={
|
||||||
<span className="comment-new__label-wrapper">
|
<span className="comment-new__label-wrapper">
|
||||||
|
{!livestream && (
|
||||||
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
||||||
|
)}
|
||||||
<SelectChannel tiny />
|
<SelectChannel tiny />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
@ -182,26 +249,25 @@ export function CommentCreate(props: Props) {
|
||||||
charCount={charCount}
|
charCount={charCount}
|
||||||
onChange={handleCommentChange}
|
onChange={handleCommentChange}
|
||||||
autoFocus={isReply}
|
autoFocus={isReply}
|
||||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
|
||||||
/>
|
/>
|
||||||
{livestream && hasChannels && (
|
{isSupportComment && <WalletTipAmountSelector amount={tipAmount} onChange={(amount) => setTipAmount(amount)} />}
|
||||||
<div className="livestream__emoji-actions">
|
|
||||||
{LIVESTREAM_EMOJIS.map((emoji) => (
|
|
||||||
<Button
|
|
||||||
key={emoji}
|
|
||||||
disabled={isPostingComment}
|
|
||||||
type="button"
|
|
||||||
button="alt"
|
|
||||||
className="button--emoji"
|
|
||||||
label={emoji}
|
|
||||||
onClick={() => {
|
|
||||||
setCommentValue(commentValue ? `${commentValue} ${emoji}` : emoji);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="section__actions section__actions--no-margin">
|
<div className="section__actions section__actions--no-margin">
|
||||||
|
{isSupportComment ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
type="button"
|
||||||
|
button="primary"
|
||||||
|
icon={ICONS.LBC}
|
||||||
|
label={__('Review')}
|
||||||
|
onClick={() => setIsReviewingSupportComment(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button disabled={disabled} button="link" label={__('Cancel')} onClick={() => setIsSupportComment(false)} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
ref={buttonref}
|
ref={buttonref}
|
||||||
button="primary"
|
button="primary"
|
||||||
|
@ -209,15 +275,18 @@ export function CommentCreate(props: Props) {
|
||||||
type="submit"
|
type="submit"
|
||||||
label={
|
label={
|
||||||
isReply
|
isReply
|
||||||
? isPostingComment
|
? isSubmitting
|
||||||
? __('Replying...')
|
? __('Replying...')
|
||||||
: __('Reply')
|
: __('Reply')
|
||||||
: isPostingComment
|
: isSubmitting
|
||||||
? __('Posting...')
|
? __('Commenting...')
|
||||||
: __('Post --[button to submit something]--')
|
: __('Comment --[button to submit something]--')
|
||||||
}
|
}
|
||||||
requiresAuth={IS_WEB}
|
requiresAuth={IS_WEB}
|
||||||
/>
|
/>
|
||||||
|
{!claimIsMine && (
|
||||||
|
<Button disabled={disabled} button="alt" icon={ICONS.LBC} onClick={() => setIsSupportComment(true)} />
|
||||||
|
)}
|
||||||
{isReply && (
|
{isReply && (
|
||||||
<Button
|
<Button
|
||||||
button="link"
|
button="link"
|
||||||
|
@ -229,6 +298,8 @@ export function CommentCreate(props: Props) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,8 +16,8 @@ import Empty from 'component/common/empty';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
comments: Array<Comment>,
|
comments: Array<Comment>,
|
||||||
fetchComments: string => void,
|
fetchComments: (string) => void,
|
||||||
fetchReacts: string => Promise<any>,
|
fetchReacts: (string) => Promise<any>,
|
||||||
uri: string,
|
uri: string,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
myChannels: ?Array<ChannelClaim>,
|
myChannels: ?Array<ChannelClaim>,
|
||||||
|
@ -129,11 +129,11 @@ function CommentList(props: Props) {
|
||||||
|
|
||||||
if (linkedComment) {
|
if (linkedComment) {
|
||||||
if (!linkedComment.parent_id) {
|
if (!linkedComment.parent_id) {
|
||||||
orderedComments = arrayOfComments.filter(c => c.comment_id !== linkedComment.comment_id);
|
orderedComments = arrayOfComments.filter((c) => c.comment_id !== linkedComment.comment_id);
|
||||||
orderedComments.unshift(linkedComment);
|
orderedComments.unshift(linkedComment);
|
||||||
} else {
|
} else {
|
||||||
const parentComment = arrayOfComments.find(c => c.comment_id === linkedComment.parent_id);
|
const parentComment = arrayOfComments.find((c) => c.comment_id === linkedComment.parent_id);
|
||||||
orderedComments = arrayOfComments.filter(c => c.comment_id !== linkedComment.parent_id);
|
orderedComments = arrayOfComments.filter((c) => c.comment_id !== linkedComment.parent_id);
|
||||||
|
|
||||||
if (parentComment) {
|
if (parentComment) {
|
||||||
orderedComments.unshift(parentComment);
|
orderedComments.unshift(parentComment);
|
||||||
|
@ -218,7 +218,7 @@ function CommentList(props: Props) {
|
||||||
<ul className="comments" ref={commentRef}>
|
<ul className="comments" ref={commentRef}>
|
||||||
{comments &&
|
{comments &&
|
||||||
displayedComments &&
|
displayedComments &&
|
||||||
displayedComments.map(comment => {
|
displayedComments.map((comment) => {
|
||||||
return (
|
return (
|
||||||
<CommentView
|
<CommentView
|
||||||
isTopLevel
|
isTopLevel
|
||||||
|
@ -235,6 +235,7 @@ function CommentList(props: Props) {
|
||||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||||
linkedComment={linkedComment}
|
linkedComment={linkedComment}
|
||||||
isPinned={comment.is_pinned}
|
isPinned={comment.is_pinned}
|
||||||
|
supportAmount={comment.support_amount}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -22,7 +22,7 @@ function CommentsReplies(props: Props) {
|
||||||
const sortedComments = comments ? [...comments].reverse() : [];
|
const sortedComments = comments ? [...comments].reverse() : [];
|
||||||
const numberOfComments = comments ? comments.length : 0;
|
const numberOfComments = comments ? comments.length : 0;
|
||||||
const linkedCommentId = linkedComment ? linkedComment.comment_id : '';
|
const linkedCommentId = linkedComment ? linkedComment.comment_id : '';
|
||||||
const commentsIndexOfLInked = comments && sortedComments.findIndex(e => e.comment_id === linkedCommentId);
|
const commentsIndexOfLInked = comments && sortedComments.findIndex((e) => e.comment_id === linkedCommentId);
|
||||||
|
|
||||||
function showMore() {
|
function showMore() {
|
||||||
if (start > 0) {
|
if (start > 0) {
|
||||||
|
@ -105,6 +105,7 @@ function CommentsReplies(props: Props) {
|
||||||
linkedComment={linkedComment}
|
linkedComment={linkedComment}
|
||||||
commentingEnabled={commentingEnabled}
|
commentingEnabled={commentingEnabled}
|
||||||
handleCommentDone={handleCommentDone}
|
handleCommentDone={handleCommentDone}
|
||||||
|
supportAmount={comment.support_amount}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -47,10 +47,12 @@ export default function Card(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
|
role="button"
|
||||||
className={classnames(className, 'card')}
|
className={classnames(className, 'card')}
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick();
|
onClick();
|
||||||
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -15,6 +15,9 @@ type Props = {
|
||||||
fee?: boolean,
|
fee?: boolean,
|
||||||
className?: string,
|
className?: string,
|
||||||
noFormat?: boolean,
|
noFormat?: boolean,
|
||||||
|
size?: number,
|
||||||
|
superChat?: boolean,
|
||||||
|
superChatLight?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
class CreditAmount extends React.PureComponent<Props> {
|
class CreditAmount extends React.PureComponent<Props> {
|
||||||
|
@ -39,8 +42,10 @@ class CreditAmount extends React.PureComponent<Props> {
|
||||||
showLBC,
|
showLBC,
|
||||||
className,
|
className,
|
||||||
noFormat,
|
noFormat,
|
||||||
|
size,
|
||||||
|
superChat,
|
||||||
|
superChatLight,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const minimumRenderableAmount = 10 ** (-1 * precision);
|
const minimumRenderableAmount = 10 ** (-1 * precision);
|
||||||
const fullPrice = formatFullPrice(amount, 2);
|
const fullPrice = formatFullPrice(amount, 2);
|
||||||
const isFree = parseFloat(amount) === 0;
|
const isFree = parseFloat(amount) === 0;
|
||||||
|
@ -66,7 +71,7 @@ class CreditAmount extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showLBC) {
|
if (showLBC) {
|
||||||
amountText = <LbcSymbol postfix={amountText} />;
|
amountText = <LbcSymbol postfix={amountText} size={size} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fee) {
|
if (fee) {
|
||||||
|
@ -75,7 +80,13 @@ class CreditAmount extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span title={fullPrice} className={classnames(className, {})}>
|
<span
|
||||||
|
title={fullPrice}
|
||||||
|
className={classnames(className, {
|
||||||
|
'super-chat': superChat,
|
||||||
|
'super-chat--light': superChatLight,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<span className="credit-amount">{amountText}</span>
|
<span className="credit-amount">{amountText}</span>
|
||||||
|
|
||||||
{isEstimate ? (
|
{isEstimate ? (
|
||||||
|
|
|
@ -3,13 +3,13 @@ import * as React from 'react';
|
||||||
import { FormField } from './form-field';
|
import { FormField } from './form-field';
|
||||||
|
|
||||||
type FormPrice = {
|
type FormPrice = {
|
||||||
amount: ?number,
|
amount: number,
|
||||||
currency: string,
|
currency: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
price: FormPrice,
|
price: FormPrice,
|
||||||
onChange: FormPrice => void,
|
onChange: (FormPrice) => void,
|
||||||
placeholder: number,
|
placeholder: number,
|
||||||
min: number,
|
min: number,
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
|
@ -27,7 +27,7 @@ export class FormFieldPrice extends React.PureComponent<Props> {
|
||||||
|
|
||||||
handleAmountChange(event: SyntheticInputEvent<*>) {
|
handleAmountChange(event: SyntheticInputEvent<*>) {
|
||||||
const { price, onChange } = this.props;
|
const { price, onChange } = this.props;
|
||||||
const amount = event.target.value ? parseFloat(event.target.value) : undefined;
|
const amount = event.target.value ? parseFloat(event.target.value) : 0;
|
||||||
onChange({
|
onChange({
|
||||||
currency: price.currency,
|
currency: price.currency,
|
||||||
amount,
|
amount,
|
||||||
|
@ -54,7 +54,7 @@ export class FormFieldPrice extends React.PureComponent<Props> {
|
||||||
className="form-field--price-amount"
|
className="form-field--price-amount"
|
||||||
min={min}
|
min={min}
|
||||||
value={price.amount}
|
value={price.amount}
|
||||||
onWheel={e => e.preventDefault()}
|
onWheel={(e) => e.preventDefault()}
|
||||||
onChange={this.handleAmountChange}
|
onChange={this.handleAmountChange}
|
||||||
placeholder={placeholder || 5}
|
placeholder={placeholder || 5}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -8,6 +8,15 @@ import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||||
import 'easymde/dist/easymde.min.css';
|
import 'easymde/dist/easymde.min.css';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import emoji from 'emoji-dictionary';
|
||||||
|
|
||||||
|
const QUICK_EMOJIS = [
|
||||||
|
emoji.getUnicode('rocket'),
|
||||||
|
emoji.getUnicode('jeans'),
|
||||||
|
emoji.getUnicode('fire'),
|
||||||
|
emoji.getUnicode('heart'),
|
||||||
|
emoji.getUnicode('open_mouth'),
|
||||||
|
];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -26,9 +35,6 @@ type Props = {
|
||||||
affixClass?: string, // class applied to prefix/postfix label
|
affixClass?: string, // class applied to prefix/postfix label
|
||||||
autoFocus?: boolean,
|
autoFocus?: boolean,
|
||||||
labelOnLeft: boolean,
|
labelOnLeft: boolean,
|
||||||
inputProps?: {
|
|
||||||
disabled?: boolean,
|
|
||||||
},
|
|
||||||
inputButton?: React$Node,
|
inputButton?: React$Node,
|
||||||
blockWrap: boolean,
|
blockWrap: boolean,
|
||||||
charCount?: number,
|
charCount?: number,
|
||||||
|
@ -38,6 +44,9 @@ type Props = {
|
||||||
max?: number,
|
max?: number,
|
||||||
quickActionLabel?: string,
|
quickActionLabel?: string,
|
||||||
quickActionHandler?: (any) => any,
|
quickActionHandler?: (any) => any,
|
||||||
|
disabled?: boolean,
|
||||||
|
onChange: (any) => void,
|
||||||
|
value?: string | number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FormField extends React.PureComponent<Props> {
|
export class FormField extends React.PureComponent<Props> {
|
||||||
|
@ -262,7 +271,25 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
ref={this.input}
|
ref={this.input}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
|
<div className="form-field__textarea-info">
|
||||||
|
<div className="form-field__quick-emojis">
|
||||||
|
{QUICK_EMOJIS.map((emoji) => (
|
||||||
|
<Button
|
||||||
|
key={emoji}
|
||||||
|
disabled={inputProps.disabled}
|
||||||
|
type="button"
|
||||||
|
className="button--emoji"
|
||||||
|
label={emoji}
|
||||||
|
onClick={() => {
|
||||||
|
inputProps.onChange({
|
||||||
|
target: { value: inputProps.value ? `${inputProps.value} ${emoji}` : emoji },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
{countInfo}
|
{countInfo}
|
||||||
|
</div>
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1336,7 +1336,7 @@ export const icons = {
|
||||||
<svg
|
<svg
|
||||||
{...props}
|
{...props}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="2 0 24 24"
|
||||||
width={props.size || '18'}
|
width={props.size || '18'}
|
||||||
height={props.size || '18'}
|
height={props.size || '18'}
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
@ -1728,6 +1728,269 @@ export const icons = {
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
|
[ICONS.LIVESTREAM]: (props: CustomProps) => (
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
width={props.size || '18'}
|
||||||
|
height={props.size || '16'}
|
||||||
|
className={props.className}
|
||||||
|
>
|
||||||
|
<g id="XMLID_505_">
|
||||||
|
<linearGradient
|
||||||
|
id="XMLID_420_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="-519.065"
|
||||||
|
y1="1525.4059"
|
||||||
|
x2="-508.6628"
|
||||||
|
y2="1525.4059"
|
||||||
|
>
|
||||||
|
<stop offset="1.970443e-002" stopColor="#FFC200" />
|
||||||
|
<stop offset="0.3866" stopColor="#FF31BD" />
|
||||||
|
<stop offset="0.6245" stopColor="#8E31BD" />
|
||||||
|
<stop offset="0.7758" stopColor="#6E8EDE" />
|
||||||
|
<stop offset="1" stopColor="#57EABA" />
|
||||||
|
</linearGradient>
|
||||||
|
<circle
|
||||||
|
id="XMLID_508_"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(XMLID_420_)"
|
||||||
|
strokeWidth="2.4678"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
cx="-513.9"
|
||||||
|
cy="1525.4"
|
||||||
|
r="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="XMLID_507_"
|
||||||
|
fill="#FF7B5B"
|
||||||
|
d="M-521,1518.3c-1.8,1.8-2.9,4.3-2.9,7.1c0,2.6,1,4.9,2.5,6.7L-521,1518.3z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="XMLID_506_"
|
||||||
|
fill="#FF7B5B"
|
||||||
|
d="M-506.9,1532.1c1.8-1.8,2.9-4.3,2.9-7.1c0-2.6-1-4.9-2.5-6.7L-506.9,1532.1z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<rect id="XMLID_125_" x="0" y="0" fill="none" width="36" height="36" stroke="none" /> {/* }//fill="#FFFFFF" */}
|
||||||
|
<linearGradient
|
||||||
|
id="XMLID_421_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="-1625.151"
|
||||||
|
y1="-2518.4661"
|
||||||
|
x2="-1596.6696"
|
||||||
|
y2="-2518.4661"
|
||||||
|
gradientTransform="matrix(-1 0 0 -1 -1589.489 -2500.4661)"
|
||||||
|
>
|
||||||
|
<stop offset="1.970443e-002" stopColor="#FFC200" />
|
||||||
|
<stop offset="0.4731" stopColor="#FF31BD" />
|
||||||
|
<stop offset="0.6947" stopColor="#8E31BD" />
|
||||||
|
<stop offset="1" stopColor="#57EABA" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
id="XMLID_124_"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#XMLID_421_)"
|
||||||
|
strokeWidth="2.94"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
d="M21.4,5.2L21.4,5.2c7.1,0,12.8,5.7,12.8,12.8v0c0,7.1-5.7,12.8-12.8,12.8H8.7V18C8.7,10.9,14.4,5.2,21.4,5.2z"
|
||||||
|
/>
|
||||||
|
<linearGradient id="XMLID_422_" gradientUnits="userSpaceOnUse" x1="18.041" y1="32.147" x2="38.7776" y2="-0.9289">
|
||||||
|
<stop offset="1.970443e-002" stopColor="#FFC200" />
|
||||||
|
<stop offset="0.4731" stopColor="#FF31BD" />
|
||||||
|
<stop offset="0.6947" stopColor="#8E31BD" />
|
||||||
|
<stop offset="1" stopColor="#57EABA" />
|
||||||
|
</linearGradient>
|
||||||
|
<rect id="XMLID_123_" x="26.9" y="13.8" fill="url(#XMLID_422_)" stroke="none" width="2.8" height="3.8" />
|
||||||
|
<linearGradient
|
||||||
|
id="XMLID_423_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="13.0856"
|
||||||
|
y1="29.0402"
|
||||||
|
x2="33.8223"
|
||||||
|
y2="-4.0356"
|
||||||
|
>
|
||||||
|
<stop offset="1.970443e-002" stopColor="#FFC200" />
|
||||||
|
<stop offset="0.4731" stopColor="#FF31BD" />
|
||||||
|
<stop offset="0.6947" stopColor="#8E31BD" />
|
||||||
|
<stop offset="1" stopColor="#57EABA" />
|
||||||
|
</linearGradient>
|
||||||
|
<rect id="XMLID_122_" x="20" y="13.8" fill="url(#XMLID_422_)" stroke="none" width="2.8" height="3.8" />
|
||||||
|
<linearGradient id="XMLID_424_" gradientUnits="userSpaceOnUse" x1="0.338" y1="17.7555" x2="17.2654" y2="17.7555">
|
||||||
|
<stop offset="1.970443e-002" stopColor="#FFC200" />
|
||||||
|
<stop offset="0.4731" stopColor="#FF31BD" />
|
||||||
|
<stop offset="0.6947" stopColor="#8E31BD" />
|
||||||
|
<stop offset="1" stopColor="#57EABA" />
|
||||||
|
</linearGradient>
|
||||||
|
<circle
|
||||||
|
id="XMLID_121_"
|
||||||
|
fill="none"
|
||||||
|
stroke="#6E8EDE"
|
||||||
|
strokeWidth="2.94"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit="10"
|
||||||
|
cx="8.8"
|
||||||
|
cy="17.8"
|
||||||
|
r="6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
[ICONS.LIVESTREAM_SOLID]: (props: CustomProps) => (
|
||||||
|
<svg
|
||||||
|
id="prefix__Layer_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
fill="none"
|
||||||
|
width={props.size || '18'}
|
||||||
|
height={props.size || '16'}
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<style>{'.prefix__st1{fill:#ff7b5b}.prefix__st3{fill:#79d1b6}'}</style>
|
||||||
|
<g id="prefix__XMLID_505_">
|
||||||
|
<linearGradient
|
||||||
|
id="prefix__XMLID_410_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1={-571.815}
|
||||||
|
y1={1525.406}
|
||||||
|
x2={-561.413}
|
||||||
|
y2={1525.406}
|
||||||
|
>
|
||||||
|
<stop offset={0.02} stopColor="#ffc200" />
|
||||||
|
<stop offset={0.387} stopColor="#ff31bd" />
|
||||||
|
<stop offset={0.625} stopColor="#8e31bd" />
|
||||||
|
<stop offset={0.776} stopColor="#6e8ede" />
|
||||||
|
<stop offset={1} stopColor="#57eaba" />
|
||||||
|
</linearGradient>
|
||||||
|
<circle
|
||||||
|
id="prefix__XMLID_508_"
|
||||||
|
cx={-566.6}
|
||||||
|
cy={1525.4}
|
||||||
|
r={4}
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#prefix__XMLID_410_)"
|
||||||
|
strokeWidth={2.468}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit={10}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="prefix__XMLID_507_"
|
||||||
|
className="prefix__st1"
|
||||||
|
d="M-573.7 1518.3c-1.8 1.8-2.9 4.3-2.9 7.1 0 2.6 1 4.9 2.5 6.7l.4-13.8z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="prefix__XMLID_506_"
|
||||||
|
className="prefix__st1"
|
||||||
|
d="M-559.6 1532.1c1.8-1.8 2.9-4.3 2.9-7.1 0-2.6-1-4.9-2.5-6.7l-.4 13.8z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
id="prefix__XMLID_20_"
|
||||||
|
d="M21.4 5.2h0c7.1 0 12.8 5.7 12.8 12.8v0c0 7.1-5.7 12.8-12.8 12.8H8.7V18c0-7.1 5.7-12.8 12.7-12.8z"
|
||||||
|
fill="none"
|
||||||
|
stroke="#e729e1"
|
||||||
|
strokeWidth={2.94}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit={10}
|
||||||
|
/>
|
||||||
|
<path id="prefix__XMLID_19_" className="prefix__st3" stroke="none" d="M26.9 13.8h2.8v3.8h-2.8z" />
|
||||||
|
<path id="prefix__XMLID_18_" className="prefix__st3" stroke="none" d="M20 13.8h2.8v3.8H20z" />
|
||||||
|
<circle
|
||||||
|
id="prefix__XMLID_17_"
|
||||||
|
cx={8.8}
|
||||||
|
cy={17.8}
|
||||||
|
r={6}
|
||||||
|
fill="none"
|
||||||
|
stroke="#ffa100"
|
||||||
|
strokeWidth={2.94}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit={10}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
[ICONS.LIVESTREAM_MONOCHROME]: (props: CustomProps) => (
|
||||||
|
<svg
|
||||||
|
id="prefix__Layer_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="currentColor"
|
||||||
|
width={props.size || '18'}
|
||||||
|
height={props.size || '16'}
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g id="prefix__XMLID_505_">
|
||||||
|
<linearGradient
|
||||||
|
id="prefix__XMLID_410_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1={-571.815}
|
||||||
|
y1={1525.406}
|
||||||
|
x2={-561.413}
|
||||||
|
y2={1525.406}
|
||||||
|
>
|
||||||
|
<stop offset={0.02} stopColor="#ffc200" />
|
||||||
|
<stop offset={0.387} stopColor="#ff31bd" />
|
||||||
|
<stop offset={0.625} stopColor="#8e31bd" />
|
||||||
|
<stop offset={0.776} stopColor="#6e8ede" />
|
||||||
|
<stop offset={1} stopColor="#57eaba" />
|
||||||
|
</linearGradient>
|
||||||
|
<circle
|
||||||
|
id="prefix__XMLID_508_"
|
||||||
|
cx={-566.6}
|
||||||
|
cy={1525.4}
|
||||||
|
r={4}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={2.468}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit={10}
|
||||||
|
/>
|
||||||
|
<path id="prefix__XMLID_507_" d="M-573.7 1518.3c-1.8 1.8-2.9 4.3-2.9 7.1 0 2.6 1 4.9 2.5 6.7l.4-13.8z" />
|
||||||
|
<path id="prefix__XMLID_506_" d="M-559.6 1532.1c1.8-1.8 2.9-4.3 2.9-7.1 0-2.6-1-4.9-2.5-6.7l-.4 13.8z" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
id="prefix__XMLID_20_"
|
||||||
|
d="M21.4 5.2h0c7.1 0 12.8 5.7 12.8 12.8v0c0 7.1-5.7 12.8-12.8 12.8H8.7V18c0-7.1 5.7-12.8 12.7-12.8z"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={2.94}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit={10}
|
||||||
|
/>
|
||||||
|
<path id="prefix__XMLID_19_" d="M26.9 13.8h2.8v3.8h-2.8z" />
|
||||||
|
<path id="prefix__XMLID_18_" d="M20 13.8h2.8v3.8H20z" />
|
||||||
|
<circle
|
||||||
|
id="prefix__XMLID_17_"
|
||||||
|
cx={8.8}
|
||||||
|
cy={17.8}
|
||||||
|
r={6}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={2.94}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit={10}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
|
||||||
[ICONS.LIVESTREAM]: (props: CustomProps) => (
|
[ICONS.LIVESTREAM]: (props: CustomProps) => (
|
||||||
<svg
|
<svg
|
||||||
version="1.1"
|
version="1.1"
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectFilePartlyDownloaded, makeSelectClaimIsMine } from 'lbry-redux';
|
import { makeSelectFilePartlyDownloaded, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
|
||||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
import FileProperties from './view';
|
import FileProperties from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
downloaded: makeSelectFilePartlyDownloaded(props.uri)(state),
|
downloaded: makeSelectFilePartlyDownloaded(props.uri)(state),
|
||||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import type { Node } from 'react';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
@ -13,22 +14,31 @@ type Props = {
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
isSubscribed: boolean,
|
isSubscribed: boolean,
|
||||||
small: boolean,
|
small: boolean,
|
||||||
|
claim: Claim,
|
||||||
|
properties?: (Claim) => ?Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileProperties(props: Props) {
|
export default function FileProperties(props: Props) {
|
||||||
const { uri, downloaded, claimIsMine, isSubscribed, small = false } = props;
|
const { uri, downloaded, claimIsMine, isSubscribed, small = false, properties, claim } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames('file-properties', {
|
className={classnames('file-properties', {
|
||||||
'file-properties--small': small,
|
'file-properties--small': small,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{typeof properties === 'function' ? (
|
||||||
|
properties(claim)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<VideoDuration uri={uri} />
|
<VideoDuration uri={uri} />
|
||||||
<FileType uri={uri} />
|
<FileType uri={uri} />
|
||||||
{isSubscribed && <Icon tooltip icon={ICONS.SUBSCRIBE} />}
|
{isSubscribed && <Icon tooltip icon={ICONS.SUBSCRIBE} />}
|
||||||
{!claimIsMine && downloaded && <Icon tooltip icon={ICONS.LIBRARY} />}
|
{!claimIsMine && downloaded && <Icon tooltip icon={ICONS.LIBRARY} />}
|
||||||
|
|
||||||
<FilePrice hideFree uri={uri} />
|
<FilePrice hideFree uri={uri} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
10
ui/component/livestreamComment/index.js
Normal file
10
ui/component/livestreamComment/index.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectStakedLevelForChannelUri, makeSelectClaimForUri } from 'lbry-redux';
|
||||||
|
import LivestreamComment from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select)(LivestreamComment);
|
78
ui/component/livestreamComment/view.jsx
Normal file
78
ui/component/livestreamComment/view.jsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// @flow
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import React from 'react';
|
||||||
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
import { Menu, MenuButton } from '@reach/menu-button';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import CommentMenuList from 'component/commentMenuList';
|
||||||
|
import UriIndicator from 'component/uriIndicator';
|
||||||
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
claim: StreamClaim,
|
||||||
|
authorUri: string,
|
||||||
|
commentId: string,
|
||||||
|
message: string,
|
||||||
|
commentIsMine: boolean,
|
||||||
|
stakedLevel: number,
|
||||||
|
supportAmount: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
function Comment(props: Props) {
|
||||||
|
const { claim, uri, authorUri, message, commentIsMine, commentId, stakedLevel, supportAmount } = props;
|
||||||
|
const [mouseIsHovering, setMouseHover] = React.useState(false);
|
||||||
|
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={classnames('livestream-comment', {
|
||||||
|
'livestream-comment--superchat': supportAmount > 0,
|
||||||
|
})}
|
||||||
|
onMouseOver={() => setMouseHover(true)}
|
||||||
|
onMouseOut={() => setMouseHover(false)}
|
||||||
|
>
|
||||||
|
{supportAmount > 0 && (
|
||||||
|
<div className="super-chat livestream-superchat__banner">
|
||||||
|
<div className="livestream-superchat__banner-corner" />
|
||||||
|
<CreditAmount amount={supportAmount} superChat className="livestream-superchat__amount" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="livestream-comment__body">
|
||||||
|
{supportAmount > 0 && <ChannelThumbnail uri={authorUri} xsmall />}
|
||||||
|
<div className="livestream-comment__info">
|
||||||
|
<UriIndicator
|
||||||
|
className={classnames('comment__author', {
|
||||||
|
'comment__author--creator': commentByOwnerOfContent,
|
||||||
|
})}
|
||||||
|
link
|
||||||
|
external
|
||||||
|
uri={authorUri}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="livestream-comment__text">
|
||||||
|
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="livestream-comment__menu">
|
||||||
|
<Menu>
|
||||||
|
<MenuButton className="menu__button">
|
||||||
|
<Icon
|
||||||
|
size={18}
|
||||||
|
className={mouseIsHovering ? 'comment__menu-icon--hovering' : 'comment__menu-icon'}
|
||||||
|
icon={ICONS.MORE_VERTICAL}
|
||||||
|
/>
|
||||||
|
</MenuButton>
|
||||||
|
<CommentMenuList uri={uri} commentId={commentId} authorUri={authorUri} commentIsMine={commentIsMine} />
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Comment;
|
|
@ -1,14 +1,26 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectClaimForUri } from 'lbry-redux';
|
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||||
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
|
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
|
||||||
import { doCommentList } from 'redux/actions/comments';
|
import { doCommentList, doSuperChatList } from 'redux/actions/comments';
|
||||||
import { makeSelectTopLevelCommentsForUri, selectIsFetchingComments } from 'redux/selectors/comments';
|
import {
|
||||||
|
makeSelectTopLevelCommentsForUri,
|
||||||
|
selectIsFetchingComments,
|
||||||
|
makeSelectSuperChatsForUri,
|
||||||
|
makeSelectSuperChatTotalAmountForUri,
|
||||||
|
} from 'redux/selectors/comments';
|
||||||
import LivestreamFeed from './view';
|
import LivestreamFeed from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
comments: makeSelectTopLevelCommentsForUri(props.uri)(state).slice(0, 75),
|
comments: makeSelectTopLevelCommentsForUri(props.uri)(state).slice(0, 75),
|
||||||
fetchingComments: selectIsFetchingComments(state),
|
fetchingComments: selectIsFetchingComments(state),
|
||||||
|
superChats: makeSelectSuperChatsForUri(props.uri)(state),
|
||||||
|
superChatsTotalAmount: makeSelectSuperChatTotalAmountForUri(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, { doCommentSocketConnect, doCommentSocketDisconnect, doCommentList })(LivestreamFeed);
|
export default connect(select, {
|
||||||
|
doCommentSocketConnect,
|
||||||
|
doCommentSocketDisconnect,
|
||||||
|
doCommentList,
|
||||||
|
doSuperChatList,
|
||||||
|
})(LivestreamFeed);
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import Card from 'component/common/card';
|
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import CommentCreate from 'component/commentCreate';
|
import CommentCreate from 'component/commentCreate';
|
||||||
import CommentView from 'component/comment';
|
import LivestreamComment from 'component/livestreamComment';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import UriIndicator from 'component/uriIndicator';
|
||||||
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -16,9 +20,15 @@ type Props = {
|
||||||
doCommentList: (string) => void,
|
doCommentList: (string) => void,
|
||||||
comments: Array<Comment>,
|
comments: Array<Comment>,
|
||||||
fetchingComments: boolean,
|
fetchingComments: boolean,
|
||||||
|
doSuperChatList: (string) => void,
|
||||||
|
superChats: Array<Comment>,
|
||||||
|
superChatsTotalAmount: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LivestreamFeed(props: Props) {
|
const VIEW_MODE_CHAT = 'view_chat';
|
||||||
|
const VIEW_MODE_SUPER_CHAT = 'view_superchat';
|
||||||
|
|
||||||
|
export default function LivestreamComments(props: Props) {
|
||||||
const {
|
const {
|
||||||
claim,
|
claim,
|
||||||
uri,
|
uri,
|
||||||
|
@ -28,16 +38,22 @@ export default function LivestreamFeed(props: Props) {
|
||||||
comments,
|
comments,
|
||||||
doCommentList,
|
doCommentList,
|
||||||
fetchingComments,
|
fetchingComments,
|
||||||
|
doSuperChatList,
|
||||||
|
superChats,
|
||||||
|
superChatsTotalAmount,
|
||||||
} = props;
|
} = props;
|
||||||
const commentsRef = React.createRef();
|
const commentsRef = React.createRef();
|
||||||
const hasScrolledComments = React.useRef();
|
const hasScrolledComments = React.useRef();
|
||||||
|
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
|
||||||
const [performedInitialScroll, setPerformedInitialScroll] = React.useState(false);
|
const [performedInitialScroll, setPerformedInitialScroll] = React.useState(false);
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const commentsLength = comments && comments.length;
|
const commentsLength = comments && comments.length;
|
||||||
|
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? comments : superChats;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (claimId) {
|
if (claimId) {
|
||||||
doCommentList(uri);
|
doCommentList(uri);
|
||||||
|
doSuperChatList(uri);
|
||||||
doCommentSocketConnect(uri, claimId);
|
doCommentSocketConnect(uri, claimId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +62,7 @@ export default function LivestreamFeed(props: Props) {
|
||||||
doCommentSocketDisconnect(claimId);
|
doCommentSocketDisconnect(claimId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [claimId, uri, doCommentList, doCommentSocketConnect, doCommentSocketDisconnect]);
|
}, [claimId, uri, doCommentList, doSuperChatList, doCommentSocketConnect, doCommentSocketDisconnect]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const element = commentsRef.current;
|
const element = commentsRef.current;
|
||||||
|
@ -92,51 +108,87 @@ export default function LivestreamFeed(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div className="card livestream__discussion">
|
||||||
title={__('Live discussion')}
|
<div className="card__header--between livestream-discussion__header">
|
||||||
smallTitle
|
<div className="livestream-discussion__title">{__('Live discussion')}</div>
|
||||||
className="livestream__discussion"
|
{superChatsTotalAmount > 0 && (
|
||||||
actions={
|
<div className="recommended-content__toggles">
|
||||||
|
<Button
|
||||||
|
className={classnames('button-toggle', {
|
||||||
|
'button-toggle--active': viewMode === VIEW_MODE_CHAT,
|
||||||
|
})}
|
||||||
|
label={__('Chat')}
|
||||||
|
onClick={() => setViewMode(VIEW_MODE_CHAT)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={classnames('button-toggle', {
|
||||||
|
'button-toggle--active': viewMode === VIEW_MODE_SUPER_CHAT,
|
||||||
|
})}
|
||||||
|
label={
|
||||||
<>
|
<>
|
||||||
{fetchingComments && (
|
<CreditAmount amount={superChatsTotalAmount} size={8} /> {__('Tipped')}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onClick={() => setViewMode(VIEW_MODE_SUPER_CHAT)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<>
|
||||||
|
{fetchingComments && !comments && (
|
||||||
<div className="main--empty">
|
<div className="main--empty">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div ref={commentsRef} className="livestream__comments-wrapper">
|
||||||
ref={commentsRef}
|
{viewMode === VIEW_MODE_CHAT && superChatsTotalAmount > 0 && (
|
||||||
className={classnames('livestream__comments-wrapper', {
|
<div className="livestream-superchats__wrapper">
|
||||||
'livestream__comments-wrapper--with-height': commentsLength > 0,
|
<div className="livestream-superchats__inner">
|
||||||
})}
|
{superChats.map((superChat: Comment) => (
|
||||||
>
|
<Tooltip key={superChat.comment_id} label={superChat.comment}>
|
||||||
{!fetchingComments && comments.length > 0 ? (
|
<div className="livestream-superchat">
|
||||||
<div className="livestream__comments">
|
<div className="livestream-superchat__thumbnail">
|
||||||
{comments.map((comment) => (
|
<ChannelThumbnail uri={superChat.channel_url} xsmall />
|
||||||
<div key={comment.comment_id} className={classnames('livestream__comment')}>
|
</div>
|
||||||
<CommentView
|
|
||||||
livestream
|
<div className="livestream-superchat__info">
|
||||||
isTopLevel
|
<UriIndicator uri={superChat.channel_url} link />
|
||||||
uri={uri}
|
<CreditAmount
|
||||||
authorUri={comment.channel_url}
|
size={10}
|
||||||
author={comment.channel_name}
|
className="livestream-superchat__amount-large"
|
||||||
claimId={comment.claim_id}
|
amount={superChat.support_amount}
|
||||||
commentId={comment.comment_id}
|
|
||||||
message={comment.comment}
|
|
||||||
timePosted={comment.timestamp * 1000}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!fetchingComments && comments.length > 0 ? (
|
||||||
|
<div className="livestream__comments">
|
||||||
|
{commentsToDisplay.map((comment) => (
|
||||||
|
<LivestreamComment
|
||||||
|
key={comment.comment_id}
|
||||||
|
uri={uri}
|
||||||
|
authorUri={comment.channel_url}
|
||||||
|
commentId={comment.comment_id}
|
||||||
|
message={comment.comment}
|
||||||
|
supportAmount={comment.support_amount}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="main--empty" />
|
<div className="main--empty" style={{ flex: 1 }} />
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="livestream__comment-create">
|
<div className="livestream__comment-create">
|
||||||
<CommentCreate livestream bottom embed={embed} uri={uri} />
|
<CommentCreate livestream bottom embed={embed} uri={uri} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
</div>
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { BITWAVE_EMBED_URL } from 'constants/livestream';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FileTitleSection from 'component/fileTitleSection';
|
import FileTitleSection from 'component/fileTitleSection';
|
||||||
import LivestreamComments from 'component/livestreamComments';
|
import LivestreamComments from 'component/livestreamComments';
|
||||||
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -13,6 +14,7 @@ type Props = {
|
||||||
|
|
||||||
export default function LivestreamLayout(props: Props) {
|
export default function LivestreamLayout(props: Props) {
|
||||||
const { claim, uri, isLive, activeViewers } = props;
|
const { claim, uri, isLive, activeViewers } = props;
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
if (!claim || !claim.signing_channel) {
|
if (!claim || !claim.signing_channel) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -41,9 +43,11 @@ export default function LivestreamLayout(props: Props) {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isMobile && <LivestreamComments uri={uri} />}
|
||||||
|
|
||||||
<FileTitleSection uri={uri} livestream isLive={isLive} activeViewers={activeViewers} />
|
<FileTitleSection uri={uri} livestream isLive={isLive} activeViewers={activeViewers} />
|
||||||
</div>
|
</div>
|
||||||
<LivestreamComments uri={uri} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import React from 'react';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import ClaimPreview from 'component/claimPreview';
|
import ClaimPreview from 'component/claimPreview';
|
||||||
import { Lbry } from 'lbry-redux';
|
import { Lbry } from 'lbry-redux';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
channelClaim: ChannelClaim,
|
channelClaim: ChannelClaim,
|
||||||
|
@ -11,6 +13,7 @@ type Props = {
|
||||||
|
|
||||||
export default function LivestreamLink(props: Props) {
|
export default function LivestreamLink(props: Props) {
|
||||||
const { channelClaim } = props;
|
const { channelClaim } = props;
|
||||||
|
const { push } = useHistory();
|
||||||
const [livestreamClaim, setLivestreamClaim] = React.useState(false);
|
const [livestreamClaim, setLivestreamClaim] = React.useState(false);
|
||||||
const [isLivestreaming, setIsLivestreaming] = React.useState(false);
|
const [isLivestreaming, setIsLivestreaming] = React.useState(false);
|
||||||
const livestreamChannelId = channelClaim.claim_id || ''; // TODO: fail in a safer way, probably
|
const livestreamChannelId = channelClaim.claim_id || ''; // TODO: fail in a safer way, probably
|
||||||
|
@ -66,7 +69,14 @@ export default function LivestreamLink(props: Props) {
|
||||||
|
|
||||||
// gonna pass the wrapper in so I don't have to rewrite the dmca/blocking logic in claimPreview.
|
// gonna pass the wrapper in so I don't have to rewrite the dmca/blocking logic in claimPreview.
|
||||||
const element = (props: { children: any }) => (
|
const element = (props: { children: any }) => (
|
||||||
<Card className="livestream__channel-link" title={__('Live stream in progress')}>
|
<Card
|
||||||
|
role="button"
|
||||||
|
className="livestream__channel-link"
|
||||||
|
title={__('Live stream in progress')}
|
||||||
|
onClick={() => {
|
||||||
|
push(formatLbryUrlForWeb(livestreamClaim.canonical_url));
|
||||||
|
}}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
6
ui/component/livestreamList/index.js
Normal file
6
ui/component/livestreamList/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import LivestreamCurrent from './view';
|
||||||
|
|
||||||
|
const select = (state) => ({});
|
||||||
|
|
||||||
|
export default connect(select)(LivestreamCurrent);
|
75
ui/component/livestreamList/view.jsx
Normal file
75
ui/component/livestreamList/view.jsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
// @flow
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import { BITWAVE_LIVE_API } from 'constants/livestream';
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
import ClaimTilesDiscover from 'component/claimTilesDiscover';
|
||||||
|
|
||||||
|
const LIVESTREAM_POLL_IN_MS = 10 * 1000;
|
||||||
|
|
||||||
|
export default function LivestreamList() {
|
||||||
|
const [loading, setLoading] = React.useState(true);
|
||||||
|
const [livestreamMap, setLivestreamMap] = React.useState();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
function checkCurrentLivestreams() {
|
||||||
|
fetch(BITWAVE_LIVE_API)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
setLoading(false);
|
||||||
|
if (!res.data) {
|
||||||
|
setLivestreamMap({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const livestreamMap = res.data.reduce((acc, curr) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[curr.claimId]: curr,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
setLivestreamMap(livestreamMap);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCurrentLivestreams();
|
||||||
|
let fetchInterval = setInterval(checkCurrentLivestreams, LIVESTREAM_POLL_IN_MS);
|
||||||
|
return () => {
|
||||||
|
if (fetchInterval) {
|
||||||
|
clearInterval(fetchInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{loading && (
|
||||||
|
<div className="main--empty">
|
||||||
|
<Spinner delayed />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{livestreamMap && Object.keys(livestreamMap).length > 0 && (
|
||||||
|
<ClaimTilesDiscover
|
||||||
|
hasNoSource
|
||||||
|
channelIds={Object.keys(livestreamMap)}
|
||||||
|
limitClaimsPerChannel={1}
|
||||||
|
renderProperties={(claim) => {
|
||||||
|
const livestream = livestreamMap[claim.signing_channel.claim_id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="livestream__viewer-count">
|
||||||
|
{livestream.viewCount} <Icon icon={ICONS.EYE} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ type Props = {
|
||||||
videoTheaterMode: boolean,
|
videoTheaterMode: boolean,
|
||||||
isMarkdown?: boolean,
|
isMarkdown?: boolean,
|
||||||
livestream?: boolean,
|
livestream?: boolean,
|
||||||
|
rightSide?: Node,
|
||||||
backout: {
|
backout: {
|
||||||
backLabel?: string,
|
backLabel?: string,
|
||||||
backNavDefault?: string,
|
backNavDefault?: string,
|
||||||
|
@ -51,6 +52,7 @@ function Page(props: Props) {
|
||||||
videoTheaterMode,
|
videoTheaterMode,
|
||||||
isMarkdown = false,
|
isMarkdown = false,
|
||||||
livestream,
|
livestream,
|
||||||
|
rightSide,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -114,6 +116,8 @@ function Page(props: Props) {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
|
{!isMobile && rightSide && <div className="main__right-side">{rightSide}</div>}
|
||||||
</main>
|
</main>
|
||||||
{/* @if TARGET='app' */}
|
{/* @if TARGET='app' */}
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
|
|
|
@ -613,7 +613,7 @@ function PublishForm(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<section>
|
<section>
|
||||||
<div className="card__actions">
|
<div className="section__actions">
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
onClick={handlePublish}
|
onClick={handlePublish}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import PasswordSetPage from 'page/passwordSet';
|
||||||
import SignInVerifyPage from 'page/signInVerify';
|
import SignInVerifyPage from 'page/signInVerify';
|
||||||
import ChannelsPage from 'page/channels';
|
import ChannelsPage from 'page/channels';
|
||||||
import LiveStreamSetupPage from 'page/livestreamSetup';
|
import LiveStreamSetupPage from 'page/livestreamSetup';
|
||||||
|
import LivestreamCurrentPage from 'page/livestreamCurrent';
|
||||||
import EmbedWrapperPage from 'page/embedWrapper';
|
import EmbedWrapperPage from 'page/embedWrapper';
|
||||||
import TopPage from 'page/top';
|
import TopPage from 'page/top';
|
||||||
import Welcome from 'page/welcome';
|
import Welcome from 'page/welcome';
|
||||||
|
@ -285,6 +286,7 @@ function AppRouter(props: Props) {
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.LIVESTREAM}`} component={LiveStreamSetupPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.LIVESTREAM}`} component={LiveStreamSetupPage} />
|
||||||
|
<PrivateRoute {...props} path={`/$/${PAGES.LIVESTREAM_CURRENT}`} component={LivestreamCurrentPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.RECEIVE}`} component={ReceivePage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.RECEIVE}`} component={ReceivePage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SEND}`} component={SendPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.SEND}`} component={SendPage} />
|
||||||
|
|
9
ui/component/walletTipAmountSelector/index.js
Normal file
9
ui/component/walletTipAmountSelector/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { selectBalance } from 'lbry-redux';
|
||||||
|
import WalletTipAmountSelector from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
balance: selectBalance(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select)(WalletTipAmountSelector);
|
123
ui/component/walletTipAmountSelector/view.jsx
Normal file
123
ui/component/walletTipAmountSelector/view.jsx
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
// @flow
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import { FormField } from 'component/common/form';
|
||||||
|
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
|
||||||
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||||
|
|
||||||
|
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
balance: number,
|
||||||
|
amount: number,
|
||||||
|
onChange: (number) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function WalletTipAmountSelector(props: Props) {
|
||||||
|
const { balance, amount, onChange } = props;
|
||||||
|
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
||||||
|
const [tipError, setTipError] = React.useState();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||||
|
const validTipInput = regexp.test(String(amount));
|
||||||
|
let tipError;
|
||||||
|
|
||||||
|
if (!amount) {
|
||||||
|
tipError = __('Amount must be a number');
|
||||||
|
} else if (amount <= 0) {
|
||||||
|
tipError = __('Amount must be a positive number');
|
||||||
|
} else if (amount < MINIMUM_PUBLISH_BID) {
|
||||||
|
tipError = __('Amount must be higher');
|
||||||
|
} else if (!validTipInput) {
|
||||||
|
tipError = __('Amount must have no more than 8 decimal places');
|
||||||
|
} else if (amount === balance) {
|
||||||
|
tipError = __('Please decrease the amount to account for transaction fees');
|
||||||
|
} else if (amount > balance) {
|
||||||
|
tipError = __('Not enough Credits');
|
||||||
|
}
|
||||||
|
setTipError(tipError);
|
||||||
|
}, [amount, balance, setTipError]);
|
||||||
|
|
||||||
|
function handleCustomPriceChange(amount: number) {
|
||||||
|
const tipAmount = parseFloat(amount);
|
||||||
|
onChange(tipAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="section">
|
||||||
|
{DEFAULT_TIP_AMOUNTS.map((defaultAmount) => (
|
||||||
|
<Button
|
||||||
|
key={defaultAmount}
|
||||||
|
disabled={amount > balance}
|
||||||
|
button="alt"
|
||||||
|
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||||
|
'button-toggle--active': defaultAmount === amount,
|
||||||
|
'button-toggle--disabled': amount > balance,
|
||||||
|
})}
|
||||||
|
label={defaultAmount}
|
||||||
|
icon={ICONS.LBC}
|
||||||
|
onClick={() => {
|
||||||
|
handleCustomPriceChange(defaultAmount);
|
||||||
|
setUseCustomTip(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
button="alt"
|
||||||
|
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||||
|
'button-toggle--active': !DEFAULT_TIP_AMOUNTS.includes(amount),
|
||||||
|
})}
|
||||||
|
icon={ICONS.LBC}
|
||||||
|
label={__('Custom')}
|
||||||
|
onClick={() => setUseCustomTip(true)}
|
||||||
|
/>
|
||||||
|
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && (
|
||||||
|
<Button
|
||||||
|
button="secondary"
|
||||||
|
className="button-toggle-group-action"
|
||||||
|
icon={ICONS.BUY}
|
||||||
|
title={__('Buy more LBRY Credits')}
|
||||||
|
navigate={`/$/${PAGES.BUY}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{useCustomTip && (
|
||||||
|
<div className="comment__tip-input">
|
||||||
|
<FormField
|
||||||
|
autoFocus
|
||||||
|
name="tip-input"
|
||||||
|
label={
|
||||||
|
<React.Fragment>
|
||||||
|
{__('Custom support amount')}{' '}
|
||||||
|
<I18nMessage tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} /> }}>
|
||||||
|
(%lbc_balance% available)
|
||||||
|
</I18nMessage>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
className="form-field--price-amount"
|
||||||
|
error={tipError}
|
||||||
|
min="0"
|
||||||
|
step="any"
|
||||||
|
type="number"
|
||||||
|
placeholder="1.23"
|
||||||
|
value={amount}
|
||||||
|
onChange={(event) => handleCustomPriceChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!useCustomTip && <WalletSpendableBalanceHelp />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WalletTipAmountSelector;
|
|
@ -278,6 +278,9 @@ export const COMMENT_MODERATION_UN_BLOCK_STARTED = 'COMMENT_MODERATION_UN_BLOCK_
|
||||||
export const COMMENT_MODERATION_UN_BLOCK_COMPLETE = 'COMMENT_MODERATION_UN_BLOCK_COMPLETE';
|
export const COMMENT_MODERATION_UN_BLOCK_COMPLETE = 'COMMENT_MODERATION_UN_BLOCK_COMPLETE';
|
||||||
export const COMMENT_MODERATION_UN_BLOCK_FAILED = 'COMMENT_MODERATION_UN_BLOCK_FAILED';
|
export const COMMENT_MODERATION_UN_BLOCK_FAILED = 'COMMENT_MODERATION_UN_BLOCK_FAILED';
|
||||||
export const COMMENT_RECEIVED = 'COMMENT_RECEIVED';
|
export const COMMENT_RECEIVED = 'COMMENT_RECEIVED';
|
||||||
|
export const COMMENT_SUPER_CHAT_LIST_STARTED = 'COMMENT_SUPER_CHAT_LIST_STARTED';
|
||||||
|
export const COMMENT_SUPER_CHAT_LIST_COMPLETED = 'COMMENT_SUPER_CHAT_LIST_COMPLETED';
|
||||||
|
export const COMMENT_SUPER_CHAT_LIST_FAILED = 'COMMENT_SUPER_CHAT_LIST_FAILED';
|
||||||
|
|
||||||
// Blocked channels
|
// Blocked channels
|
||||||
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export const FF_MAX_CHARS_DEFAULT = 2000;
|
export const FF_MAX_CHARS_DEFAULT = 2000;
|
||||||
export const FF_MAX_CHARS_IN_COMMENT = 2000;
|
export const FF_MAX_CHARS_IN_COMMENT = 2000;
|
||||||
|
export const FF_MAX_CHARS_IN_LIVESTREAM_COMMENT = 500;
|
||||||
export const FF_MAX_CHARS_IN_DESCRIPTION = 5000;
|
export const FF_MAX_CHARS_IN_DESCRIPTION = 5000;
|
||||||
export const FF_MAX_CHARS_REPORT_CONTENT_DETAILS = 500;
|
export const FF_MAX_CHARS_REPORT_CONTENT_DETAILS = 500;
|
||||||
export const FF_MAX_CHARS_REPORT_CONTENT_ADDRESS = 255;
|
export const FF_MAX_CHARS_REPORT_CONTENT_ADDRESS = 255;
|
||||||
|
|
|
@ -65,3 +65,4 @@ exports.CHANNEL_NEW = 'channel/new';
|
||||||
exports.NOTIFICATIONS = 'notifications';
|
exports.NOTIFICATIONS = 'notifications';
|
||||||
exports.YOUTUBE_SYNC = 'youtube';
|
exports.YOUTUBE_SYNC = 'youtube';
|
||||||
exports.LIVESTREAM = 'livestream';
|
exports.LIVESTREAM = 'livestream';
|
||||||
|
exports.LIVESTREAM_CURRENT = 'live';
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { BITWAVE_LIVE_API } from 'constants/livestream';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import LivestreamLayout from 'component/livestreamLayout';
|
import LivestreamLayout from 'component/livestreamLayout';
|
||||||
|
import LivestreamComments from 'component/livestreamComments';
|
||||||
import analytics from 'analytics';
|
import analytics from 'analytics';
|
||||||
import { Lbry } from 'lbry-redux';
|
import { Lbry } from 'lbry-redux';
|
||||||
|
|
||||||
|
@ -111,7 +112,7 @@ export default function LivestreamPage(props: Props) {
|
||||||
}, [doSetPlayingUri]);
|
}, [doSetPlayingUri]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page className="file-page" filePage livestream>
|
<Page className="file-page" noFooter livestream rightSide={<LivestreamComments uri={uri} />}>
|
||||||
<LivestreamLayout uri={uri} activeViewers={activeViewers} isLive={isLive} />
|
<LivestreamLayout uri={uri} activeViewers={activeViewers} isLive={isLive} />
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
9
ui/page/livestreamCurrent/index.js
Normal file
9
ui/page/livestreamCurrent/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { selectUser } from 'redux/selectors/user';
|
||||||
|
import LivestreamCurrent from './view';
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
user: selectUser(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select)(LivestreamCurrent);
|
34
ui/page/livestreamCurrent/view.jsx
Normal file
34
ui/page/livestreamCurrent/view.jsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import LivestreamList from 'component/livestreamList';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Page from 'component/page';
|
||||||
|
import Yrbl from 'component/yrbl';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: ?User,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LivestreamCurrentPage(props: Props) {
|
||||||
|
const { user } = props;
|
||||||
|
const canView = user && user.global_mod;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
{canView ? (
|
||||||
|
<LivestreamList />
|
||||||
|
) : (
|
||||||
|
<Yrbl
|
||||||
|
type="sad"
|
||||||
|
title={__("This page isn't quite ready")}
|
||||||
|
subtitle={__('Check back later.')}
|
||||||
|
actions={
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button button="primary" navigate="/" label={__('Go Home')} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
|
@ -60,6 +60,44 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doSuperChatList(uri: string) {
|
||||||
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const claim = selectClaimsByUri(state)[uri];
|
||||||
|
const claimId = claim ? claim.claim_id : null;
|
||||||
|
|
||||||
|
if (!claimId) {
|
||||||
|
console.error('No claimId found for uri: ', uri); //eslint-disable-line
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_SUPER_CHAT_LIST_STARTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Comments.super_list({
|
||||||
|
claim_id: claimId,
|
||||||
|
})
|
||||||
|
.then((result: CommentListResponse) => {
|
||||||
|
const { items: comments, total_amount: totalAmount } = result;
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_SUPER_CHAT_LIST_COMPLETED,
|
||||||
|
data: {
|
||||||
|
comments,
|
||||||
|
totalAmount,
|
||||||
|
uri: uri,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_SUPER_CHAT_LIST_FAILED,
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function doCommentReactList(uri: string | null, commentId?: string) {
|
export function doCommentReactList(uri: string | null, commentId?: string) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -201,9 +239,10 @@ export function doCommentCreate(
|
||||||
claim_id: string = '',
|
claim_id: string = '',
|
||||||
parent_id?: string,
|
parent_id?: string,
|
||||||
uri: string,
|
uri: string,
|
||||||
livestream?: boolean = false
|
livestream?: boolean = false,
|
||||||
|
txid?: string
|
||||||
) {
|
) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
|
|
||||||
|
@ -216,6 +255,16 @@ export function doCommentCreate(
|
||||||
type: ACTIONS.COMMENT_CREATE_STARTED,
|
type: ACTIONS.COMMENT_CREATE_STARTED,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let signatureData;
|
||||||
|
if (activeChannelClaim) {
|
||||||
|
try {
|
||||||
|
signatureData = await Lbry.channel_sign({
|
||||||
|
channel_id: activeChannelClaim.claim_id,
|
||||||
|
hexdata: toHex(comment),
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
if (parent_id) {
|
if (parent_id) {
|
||||||
const notification = makeSelectNotificationForCommentId(parent_id)(state);
|
const notification = makeSelectNotificationForCommentId(parent_id)(state);
|
||||||
if (notification && !notification.is_seen) {
|
if (notification && !notification.is_seen) {
|
||||||
|
@ -223,11 +272,19 @@ export function doCommentCreate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Lbry.comment_create({
|
if (!signatureData) {
|
||||||
|
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Comments.comment_create({
|
||||||
comment: comment,
|
comment: comment,
|
||||||
claim_id: claim_id,
|
claim_id: claim_id,
|
||||||
channel_id: activeChannelClaim.claim_id,
|
channel_id: activeChannelClaim.claim_id,
|
||||||
|
channel_name: activeChannelClaim.name,
|
||||||
parent_id: parent_id,
|
parent_id: parent_id,
|
||||||
|
signature: signatureData.signature,
|
||||||
|
signing_ts: signatureData.signing_ts,
|
||||||
|
...(txid ? { support_tx_id: txid } : {}),
|
||||||
})
|
})
|
||||||
.then((result: CommentCreateResponse) => {
|
.then((result: CommentCreateResponse) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -258,6 +315,8 @@ export function doCommentCreate(
|
||||||
isError: true,
|
isError: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ const defaultState: CommentsState = {
|
||||||
// Remove commentsByUri
|
// Remove commentsByUri
|
||||||
// It is not needed and doesn't provide anything but confusion
|
// It is not needed and doesn't provide anything but confusion
|
||||||
commentsByUri: {}, // URI -> claimId
|
commentsByUri: {}, // URI -> claimId
|
||||||
|
superChatsByUri: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isCommenting: false,
|
isCommenting: false,
|
||||||
myComments: undefined,
|
myComments: undefined,
|
||||||
|
@ -213,6 +214,28 @@ export default handleActions(
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[ACTIONS.COMMENT_SUPER_CHAT_LIST_FAILED]: (state: CommentsState, action: any) => ({
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
}),
|
||||||
|
[ACTIONS.COMMENT_SUPER_CHAT_LIST_STARTED]: (state) => ({ ...state, isLoading: true }),
|
||||||
|
|
||||||
|
[ACTIONS.COMMENT_SUPER_CHAT_LIST_COMPLETED]: (state: CommentsState, action: any) => {
|
||||||
|
const { comments, totalAmount, uri } = action.data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
superChatsByUri: {
|
||||||
|
...state.superChatsByUri,
|
||||||
|
[uri]: {
|
||||||
|
comments,
|
||||||
|
totalAmount,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
[ACTIONS.COMMENT_LIST_FAILED]: (state: CommentsState, action: any) => ({
|
[ACTIONS.COMMENT_LIST_FAILED]: (state: CommentsState, action: any) => ({
|
||||||
...state,
|
...state,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
@ -224,6 +247,7 @@ export default handleActions(
|
||||||
const commentsByClaimId = Object.assign({}, state.byId);
|
const commentsByClaimId = Object.assign({}, state.byId);
|
||||||
const allCommentsById = Object.assign({}, state.commentById);
|
const allCommentsById = Object.assign({}, state.commentById);
|
||||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById);
|
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById);
|
||||||
|
const superChatsByUri = Object.assign({}, state.superChatsByUri);
|
||||||
const commentsForId = topLevelCommentsById[claimId];
|
const commentsForId = topLevelCommentsById[claimId];
|
||||||
|
|
||||||
allCommentsById[comment.comment_id] = comment;
|
allCommentsById[comment.comment_id] = comment;
|
||||||
|
@ -244,12 +268,47 @@ export default handleActions(
|
||||||
// We don't care to keep existing lower level comments since this is just for livestreams
|
// We don't care to keep existing lower level comments since this is just for livestreams
|
||||||
commentsByClaimId[claimId] = topLevelCommentsById[claimId];
|
commentsByClaimId[claimId] = topLevelCommentsById[claimId];
|
||||||
|
|
||||||
|
if (comment.support_amount > 0) {
|
||||||
|
const superChatForUri = superChatsByUri[uri];
|
||||||
|
const superChatCommentsForUri = superChatForUri && superChatForUri.comments;
|
||||||
|
|
||||||
|
let sortedSuperChatComments = [];
|
||||||
|
let hasAddedNewComment = false;
|
||||||
|
if (superChatCommentsForUri && superChatCommentsForUri.length > 0) {
|
||||||
|
// Go for the entire length of superChatCommentsForUri since a comment will be added to this list
|
||||||
|
for (var i = 0; i < superChatCommentsForUri.length; i++) {
|
||||||
|
const existingSuperChat = superChatCommentsForUri[i];
|
||||||
|
if (existingSuperChat.support_amount < comment.support_amount && !hasAddedNewComment) {
|
||||||
|
hasAddedNewComment = true;
|
||||||
|
sortedSuperChatComments.push(comment);
|
||||||
|
sortedSuperChatComments.push(existingSuperChat);
|
||||||
|
} else {
|
||||||
|
sortedSuperChatComments.push(existingSuperChat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the new superchat hasn't been added yet, it must be the smallest superchat in the list
|
||||||
|
if (
|
||||||
|
i === superChatCommentsForUri.length - 1 &&
|
||||||
|
sortedSuperChatComments.length === superChatCommentsForUri.length
|
||||||
|
) {
|
||||||
|
sortedSuperChatComments.push(comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
superChatsByUri[uri].comments = sortedSuperChatComments;
|
||||||
|
superChatsByUri[uri].totalAmount += 1;
|
||||||
|
} else {
|
||||||
|
superChatsByUri[uri] = { comments: [comment], totalAmount: comment.support_amount };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
byId: commentsByClaimId,
|
byId: commentsByClaimId,
|
||||||
commentById: allCommentsById,
|
commentById: allCommentsById,
|
||||||
commentsByUri,
|
commentsByUri,
|
||||||
topLevelCommentsById,
|
topLevelCommentsById,
|
||||||
|
superChatsByUri,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,8 @@ export const selectCommentsByClaimId = createSelector(selectState, selectComment
|
||||||
return comments;
|
return comments;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const selectSuperchatsByUri = createSelector(selectState, (state) => state.superChatsByUri);
|
||||||
|
|
||||||
export const selectTopLevelCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
export const selectTopLevelCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
||||||
const byClaimId = state.topLevelCommentsById || {};
|
const byClaimId = state.topLevelCommentsById || {};
|
||||||
const comments = {};
|
const comments = {};
|
||||||
|
@ -299,3 +301,26 @@ export const makeSelectUriIsBlockingOrUnBlocking = (uri: string) =>
|
||||||
createSelector(selectBlockingByUri, selectUnBlockingByUri, (blockingByUri, unBlockingByUri) => {
|
createSelector(selectBlockingByUri, selectUnBlockingByUri, (blockingByUri, unBlockingByUri) => {
|
||||||
return blockingByUri[uri] || unBlockingByUri[uri];
|
return blockingByUri[uri] || unBlockingByUri[uri];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const makeSelectSuperChatDataForUri = (uri: string) =>
|
||||||
|
createSelector(selectSuperchatsByUri, (byUri) => {
|
||||||
|
return byUri[uri];
|
||||||
|
});
|
||||||
|
|
||||||
|
export const makeSelectSuperChatsForUri = (uri: string) =>
|
||||||
|
createSelector(makeSelectSuperChatDataForUri(uri), (superChatData) => {
|
||||||
|
if (!superChatData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return superChatData.comments;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const makeSelectSuperChatTotalAmountForUri = (uri: string) =>
|
||||||
|
createSelector(makeSelectSuperChatDataForUri(uri), (superChatData) => {
|
||||||
|
if (!superChatData) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return superChatData.totalAmount;
|
||||||
|
});
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
@import 'component/spinner';
|
@import 'component/spinner';
|
||||||
@import 'component/splash';
|
@import 'component/splash';
|
||||||
@import 'component/status-bar';
|
@import 'component/status-bar';
|
||||||
|
@import 'component/superchat';
|
||||||
@import 'component/syntax-highlighter';
|
@import 'component/syntax-highlighter';
|
||||||
@import 'component/table';
|
@import 'component/table';
|
||||||
@import 'component/livestream';
|
@import 'component/livestream';
|
||||||
|
|
|
@ -232,7 +232,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--emoji {
|
.button--emoji {
|
||||||
font-size: 1.25rem;
|
font-size: 1.1rem;
|
||||||
border-radius: 3rem;
|
border-radius: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,10 +282,6 @@ svg + .button__label {
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
border-top-right-radius: var(--border-radius);
|
border-top-right-radius: var(--border-radius);
|
||||||
border-bottom-right-radius: var(--border-radius);
|
border-bottom-right-radius: var(--border-radius);
|
||||||
// since we're abusing "button-toggle" let it stand alone properly
|
|
||||||
&:not(:first-of-type) {
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
.card--section {
|
.card--section {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: var(--spacing-l);
|
padding: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card--reward-total {
|
.card--reward-total {
|
||||||
|
@ -82,23 +82,22 @@
|
||||||
|
|
||||||
.card__title-section {
|
.card__title-section {
|
||||||
@extend .section__flex;
|
@extend .section__flex;
|
||||||
padding: var(--spacing-m) var(--spacing-l);
|
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
padding: 0;
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__title-section--body-list {
|
.card__title-section--body-list {
|
||||||
padding: var(--spacing-m);
|
padding-left: var(--spacing-s);
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
padding: 0;
|
padding: var(--spacing-m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__title-section--small {
|
.card__title-section--small {
|
||||||
padding: var(--spacing-s) var(--spacing-m);
|
padding: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__actions--inline {
|
.card__actions--inline {
|
||||||
|
@ -175,7 +174,7 @@
|
||||||
.card__title {
|
.card__title {
|
||||||
display: block;
|
display: block;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: var(--font-title);
|
font-size: var(--font-large);
|
||||||
font-weight: var(--font-weight-light);
|
font-weight: var(--font-weight-light);
|
||||||
|
|
||||||
& > *:not(:last-child) {
|
& > *:not(:last-child) {
|
||||||
|
@ -200,11 +199,9 @@
|
||||||
|
|
||||||
.card__title-actions {
|
.card__title-actions {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
padding: var(--spacing-m);
|
|
||||||
padding-right: var(--spacing-l);
|
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
padding: 0;
|
padding: var(--spacing-s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,6 +245,13 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__header--nowrap {
|
.card__header--nowrap {
|
||||||
|
@ -262,7 +266,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__body {
|
.card__body {
|
||||||
padding: var(--spacing-l);
|
padding: var(--spacing-m);
|
||||||
|
|
||||||
&:not(.card__body--no-title) {
|
&:not(.card__body--no-title) {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
@ -279,10 +283,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__main-actions {
|
.card__main-actions {
|
||||||
padding: var(--spacing-l);
|
padding: var(--spacing-m);
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
margin-bottom: var(--spacing-l);
|
margin-bottom: var(--spacing-s);
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
&:only-child {
|
&:only-child {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
|
@ -290,10 +295,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__body-actions {
|
.card__body-actions {
|
||||||
padding: var(--spacing-l);
|
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
padding: var(--spacing-m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,15 +322,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__header,
|
|
||||||
.card__body,
|
.card__body,
|
||||||
.card__main-actions {
|
.card__main-actions {
|
||||||
@media (max-width: $breakpoint-small) {
|
padding: var(--spacing-m);
|
||||||
padding: var(--spacing-s);
|
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-m);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__bottom-gutter {
|
.card__bottom-gutter {
|
||||||
|
|
|
@ -77,6 +77,12 @@ $metadata-z-index: 1;
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel-thumbnail--xsmall {
|
||||||
|
height: 2.1rem;
|
||||||
|
width: 2.1rem;
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
.chanel-thumbnail--waiting {
|
.chanel-thumbnail--waiting {
|
||||||
background-color: var(--color-gray-5);
|
background-color: var(--color-gray-5);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
@ -400,7 +406,7 @@ $metadata-z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-staked__indicator {
|
.channel-staked__indicator {
|
||||||
margin-left: 2px;
|
margin-left: 1px;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
fill: var(--color-gray-3);
|
fill: var(--color-gray-3);
|
||||||
}
|
}
|
||||||
|
|
|
@ -356,7 +356,8 @@
|
||||||
|
|
||||||
.claim-grid__header {
|
.claim-grid__header {
|
||||||
margin-bottom: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
display: inline-block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -90,13 +90,9 @@
|
||||||
.claim-search__menu-group {
|
.claim-search__menu-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
&:last-of-type {
|
|
||||||
.button-toggle:last-of-type {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-search__menu-group--between {
|
.claim-search__menu-group--between {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
|
|
@ -15,6 +15,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
|
|
||||||
.comment__sort {
|
.comment__sort {
|
||||||
margin: var(--spacing-s) 0;
|
margin: var(--spacing-s) 0;
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
|
@ -24,7 +25,6 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__create {
|
.comment__create {
|
||||||
padding-bottom: var(--spacing-m);
|
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
|
@ -53,15 +54,13 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-staked__wrapper {
|
.channel-staked__wrapper {
|
||||||
@media (max-width: $breakpoint-small) {
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
left: 0;
|
left: calc(#{$thumbnailWidthSmall} / 4);
|
||||||
bottom: -1rem;
|
bottom: -1rem;
|
||||||
padding: -1rem;
|
padding: -1rem;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__thumbnail-wrapper {
|
.comment__thumbnail-wrapper {
|
||||||
|
@ -102,14 +101,28 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment--livestream {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment--slimed {
|
.comment--slimed {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment__sc-preview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
margin: var(--spacing-s) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__edit-input {
|
||||||
|
margin-top: var(--spacing-xxs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__scpreview-amount {
|
||||||
|
margin-right: var(--spacing-m);
|
||||||
|
font-size: var(--font-large);
|
||||||
|
}
|
||||||
|
|
||||||
.comment__threadline {
|
.comment__threadline {
|
||||||
@extend .button--alt;
|
@extend .button--alt;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -155,12 +168,12 @@ $thumbnailWidthSmall: 1rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__body_container {
|
.comment__body-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-left: var(--spacing-xs);
|
margin-left: var(--spacing-xs);
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
margin-left: var(--spacing-m);
|
margin-left: var(--spacing-s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +244,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-left: var(--spacing-xs);
|
margin-right: var(--spacing-xs);
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@include linkFocus;
|
@include linkFocus;
|
||||||
|
@ -252,9 +265,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__char-count {
|
.comment__char-count {
|
||||||
align-self: flex-end;
|
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
padding-top: var(--spacing-xxs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__char-count-mde {
|
.comment__char-count-mde {
|
||||||
|
@ -407,3 +418,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
margin-right: var(--spacing-s);
|
margin-right: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment__tip-input {
|
||||||
|
margin: var(--spacing-s) 0;
|
||||||
|
}
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
.card + .file-render,
|
.card + .file-render,
|
||||||
.card + .file-page__video-container,
|
.card + .file-page__video-container,
|
||||||
.card + .content__cover {
|
.card + .content__cover {
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card + .file-render {
|
.card + .file-render {
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-page__md {
|
.file-page__md {
|
||||||
|
|
|
@ -440,6 +440,21 @@ fieldset-group {
|
||||||
margin-top: 2.5%;
|
margin-top: 2.5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-field__textarea-info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: var(--spacing-xxs);
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field__quick-emojis {
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fieldset-section {
|
fieldset-section {
|
||||||
.form-field__internal-option {
|
.form-field__internal-option {
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
$discussion-header__height: 3rem;
|
||||||
|
|
||||||
.livestream {
|
.livestream {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -23,52 +25,125 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__discussion {
|
.livestream__discussion {
|
||||||
min-height: 0%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
width: 35rem;
|
margin: 0;
|
||||||
margin-left: var(--spacing-m);
|
width: var(--livestream-comments-width);
|
||||||
margin-top: 0;
|
height: calc(100vh - var(--header-height));
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: var(--header-height);
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: none;
|
||||||
|
|
||||||
|
.card__main-actions {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__comments-wrapper {
|
.livestream-discussion__header {
|
||||||
overflow-y: scroll;
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding-bottom: var(--spacing-s);
|
||||||
|
margin-bottom: 0;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
height: $discussion-header__height;
|
||||||
|
padding: 0 var(--spacing-s);
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__comments-wrapper--with-height {
|
.livestream-discussion__title {
|
||||||
height: 40vh;
|
@extend .card__title-section;
|
||||||
|
@extend .card__title-section--small;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__comments-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - var(--header-height) - #{$discussion-header__height});
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__comments {
|
.livestream__comments {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: visible;
|
||||||
|
padding-top: var(--spacing-s);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__comment {
|
.livestream-comment {
|
||||||
margin-top: var(--spacing-s);
|
list-style-type: none;
|
||||||
display: flex;
|
position: relative;
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.comment__body_container {
|
.channel-name {
|
||||||
margin-left: 0;
|
font-size: var(--font-xsmall);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
padding-top: var(--spacing-m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__comment-author {
|
.livestream-comment--superchat {
|
||||||
font-weight: var(--font-weight-bold);
|
+ .livestream-comment--superchat {
|
||||||
color: #888;
|
margin-bottom: var(--spacing-xxs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-comment__info {
|
||||||
|
margin-top: calc(var(--spacing-xxs) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 4rem;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
width: 5px;
|
||||||
|
background-color: var(--color-superchat);
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__comment-author--streamer {
|
.livestream-comment__body {
|
||||||
color: var(--color-primary);
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-comment__body {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
|
||||||
|
.channel-thumbnail {
|
||||||
|
margin-top: var(--spacing-xxs);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-comment__menu {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--spacing-xs);
|
||||||
|
top: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__comment-create {
|
.livestream__comment-create {
|
||||||
margin-top: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__channel-link {
|
.livestream__channel-link {
|
||||||
|
@ -119,93 +194,145 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__emoji-actions {
|
|
||||||
margin-bottom: var(--spacing-m);
|
|
||||||
|
|
||||||
> *:not(:last-child) {
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__embed-page {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.file-viewer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__embed-wrapper {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #000000;
|
|
||||||
|
|
||||||
.livestream {
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__embed-countdown {
|
|
||||||
@extend .livestream__embed-wrapper;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__embed {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__embed-comments {
|
|
||||||
width: 30vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
.livestream__discussion {
|
|
||||||
height: 100vh;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__main-actions {
|
|
||||||
height: 100%;
|
|
||||||
width: 30vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__comments-wrapper--with-height {
|
|
||||||
height: calc(100% - 200px - (var(--spacing-l)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__publish-intro {
|
.livestream__publish-intro {
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.livestream__viewer-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-left: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-superchats__wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: scroll;
|
||||||
|
padding: var(--spacing-s) var(--spacing-xs);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
font-size: var(--font-small);
|
||||||
|
background-color: var(--color-card-background);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
width: var(--livestream-comments-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-superchat__amount-large {
|
||||||
|
.credit-amount {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-superchats__inner {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-superchat {
|
||||||
|
display: flex;
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
padding: var(--spacing-xxs);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
|
.channel-thumbnail {
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
background-color: var(--color-superchat);
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
max-width: 8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-of-type(2) {
|
||||||
|
background-color: var(--color-superchat-2);
|
||||||
|
}
|
||||||
|
&:nth-of-type(3) {
|
||||||
|
background-color: var(--color-superchat-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-of-type(-n + 3) {
|
||||||
|
.channel-name,
|
||||||
|
.credit-amount {
|
||||||
|
color: var(--color-black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
max-width: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-superchat__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: var(--font-xsmall);
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-superchat__banner {
|
||||||
|
border-top-right-radius: var(--border-radius);
|
||||||
|
border-bottom-right-radius: var(--border-radius);
|
||||||
|
padding: 0.25rem var(--spacing-s);
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is just a two small circles that overlap to make it look like
|
||||||
|
// the banner and the left border are connected
|
||||||
|
.livestream-superchat__banner-corner {
|
||||||
|
height: calc(var(--border-radius) * 2);
|
||||||
|
width: calc(var(--border-radius) * 2);
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--color-superchat);
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
transform: translateX(25%) translateY(50%);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
height: calc(var(--border-radius) * 2);
|
||||||
|
width: calc(var(--border-radius) * 2);
|
||||||
|
border-top-left-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-card-background);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
transform: translateX(25%) translateY(50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-comment__text {
|
||||||
|
padding-right: var(--spacing-xxs);
|
||||||
|
padding-bottom: var(--spacing-xxs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-superchat__tooltip-amount {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
margin-left: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__superchat-comment {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
max-width: 5rem;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream-superchat__amount-large {
|
||||||
|
min-width: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.table--livestream-data {
|
.table--livestream-data {
|
||||||
td:nth-of-type(1) {
|
td:nth-of-type(1) {
|
||||||
max-width: 4rem;
|
max-width: 4rem;
|
||||||
|
|
|
@ -169,7 +169,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.main--livestream {
|
.main--livestream {
|
||||||
margin-top: var(--spacing-m);
|
width: calc(100vw - var(--spacing-xs) * 2);
|
||||||
|
margin: var(--spacing-xs);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
margin: var(--spacing-m);
|
||||||
|
margin-right: calc(var(--livestream-comments-width) + var(--spacing-m));
|
||||||
|
width: calc(100vw - var(--livestream-comments-width) - var(--spacing-m) * 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main--full-width {
|
.main--full-width {
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
@media (max-width: $breakpoint-medium) {
|
@media (max-width: $breakpoint-medium) {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -127,6 +127,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.recommended-content__toggles {
|
.recommended-content__toggles {
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 0 var(--spacing-xs);
|
padding: 0 var(--spacing-xs);
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
|
20
ui/scss/component/_superchat.scss
Normal file
20
ui/scss/component/_superchat.scss
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
.super-chat {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background: linear-gradient(to right, var(--color-superchat), var(--color-superchat-3));
|
||||||
|
padding: 0.2rem var(--spacing-xs);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
font-size: var(--font-xsmall);
|
||||||
|
|
||||||
|
.credit-amount {
|
||||||
|
color: var(--color-superchat-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-chat--light {
|
||||||
|
@extend .super-chat;
|
||||||
|
background: linear-gradient(to right, var(--color-superchat__light), var(--color-superchat-3__light));
|
||||||
|
|
||||||
|
.credit-amount {
|
||||||
|
color: var(--color-superchat-text__light);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,11 +7,11 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
padding-left: var(--spacing-l);
|
padding-left: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
padding-right: var(--spacing-l);
|
padding-right: var(--spacing-m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ td {
|
||||||
|
|
||||||
.table__header-text {
|
.table__header-text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 var(--spacing-s);
|
margin-right: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table__header-text--between {
|
.table__header-text--between {
|
||||||
|
|
|
@ -127,10 +127,10 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-m);
|
||||||
|
|
||||||
~ .section {
|
~ .section {
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:only-child,
|
&:only-child,
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
> *:not(:last-child) {
|
> *:not(:last-child) {
|
||||||
margin-right: var(--spacing-m);
|
margin-right: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
|
@ -161,7 +161,6 @@
|
||||||
|
|
||||||
.button--primary ~ .button--link,
|
.button--primary ~ .button--link,
|
||||||
.button--secondary ~ .button--link {
|
.button--secondary ~ .button--link {
|
||||||
margin-left: var(--spacing-s);
|
|
||||||
padding: var(--spacing-s) var(--spacing-m);
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
height: var(--button-height);
|
height: var(--button-height);
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,8 @@ $breakpoint-large: 1600px;
|
||||||
--file-list-thumbnail-width: 10rem;
|
--file-list-thumbnail-width: 10rem;
|
||||||
|
|
||||||
--tag-height: 1.5rem;
|
--tag-height: 1.5rem;
|
||||||
|
|
||||||
|
--livestream-comments-width: 30rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
|
|
|
@ -142,4 +142,14 @@
|
||||||
// Scrollbar
|
// Scrollbar
|
||||||
--color-scrollbar-thumb-bg: rgba(255, 255, 255, 0.2);
|
--color-scrollbar-thumb-bg: rgba(255, 255, 255, 0.2);
|
||||||
--color-scrollbar-track-bg: transparent;
|
--color-scrollbar-track-bg: transparent;
|
||||||
|
|
||||||
|
// Superchat
|
||||||
|
--color-superchat-text: var(--color-black);
|
||||||
|
--color-superchat-text__light: var(--color-text);
|
||||||
|
--color-superchat: #fcd34d;
|
||||||
|
--color-superchat__light: #ef4e1647;
|
||||||
|
--color-superchat-2: #fde68a;
|
||||||
|
--color-superchat-3: #fef3c7;
|
||||||
|
--color-superchat-3__light: #58066087;
|
||||||
|
--color-superchat-4: #fffbeb;
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,4 +108,13 @@
|
||||||
// Scrollbar
|
// Scrollbar
|
||||||
--color-scrollbar-thumb-bg: rgba(0, 0, 0, 0.2);
|
--color-scrollbar-thumb-bg: rgba(0, 0, 0, 0.2);
|
||||||
--color-scrollbar-track-bg: transparent;
|
--color-scrollbar-track-bg: transparent;
|
||||||
|
|
||||||
|
// Superchat
|
||||||
|
--color-superchat-text: var(--color-black);
|
||||||
|
--color-superchat: #fcd34d;
|
||||||
|
--color-superchat__light: #fcd34d50;
|
||||||
|
--color-superchat-2: #fde68a;
|
||||||
|
--color-superchat-3: #fef3c7;
|
||||||
|
--color-superchat-3__light: #fef3c750;
|
||||||
|
--color-superchat-4: #fffbeb;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
export function toHex(str: string): string {
|
export function toHex(str: string): string {
|
||||||
const array = Array.from(str);
|
let s = unescape(encodeURIComponent(str));
|
||||||
|
|
||||||
let result = '';
|
let result = '';
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
for (var i = 0; i < array.length; i++) {
|
result += s.charCodeAt(i).toString(16).padStart(2, '0');
|
||||||
const val = array[i];
|
|
||||||
const utf = toUTF8Array(val)
|
|
||||||
.map((num) => num.toString(16))
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
result += utf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://gist.github.com/joni/3760795
|
|
||||||
// See comment that fixes an issue in the original gist
|
|
||||||
function toUTF8Array(str: string): Array<number> {
|
|
||||||
var utf8 = [];
|
|
||||||
for (var i = 0; i < str.length; i++) {
|
|
||||||
var charcode = str.charCodeAt(i);
|
|
||||||
if (charcode < 0x80) utf8.push(charcode);
|
|
||||||
else if (charcode < 0x800) {
|
|
||||||
utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
|
|
||||||
} else if (charcode < 0xd800 || charcode >= 0xe000) {
|
|
||||||
utf8.push(0xe0 | (charcode >> 12), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f));
|
|
||||||
}
|
|
||||||
// surrogate pair
|
|
||||||
else {
|
|
||||||
i++;
|
|
||||||
charcode = (((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)) + 0x010000;
|
|
||||||
utf8.push(
|
|
||||||
0xf0 | (charcode >> 18),
|
|
||||||
0x80 | ((charcode >> 12) & 0x3f),
|
|
||||||
0x80 | ((charcode >> 6) & 0x3f),
|
|
||||||
0x80 | (charcode & 0x3f)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return utf8;
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ function encodeWithApostropheEncode(string) {
|
||||||
return encodeURIComponent(string).replace(/'/g, '%27');
|
return encodeURIComponent(string).replace(/'/g, '%27');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatLbryUrlForWeb = uri => {
|
export const formatLbryUrlForWeb = (uri) => {
|
||||||
let newUrl = uri.replace('lbry://', '/').replace(/#/g, ':');
|
let newUrl = uri.replace('lbry://', '/').replace(/#/g, ':');
|
||||||
if (newUrl.startsWith('/?')) {
|
if (newUrl.startsWith('/?')) {
|
||||||
// This is a lbry link to an internal page ex: lbry://?rewards
|
// This is a lbry link to an internal page ex: lbry://?rewards
|
||||||
|
@ -18,7 +18,7 @@ export const formatLbryUrlForWeb = uri => {
|
||||||
return newUrl;
|
return newUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatFileSystemPath = path => {
|
export const formatFileSystemPath = (path) => {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ export const formatFileSystemPath = path => {
|
||||||
ex: lbry://?rewards
|
ex: lbry://?rewards
|
||||||
ex: open.lbry.com/?rewards
|
ex: open.lbry.com/?rewards
|
||||||
*/
|
*/
|
||||||
export const formatInAppUrl = path => {
|
export const formatInAppUrl = (path) => {
|
||||||
// Determine if we need to add a leading "/$/" for app pages
|
// Determine if we need to add a leading "/$/" for app pages
|
||||||
const APP_PAGE_REGEX = /(\?)([a-z]*)(.*)/;
|
const APP_PAGE_REGEX = /(\?)([a-z]*)(.*)/;
|
||||||
const appPageMatches = APP_PAGE_REGEX.exec(path);
|
const appPageMatches = APP_PAGE_REGEX.exec(path);
|
||||||
|
@ -75,7 +75,7 @@ export const formatWebUrlIntoLbryUrl = (pathname, search) => {
|
||||||
return appLink;
|
return appLink;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateInitialUrl = hash => {
|
export const generateInitialUrl = (hash) => {
|
||||||
let url = '/';
|
let url = '/';
|
||||||
if (hash) {
|
if (hash) {
|
||||||
hash = hash.replace('#', '');
|
hash = hash.replace('#', '');
|
||||||
|
@ -88,7 +88,7 @@ export const generateLbryContentUrl = (canonicalUrl, permanentUrl) => {
|
||||||
return canonicalUrl ? canonicalUrl.split('lbry://')[1] : permanentUrl.split('lbry://')[1];
|
return canonicalUrl ? canonicalUrl.split('lbry://')[1] : permanentUrl.split('lbry://')[1];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateLbryWebUrl = lbryUrl => {
|
export const generateLbryWebUrl = (lbryUrl) => {
|
||||||
return lbryUrl.replace(/#/g, ':');
|
return lbryUrl.replace(/#/g, ':');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6951,9 +6951,9 @@ lazy-val@^1.0.4:
|
||||||
yargs "^13.2.2"
|
yargs "^13.2.2"
|
||||||
zstd-codec "^0.1.1"
|
zstd-codec "^0.1.1"
|
||||||
|
|
||||||
lbry-redux@lbryio/lbry-redux#eb37009a987410a60e9f2ba79708049c9904687c:
|
lbry-redux@lbryio/lbry-redux#7e173446838b381491492526ff29ca8312819879:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/eb37009a987410a60e9f2ba79708049c9904687c"
|
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/7e173446838b381491492526ff29ca8312819879"
|
||||||
dependencies:
|
dependencies:
|
||||||
proxy-polyfill "0.1.6"
|
proxy-polyfill "0.1.6"
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
|
|
Loading…
Reference in a new issue