diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index 816b9123f..fae198fd8 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -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 } }, byId: { [string]: Array }, repliesByParentId: { [string]: Array }, // ParentCommentID -> list of reply comments topLevelCommentsById: { [string]: Array }, // 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, + 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 = {}; diff --git a/flow-typed/comments.js b/flow-typed/comments.js deleted file mode 100644 index ee033cd62..000000000 --- a/flow-typed/comments.js +++ /dev/null @@ -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 = {}; diff --git a/flow-typed/user.js b/flow-typed/user.js index 28f7d04d1..ac4ee7025 100644 --- a/flow-typed/user.js +++ b/flow-typed/user.js @@ -30,4 +30,5 @@ declare type User = { experimental_ui: boolean, odysee_live_enabled: boolean, odysee_live_disabled: boolean, + global_mod: boolean, }; diff --git a/package.json b/package.json index 11622cf9f..71899e6c7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/ui/comments.js b/ui/comments.js index fe7ed5b16..eb94edca7 100644 --- a/ui/comments.js +++ b/ui/comments.js @@ -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: {}) { diff --git a/ui/component/channelThumbnail/view.jsx b/ui/component/channelThumbnail/view.jsx index d3af5c508..09692fb15 100644 --- a/ui/component/channelThumbnail/view.jsx +++ b/ui/component/channelThumbnail/view.jsx @@ -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, })} > diff --git a/ui/component/claimList/view.jsx b/ui/component/claimList/view.jsx index 8bcf73844..64a5d0281 100644 --- a/ui/component/claimList/view.jsx +++ b/ui/component/claimList/view.jsx @@ -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 ? (
{urisLength > 0 && - uris.map((uri) => )} + uris.map((uri) => ( + + ))} {!timedOut && urisLength === 0 && !loading &&
{empty || noResultMsg}
} {timedOut && timedOutMessage &&
{timedOutMessage}
}
diff --git a/ui/component/claimPreview/view.jsx b/ui/component/claimPreview/view.jsx index b4ce138da..2a4c73930 100644 --- a/ui/component/claimPreview/view.jsx +++ b/ui/component/claimPreview/view.jsx @@ -301,10 +301,6 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { )} - - {/* {type !== 'small' && !isChannelUri && signingChannel && SIMPLE_SITE && ( - - )} */} {(pending || !!reflectingProgress) && } diff --git a/ui/component/claimPreviewTile/view.jsx b/ui/component/claimPreviewTile/view.jsx index 6532842a8..511772ad3 100644 --- a/ui/component/claimPreviewTile/view.jsx +++ b/ui/component/claimPreviewTile/view.jsx @@ -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) { {/* @endif */}
- +
)} diff --git a/ui/component/claimTilesDiscover/view.jsx b/ui/component/claimTilesDiscover/view.jsx index 0b283e428..4019066c7 100644 --- a/ui/component/claimTilesDiscover/view.jsx +++ b/ui/component/claimTilesDiscover/view.jsx @@ -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 (
    {uris && uris.length - ? uris.map((uri) => ) + ? uris.map((uri) => ) : new Array(pageSize).fill(1).map((x, i) => )}
); diff --git a/ui/component/comment/view.jsx b/ui/component/comment/view.jsx index 2f8fd8b88..735ad4acf 100644 --- a/ui/component/comment/view.jsx +++ b/ui/component/comment/view.jsx @@ -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 && ( -
- {authorUri ? ( - - ) : ( - - )} -
- )} +
+ {authorUri ? ( + + ) : ( + + )} +
-
+
{!author ? ( @@ -205,17 +199,16 @@ function Comment(props: Props) { 'comment__author--creator': commentByOwnerOfContent, })} link - external={livestream} uri={authorUri} /> )} - {!livestream && ( -
- )} +
+ {threadDepth !== 0 && ( +
{isReplying && ( ({ 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); diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index 2ffcab174..51c23e2ec 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -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 = 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 (
-
+
); } + if (isReviewingSupportComment && activeChannelClaim) { + return ( +
+
+ + + +
+ +
{commentValue}
+
+
+
+
+
+ ); + } + return ( -
{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}
+ {!livestream && ( +
{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}
+ )} } @@ -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 && ( -
- {LIVESTREAM_EMOJIS.map((emoji) => ( -
- )} + {isSupportComment && setTipAmount(amount)} />}
-
diff --git a/ui/component/commentsList/view.jsx b/ui/component/commentsList/view.jsx index 2a5d7f50f..fd1f457be 100644 --- a/ui/component/commentsList/view.jsx +++ b/ui/component/commentsList/view.jsx @@ -16,8 +16,8 @@ import Empty from 'component/common/empty'; type Props = { comments: Array, - fetchComments: string => void, - fetchReacts: string => Promise, + fetchComments: (string) => void, + fetchReacts: (string) => Promise, uri: string, claimIsMine: boolean, myChannels: ?Array, @@ -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) {
    {comments && displayedComments && - displayedComments.map(comment => { + displayedComments.map((comment) => { return ( ); })} diff --git a/ui/component/commentsReplies/view.jsx b/ui/component/commentsReplies/view.jsx index 69ab12016..416cfd60d 100644 --- a/ui/component/commentsReplies/view.jsx +++ b/ui/component/commentsReplies/view.jsx @@ -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} /> ); })} diff --git a/ui/component/common/card.jsx b/ui/component/common/card.jsx index 64ce71d81..d7dad7dac 100644 --- a/ui/component/common/card.jsx +++ b/ui/component/common/card.jsx @@ -47,10 +47,12 @@ export default function Card(props: Props) { return (
    { + onClick={(e) => { if (onClick) { onClick(); + e.stopPropagation(); } }} > diff --git a/ui/component/common/credit-amount.jsx b/ui/component/common/credit-amount.jsx index f4c52139e..dc8036dc9 100644 --- a/ui/component/common/credit-amount.jsx +++ b/ui/component/common/credit-amount.jsx @@ -15,6 +15,9 @@ type Props = { fee?: boolean, className?: string, noFormat?: boolean, + size?: number, + superChat?: boolean, + superChatLight?: boolean, }; class CreditAmount extends React.PureComponent { @@ -39,8 +42,10 @@ class CreditAmount extends React.PureComponent { 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 { } if (showLBC) { - amountText = ; + amountText = ; } if (fee) { @@ -75,7 +80,13 @@ class CreditAmount extends React.PureComponent { } return ( - + {amountText} {isEstimate ? ( diff --git a/ui/component/common/form-components/form-field-price.jsx b/ui/component/common/form-components/form-field-price.jsx index 452afdc6d..e46526ba4 100644 --- a/ui/component/common/form-components/form-field-price.jsx +++ b/ui/component/common/form-components/form-field-price.jsx @@ -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 { 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 { 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} diff --git a/ui/component/common/form-components/form-field.jsx b/ui/component/common/form-components/form-field.jsx index cc7e7fd3f..b8e49dbc4 100644 --- a/ui/component/common/form-components/form-field.jsx +++ b/ui/component/common/form-components/form-field.jsx @@ -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 { @@ -262,7 +271,25 @@ export class FormField extends React.PureComponent { ref={this.input} {...inputProps} /> - {countInfo} +
    +
    + {QUICK_EMOJIS.map((emoji) => ( +
    + {countInfo} +
    ); } else { diff --git a/ui/component/common/icon-custom.jsx b/ui/component/common/icon-custom.jsx index c6719b0da..63fcb9fac 100644 --- a/ui/component/common/icon-custom.jsx +++ b/ui/component/common/icon-custom.jsx @@ -1336,7 +1336,7 @@ export const icons = { ), + [ICONS.LIVESTREAM]: (props: CustomProps) => ( + + + + + + + + + + + + + + {/* }//fill="#FFFFFF" */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + [ICONS.LIVESTREAM_SOLID]: (props: CustomProps) => ( + + + + + + + + + + + + + + + + + + + + ), + [ICONS.LIVESTREAM_MONOCHROME]: (props: CustomProps) => ( + + + + + + + + + + + + + + + + + + + ), + [ICONS.LIVESTREAM]: (props: CustomProps) => ( ({ + claim: makeSelectClaimForUri(props.uri)(state), downloaded: makeSelectFilePartlyDownloaded(props.uri)(state), isSubscribed: makeSelectIsSubscribed(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state), diff --git a/ui/component/fileProperties/view.jsx b/ui/component/fileProperties/view.jsx index 418888cc9..099daca36 100644 --- a/ui/component/fileProperties/view.jsx +++ b/ui/component/fileProperties/view.jsx @@ -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 (
    - - - {isSubscribed && } - {!claimIsMine && downloaded && } + {typeof properties === 'function' ? ( + properties(claim) + ) : ( + <> + + + {isSubscribed && } + {!claimIsMine && downloaded && } - + + + )}
    ); } diff --git a/ui/component/livestreamComment/index.js b/ui/component/livestreamComment/index.js new file mode 100644 index 000000000..8f6527467 --- /dev/null +++ b/ui/component/livestreamComment/index.js @@ -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); diff --git a/ui/component/livestreamComment/view.jsx b/ui/component/livestreamComment/view.jsx new file mode 100644 index 000000000..26380dde7 --- /dev/null +++ b/ui/component/livestreamComment/view.jsx @@ -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 ( +
  • 0, + })} + onMouseOver={() => setMouseHover(true)} + onMouseOut={() => setMouseHover(false)} + > + {supportAmount > 0 && ( +
    +
    + +
    + )} + +
    + {supportAmount > 0 && } +
    + + +
    + +
    +
    +
    + +
    + + + + + + +
    +
  • + ); +} + +export default Comment; diff --git a/ui/component/livestreamComments/index.js b/ui/component/livestreamComments/index.js index 9544a5b87..c71f6e35d 100644 --- a/ui/component/livestreamComments/index.js +++ b/ui/component/livestreamComments/index.js @@ -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); diff --git a/ui/component/livestreamComments/view.jsx b/ui/component/livestreamComments/view.jsx index 2cc7ef918..c442a87c1 100644 --- a/ui/component/livestreamComments/view.jsx +++ b/ui/component/livestreamComments/view.jsx @@ -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, fetchingComments: boolean, + doSuperChatList: (string) => void, + superChats: Array, + 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 ( - - {fetchingComments && ( -
    - -
    - )} -
    0, - })} - > - {!fetchingComments && comments.length > 0 ? ( -
    - {comments.map((comment) => ( -
    - -
    +
    +
    +
    {__('Live discussion')}
    + {superChatsTotalAmount > 0 && ( +
    +
    + )} +
    + <> + {fetchingComments && !comments && ( +
    + +
    + )} +
    + {viewMode === VIEW_MODE_CHAT && superChatsTotalAmount > 0 && ( +
    +
    + {superChats.map((superChat: Comment) => ( + +
    +
    + +
    + +
    + + +
    +
    +
    ))}
    - ) : ( -
    - )} -
    +
    + )} + + {!fetchingComments && comments.length > 0 ? ( +
    + {commentsToDisplay.map((comment) => ( + + ))} +
    + ) : ( +
    + )}
    - - } - /> +
    + +
    ); } diff --git a/ui/component/livestreamLayout/view.jsx b/ui/component/livestreamLayout/view.jsx index 85c35cd0c..9e474c1c4 100644 --- a/ui/component/livestreamLayout/view.jsx +++ b/ui/component/livestreamLayout/view.jsx @@ -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) { })}
    )} + + {isMobile && } +
    - ); } diff --git a/ui/component/livestreamLink/view.jsx b/ui/component/livestreamLink/view.jsx index 134bc4c5f..92469595f 100644 --- a/ui/component/livestreamLink/view.jsx +++ b/ui/component/livestreamLink/view.jsx @@ -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 }) => ( - + { + push(formatLbryUrlForWeb(livestreamClaim.canonical_url)); + }} + > {props.children} ); diff --git a/ui/component/livestreamList/index.js b/ui/component/livestreamList/index.js new file mode 100644 index 000000000..636241cf4 --- /dev/null +++ b/ui/component/livestreamList/index.js @@ -0,0 +1,6 @@ +import { connect } from 'react-redux'; +import LivestreamCurrent from './view'; + +const select = (state) => ({}); + +export default connect(select)(LivestreamCurrent); diff --git a/ui/component/livestreamList/view.jsx b/ui/component/livestreamList/view.jsx new file mode 100644 index 000000000..c4aa836b9 --- /dev/null +++ b/ui/component/livestreamList/view.jsx @@ -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 && ( +
    + +
    + )} + + {livestreamMap && Object.keys(livestreamMap).length > 0 && ( + { + const livestream = livestreamMap[claim.signing_channel.claim_id]; + + return ( + + {livestream.viewCount} + + ); + }} + /> + )} + + ); +} diff --git a/ui/component/page/view.jsx b/ui/component/page/view.jsx index 4fe613deb..8e8732278 100644 --- a/ui/component/page/view.jsx +++ b/ui/component/page/view.jsx @@ -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 &&
    {rightSide}
    } {/* @if TARGET='app' */} diff --git a/ui/component/publishForm/view.jsx b/ui/component/publishForm/view.jsx index f8028f775..bfefb3c54 100644 --- a/ui/component/publishForm/view.jsx +++ b/ui/component/publishForm/view.jsx @@ -613,7 +613,7 @@ function PublishForm(props: Props) {
    )}
    -
    +
    + + {useCustomTip && ( +
    + + {__('Custom support amount')}{' '} + }}> + (%lbc_balance% available) + + + } + className="form-field--price-amount" + error={tipError} + min="0" + step="any" + type="number" + placeholder="1.23" + value={amount} + onChange={(event) => handleCustomPriceChange(event.target.value)} + /> +
    + )} + + {!useCustomTip && } + + ); +} + +export default WalletTipAmountSelector; diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index ad79ca69b..134d4ec1e 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -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'; diff --git a/ui/constants/form-field.js b/ui/constants/form-field.js index 7ca0ece5d..f282e3116 100644 --- a/ui/constants/form-field.js +++ b/ui/constants/form-field.js @@ -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; diff --git a/ui/constants/pages.js b/ui/constants/pages.js index 5c32f3ca2..30e6f7670 100644 --- a/ui/constants/pages.js +++ b/ui/constants/pages.js @@ -65,3 +65,4 @@ exports.CHANNEL_NEW = 'channel/new'; exports.NOTIFICATIONS = 'notifications'; exports.YOUTUBE_SYNC = 'youtube'; exports.LIVESTREAM = 'livestream'; +exports.LIVESTREAM_CURRENT = 'live'; diff --git a/ui/page/livestream/view.jsx b/ui/page/livestream/view.jsx index 5b7a7157a..9e2462299 100644 --- a/ui/page/livestream/view.jsx +++ b/ui/page/livestream/view.jsx @@ -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 ( - + }> ); diff --git a/ui/page/livestreamCurrent/index.js b/ui/page/livestreamCurrent/index.js new file mode 100644 index 000000000..972e69dee --- /dev/null +++ b/ui/page/livestreamCurrent/index.js @@ -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); diff --git a/ui/page/livestreamCurrent/view.jsx b/ui/page/livestreamCurrent/view.jsx new file mode 100644 index 000000000..bd981b982 --- /dev/null +++ b/ui/page/livestreamCurrent/view.jsx @@ -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 ( + + {canView ? ( + + ) : ( + +
    + } + /> + )} + + ); +} diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index e5737c8c2..0130ec753 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -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); }); }; } diff --git a/ui/redux/reducers/comments.js b/ui/redux/reducers/comments.js index 9307a698f..45b995a1f 100644 --- a/ui/redux/reducers/comments.js +++ b/ui/redux/reducers/comments.js @@ -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, }; }, diff --git a/ui/redux/selectors/comments.js b/ui/redux/selectors/comments.js index 0e8fcab21..1db8981bf 100644 --- a/ui/redux/selectors/comments.js +++ b/ui/redux/selectors/comments.js @@ -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; + }); diff --git a/ui/scss/all.scss b/ui/scss/all.scss index ea4773be4..c6588e0ba 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -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'; diff --git a/ui/scss/component/_button.scss b/ui/scss/component/_button.scss index 0b26542c0..1bf356e64 100644 --- a/ui/scss/component/_button.scss +++ b/ui/scss/component/_button.scss @@ -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); - } } } diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index b2b83de49..f62fccb6c 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -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 { diff --git a/ui/scss/component/_channel.scss b/ui/scss/component/_channel.scss index e9f0eb223..838b117dc 100644 --- a/ui/scss/component/_channel.scss +++ b/ui/scss/component/_channel.scss @@ -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); } diff --git a/ui/scss/component/_claim-list.scss b/ui/scss/component/_claim-list.scss index f6cb92cbb..e88c084eb 100644 --- a/ui/scss/component/_claim-list.scss +++ b/ui/scss/component/_claim-list.scss @@ -356,7 +356,8 @@ .claim-grid__header { margin-bottom: var(--spacing-m); - display: inline-block; + display: flex; + align-items: center; .button { &:hover { diff --git a/ui/scss/component/_claim-search.scss b/ui/scss/component/_claim-search.scss index 7bc56b155..f2db0239d 100644 --- a/ui/scss/component/_claim-search.scss +++ b/ui/scss/component/_claim-search.scss @@ -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; diff --git a/ui/scss/component/_comments.scss b/ui/scss/component/_comments.scss index 4c0640144..5d074bfef 100644 --- a/ui/scss/component/_comments.scss +++ b/ui/scss/component/_comments.scss @@ -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; +} diff --git a/ui/scss/component/_file-render.scss b/ui/scss/component/_file-render.scss index 387c58e03..eba45692b 100644 --- a/ui/scss/component/_file-render.scss +++ b/ui/scss/component/_file-render.scss @@ -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 { diff --git a/ui/scss/component/_form-field.scss b/ui/scss/component/_form-field.scss index d619d425c..71ef0fd0f 100644 --- a/ui/scss/component/_form-field.scss +++ b/ui/scss/component/_form-field.scss @@ -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); diff --git a/ui/scss/component/_livestream.scss b/ui/scss/component/_livestream.scss index 26d561104..a0f2ef030 100644 --- a/ui/scss/component/_livestream.scss +++ b/ui/scss/component/_livestream.scss @@ -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; diff --git a/ui/scss/component/_main.scss b/ui/scss/component/_main.scss index 03dbd9a45..23507aded 100644 --- a/ui/scss/component/_main.scss +++ b/ui/scss/component/_main.scss @@ -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 { diff --git a/ui/scss/component/_media.scss b/ui/scss/component/_media.scss index 7fd328647..a743a528c 100644 --- a/ui/scss/component/_media.scss +++ b/ui/scss/component/_media.scss @@ -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; diff --git a/ui/scss/component/_search.scss b/ui/scss/component/_search.scss index 6c91c8afa..631201142 100644 --- a/ui/scss/component/_search.scss +++ b/ui/scss/component/_search.scss @@ -127,6 +127,8 @@ } .recommended-content__toggles { + margin-right: var(--spacing-s); + button { padding: 0 var(--spacing-xs); height: 2rem; diff --git a/ui/scss/component/_superchat.scss b/ui/scss/component/_superchat.scss new file mode 100644 index 000000000..b2f762e66 --- /dev/null +++ b/ui/scss/component/_superchat.scss @@ -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); + } +} diff --git a/ui/scss/component/_table.scss b/ui/scss/component/_table.scss index b102bc948..99171c794 100644 --- a/ui/scss/component/_table.scss +++ b/ui/scss/component/_table.scss @@ -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 { diff --git a/ui/scss/component/section.scss b/ui/scss/component/section.scss index 1dd1aa687..5eae20b0e 100644 --- a/ui/scss/component/section.scss +++ b/ui/scss/component/section.scss @@ -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); } diff --git a/ui/scss/init/_vars.scss b/ui/scss/init/_vars.scss index e31f3dc1a..31b8c566b 100644 --- a/ui/scss/init/_vars.scss +++ b/ui/scss/init/_vars.scss @@ -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) { diff --git a/ui/scss/themes/dark.scss b/ui/scss/themes/dark.scss index d3bd93ccb..1e01798e2 100644 --- a/ui/scss/themes/dark.scss +++ b/ui/scss/themes/dark.scss @@ -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; } diff --git a/ui/scss/themes/light.scss b/ui/scss/themes/light.scss index 7a5af3b88..50d97064e 100644 --- a/ui/scss/themes/light.scss +++ b/ui/scss/themes/light.scss @@ -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; } diff --git a/ui/util/hex.js b/ui/util/hex.js index c534797be..950c93c07 100644 --- a/ui/util/hex.js +++ b/ui/util/hex.js @@ -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 { - 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; -} diff --git a/ui/util/url.js b/ui/util/url.js index fa3307674..9a815e451 100644 --- a/ui/util/url.js +++ b/ui/util/url.js @@ -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, ':'); }; diff --git a/yarn.lock b/yarn.lock index a59fb32ed..7bcc2f914 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"