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
|
||||
parent_id?: number, // comment_id of comment this is in reply to
|
||||
is_pinned: boolean,
|
||||
support_amount: number,
|
||||
};
|
||||
|
||||
// todo: relate individual comments to their commentId
|
||||
declare type CommentsState = {
|
||||
commentsByUri: { [string]: string },
|
||||
superChatsByUri: { [string]: { totalAmount: number, comments: Array<Comment> } },
|
||||
byId: { [string]: Array<string> },
|
||||
repliesByParentId: { [string]: Array<string> }, // ParentCommentID -> list of reply comments
|
||||
topLevelCommentsById: { [string]: Array<string> }, // ClaimID -> list of top level comments
|
||||
|
@ -41,3 +43,36 @@ declare type CommentReactParams = {
|
|||
clear_types?: string,
|
||||
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,
|
||||
odysee_live_enabled: boolean,
|
||||
odysee_live_disabled: boolean,
|
||||
global_mod: boolean,
|
||||
};
|
||||
|
|
|
@ -142,7 +142,7 @@
|
|||
"imagesloaded": "^4.1.4",
|
||||
"json-loader": "^0.5.4",
|
||||
"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",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
|
|
@ -10,6 +10,8 @@ const Comments = {
|
|||
moderation_block_list: (params: ModerationBlockParams) => fetchCommentsApi('moderation.BlockedList', params),
|
||||
comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', 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: {}) {
|
||||
|
|
|
@ -19,6 +19,7 @@ type Props = {
|
|||
isResolving: boolean,
|
||||
showDelayedMessage?: boolean,
|
||||
hideStakedIndicator?: boolean,
|
||||
xsmall?: boolean,
|
||||
};
|
||||
|
||||
function ChannelThumbnail(props: Props) {
|
||||
|
@ -29,6 +30,7 @@ function ChannelThumbnail(props: Props) {
|
|||
thumbnailPreview: rawThumbnailPreview,
|
||||
obscure,
|
||||
small = false,
|
||||
xsmall = false,
|
||||
allowGifs = false,
|
||||
claim,
|
||||
doResolveUri,
|
||||
|
@ -72,6 +74,7 @@ function ChannelThumbnail(props: Props) {
|
|||
className={classnames('channel-thumbnail', className, {
|
||||
[colorClassName]: !showThumb,
|
||||
'channel-thumbnail--small': small,
|
||||
'channel-thumbnail--xsmall': xsmall,
|
||||
'channel-thumbnail--resolving': isResolving,
|
||||
})}
|
||||
>
|
||||
|
|
|
@ -30,12 +30,12 @@ type Props = {
|
|||
persistedStorageKey?: string,
|
||||
showHiddenByUser: boolean,
|
||||
showUnresolvedClaims?: boolean,
|
||||
renderProperties: ?(Claim) => Node,
|
||||
renderActions?: (Claim) => ?Node,
|
||||
renderProperties?: (Claim) => ?Node,
|
||||
includeSupportAction?: boolean,
|
||||
injectedItem: ?Node,
|
||||
timedOutMessage?: Node,
|
||||
tileLayout?: boolean,
|
||||
renderActions?: (Claim) => ?Node,
|
||||
searchInLanguage: boolean,
|
||||
hideMenu?: boolean,
|
||||
};
|
||||
|
@ -55,12 +55,12 @@ export default function ClaimList(props: Props) {
|
|||
page,
|
||||
showHiddenByUser,
|
||||
showUnresolvedClaims,
|
||||
renderProperties,
|
||||
includeSupportAction,
|
||||
injectedItem,
|
||||
timedOutMessage,
|
||||
tileLayout = false,
|
||||
renderActions,
|
||||
renderProperties,
|
||||
searchInLanguage,
|
||||
hideMenu,
|
||||
} = props;
|
||||
|
@ -101,7 +101,9 @@ export default function ClaimList(props: Props) {
|
|||
return tileLayout && !header ? (
|
||||
<section className="claim-grid">
|
||||
{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 && timedOutMessage && <div className="empty main--empty">{timedOutMessage}</div>}
|
||||
</section>
|
||||
|
|
|
@ -301,10 +301,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
<ClaimPreviewTitle uri={uri} />
|
||||
</NavLink>
|
||||
)}
|
||||
|
||||
{/* {type !== 'small' && !isChannelUri && signingChannel && SIMPLE_SITE && (
|
||||
<ChannelThumbnail uri={signingChannel.permanent_url} />
|
||||
)} */}
|
||||
</div>
|
||||
<ClaimPreviewSubtitle uri={uri} type={type} />
|
||||
{(pending || !!reflectingProgress) && <PublishPending uri={uri} />}
|
||||
|
|
|
@ -40,6 +40,7 @@ type Props = {
|
|||
isMature: boolean,
|
||||
showMature: boolean,
|
||||
showHiddenByUser?: boolean,
|
||||
properties?: (Claim) => void,
|
||||
};
|
||||
|
||||
function ClaimPreviewTile(props: Props) {
|
||||
|
@ -60,6 +61,7 @@ function ClaimPreviewTile(props: Props) {
|
|||
isMature,
|
||||
showMature,
|
||||
showHiddenByUser,
|
||||
properties,
|
||||
} = props;
|
||||
const isRepost = claim && claim.repost_channel_url;
|
||||
const shouldFetch = claim === undefined;
|
||||
|
@ -171,7 +173,7 @@ function ClaimPreviewTile(props: Props) {
|
|||
</div>
|
||||
{/* @endif */}
|
||||
<div className="claim-preview__file-property-overlay">
|
||||
<FileProperties uri={uri} small />
|
||||
<FileProperties uri={uri} small properties={properties} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config';
|
||||
import * as CS from 'constants/claim_search';
|
||||
import type { Node } from 'react';
|
||||
import React from 'react';
|
||||
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
|
||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||
|
@ -34,6 +35,8 @@ type Props = {
|
|||
timestamp?: string,
|
||||
feeAmount?: string,
|
||||
limitClaimsPerChannel?: number,
|
||||
hasNoSource?: boolean,
|
||||
renderProperties?: (Claim) => ?Node,
|
||||
};
|
||||
|
||||
function ClaimTilesDiscover(props: Props) {
|
||||
|
@ -57,6 +60,8 @@ function ClaimTilesDiscover(props: Props) {
|
|||
feeAmount,
|
||||
limitClaimsPerChannel,
|
||||
fetchingClaimSearchByQuery,
|
||||
hasNoSource,
|
||||
renderProperties,
|
||||
} = props;
|
||||
const { location } = useHistory();
|
||||
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,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -149,7 +156,7 @@ function ClaimTilesDiscover(props: Props) {
|
|||
return (
|
||||
<ul className="claim-grid">
|
||||
{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 />)}
|
||||
</ul>
|
||||
);
|
||||
|
|
|
@ -21,6 +21,7 @@ import { useHistory } from 'react-router';
|
|||
import CommentCreate from 'component/commentCreate';
|
||||
import CommentMenuList from 'component/commentMenuList';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
|
||||
type Props = {
|
||||
clearPlayingUri: () => void,
|
||||
|
@ -51,7 +52,7 @@ type Props = {
|
|||
activeChannelClaim: ?ChannelClaim,
|
||||
playingUri: ?PlayingUri,
|
||||
stakedLevel: number,
|
||||
livestream?: boolean,
|
||||
supportAmount: number,
|
||||
};
|
||||
|
||||
const LENGTH_TO_COLLAPSE = 300;
|
||||
|
@ -80,7 +81,7 @@ function Comment(props: Props) {
|
|||
othersReacts,
|
||||
playingUri,
|
||||
stakedLevel,
|
||||
livestream,
|
||||
supportAmount,
|
||||
} = props;
|
||||
const {
|
||||
push,
|
||||
|
@ -167,7 +168,7 @@ function Comment(props: Props) {
|
|||
className={classnames('comment', {
|
||||
'comment--top-level': isTopLevel,
|
||||
'comment--reply': !isTopLevel,
|
||||
'comment--livestream': livestream,
|
||||
'comment--superchat': supportAmount > 0,
|
||||
})}
|
||||
id={commentId}
|
||||
onMouseOver={() => setMouseHover(true)}
|
||||
|
@ -179,22 +180,15 @@ function Comment(props: Props) {
|
|||
'comment--slimed': slimedToDeath && !displayDeadComment,
|
||||
})}
|
||||
>
|
||||
{!livestream && (
|
||||
<div className="comment__thumbnail-wrapper">
|
||||
{authorUri ? (
|
||||
<ChannelThumbnail
|
||||
uri={authorUri}
|
||||
obscure={channelIsBlocked}
|
||||
small
|
||||
className="comment__author-thumbnail"
|
||||
/>
|
||||
) : (
|
||||
<ChannelThumbnail small className="comment__author-thumbnail" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="comment__thumbnail-wrapper">
|
||||
{authorUri ? (
|
||||
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} small className="comment__author-thumbnail" />
|
||||
) : (
|
||||
<ChannelThumbnail small className="comment__author-thumbnail" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="comment__body_container">
|
||||
<div className="comment__body-container">
|
||||
<div className="comment__meta">
|
||||
<div className="comment__meta-information">
|
||||
{!author ? (
|
||||
|
@ -205,17 +199,16 @@ function Comment(props: Props) {
|
|||
'comment__author--creator': commentByOwnerOfContent,
|
||||
})}
|
||||
link
|
||||
external={livestream}
|
||||
uri={authorUri}
|
||||
/>
|
||||
)}
|
||||
{!livestream && (
|
||||
<Button
|
||||
className="comment__time"
|
||||
onClick={handleTimeClick}
|
||||
label={<DateTime date={timePosted} timeAgo />}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
className="comment__time"
|
||||
onClick={handleTimeClick}
|
||||
label={<DateTime date={timePosted} timeAgo />}
|
||||
/>
|
||||
|
||||
{supportAmount > 0 && <CreditAmount amount={supportAmount} superChatLight size={12} />}
|
||||
|
||||
{isPinned && (
|
||||
<span className="comment__pin">
|
||||
|
@ -251,6 +244,7 @@ function Comment(props: Props) {
|
|||
{isEditing ? (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormField
|
||||
className="comment__edit-input"
|
||||
type={!SIMPLE_SITE && advancedEditor ? 'markdown' : 'textarea'}
|
||||
name="editing_comment"
|
||||
value={editedMessage}
|
||||
|
@ -258,7 +252,7 @@ function Comment(props: Props) {
|
|||
onChange={handleEditMessageChanged}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
<div className="section__actions">
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
<Button
|
||||
button="primary"
|
||||
type="submit"
|
||||
|
@ -295,20 +289,18 @@ function Comment(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{!livestream && (
|
||||
<div className="comment__actions">
|
||||
{threadDepth !== 0 && (
|
||||
<Button
|
||||
requiresAuth={IS_WEB}
|
||||
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
|
||||
className="comment__action"
|
||||
onClick={handleCommentReply}
|
||||
icon={ICONS.REPLY}
|
||||
/>
|
||||
)}
|
||||
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
|
||||
</div>
|
||||
)}
|
||||
<div className="comment__actions">
|
||||
{threadDepth !== 0 && (
|
||||
<Button
|
||||
requiresAuth={IS_WEB}
|
||||
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
|
||||
className="comment__action"
|
||||
onClick={handleCommentReply}
|
||||
icon={ICONS.REPLY}
|
||||
/>
|
||||
)}
|
||||
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
|
||||
</div>
|
||||
|
||||
{isReplying && (
|
||||
<CommentCreate
|
||||
|
|
|
@ -4,8 +4,8 @@ import {
|
|||
makeSelectClaimIsMine,
|
||||
selectMyChannelClaims,
|
||||
selectFetchingMyChannels,
|
||||
doSendTip,
|
||||
} from 'lbry-redux';
|
||||
import { selectIsPostingComment } from 'redux/selectors/comments';
|
||||
import { doOpenModal, doSetActiveChannel } from 'redux/actions/app';
|
||||
import { doCommentCreate } from 'redux/actions/comments';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
|
@ -18,17 +18,17 @@ const select = (state, props) => ({
|
|||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
channels: selectMyChannelClaims(state),
|
||||
isFetchingChannels: selectFetchingMyChannels(state),
|
||||
isPostingComment: selectIsPostingComment(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch, ownProps) => ({
|
||||
createComment: (comment, claimId, parentId) =>
|
||||
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream)),
|
||||
createComment: (comment, claimId, parentId, txid) =>
|
||||
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream, txid)),
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
||||
toast: (message) => dispatch(doToast({ message, isError: true })),
|
||||
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CommentCreate);
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
// @flow
|
||||
import type { ElementRef } from 'react';
|
||||
import { SIMPLE_SITE } from 'config';
|
||||
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 { FormField, Form } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import SelectChannel from 'component/selectChannel';
|
||||
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 type { ElementRef } from 'react';
|
||||
import emoji from 'emoji-dictionary';
|
||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
|
||||
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 = {
|
||||
uri: string,
|
||||
|
@ -32,12 +29,12 @@ type Props = {
|
|||
isFetchingChannels: boolean,
|
||||
parentId: string,
|
||||
isReply: boolean,
|
||||
isPostingComment: boolean,
|
||||
activeChannel: string,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
livestream?: boolean,
|
||||
toast: (string) => void,
|
||||
claimIsMine: boolean,
|
||||
sendTip: ({}, (any) => void, (any) => void) => void,
|
||||
};
|
||||
|
||||
export function CommentCreate(props: Props) {
|
||||
|
@ -51,24 +48,28 @@ export function CommentCreate(props: Props) {
|
|||
isFetchingChannels,
|
||||
isReply,
|
||||
parentId,
|
||||
isPostingComment,
|
||||
activeChannelClaim,
|
||||
livestream,
|
||||
toast,
|
||||
claimIsMine,
|
||||
sendTip,
|
||||
} = props;
|
||||
const buttonref: ElementRef<any> = React.useRef();
|
||||
const {
|
||||
push,
|
||||
location: { pathname },
|
||||
} = useHistory();
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
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 [lastCommentTime, setLastCommentTime] = React.useState();
|
||||
const [charCount, setCharCount] = useState(commentValue.length);
|
||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||
const hasChannels = channels && channels.length;
|
||||
const disabled = isPostingComment || !activeChannelClaim || !commentValue.length;
|
||||
const disabled = isSubmitting || !activeChannelClaim || !commentValue.length;
|
||||
const charCount = commentValue.length;
|
||||
|
||||
function handleCommentChange(event) {
|
||||
let commentValue;
|
||||
|
@ -108,25 +109,63 @@ export function CommentCreate(props: Props) {
|
|||
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) {
|
||||
setCommentValue('');
|
||||
setLastCommentTime(Date.now());
|
||||
setIsReviewingSupportComment(false);
|
||||
setIsSupportComment(false);
|
||||
|
||||
if (onDoneReplying) {
|
||||
onDoneReplying();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleEditorMode() {
|
||||
setAdvancedEditor(!advancedEditor);
|
||||
}
|
||||
|
||||
useEffect(() => setCharCount(commentValue.length), [commentValue]);
|
||||
|
||||
if (!hasChannels) {
|
||||
return (
|
||||
<div
|
||||
|
@ -146,13 +185,39 @@ export function CommentCreate(props: Props) {
|
|||
placeholder={__('Say something about this...')}
|
||||
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} />
|
||||
</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 (
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
|
@ -167,7 +232,9 @@ export function CommentCreate(props: Props) {
|
|||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
label={
|
||||
<span className="comment-new__label-wrapper">
|
||||
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
||||
{!livestream && (
|
||||
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
||||
)}
|
||||
<SelectChannel tiny />
|
||||
</span>
|
||||
}
|
||||
|
@ -182,52 +249,56 @@ export function CommentCreate(props: Props) {
|
|||
charCount={charCount}
|
||||
onChange={handleCommentChange}
|
||||
autoFocus={isReply}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
{livestream && hasChannels && (
|
||||
<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>
|
||||
)}
|
||||
{isSupportComment && <WalletTipAmountSelector amount={tipAmount} onChange={(amount) => setTipAmount(amount)} />}
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
<Button
|
||||
ref={buttonref}
|
||||
button="primary"
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
label={
|
||||
isReply
|
||||
? isPostingComment
|
||||
? __('Replying...')
|
||||
: __('Reply')
|
||||
: isPostingComment
|
||||
? __('Posting...')
|
||||
: __('Post --[button to submit something]--')
|
||||
}
|
||||
requiresAuth={IS_WEB}
|
||||
/>
|
||||
{isReply && (
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => {
|
||||
if (onCancelReplying) {
|
||||
onCancelReplying();
|
||||
{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
|
||||
ref={buttonref}
|
||||
button="primary"
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
label={
|
||||
isReply
|
||||
? isSubmitting
|
||||
? __('Replying...')
|
||||
: __('Reply')
|
||||
: isSubmitting
|
||||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
requiresAuth={IS_WEB}
|
||||
/>
|
||||
{!claimIsMine && (
|
||||
<Button disabled={disabled} button="alt" icon={ICONS.LBC} onClick={() => setIsSupportComment(true)} />
|
||||
)}
|
||||
{isReply && (
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => {
|
||||
if (onCancelReplying) {
|
||||
onCancelReplying();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
@ -16,8 +16,8 @@ import Empty from 'component/common/empty';
|
|||
|
||||
type Props = {
|
||||
comments: Array<Comment>,
|
||||
fetchComments: string => void,
|
||||
fetchReacts: string => Promise<any>,
|
||||
fetchComments: (string) => void,
|
||||
fetchReacts: (string) => Promise<any>,
|
||||
uri: string,
|
||||
claimIsMine: boolean,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
|
@ -129,11 +129,11 @@ function CommentList(props: Props) {
|
|||
|
||||
if (linkedComment) {
|
||||
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);
|
||||
} else {
|
||||
const parentComment = arrayOfComments.find(c => c.comment_id === linkedComment.parent_id);
|
||||
orderedComments = arrayOfComments.filter(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);
|
||||
|
||||
if (parentComment) {
|
||||
orderedComments.unshift(parentComment);
|
||||
|
@ -218,7 +218,7 @@ function CommentList(props: Props) {
|
|||
<ul className="comments" ref={commentRef}>
|
||||
{comments &&
|
||||
displayedComments &&
|
||||
displayedComments.map(comment => {
|
||||
displayedComments.map((comment) => {
|
||||
return (
|
||||
<CommentView
|
||||
isTopLevel
|
||||
|
@ -235,6 +235,7 @@ function CommentList(props: Props) {
|
|||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
linkedComment={linkedComment}
|
||||
isPinned={comment.is_pinned}
|
||||
supportAmount={comment.support_amount}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -22,7 +22,7 @@ function CommentsReplies(props: Props) {
|
|||
const sortedComments = comments ? [...comments].reverse() : [];
|
||||
const numberOfComments = comments ? comments.length : 0;
|
||||
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() {
|
||||
if (start > 0) {
|
||||
|
@ -105,6 +105,7 @@ function CommentsReplies(props: Props) {
|
|||
linkedComment={linkedComment}
|
||||
commentingEnabled={commentingEnabled}
|
||||
handleCommentDone={handleCommentDone}
|
||||
supportAmount={comment.support_amount}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -47,10 +47,12 @@ export default function Card(props: Props) {
|
|||
|
||||
return (
|
||||
<section
|
||||
role="button"
|
||||
className={classnames(className, 'card')}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -15,6 +15,9 @@ type Props = {
|
|||
fee?: boolean,
|
||||
className?: string,
|
||||
noFormat?: boolean,
|
||||
size?: number,
|
||||
superChat?: boolean,
|
||||
superChatLight?: boolean,
|
||||
};
|
||||
|
||||
class CreditAmount extends React.PureComponent<Props> {
|
||||
|
@ -39,8 +42,10 @@ class CreditAmount extends React.PureComponent<Props> {
|
|||
showLBC,
|
||||
className,
|
||||
noFormat,
|
||||
size,
|
||||
superChat,
|
||||
superChatLight,
|
||||
} = this.props;
|
||||
|
||||
const minimumRenderableAmount = 10 ** (-1 * precision);
|
||||
const fullPrice = formatFullPrice(amount, 2);
|
||||
const isFree = parseFloat(amount) === 0;
|
||||
|
@ -66,7 +71,7 @@ class CreditAmount extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
if (showLBC) {
|
||||
amountText = <LbcSymbol postfix={amountText} />;
|
||||
amountText = <LbcSymbol postfix={amountText} size={size} />;
|
||||
}
|
||||
|
||||
if (fee) {
|
||||
|
@ -75,7 +80,13 @@ class CreditAmount extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
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>
|
||||
|
||||
{isEstimate ? (
|
||||
|
|
|
@ -3,13 +3,13 @@ import * as React from 'react';
|
|||
import { FormField } from './form-field';
|
||||
|
||||
type FormPrice = {
|
||||
amount: ?number,
|
||||
amount: number,
|
||||
currency: string,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
price: FormPrice,
|
||||
onChange: FormPrice => void,
|
||||
onChange: (FormPrice) => void,
|
||||
placeholder: number,
|
||||
min: number,
|
||||
disabled: boolean,
|
||||
|
@ -27,7 +27,7 @@ export class FormFieldPrice extends React.PureComponent<Props> {
|
|||
|
||||
handleAmountChange(event: SyntheticInputEvent<*>) {
|
||||
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({
|
||||
currency: price.currency,
|
||||
amount,
|
||||
|
@ -54,7 +54,7 @@ export class FormFieldPrice extends React.PureComponent<Props> {
|
|||
className="form-field--price-amount"
|
||||
min={min}
|
||||
value={price.amount}
|
||||
onWheel={e => e.preventDefault()}
|
||||
onWheel={(e) => e.preventDefault()}
|
||||
onChange={this.handleAmountChange}
|
||||
placeholder={placeholder || 5}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -8,6 +8,15 @@ import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
|||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
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 = {
|
||||
name: string,
|
||||
|
@ -26,9 +35,6 @@ type Props = {
|
|||
affixClass?: string, // class applied to prefix/postfix label
|
||||
autoFocus?: boolean,
|
||||
labelOnLeft: boolean,
|
||||
inputProps?: {
|
||||
disabled?: boolean,
|
||||
},
|
||||
inputButton?: React$Node,
|
||||
blockWrap: boolean,
|
||||
charCount?: number,
|
||||
|
@ -38,6 +44,9 @@ type Props = {
|
|||
max?: number,
|
||||
quickActionLabel?: string,
|
||||
quickActionHandler?: (any) => any,
|
||||
disabled?: boolean,
|
||||
onChange: (any) => void,
|
||||
value?: string | number,
|
||||
};
|
||||
|
||||
export class FormField extends React.PureComponent<Props> {
|
||||
|
@ -262,7 +271,25 @@ export class FormField extends React.PureComponent<Props> {
|
|||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
{countInfo}
|
||||
<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}
|
||||
</div>
|
||||
</fieldset-section>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -1336,7 +1336,7 @@ export const icons = {
|
|||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
viewBox="2 0 24 24"
|
||||
width={props.size || '18'}
|
||||
height={props.size || '18'}
|
||||
fill="currentColor"
|
||||
|
@ -1728,6 +1728,269 @@ export const icons = {
|
|||
</g>
|
||||
</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) => (
|
||||
<svg
|
||||
version="1.1"
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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 FileProperties from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
downloaded: makeSelectFilePartlyDownloaded(props.uri)(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// @flow
|
||||
import type { Node } from 'react';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
@ -13,22 +14,31 @@ type Props = {
|
|||
claimIsMine: boolean,
|
||||
isSubscribed: boolean,
|
||||
small: boolean,
|
||||
claim: Claim,
|
||||
properties?: (Claim) => ?Node,
|
||||
};
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={classnames('file-properties', {
|
||||
'file-properties--small': small,
|
||||
})}
|
||||
>
|
||||
<VideoDuration uri={uri} />
|
||||
<FileType uri={uri} />
|
||||
{isSubscribed && <Icon tooltip icon={ICONS.SUBSCRIBE} />}
|
||||
{!claimIsMine && downloaded && <Icon tooltip icon={ICONS.LIBRARY} />}
|
||||
{typeof properties === 'function' ? (
|
||||
properties(claim)
|
||||
) : (
|
||||
<>
|
||||
<VideoDuration uri={uri} />
|
||||
<FileType uri={uri} />
|
||||
{isSubscribed && <Icon tooltip icon={ICONS.SUBSCRIBE} />}
|
||||
{!claimIsMine && downloaded && <Icon tooltip icon={ICONS.LIBRARY} />}
|
||||
|
||||
<FilePrice hideFree uri={uri} />
|
||||
<FilePrice hideFree uri={uri} />
|
||||
</>
|
||||
)}
|
||||
</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 { makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
|
||||
import { doCommentList } from 'redux/actions/comments';
|
||||
import { makeSelectTopLevelCommentsForUri, selectIsFetchingComments } from 'redux/selectors/comments';
|
||||
import { doCommentList, doSuperChatList } from 'redux/actions/comments';
|
||||
import {
|
||||
makeSelectTopLevelCommentsForUri,
|
||||
selectIsFetchingComments,
|
||||
makeSelectSuperChatsForUri,
|
||||
makeSelectSuperChatTotalAmountForUri,
|
||||
} from 'redux/selectors/comments';
|
||||
import LivestreamFeed from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
comments: makeSelectTopLevelCommentsForUri(props.uri)(state).slice(0, 75),
|
||||
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
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Card from 'component/common/card';
|
||||
import Spinner from 'component/spinner';
|
||||
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 = {
|
||||
uri: string,
|
||||
|
@ -16,9 +20,15 @@ type Props = {
|
|||
doCommentList: (string) => void,
|
||||
comments: Array<Comment>,
|
||||
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 {
|
||||
claim,
|
||||
uri,
|
||||
|
@ -28,16 +38,22 @@ export default function LivestreamFeed(props: Props) {
|
|||
comments,
|
||||
doCommentList,
|
||||
fetchingComments,
|
||||
doSuperChatList,
|
||||
superChats,
|
||||
superChatsTotalAmount,
|
||||
} = props;
|
||||
const commentsRef = React.createRef();
|
||||
const hasScrolledComments = React.useRef();
|
||||
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
|
||||
const [performedInitialScroll, setPerformedInitialScroll] = React.useState(false);
|
||||
const claimId = claim && claim.claim_id;
|
||||
const commentsLength = comments && comments.length;
|
||||
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? comments : superChats;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (claimId) {
|
||||
doCommentList(uri);
|
||||
doSuperChatList(uri);
|
||||
doCommentSocketConnect(uri, claimId);
|
||||
}
|
||||
|
||||
|
@ -46,7 +62,7 @@ export default function LivestreamFeed(props: Props) {
|
|||
doCommentSocketDisconnect(claimId);
|
||||
}
|
||||
};
|
||||
}, [claimId, uri, doCommentList, doCommentSocketConnect, doCommentSocketDisconnect]);
|
||||
}, [claimId, uri, doCommentList, doSuperChatList, doCommentSocketConnect, doCommentSocketDisconnect]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const element = commentsRef.current;
|
||||
|
@ -92,51 +108,87 @@ export default function LivestreamFeed(props: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={__('Live discussion')}
|
||||
smallTitle
|
||||
className="livestream__discussion"
|
||||
actions={
|
||||
<>
|
||||
{fetchingComments && (
|
||||
<div className="main--empty">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={commentsRef}
|
||||
className={classnames('livestream__comments-wrapper', {
|
||||
'livestream__comments-wrapper--with-height': commentsLength > 0,
|
||||
})}
|
||||
>
|
||||
{!fetchingComments && comments.length > 0 ? (
|
||||
<div className="livestream__comments">
|
||||
{comments.map((comment) => (
|
||||
<div key={comment.comment_id} className={classnames('livestream__comment')}>
|
||||
<CommentView
|
||||
livestream
|
||||
isTopLevel
|
||||
uri={uri}
|
||||
authorUri={comment.channel_url}
|
||||
author={comment.channel_name}
|
||||
claimId={comment.claim_id}
|
||||
commentId={comment.comment_id}
|
||||
message={comment.comment}
|
||||
timePosted={comment.timestamp * 1000}
|
||||
/>
|
||||
</div>
|
||||
<div className="card livestream__discussion">
|
||||
<div className="card__header--between livestream-discussion__header">
|
||||
<div className="livestream-discussion__title">{__('Live discussion')}</div>
|
||||
{superChatsTotalAmount > 0 && (
|
||||
<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={
|
||||
<>
|
||||
<CreditAmount amount={superChatsTotalAmount} size={8} /> {__('Tipped')}
|
||||
</>
|
||||
}
|
||||
onClick={() => setViewMode(VIEW_MODE_SUPER_CHAT)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
{fetchingComments && !comments && (
|
||||
<div className="main--empty">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div ref={commentsRef} className="livestream__comments-wrapper">
|
||||
{viewMode === VIEW_MODE_CHAT && superChatsTotalAmount > 0 && (
|
||||
<div className="livestream-superchats__wrapper">
|
||||
<div className="livestream-superchats__inner">
|
||||
{superChats.map((superChat: Comment) => (
|
||||
<Tooltip key={superChat.comment_id} label={superChat.comment}>
|
||||
<div className="livestream-superchat">
|
||||
<div className="livestream-superchat__thumbnail">
|
||||
<ChannelThumbnail uri={superChat.channel_url} xsmall />
|
||||
</div>
|
||||
|
||||
<div className="livestream-superchat__info">
|
||||
<UriIndicator uri={superChat.channel_url} link />
|
||||
<CreditAmount
|
||||
size={10}
|
||||
className="livestream-superchat__amount-large"
|
||||
amount={superChat.support_amount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="main--empty" />
|
||||
)}
|
||||
</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 className="main--empty" style={{ flex: 1 }} />
|
||||
)}
|
||||
|
||||
<div className="livestream__comment-create">
|
||||
<CommentCreate livestream bottom embed={embed} uri={uri} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { BITWAVE_EMBED_URL } from 'constants/livestream';
|
|||
import React from 'react';
|
||||
import FileTitleSection from 'component/fileTitleSection';
|
||||
import LivestreamComments from 'component/livestreamComments';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -13,6 +14,7 @@ type Props = {
|
|||
|
||||
export default function LivestreamLayout(props: Props) {
|
||||
const { claim, uri, isLive, activeViewers } = props;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (!claim || !claim.signing_channel) {
|
||||
return null;
|
||||
|
@ -41,9 +43,11 @@ export default function LivestreamLayout(props: Props) {
|
|||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isMobile && <LivestreamComments uri={uri} />}
|
||||
|
||||
<FileTitleSection uri={uri} livestream isLive={isLive} activeViewers={activeViewers} />
|
||||
</div>
|
||||
<LivestreamComments uri={uri} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import React from 'react';
|
|||
import Card from 'component/common/card';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import { useHistory } from 'react-router';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
|
||||
type Props = {
|
||||
channelClaim: ChannelClaim,
|
||||
|
@ -11,6 +13,7 @@ type Props = {
|
|||
|
||||
export default function LivestreamLink(props: Props) {
|
||||
const { channelClaim } = props;
|
||||
const { push } = useHistory();
|
||||
const [livestreamClaim, setLivestreamClaim] = React.useState(false);
|
||||
const [isLivestreaming, setIsLivestreaming] = React.useState(false);
|
||||
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.
|
||||
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}
|
||||
</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,
|
||||
isMarkdown?: boolean,
|
||||
livestream?: boolean,
|
||||
rightSide?: Node,
|
||||
backout: {
|
||||
backLabel?: string,
|
||||
backNavDefault?: string,
|
||||
|
@ -51,6 +52,7 @@ function Page(props: Props) {
|
|||
videoTheaterMode,
|
||||
isMarkdown = false,
|
||||
livestream,
|
||||
rightSide,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
|
@ -114,6 +116,8 @@ function Page(props: Props) {
|
|||
})}
|
||||
>
|
||||
{children}
|
||||
|
||||
{!isMobile && rightSide && <div className="main__right-side">{rightSide}</div>}
|
||||
</main>
|
||||
{/* @if TARGET='app' */}
|
||||
<StatusBar />
|
||||
|
|
|
@ -613,7 +613,7 @@ function PublishForm(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
<section>
|
||||
<div className="card__actions">
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
onClick={handlePublish}
|
||||
|
|
|
@ -38,6 +38,7 @@ import PasswordSetPage from 'page/passwordSet';
|
|||
import SignInVerifyPage from 'page/signInVerify';
|
||||
import ChannelsPage from 'page/channels';
|
||||
import LiveStreamSetupPage from 'page/livestreamSetup';
|
||||
import LivestreamCurrentPage from 'page/livestreamCurrent';
|
||||
import EmbedWrapperPage from 'page/embedWrapper';
|
||||
import TopPage from 'page/top';
|
||||
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.CHANNELS}`} component={ChannelsPage} />
|
||||
<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.RECEIVE}`} component={ReceivePage} />
|
||||
<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_FAILED = 'COMMENT_MODERATION_UN_BLOCK_FAILED';
|
||||
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
|
||||
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export const FF_MAX_CHARS_DEFAULT = 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_REPORT_CONTENT_DETAILS = 500;
|
||||
export const FF_MAX_CHARS_REPORT_CONTENT_ADDRESS = 255;
|
||||
|
|
|
@ -65,3 +65,4 @@ exports.CHANNEL_NEW = 'channel/new';
|
|||
exports.NOTIFICATIONS = 'notifications';
|
||||
exports.YOUTUBE_SYNC = 'youtube';
|
||||
exports.LIVESTREAM = 'livestream';
|
||||
exports.LIVESTREAM_CURRENT = 'live';
|
||||
|
|
|
@ -3,6 +3,7 @@ import { BITWAVE_LIVE_API } from 'constants/livestream';
|
|||
import React from 'react';
|
||||
import Page from 'component/page';
|
||||
import LivestreamLayout from 'component/livestreamLayout';
|
||||
import LivestreamComments from 'component/livestreamComments';
|
||||
import analytics from 'analytics';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
|
||||
|
@ -111,7 +112,7 @@ export default function LivestreamPage(props: Props) {
|
|||
}, [doSetPlayingUri]);
|
||||
|
||||
return (
|
||||
<Page className="file-page" filePage livestream>
|
||||
<Page className="file-page" noFooter livestream rightSide={<LivestreamComments uri={uri} />}>
|
||||
<LivestreamLayout uri={uri} activeViewers={activeViewers} isLive={isLive} />
|
||||
</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) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
|
@ -201,9 +239,10 @@ export function doCommentCreate(
|
|||
claim_id: string = '',
|
||||
parent_id?: 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 activeChannelClaim = selectActiveChannelClaim(state);
|
||||
|
||||
|
@ -216,6 +255,16 @@ export function doCommentCreate(
|
|||
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) {
|
||||
const notification = makeSelectNotificationForCommentId(parent_id)(state);
|
||||
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,
|
||||
claim_id: claim_id,
|
||||
channel_id: activeChannelClaim.claim_id,
|
||||
channel_name: activeChannelClaim.name,
|
||||
parent_id: parent_id,
|
||||
signature: signatureData.signature,
|
||||
signing_ts: signatureData.signing_ts,
|
||||
...(txid ? { support_tx_id: txid } : {}),
|
||||
})
|
||||
.then((result: CommentCreateResponse) => {
|
||||
dispatch({
|
||||
|
@ -258,6 +315,8 @@ export function doCommentCreate(
|
|||
isError: true,
|
||||
})
|
||||
);
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ const defaultState: CommentsState = {
|
|||
// Remove commentsByUri
|
||||
// It is not needed and doesn't provide anything but confusion
|
||||
commentsByUri: {}, // URI -> claimId
|
||||
superChatsByUri: {},
|
||||
isLoading: false,
|
||||
isCommenting: false,
|
||||
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) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
|
@ -224,6 +247,7 @@ export default handleActions(
|
|||
const commentsByClaimId = Object.assign({}, state.byId);
|
||||
const allCommentsById = Object.assign({}, state.commentById);
|
||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById);
|
||||
const superChatsByUri = Object.assign({}, state.superChatsByUri);
|
||||
const commentsForId = topLevelCommentsById[claimId];
|
||||
|
||||
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
|
||||
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 {
|
||||
...state,
|
||||
byId: commentsByClaimId,
|
||||
commentById: allCommentsById,
|
||||
commentsByUri,
|
||||
topLevelCommentsById,
|
||||
superChatsByUri,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -39,6 +39,8 @@ export const selectCommentsByClaimId = createSelector(selectState, selectComment
|
|||
return comments;
|
||||
});
|
||||
|
||||
export const selectSuperchatsByUri = createSelector(selectState, (state) => state.superChatsByUri);
|
||||
|
||||
export const selectTopLevelCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
||||
const byClaimId = state.topLevelCommentsById || {};
|
||||
const comments = {};
|
||||
|
@ -299,3 +301,26 @@ export const makeSelectUriIsBlockingOrUnBlocking = (uri: string) =>
|
|||
createSelector(selectBlockingByUri, selectUnBlockingByUri, (blockingByUri, unBlockingByUri) => {
|
||||
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/splash';
|
||||
@import 'component/status-bar';
|
||||
@import 'component/superchat';
|
||||
@import 'component/syntax-highlighter';
|
||||
@import 'component/table';
|
||||
@import 'component/livestream';
|
||||
|
|
|
@ -232,7 +232,7 @@
|
|||
}
|
||||
|
||||
.button--emoji {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.1rem;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
||||
|
@ -282,10 +282,6 @@ svg + .button__label {
|
|||
&:last-of-type {
|
||||
border-top-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 {
|
||||
position: relative;
|
||||
padding: var(--spacing-l);
|
||||
padding: var(--spacing-m);
|
||||
}
|
||||
|
||||
.card--reward-total {
|
||||
|
@ -82,23 +82,22 @@
|
|||
|
||||
.card__title-section {
|
||||
@extend .section__flex;
|
||||
padding: var(--spacing-m) var(--spacing-l);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: 0;
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.card__title-section--body-list {
|
||||
padding: var(--spacing-m);
|
||||
padding-left: var(--spacing-s);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: 0;
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.card__title-section--small {
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
padding: var(--spacing-s);
|
||||
}
|
||||
|
||||
.card__actions--inline {
|
||||
|
@ -175,7 +174,7 @@
|
|||
.card__title {
|
||||
display: block;
|
||||
align-items: center;
|
||||
font-size: var(--font-title);
|
||||
font-size: var(--font-large);
|
||||
font-weight: var(--font-weight-light);
|
||||
|
||||
& > *:not(:last-child) {
|
||||
|
@ -200,11 +199,9 @@
|
|||
|
||||
.card__title-actions {
|
||||
align-self: flex-start;
|
||||
padding: var(--spacing-m);
|
||||
padding-right: var(--spacing-l);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: 0;
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,6 +245,13 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
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 {
|
||||
|
@ -262,7 +266,7 @@
|
|||
}
|
||||
|
||||
.card__body {
|
||||
padding: var(--spacing-l);
|
||||
padding: var(--spacing-m);
|
||||
|
||||
&:not(.card__body--no-title) {
|
||||
padding-top: 0;
|
||||
|
@ -279,10 +283,11 @@
|
|||
}
|
||||
|
||||
.card__main-actions {
|
||||
padding: var(--spacing-l);
|
||||
padding: var(--spacing-m);
|
||||
padding-bottom: 0;
|
||||
margin-bottom: var(--spacing-l);
|
||||
margin-bottom: var(--spacing-s);
|
||||
border-top: 1px solid var(--color-border);
|
||||
height: 100%;
|
||||
|
||||
&:only-child {
|
||||
border-top: none;
|
||||
|
@ -290,10 +295,10 @@
|
|||
}
|
||||
|
||||
.card__body-actions {
|
||||
padding: var(--spacing-l);
|
||||
padding: var(--spacing-s);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: var(--spacing-s);
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,15 +322,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.card__header,
|
||||
.card__body,
|
||||
.card__main-actions {
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: var(--spacing-s);
|
||||
padding-bottom: 0;
|
||||
margin: 0;
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
padding: var(--spacing-m);
|
||||
padding-bottom: 0;
|
||||
margin: 0;
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
|
||||
.card__bottom-gutter {
|
||||
|
|
|
@ -77,6 +77,12 @@ $metadata-z-index: 1;
|
|||
width: 3rem;
|
||||
}
|
||||
|
||||
.channel-thumbnail--xsmall {
|
||||
height: 2.1rem;
|
||||
width: 2.1rem;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.chanel-thumbnail--waiting {
|
||||
background-color: var(--color-gray-5);
|
||||
border-radius: var(--border-radius);
|
||||
|
@ -400,7 +406,7 @@ $metadata-z-index: 1;
|
|||
}
|
||||
|
||||
.channel-staked__indicator {
|
||||
margin-left: 2px;
|
||||
margin-left: 1px;
|
||||
z-index: 3;
|
||||
fill: var(--color-gray-3);
|
||||
}
|
||||
|
|
|
@ -356,7 +356,8 @@
|
|||
|
||||
.claim-grid__header {
|
||||
margin-bottom: var(--spacing-m);
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.button {
|
||||
&:hover {
|
||||
|
|
|
@ -90,13 +90,9 @@
|
|||
.claim-search__menu-group {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
&:last-of-type {
|
||||
.button-toggle:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
.claim-search__menu-group--between {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
|
|
@ -15,6 +15,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
|
||||
.comment__sort {
|
||||
margin: var(--spacing-s) 0;
|
||||
margin-right: var(--spacing-s);
|
||||
display: block;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
|
@ -24,7 +25,6 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
|
||||
.comment__create {
|
||||
padding-bottom: var(--spacing-m);
|
||||
font-size: var(--font-small);
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
flex-direction: column;
|
||||
font-size: var(--font-small);
|
||||
margin: 0;
|
||||
position: relative;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: var(--spacing-l);
|
||||
|
@ -53,13 +54,11 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
|
||||
.channel-staked__wrapper {
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: 0;
|
||||
left: 0;
|
||||
bottom: -1rem;
|
||||
padding: -1rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
padding: 0;
|
||||
left: calc(#{$thumbnailWidthSmall} / 4);
|
||||
bottom: -1rem;
|
||||
padding: -1rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,14 +101,28 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment--livestream {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.comment--slimed {
|
||||
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 {
|
||||
@extend .button--alt;
|
||||
height: auto;
|
||||
|
@ -155,12 +168,12 @@ $thumbnailWidthSmall: 1rem;
|
|||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.comment__body_container {
|
||||
.comment__body-container {
|
||||
flex: 1;
|
||||
margin-left: var(--spacing-xs);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: var(--spacing-m);
|
||||
margin-left: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,7 +244,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
opacity: 0.5;
|
||||
white-space: nowrap;
|
||||
height: 100%;
|
||||
margin-left: var(--spacing-xs);
|
||||
margin-right: var(--spacing-xs);
|
||||
|
||||
&:focus {
|
||||
@include linkFocus;
|
||||
|
@ -252,9 +265,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
|
||||
.comment__char-count {
|
||||
align-self: flex-end;
|
||||
font-size: var(--font-xsmall);
|
||||
padding-top: var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.comment__char-count-mde {
|
||||
|
@ -407,3 +418,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
white-space: pre-line;
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
.comment__tip-input {
|
||||
margin: var(--spacing-s) 0;
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
.card + .file-render,
|
||||
.card + .file-page__video-container,
|
||||
.card + .content__cover {
|
||||
margin-top: var(--spacing-l);
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
.card + .file-render {
|
||||
margin-top: var(--spacing-l);
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
.file-page__md {
|
||||
|
|
|
@ -440,6 +440,21 @@ fieldset-group {
|
|||
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 {
|
||||
.form-field__internal-option {
|
||||
margin-top: var(--spacing-s);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
$discussion-header__height: 3rem;
|
||||
|
||||
.livestream {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
@ -23,52 +25,125 @@
|
|||
}
|
||||
|
||||
.livestream__discussion {
|
||||
min-height: 0%;
|
||||
width: 100%;
|
||||
margin-top: var(--spacing-m);
|
||||
margin-bottom: var(--spacing-s);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
width: 35rem;
|
||||
margin-left: var(--spacing-m);
|
||||
margin-top: 0;
|
||||
margin: 0;
|
||||
width: var(--livestream-comments-width);
|
||||
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 {
|
||||
overflow-y: scroll;
|
||||
.livestream-discussion__header {
|
||||
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 {
|
||||
height: 40vh;
|
||||
.livestream-discussion__title {
|
||||
@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 {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
font-size: var(--font-small);
|
||||
overflow-y: scroll;
|
||||
overflow-x: visible;
|
||||
padding-top: var(--spacing-s);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.livestream__comment {
|
||||
margin-top: var(--spacing-s);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.livestream-comment {
|
||||
list-style-type: none;
|
||||
position: relative;
|
||||
|
||||
.comment__body_container {
|
||||
margin-left: 0;
|
||||
.channel-name {
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
padding-top: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.livestream__comment-author {
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: #888;
|
||||
.livestream-comment--superchat {
|
||||
+ .livestream-comment--superchat {
|
||||
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 {
|
||||
color: var(--color-primary);
|
||||
.livestream-comment__body {
|
||||
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 {
|
||||
margin-top: var(--spacing-s);
|
||||
padding: var(--spacing-s);
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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 {
|
||||
td:nth-of-type(1) {
|
||||
max-width: 4rem;
|
||||
|
|
|
@ -169,7 +169,14 @@
|
|||
}
|
||||
|
||||
.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 {
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
display: block;
|
||||
|
|
|
@ -127,6 +127,8 @@
|
|||
}
|
||||
|
||||
.recommended-content__toggles {
|
||||
margin-right: var(--spacing-s);
|
||||
|
||||
button {
|
||||
padding: 0 var(--spacing-xs);
|
||||
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;
|
||||
|
||||
&:first-of-type {
|
||||
padding-left: var(--spacing-l);
|
||||
padding-left: var(--spacing-m);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
padding-right: var(--spacing-l);
|
||||
padding-right: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,7 +118,7 @@ td {
|
|||
|
||||
.table__header-text {
|
||||
width: 100%;
|
||||
margin: 0 var(--spacing-s);
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
.table__header-text--between {
|
||||
|
|
|
@ -127,10 +127,10 @@
|
|||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-l);
|
||||
margin-top: var(--spacing-m);
|
||||
|
||||
~ .section {
|
||||
margin-top: var(--spacing-l);
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
&:only-child,
|
||||
|
@ -139,7 +139,7 @@
|
|||
}
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: var(--spacing-m);
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
|
@ -161,7 +161,6 @@
|
|||
|
||||
.button--primary ~ .button--link,
|
||||
.button--secondary ~ .button--link {
|
||||
margin-left: var(--spacing-s);
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
height: var(--button-height);
|
||||
}
|
||||
|
|
|
@ -96,6 +96,8 @@ $breakpoint-large: 1600px;
|
|||
--file-list-thumbnail-width: 10rem;
|
||||
|
||||
--tag-height: 1.5rem;
|
||||
|
||||
--livestream-comments-width: 30rem;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
|
|
|
@ -142,4 +142,14 @@
|
|||
// Scrollbar
|
||||
--color-scrollbar-thumb-bg: rgba(255, 255, 255, 0.2);
|
||||
--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
|
||||
--color-scrollbar-thumb-bg: rgba(0, 0, 0, 0.2);
|
||||
--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
|
||||
|
||||
export function toHex(str: string): string {
|
||||
const array = Array.from(str);
|
||||
|
||||
let s = unescape(encodeURIComponent(str));
|
||||
let result = '';
|
||||
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
const val = array[i];
|
||||
const utf = toUTF8Array(val)
|
||||
.map((num) => num.toString(16))
|
||||
.join('');
|
||||
|
||||
result += utf;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
result += s.charCodeAt(i).toString(16).padStart(2, '0');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
export const formatLbryUrlForWeb = uri => {
|
||||
export const formatLbryUrlForWeb = (uri) => {
|
||||
let newUrl = uri.replace('lbry://', '/').replace(/#/g, ':');
|
||||
if (newUrl.startsWith('/?')) {
|
||||
// This is a lbry link to an internal page ex: lbry://?rewards
|
||||
|
@ -18,7 +18,7 @@ export const formatLbryUrlForWeb = uri => {
|
|||
return newUrl;
|
||||
};
|
||||
|
||||
export const formatFileSystemPath = path => {
|
||||
export const formatFileSystemPath = (path) => {
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ export const formatFileSystemPath = path => {
|
|||
ex: lbry://?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
|
||||
const APP_PAGE_REGEX = /(\?)([a-z]*)(.*)/;
|
||||
const appPageMatches = APP_PAGE_REGEX.exec(path);
|
||||
|
@ -75,7 +75,7 @@ export const formatWebUrlIntoLbryUrl = (pathname, search) => {
|
|||
return appLink;
|
||||
};
|
||||
|
||||
export const generateInitialUrl = hash => {
|
||||
export const generateInitialUrl = (hash) => {
|
||||
let url = '/';
|
||||
if (hash) {
|
||||
hash = hash.replace('#', '');
|
||||
|
@ -88,7 +88,7 @@ export const generateLbryContentUrl = (canonicalUrl, permanentUrl) => {
|
|||
return canonicalUrl ? canonicalUrl.split('lbry://')[1] : permanentUrl.split('lbry://')[1];
|
||||
};
|
||||
|
||||
export const generateLbryWebUrl = lbryUrl => {
|
||||
export const generateLbryWebUrl = (lbryUrl) => {
|
||||
return lbryUrl.replace(/#/g, ':');
|
||||
};
|
||||
|
||||
|
|
|
@ -6951,9 +6951,9 @@ lazy-val@^1.0.4:
|
|||
yargs "^13.2.2"
|
||||
zstd-codec "^0.1.1"
|
||||
|
||||
lbry-redux@lbryio/lbry-redux#eb37009a987410a60e9f2ba79708049c9904687c:
|
||||
lbry-redux@lbryio/lbry-redux#7e173446838b381491492526ff29ca8312819879:
|
||||
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:
|
||||
proxy-polyfill "0.1.6"
|
||||
reselect "^3.0.0"
|
||||
|
|
Loading…
Reference in a new issue