Add CommentCreate to Modal on Mobile
- Move stickers and emojis to a single menu comment-selectors on both mobile and desktop - More style improvements - Some fixes - Fix livechat scrolling
This commit is contained in:
parent
eef6691557
commit
c90efc2078
34 changed files with 1214 additions and 696 deletions
|
@ -21,6 +21,7 @@
|
|||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
|
||||
<link rel="preload" href="/public/font/v1/300.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/public/font/v1/300i.woff" as="font" type="font/woff" crossorigin />
|
||||
|
|
|
@ -213,17 +213,27 @@ function Comment(props: Props) {
|
|||
replace(`${pathname}?${urlParams.toString()}`);
|
||||
}
|
||||
|
||||
const linkedCommentRef = React.useCallback((node) => {
|
||||
if (node !== null && window.pendingLinkedCommentScroll) {
|
||||
const ROUGH_HEADER_HEIGHT = 125; // @see: --header-height
|
||||
delete window.pendingLinkedCommentScroll;
|
||||
window.scrollTo({
|
||||
top: node.getBoundingClientRect().top + window.scrollY - ROUGH_HEADER_HEIGHT,
|
||||
left: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
const linkedCommentRef = React.useCallback(
|
||||
(node) => {
|
||||
if (node !== null && window.pendingLinkedCommentScroll) {
|
||||
const ROUGH_HEADER_HEIGHT = 125; // @see: --header-height
|
||||
delete window.pendingLinkedCommentScroll;
|
||||
|
||||
const elem = isMobile ? document.querySelector('.MuiPaper-root .card--enable-overflow') : window;
|
||||
|
||||
if (elem) {
|
||||
// $FlowFixMe
|
||||
elem.scrollTo({
|
||||
// $FlowFixMe
|
||||
top: node.getBoundingClientRect().top + elem.scrollY - (isMobile ? 0 : ROUGH_HEADER_HEIGHT),
|
||||
left: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[isMobile]
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
|
@ -313,6 +323,7 @@ function Comment(props: Props) {
|
|||
charCount={charCount}
|
||||
onChange={handleEditMessageChanged}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
<Button
|
||||
|
|
150
ui/component/commentCreate/comment-selectors.jsx
Normal file
150
ui/component/commentCreate/comment-selectors.jsx
Normal file
|
@ -0,0 +1,150 @@
|
|||
// @flow
|
||||
import 'scss/component/_comment-selectors.scss';
|
||||
|
||||
import { EMOTES_48px as EMOTES } from 'constants/emotes';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import React from 'react';
|
||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||
import { FREE_GLOBAL_STICKERS, PAID_GLOBAL_STICKERS } from 'constants/stickers';
|
||||
|
||||
type Props = {
|
||||
claimIsMine?: boolean,
|
||||
addEmoteToComment: (string) => void,
|
||||
handleSelectSticker: (any) => void,
|
||||
closeSelector?: () => void,
|
||||
};
|
||||
|
||||
export default function CommentSelectors(props: Props) {
|
||||
const { claimIsMine, addEmoteToComment, handleSelectSticker, closeSelector } = props;
|
||||
|
||||
const tabProps = { closeSelector };
|
||||
|
||||
return (
|
||||
<Tabs>
|
||||
<TabList className="tabs__list--comment-selector">
|
||||
<Tab>{__('Emojis')}</Tab>
|
||||
<Tab>{__('Stickers')}</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<EmojisPanel handleSelect={(emote) => addEmoteToComment(emote)} {...tabProps} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<StickersPanel
|
||||
handleSelect={(sticker) => handleSelectSticker(sticker)}
|
||||
claimIsMine={claimIsMine}
|
||||
{...tabProps}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
type EmojisProps = {
|
||||
handleSelect: (emoteName: string) => void,
|
||||
closeSelector: () => void,
|
||||
};
|
||||
|
||||
const EmojisPanel = (emojisProps: EmojisProps) => {
|
||||
const { handleSelect, closeSelector } = emojisProps;
|
||||
|
||||
return (
|
||||
<div className="selector-menu">
|
||||
<Button button="close" icon={ICONS.REMOVE} onClick={closeSelector} />
|
||||
|
||||
<div className="emote-selector__items">
|
||||
{EMOTES.map((emote) => {
|
||||
const { name, url } = emote;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={name}
|
||||
title={name}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => handleSelect(name)}
|
||||
>
|
||||
<img src={url} loading="lazy" />
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type StickersProps = {
|
||||
claimIsMine: any,
|
||||
handleSelect: (any) => void,
|
||||
closeSelector: () => void,
|
||||
};
|
||||
|
||||
const StickersPanel = (stickersProps: StickersProps) => {
|
||||
const { claimIsMine, handleSelect, closeSelector } = stickersProps;
|
||||
|
||||
const defaultRowProps = { handleSelect };
|
||||
|
||||
return (
|
||||
<div className="selector-menu--stickers">
|
||||
<Button button="close" icon={ICONS.REMOVE} onClick={closeSelector} />
|
||||
|
||||
<StickersRow title={__('Free')} stickers={FREE_GLOBAL_STICKERS} {...defaultRowProps} />
|
||||
{!claimIsMine && <StickersRow title={__('Tips')} stickers={PAID_GLOBAL_STICKERS} {...defaultRowProps} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type RowProps = {
|
||||
title: string,
|
||||
stickers: any,
|
||||
handleSelect: (string) => void,
|
||||
};
|
||||
|
||||
const StickersRow = (rowProps: RowProps) => {
|
||||
const { title, stickers, handleSelect } = rowProps;
|
||||
|
||||
return (
|
||||
<div className="sticker-selector__body-row">
|
||||
<label id={title} className="sticker-selector__row-title">
|
||||
{title}
|
||||
</label>
|
||||
|
||||
<div className="sticker-selector__items">
|
||||
{stickers.map((sticker) => {
|
||||
const { price, url, name } = sticker;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={name}
|
||||
title={name}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => handleSelect(sticker)}
|
||||
>
|
||||
<StickerWrapper price={price}>
|
||||
<img src={url} loading="lazy" />
|
||||
{price && price > 0 && <CreditAmount superChatLight amount={price} size={2} isFiat />}
|
||||
</StickerWrapper>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type StickerProps = {
|
||||
price?: number,
|
||||
children: any,
|
||||
};
|
||||
|
||||
const StickerWrapper = (stickerProps: StickerProps) => {
|
||||
const { price, children } = stickerProps;
|
||||
|
||||
return price ? <div className="sticker-item--priced">{children}</div> : children;
|
||||
};
|
|
@ -1,66 +0,0 @@
|
|||
// @flow
|
||||
import 'scss/component/_emote-selector.scss';
|
||||
import { EMOTES_48px as EMOTES } from 'constants/emotes';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import EMOJIS from 'emoji-dictionary';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
|
||||
const OLD_QUICK_EMOJIS = [
|
||||
EMOJIS.getUnicode('rocket'),
|
||||
EMOJIS.getUnicode('jeans'),
|
||||
EMOJIS.getUnicode('fire'),
|
||||
EMOJIS.getUnicode('heart'),
|
||||
EMOJIS.getUnicode('open_mouth'),
|
||||
];
|
||||
|
||||
type Props = { commentValue: string, setCommentValue: (string) => void, closeSelector: () => void };
|
||||
|
||||
export default function EmoteSelector(props: Props) {
|
||||
const { commentValue, setCommentValue, closeSelector } = props;
|
||||
|
||||
function addEmoteToComment(emote: string) {
|
||||
setCommentValue(
|
||||
commentValue + (commentValue && commentValue.charAt(commentValue.length - 1) !== ' ' ? ` ${emote} ` : `${emote} `)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="emoteSelector">
|
||||
<Button button="close" icon={ICONS.REMOVE} onClick={closeSelector} />
|
||||
|
||||
<div className="emoteSelector__list">
|
||||
<div className="emoteSelector__listRow">
|
||||
<div className="emoteSelector__listRowItems">
|
||||
{OLD_QUICK_EMOJIS.map((emoji) => (
|
||||
<Button
|
||||
key={emoji}
|
||||
label={emoji}
|
||||
title={`:${EMOJIS.getName(emoji)}:`}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => addEmoteToComment(emoji)}
|
||||
/>
|
||||
))}
|
||||
{EMOTES.map((emote) => {
|
||||
const emoteName = emote.name;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={emoteName}
|
||||
title={emoteName}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => addEmoteToComment(emoteName)}
|
||||
>
|
||||
<OptimizedImage src={emote.url} waitLoad />
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
67
ui/component/commentCreate/extra-contents.jsx
Normal file
67
ui/component/commentCreate/extra-contents.jsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
// @flow
|
||||
import 'scss/component/_comment-selectors.scss';
|
||||
|
||||
import React from 'react';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import Icon from 'component/common/icon';
|
||||
import SelectChannel from 'component/selectChannel';
|
||||
|
||||
type SelectorProps = {
|
||||
isReply: boolean,
|
||||
isLivestream: boolean,
|
||||
};
|
||||
|
||||
export const FormChannelSelector = (selectorProps: SelectorProps) => {
|
||||
const { isReply, isLivestream } = selectorProps;
|
||||
|
||||
return (
|
||||
<div className="comment-create__label-wrapper">
|
||||
<span className="comment-create__label">
|
||||
{(isReply ? __('Replying as') : isLivestream ? __('Chat as') : __('Comment as')) + ' '}
|
||||
</span>
|
||||
|
||||
<SelectChannel tiny />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type HelpTextProps = {
|
||||
deletedComment: boolean,
|
||||
minAmount: number,
|
||||
minSuper: number,
|
||||
minTip: number,
|
||||
};
|
||||
|
||||
export const HelpText = (helpTextProps: HelpTextProps) => {
|
||||
const { deletedComment, minAmount, minSuper, minTip } = helpTextProps;
|
||||
|
||||
return (
|
||||
<>
|
||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||
|
||||
{!!minAmount && (
|
||||
<div className="help--notice commentCreate__minAmountNotice">
|
||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||
</I18nMessage>
|
||||
|
||||
<Icon
|
||||
customTooltipText={
|
||||
minTip
|
||||
? __('This channel requires a minimum tip for each comment.')
|
||||
: minSuper
|
||||
? __('This channel requires a minimum amount for HyperChats to be visible.')
|
||||
: ''
|
||||
}
|
||||
className="icon--help"
|
||||
icon={ICONS.HELP}
|
||||
tooltip
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
59
ui/component/commentCreate/sticker-contents.jsx
Normal file
59
ui/component/commentCreate/sticker-contents.jsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
// @flow
|
||||
import 'scss/component/_comment-selectors.scss';
|
||||
|
||||
import Button from 'component/button';
|
||||
import React from 'react';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
|
||||
type Props = {
|
||||
activeChannelUrl: string,
|
||||
src: string,
|
||||
price: number,
|
||||
exchangeRate?: number,
|
||||
};
|
||||
|
||||
export const StickerReviewBox = (props: Props) => {
|
||||
const { activeChannelUrl, src, price, exchangeRate } = props;
|
||||
|
||||
return (
|
||||
<div className="commentCreate__stickerPreview">
|
||||
<div className="commentCreate__stickerPreviewInfo">
|
||||
<ChannelThumbnail xsmall uri={activeChannelUrl} />
|
||||
<UriIndicator uri={activeChannelUrl} link />
|
||||
</div>
|
||||
|
||||
<div className="commentCreate__stickerPreviewImage">
|
||||
<OptimizedImage src={src} waitLoad loading="lazy" />
|
||||
</div>
|
||||
|
||||
{Boolean(price && exchangeRate) && (
|
||||
<FilePrice
|
||||
customPrices={{
|
||||
priceFiat: price,
|
||||
priceLBC: Number(exchangeRate) !== 0 ? price / Number(exchangeRate) : 0,
|
||||
}}
|
||||
isFiat
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type StickerButtonProps = {
|
||||
isReviewingStickerComment: boolean,
|
||||
};
|
||||
|
||||
export const StickerActionButton = (stickerButtonProps: StickerButtonProps) => {
|
||||
const { isReviewingStickerComment, ...buttonProps } = stickerButtonProps;
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...buttonProps}
|
||||
title={__('Stickers')}
|
||||
label={isReviewingStickerComment ? __('Different Sticker') : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,102 +0,0 @@
|
|||
// @flow
|
||||
import 'scss/component/_sticker-selector.scss';
|
||||
import { FREE_GLOBAL_STICKERS, PAID_GLOBAL_STICKERS } from 'constants/stickers';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
|
||||
const buildStickerSideLink = (section: string, icon: string) => ({ section, icon });
|
||||
|
||||
const STICKER_SIDE_LINKS = [
|
||||
buildStickerSideLink(__('Free'), ICONS.TAG),
|
||||
buildStickerSideLink(__('Tips'), ICONS.FINANCE),
|
||||
// Future work may include Channel, Subscriptions, ...
|
||||
];
|
||||
|
||||
type Props = { claimIsMine: boolean, onSelect: (any) => void };
|
||||
|
||||
export default function StickerSelector(props: Props) {
|
||||
const { claimIsMine, onSelect } = props;
|
||||
|
||||
function scrollToStickerSection(section: string) {
|
||||
const listBodyEl = document.querySelector('.stickerSelector__listBody');
|
||||
const sectionToScroll = document.getElementById(section);
|
||||
|
||||
if (listBodyEl && sectionToScroll) {
|
||||
// $FlowFixMe
|
||||
listBodyEl.scrollTo({
|
||||
top: sectionToScroll.offsetTop - sectionToScroll.getBoundingClientRect().height * 2,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const StickerWrapper = (stickerProps: any) => {
|
||||
const { price, children } = stickerProps;
|
||||
|
||||
return price ? <div className="stickerItem--paid">{children}</div> : children;
|
||||
};
|
||||
|
||||
const getListRow = (rowTitle: string, rowStickers: any) => (
|
||||
<div className="stickerSelector__listBody-row">
|
||||
<div id={rowTitle} className="stickerSelector__listBody-rowTitle">
|
||||
{rowTitle}
|
||||
</div>
|
||||
<div className="stickerSelector__listBody-rowItems">
|
||||
{rowStickers.map((sticker) => (
|
||||
<Button
|
||||
key={sticker.name}
|
||||
title={sticker.name}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => onSelect(sticker)}
|
||||
>
|
||||
<StickerWrapper price={sticker.price}>
|
||||
<OptimizedImage src={sticker.url} waitLoad loading="lazy" />
|
||||
{sticker.price && sticker.price > 0 && (
|
||||
<CreditAmount superChatLight amount={sticker.price} size={2} isFiat />
|
||||
)}
|
||||
</StickerWrapper>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="stickerSelector">
|
||||
<div className="stickerSelector__header card__header--between">
|
||||
<div className="stickerSelector__headerTitle card__title-section--small">{__('Stickers')}</div>
|
||||
</div>
|
||||
|
||||
<div className="stickerSelector__list">
|
||||
<div className="stickerSelector__listBody">
|
||||
{getListRow(__('Free'), FREE_GLOBAL_STICKERS)}
|
||||
{!claimIsMine && getListRow(__('Tips'), PAID_GLOBAL_STICKERS)}
|
||||
</div>
|
||||
|
||||
<div className="navigation__wrapper">
|
||||
<ul className="navigation-links">
|
||||
{STICKER_SIDE_LINKS.map(
|
||||
(linkProps) =>
|
||||
((claimIsMine && linkProps.section !== 'Tips') || !claimIsMine) && (
|
||||
<li key={linkProps.section}>
|
||||
<Button
|
||||
label={__(linkProps.section)}
|
||||
title={__(linkProps.section)}
|
||||
icon={linkProps.icon}
|
||||
iconSize={1}
|
||||
className="navigation-link"
|
||||
onClick={() => scrollToStickerSection(linkProps.section)}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
62
ui/component/commentCreate/tip-contents.jsx
Normal file
62
ui/component/commentCreate/tip-contents.jsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
// @flow
|
||||
import 'scss/component/_comment-selectors.scss';
|
||||
|
||||
import Button from 'component/button';
|
||||
import React from 'react';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
|
||||
const TAB_FIAT = 'TabFiat';
|
||||
const TAB_LBC = 'TabLBC';
|
||||
|
||||
type Props = {
|
||||
activeChannelUrl: string,
|
||||
tipAmount: number,
|
||||
activeTab: string,
|
||||
message: string,
|
||||
};
|
||||
|
||||
export const TipReviewBox = (props: Props) => {
|
||||
const { activeChannelUrl, tipAmount, activeTab, message } = props;
|
||||
|
||||
return (
|
||||
<div className="commentCreate__supportCommentPreview">
|
||||
<CreditAmount
|
||||
amount={tipAmount}
|
||||
className="commentCreate__supportCommentPreviewAmount"
|
||||
isFiat={activeTab === TAB_FIAT}
|
||||
size={activeTab === TAB_LBC ? 18 : 2}
|
||||
/>
|
||||
|
||||
<ChannelThumbnail xsmall uri={activeChannelUrl} />
|
||||
<div className="commentCreate__supportCommentBody">
|
||||
<UriIndicator uri={activeChannelUrl} link />
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type TipButtonProps = {
|
||||
name: string,
|
||||
tab: string,
|
||||
activeTab: string,
|
||||
tipSelectorOpen: boolean,
|
||||
onClick: (tab: string) => void,
|
||||
};
|
||||
|
||||
export const TipActionButton = (tipButtonProps: TipButtonProps) => {
|
||||
const { name, tab, activeTab, tipSelectorOpen, onClick, ...buttonProps } = tipButtonProps;
|
||||
|
||||
return (
|
||||
(!tipSelectorOpen || activeTab !== tab) && (
|
||||
<Button
|
||||
{...buttonProps}
|
||||
title={name}
|
||||
label={tipSelectorOpen ? __('Switch to %tip_method%', { tip_method: name }) : undefined}
|
||||
onClick={() => onClick(tab)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
|
@ -14,23 +14,17 @@ import * as KEYCODES from 'constants/keycodes';
|
|||
import * as PAGES from 'constants/pages';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import Button from 'component/button';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import classnames from 'classnames';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import EmoteSelector from './emote-selector';
|
||||
import CommentSelectors from './comment-selectors';
|
||||
import Empty from 'component/common/empty';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import Icon from 'component/common/icon';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
import SelectChannel from 'component/selectChannel';
|
||||
import StickerSelector from './sticker-selector';
|
||||
import type { ElementRef } from 'react';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import { StickerReviewBox, StickerActionButton } from './sticker-contents';
|
||||
import { TipReviewBox, TipActionButton } from './tip-contents';
|
||||
import { FormChannelSelector, HelpText } from './extra-contents';
|
||||
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
const stripeEnvironment = getStripeEnvironment();
|
||||
|
@ -59,6 +53,7 @@ type Props = {
|
|||
supportDisabled: boolean,
|
||||
uri: string,
|
||||
disableInput?: boolean,
|
||||
onSlimInputClick?: () => void,
|
||||
createComment: (string, string, string, ?string, ?string, ?string, boolean) => Promise<any>,
|
||||
doFetchCreatorSettings: (channelId: string) => Promise<any>,
|
||||
doToast: ({ message: string }) => void,
|
||||
|
@ -90,6 +85,7 @@ export function CommentCreate(props: Props) {
|
|||
supportDisabled,
|
||||
uri,
|
||||
disableInput,
|
||||
onSlimInputClick,
|
||||
createComment,
|
||||
doFetchCreatorSettings,
|
||||
doToast,
|
||||
|
@ -115,7 +111,7 @@ export function CommentCreate(props: Props) {
|
|||
const [isSubmitting, setSubmitting] = React.useState(false);
|
||||
const [commentFailure, setCommentFailure] = React.useState(false);
|
||||
const [successTip, setSuccessTip] = React.useState({ txid: undefined, tipAmount: undefined });
|
||||
const [isSupportComment, setIsSupportComment] = React.useState();
|
||||
const [tipSelectorOpen, setTipSelector] = React.useState();
|
||||
const [isReviewingSupportComment, setReviewingSupportComment] = React.useState();
|
||||
const [isReviewingStickerComment, setReviewingStickerComment] = React.useState();
|
||||
const [selectedSticker, setSelectedSticker] = React.useState();
|
||||
|
@ -123,18 +119,19 @@ export function CommentCreate(props: Props) {
|
|||
const [convertedAmount, setConvertedAmount] = React.useState();
|
||||
const [commentValue, setCommentValue] = React.useState('');
|
||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||
const [stickerSelector, setStickerSelector] = React.useState();
|
||||
const [activeTab, setActiveTab] = React.useState();
|
||||
const [tipError, setTipError] = React.useState();
|
||||
const [deletedComment, setDeletedComment] = React.useState(false);
|
||||
const [showEmotes, setShowEmotes] = React.useState(false);
|
||||
const [showSelectors, setShowSelectors] = React.useState(false);
|
||||
const [disableReviewButton, setDisableReviewButton] = React.useState();
|
||||
const [exchangeRate, setExchangeRate] = React.useState();
|
||||
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(undefined);
|
||||
const [tipModalOpen, setTipModalOpen] = React.useState(undefined);
|
||||
|
||||
const claimId = claim && claim.claim_id;
|
||||
const charCount = commentValue ? commentValue.length : 0;
|
||||
const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length || disableInput;
|
||||
const hasNothingToSumbit = !commentValue.length && !selectedSticker;
|
||||
const disabled = deletedComment || isSubmitting || isFetchingChannels || hasNothingToSumbit || disableInput;
|
||||
const channelId = getChannelIdFromClaim(claim);
|
||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||
const minSuper = (channelSettings && channelSettings.min_tip_amount_super_chat) || 0;
|
||||
|
@ -142,6 +139,7 @@ export function CommentCreate(props: Props) {
|
|||
const minAmount = minTip || minSuper || 0;
|
||||
const minAmountMet = minAmount === 0 || tipAmount >= minAmount;
|
||||
const stickerPrice = selectedSticker && selectedSticker.price;
|
||||
const tipSelectorError = tipError || disableReviewButton;
|
||||
|
||||
const minAmountRef = React.useRef(minAmount);
|
||||
minAmountRef.current = minAmount;
|
||||
|
@ -150,16 +148,52 @@ export function CommentCreate(props: Props) {
|
|||
// Functions
|
||||
// **************************************************************************
|
||||
|
||||
function addEmoteToComment(emote: string) {
|
||||
setCommentValue(
|
||||
commentValue + (commentValue && commentValue.charAt(commentValue.length - 1) !== ' ' ? ` ${emote} ` : `${emote} `)
|
||||
);
|
||||
|
||||
const inputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input && formFieldRef.current.input;
|
||||
if (inputRef && inputRef.current) inputRef.current.focus();
|
||||
}
|
||||
|
||||
function handleSelectTipComment(tab: string) {
|
||||
setActiveTab(tab);
|
||||
setTipSelector(true);
|
||||
}
|
||||
|
||||
function handleStickerComment() {
|
||||
if (selectedSticker) setReviewingStickerComment(false);
|
||||
setTipSelector(false);
|
||||
setShowSelectors(!showSelectors);
|
||||
}
|
||||
|
||||
function handleSelectSticker(sticker: any) {
|
||||
// $FlowFixMe
|
||||
setSelectedSticker(sticker);
|
||||
setReviewingStickerComment(true);
|
||||
setTipAmount(sticker.price || 0);
|
||||
setStickerSelector(false);
|
||||
setShowSelectors(false);
|
||||
|
||||
if (sticker.price && sticker.price > 0) {
|
||||
setActiveTab(canReceiveFiatTip ? TAB_FIAT : TAB_LBC);
|
||||
setIsSupportComment(true);
|
||||
setTipSelector(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelSticker() {
|
||||
setReviewingStickerComment(false);
|
||||
setSelectedSticker(null);
|
||||
}
|
||||
|
||||
function handleCancelSupport() {
|
||||
if (!isReviewingSupportComment) setTipSelector(false);
|
||||
setReviewingSupportComment(false);
|
||||
|
||||
if (stickerPrice) {
|
||||
setReviewingStickerComment(false);
|
||||
setShowSelectors(false);
|
||||
setSelectedSticker(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,7 +284,7 @@ export function CommentCreate(props: Props) {
|
|||
|
||||
setCommentValue('');
|
||||
setReviewingSupportComment(false);
|
||||
setIsSupportComment(false);
|
||||
setTipSelector(false);
|
||||
setCommentFailure(false);
|
||||
setSubmitting(false);
|
||||
});
|
||||
|
@ -266,7 +300,6 @@ export function CommentCreate(props: Props) {
|
|||
function handleCreateComment(txid, payment_intent_id, environment) {
|
||||
if (isSubmitting || disableInput) return;
|
||||
|
||||
setShowEmotes(false);
|
||||
setSubmitting(true);
|
||||
|
||||
const stickerValue = selectedSticker && buildValidSticker(selectedSticker.name);
|
||||
|
@ -279,7 +312,7 @@ export function CommentCreate(props: Props) {
|
|||
if (res && res.signature) {
|
||||
if (!stickerValue) setCommentValue('');
|
||||
setReviewingSupportComment(false);
|
||||
setIsSupportComment(false);
|
||||
setTipSelector(false);
|
||||
setCommentFailure(false);
|
||||
|
||||
if (onDoneReplying) {
|
||||
|
@ -299,6 +332,19 @@ export function CommentCreate(props: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
function handleSubmitSticker() {
|
||||
if (isReviewingSupportComment) {
|
||||
handleSupportComment();
|
||||
} else {
|
||||
handleCreateComment();
|
||||
}
|
||||
|
||||
setSelectedSticker(null);
|
||||
setReviewingStickerComment(false);
|
||||
setShowSelectors(false);
|
||||
setTipSelector(false);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// Effects
|
||||
// **************************************************************************
|
||||
|
@ -327,7 +373,7 @@ export function CommentCreate(props: Props) {
|
|||
// Stickers: Check if creator has a tip account saved (on selector so that if a paid sticker is selected,
|
||||
// it defaults to LBC tip instead of USD)
|
||||
React.useEffect(() => {
|
||||
if (!stripeEnvironment || !stickerSelector || canReceiveFiatTip !== undefined) return;
|
||||
if (!stripeEnvironment || !showSelectors || canReceiveFiatTip !== undefined) return;
|
||||
|
||||
const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||
|
@ -350,7 +396,7 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
||||
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, showSelectors]);
|
||||
|
||||
// Handle keyboard shortcut comment creation
|
||||
React.useEffect(() => {
|
||||
|
@ -384,14 +430,6 @@ export function CommentCreate(props: Props) {
|
|||
// Render
|
||||
// **************************************************************************
|
||||
|
||||
const getActionButton = (
|
||||
title: string,
|
||||
label?: string,
|
||||
icon: string,
|
||||
handleClick: () => void,
|
||||
disabled?: boolean
|
||||
) => <Button title={title} label={label} button="alt" icon={icon} onClick={handleClick} disabled={disabled} />;
|
||||
|
||||
if (channelSettings && !channelSettings.comments_enabled) {
|
||||
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
|
||||
}
|
||||
|
@ -400,6 +438,7 @@ export function CommentCreate(props: Props) {
|
|||
return (
|
||||
<div
|
||||
role="button"
|
||||
className="comment-create__auth"
|
||||
onClick={() => {
|
||||
if (embed) {
|
||||
window.open(`https://odysee.com/$/${PAGES.AUTH}?redirect=/$/${PAGES.LIVESTREAM}`);
|
||||
|
@ -422,6 +461,18 @@ export function CommentCreate(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
const commentSelectorsProps = { claimIsMine, addEmoteToComment, handleSelectSticker };
|
||||
const submitButtonProps = { button: 'primary', type: 'submit', requiresAuth: true };
|
||||
const actionButtonProps = { button: 'alt', isReviewingStickerComment };
|
||||
const tipButtonProps = {
|
||||
...actionButtonProps,
|
||||
disabled: !commentValue.length && !selectedSticker,
|
||||
tipSelectorOpen,
|
||||
activeTab,
|
||||
onClick: handleSelectTipComment,
|
||||
};
|
||||
const cancelButtonProps = { button: 'link', label: __('Cancel') };
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={() => {}}
|
||||
|
@ -431,50 +482,29 @@ export function CommentCreate(props: Props) {
|
|||
'commentCreate--bottom': bottom,
|
||||
})}
|
||||
>
|
||||
{/* Input Box/Preview Box */}
|
||||
{stickerSelector ? (
|
||||
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
||||
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
||||
<div className="commentCreate__stickerPreview">
|
||||
<div className="commentCreate__stickerPreviewInfo">
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
</div>
|
||||
|
||||
<div className="commentCreate__stickerPreviewImage">
|
||||
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
||||
</div>
|
||||
|
||||
{selectedSticker.price && exchangeRate && (
|
||||
<FilePrice
|
||||
customPrices={{ priceFiat: selectedSticker.price, priceLBC: selectedSticker.price / exchangeRate }}
|
||||
isFiat
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||
<div className="commentCreate__supportCommentPreview">
|
||||
<CreditAmount
|
||||
amount={tipAmount}
|
||||
className="commentCreate__supportCommentPreviewAmount"
|
||||
isFiat={activeTab === TAB_FIAT}
|
||||
size={activeTab === TAB_LBC ? 18 : 2}
|
||||
{selectedSticker ? (
|
||||
activeChannelClaim && (
|
||||
<StickerReviewBox
|
||||
activeChannelUrl={activeChannelClaim.canonical_url}
|
||||
src={selectedSticker.url}
|
||||
price={selectedSticker.price || 0}
|
||||
exchangeRate={exchangeRate}
|
||||
/>
|
||||
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<div className="commentCreate__supportCommentBody">
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
<div>{commentValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : isReviewingSupportComment ? (
|
||||
activeChannelClaim &&
|
||||
activeTab && (
|
||||
<TipReviewBox
|
||||
activeChannelUrl={activeChannelClaim.canonical_url}
|
||||
tipAmount={tipAmount}
|
||||
activeTab={activeTab}
|
||||
message={commentValue}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{showEmotes && (
|
||||
<EmoteSelector
|
||||
commentValue={commentValue}
|
||||
setCommentValue={setCommentValue}
|
||||
closeSelector={() => setShowEmotes(false)}
|
||||
/>
|
||||
{!isMobile && showSelectors && (
|
||||
<CommentSelectors {...commentSelectorsProps} closeSelector={() => setShowSelectors(false)} />
|
||||
)}
|
||||
|
||||
<FormField
|
||||
|
@ -483,30 +513,31 @@ export function CommentCreate(props: Props) {
|
|||
className={isReply ? 'create__reply' : 'create__comment'}
|
||||
disabled={isFetchingChannels || disableInput}
|
||||
isLivestream={isLivestream}
|
||||
label={
|
||||
<div className="comment-create__label-wrapper">
|
||||
<span className="comment-create__label">
|
||||
{(isReply ? __('Replying as') : isLivestream ? __('Chat as') : __('Comment as')) + ' '}
|
||||
</span>
|
||||
<SelectChannel tiny />
|
||||
</div>
|
||||
}
|
||||
label={<FormChannelSelector isReply={Boolean(isReply)} isLivestream={Boolean(isLivestream)} />}
|
||||
name={isReply ? 'create__reply' : 'create__comment'}
|
||||
onChange={(e) => setCommentValue(SIMPLE_SITE || !advancedEditor || isReply ? e.target.value : e)}
|
||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||
handleTip={(isLBC) =>
|
||||
handleTip={(isLBC) => {
|
||||
setActiveTab(isLBC ? TAB_LBC : TAB_FIAT);
|
||||
setTipModalOpen(true);
|
||||
doOpenModal(MODALS.SEND_TIP, {
|
||||
uri,
|
||||
isTipOnly: true,
|
||||
hasSelectedTab: isLBC ? TAB_LBC : TAB_FIAT,
|
||||
customText: __('Preview Comment Tip'),
|
||||
setAmount: (amount) => {
|
||||
setTipAmount(amount);
|
||||
setReviewingSupportComment(true);
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
handleSubmit={handleCreateComment}
|
||||
noEmojis={isMobile}
|
||||
slimInput={isMobile}
|
||||
onSlimInputClick={onSlimInputClick}
|
||||
commentSelectorsProps={commentSelectorsProps}
|
||||
submitButtonRef={buttonRef}
|
||||
setShowSelectors={setShowSelectors}
|
||||
showSelectors={showSelectors}
|
||||
tipModalOpen={tipModalOpen}
|
||||
placeholder={__('Say something about this...')}
|
||||
quickActionHandler={!SIMPLE_SITE ? () => setAdvancedEditor(!advancedEditor) : undefined}
|
||||
quickActionLabel={
|
||||
|
@ -521,7 +552,7 @@ export function CommentCreate(props: Props) {
|
|||
</>
|
||||
)}
|
||||
|
||||
{!isMobile && (isSupportComment || (isReviewingStickerComment && stickerPrice)) && (
|
||||
{(!isMobile || isReviewingStickerComment) && (tipSelectorOpen || (isReviewingStickerComment && stickerPrice)) && (
|
||||
<WalletTipAmountSelector
|
||||
activeTab={activeTab}
|
||||
amount={tipAmount}
|
||||
|
@ -538,14 +569,13 @@ export function CommentCreate(props: Props) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Bottom Action Buttons */}
|
||||
{!isMobile && (
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
{(!isMobile || !isLivestream || isReviewingStickerComment || isReviewingSupportComment) && (
|
||||
<div className="section__actions">
|
||||
{/* Submit Button */}
|
||||
{isReviewingSupportComment ? (
|
||||
<Button
|
||||
{...submitButtonProps}
|
||||
autoFocus
|
||||
button="primary"
|
||||
disabled={disabled || !minAmountMet}
|
||||
label={
|
||||
isSubmitting
|
||||
|
@ -556,40 +586,21 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
onClick={handleSupportComment}
|
||||
/>
|
||||
) : isReviewingStickerComment && selectedSticker ? (
|
||||
) : tipSelectorOpen ? (
|
||||
<Button
|
||||
button="primary"
|
||||
label={__('Send')}
|
||||
disabled={(isSupportComment && (tipError || disableReviewButton)) || disableInput}
|
||||
onClick={() => {
|
||||
if (isSupportComment) {
|
||||
handleSupportComment();
|
||||
} else {
|
||||
handleCreateComment();
|
||||
}
|
||||
setSelectedSticker(null);
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
setIsSupportComment(false);
|
||||
}}
|
||||
/>
|
||||
) : isSupportComment ? (
|
||||
<Button
|
||||
disabled={disabled || tipError || disableReviewButton || !minAmountMet}
|
||||
type="button"
|
||||
button="primary"
|
||||
{...submitButtonProps}
|
||||
disabled={disabled || tipSelectorError || !minAmountMet}
|
||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||
label={__('Review')}
|
||||
onClick={() => setReviewingSupportComment(true)}
|
||||
requiresAuth
|
||||
/>
|
||||
) : (
|
||||
(!isMobile || selectedSticker) &&
|
||||
(!minTip || claimIsMine) && (
|
||||
<Button
|
||||
{...submitButtonProps}
|
||||
ref={buttonRef}
|
||||
button="primary"
|
||||
disabled={disabled || stickerSelector}
|
||||
type="submit"
|
||||
disabled={disabled}
|
||||
label={
|
||||
isReply
|
||||
? isSubmitting
|
||||
|
@ -599,135 +610,36 @@ export function CommentCreate(props: Props) {
|
|||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
}
|
||||
requiresAuth
|
||||
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||
onClick={() => (selectedSticker ? handleSubmitSticker() : handleCreateComment())}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{/** Stickers/Support Buttons **/}
|
||||
{!supportDisabled && !stickerSelector && (
|
||||
{!isMobile && (
|
||||
<>
|
||||
{getActionButton(
|
||||
__('Stickers'),
|
||||
isReviewingStickerComment ? __('Different Sticker') : undefined,
|
||||
ICONS.STICKER,
|
||||
() => {
|
||||
if (isReviewingStickerComment) setReviewingStickerComment(false);
|
||||
setIsSupportComment(false);
|
||||
setStickerSelector(true);
|
||||
}
|
||||
)}
|
||||
<StickerActionButton {...actionButtonProps} icon={ICONS.STICKER} onClick={handleStickerComment} />
|
||||
|
||||
{!claimIsMine && (
|
||||
{!supportDisabled && (
|
||||
<>
|
||||
{(!isSupportComment || activeTab !== TAB_LBC) &&
|
||||
getActionButton(
|
||||
__('Credits'),
|
||||
isSupportComment ? __('Switch to Credits') : undefined,
|
||||
ICONS.LBC,
|
||||
() => {
|
||||
setActiveTab(TAB_LBC);
|
||||
<TipActionButton {...tipButtonProps} name={__('Credits')} icon={ICONS.LBC} tab={TAB_LBC} />
|
||||
|
||||
if (isMobile) {
|
||||
doOpenModal(MODALS.SEND_TIP, {
|
||||
uri,
|
||||
isTipOnly: true,
|
||||
hasSelectedTab: TAB_LBC,
|
||||
setAmount: (amount) => {
|
||||
setTipAmount(amount);
|
||||
setReviewingSupportComment(true);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setIsSupportComment(true);
|
||||
}
|
||||
},
|
||||
!commentValue.length
|
||||
)}
|
||||
|
||||
{stripeEnvironment &&
|
||||
(!isSupportComment || activeTab !== TAB_FIAT) &&
|
||||
getActionButton(
|
||||
__('Cash'),
|
||||
isSupportComment ? __('Switch to Cash') : undefined,
|
||||
ICONS.FINANCE,
|
||||
() => {
|
||||
setActiveTab(TAB_FIAT);
|
||||
|
||||
if (isMobile) {
|
||||
doOpenModal(MODALS.SEND_TIP, {
|
||||
uri,
|
||||
isTipOnly: true,
|
||||
hasSelectedTab: TAB_FIAT,
|
||||
setAmount: (amount) => {
|
||||
setTipAmount(amount);
|
||||
setReviewingSupportComment(true);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setIsSupportComment(true);
|
||||
}
|
||||
},
|
||||
!commentValue.length
|
||||
)}
|
||||
{stripeEnvironment && (
|
||||
<TipActionButton {...tipButtonProps} name={__('Cash')} icon={ICONS.FINANCE} tab={TAB_FIAT} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Cancel Button */}
|
||||
{(isSupportComment ||
|
||||
isReviewingSupportComment ||
|
||||
stickerSelector ||
|
||||
isReviewingStickerComment ||
|
||||
(isReply && !minTip)) && (
|
||||
<Button
|
||||
disabled={isSupportComment && isSubmitting}
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => {
|
||||
if (isSupportComment || isReviewingSupportComment) {
|
||||
if (!isReviewingSupportComment) setIsSupportComment(false);
|
||||
setReviewingSupportComment(false);
|
||||
if (stickerPrice) {
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
setSelectedSticker(null);
|
||||
}
|
||||
} else if (stickerSelector || isReviewingStickerComment) {
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
setSelectedSticker(null);
|
||||
} else if (isReply && !minTip && onCancelReplying) {
|
||||
onCancelReplying();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{tipSelectorOpen || isReviewingSupportComment ? (
|
||||
<Button {...cancelButtonProps} disabled={isSubmitting} onClick={handleCancelSupport} />
|
||||
) : isReviewingStickerComment ? (
|
||||
<Button {...cancelButtonProps} onClick={handleCancelSticker} />
|
||||
) : (
|
||||
onCancelReplying && <Button {...cancelButtonProps} onClick={onCancelReplying} />
|
||||
)}
|
||||
|
||||
{/* Help Text */}
|
||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||
{!!minAmount && (
|
||||
<div className="help--notice commentCreate__minAmountNotice">
|
||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||
</I18nMessage>
|
||||
<Icon
|
||||
customTooltipText={
|
||||
minTip
|
||||
? __('This channel requires a minimum tip for each comment.')
|
||||
: minSuper
|
||||
? __('This channel requires a minimum amount for HyperChats to be visible.')
|
||||
: ''
|
||||
}
|
||||
className="icon--help"
|
||||
icon={ICONS.HELP}
|
||||
tooltip
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<HelpText deletedComment={deletedComment} minAmount={minAmount} minSuper={minSuper} minTip={minTip} />
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as MODALS from 'constants/modal_types';
|
|||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import Icon from 'component/common/icon';
|
||||
import React from 'react';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
|
||||
type Props = {
|
||||
uri: ?string,
|
||||
|
@ -67,6 +68,8 @@ function CommentMenuList(props: Props) {
|
|||
handleDismissPin,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const {
|
||||
location: { pathname, search },
|
||||
} = useHistory();
|
||||
|
@ -253,7 +256,7 @@ function CommentMenuList(props: Props) {
|
|||
</MenuItem>
|
||||
)}
|
||||
|
||||
{isPinned && isLiveComment && (
|
||||
{isPinned && isLiveComment && isMobile && (
|
||||
<MenuItem className="comment__menu-option menu__link" onSelect={handleDismissPin}>
|
||||
<Icon aria-hidden icon={ICONS.DISMISS_ALL} />
|
||||
{__('Dismiss Pin')}
|
||||
|
|
|
@ -269,37 +269,17 @@ function CommentList(props: Props) {
|
|||
/>
|
||||
));
|
||||
|
||||
const sortButton = (label, icon, sortOption) => (
|
||||
<Button
|
||||
button="alt"
|
||||
label={label}
|
||||
icon={icon}
|
||||
iconSize={18}
|
||||
onClick={() => changeSort(sortOption)}
|
||||
className={classnames(`button-toggle`, {
|
||||
'button-toggle--active': sort === sortOption,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
const actionButtonsProps = { totalComments, sort, changeSort, setPage };
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="card--enable-overflow"
|
||||
title={!isMobile && title}
|
||||
titleActions={
|
||||
<>
|
||||
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
||||
<span className="comment__sort">
|
||||
{sortButton(__('Best'), ICONS.BEST, SORT_BY.POPULARITY)}
|
||||
{sortButton(__('Controversial'), ICONS.CONTROVERSIAL, SORT_BY.CONTROVERSY)}
|
||||
{sortButton(__('New'), ICONS.NEW, SORT_BY.NEWEST)}
|
||||
</span>
|
||||
)}
|
||||
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
||||
</>
|
||||
}
|
||||
titleActions={<CommentActionButtons {...actionButtonsProps} />}
|
||||
actions={
|
||||
<>
|
||||
{isMobile && <CommentActionButtons {...actionButtonsProps} />}
|
||||
|
||||
<CommentCreate uri={uri} />
|
||||
|
||||
{channelSettings && channelSettings.comments_enabled && !isFetchingComments && !totalComments && (
|
||||
|
@ -349,3 +329,55 @@ function CommentList(props: Props) {
|
|||
}
|
||||
|
||||
export default CommentList;
|
||||
|
||||
type ActionButtonsProps = {
|
||||
totalComments: number,
|
||||
sort: string,
|
||||
changeSort: (string) => void,
|
||||
setPage: (number) => void,
|
||||
};
|
||||
|
||||
const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||
const { totalComments, sort, changeSort, setPage } = actionButtonsProps;
|
||||
|
||||
const sortButtonProps = { activeSort: sort, changeSort };
|
||||
|
||||
return (
|
||||
<>
|
||||
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
||||
<span className="comment__sort">
|
||||
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
|
||||
<SortButton
|
||||
{...sortButtonProps}
|
||||
label={__('Controversial')}
|
||||
icon={ICONS.CONTROVERSIAL}
|
||||
sortOption={SORT_BY.CONTROVERSY}
|
||||
/>
|
||||
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type SortButtonProps = {
|
||||
activeSort: string,
|
||||
sortOption: string,
|
||||
changeSort: (string) => void,
|
||||
};
|
||||
|
||||
const SortButton = (sortButtonProps: SortButtonProps) => {
|
||||
const { activeSort, sortOption, changeSort, ...buttonProps } = sortButtonProps;
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...buttonProps}
|
||||
className={classnames(`button-toggle`, { 'button-toggle--active': activeSort === sortOption })}
|
||||
button="alt"
|
||||
iconSize={18}
|
||||
onClick={() => changeSort(sortOption)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,13 +3,14 @@ import 'easymde/dist/easymde.min.css';
|
|||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||
import { lazyImport } from 'util/lazyImport';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import SimpleMDE from 'react-simplemde-editor';
|
||||
import type { ElementRef, Node } from 'react';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import CommentSelectors from 'component/commentCreate/comment-selectors';
|
||||
|
||||
// prettier-ignore
|
||||
const TextareaWithSuggestions = lazyImport(() => import('component/textareaWithSuggestions' /* webpackChunkName: "suggestions" */));
|
||||
|
@ -33,7 +34,6 @@ type Props = {
|
|||
max?: number,
|
||||
min?: number,
|
||||
name: string,
|
||||
noEmojis?: boolean,
|
||||
placeholder?: string | number,
|
||||
postfix?: string,
|
||||
prefix?: string,
|
||||
|
@ -44,15 +44,25 @@ type Props = {
|
|||
textAreaMaxLength?: number,
|
||||
type?: string,
|
||||
value?: string | number,
|
||||
slimInput?: boolean,
|
||||
commentSelectorsProps?: any,
|
||||
showSelectors?: boolean,
|
||||
submitButtonRef?: any,
|
||||
tipModalOpen?: boolean,
|
||||
onSlimInputClick?: () => void,
|
||||
onChange?: (any) => any,
|
||||
openEmoteMenu?: () => void,
|
||||
setShowSelectors?: (boolean) => void,
|
||||
quickActionHandler?: (any) => any,
|
||||
render?: () => React$Node,
|
||||
handleTip?: (isLBC: boolean) => any,
|
||||
handleSubmit?: () => any,
|
||||
};
|
||||
|
||||
export class FormField extends React.PureComponent<Props> {
|
||||
type State = {
|
||||
drawerOpen: boolean,
|
||||
};
|
||||
|
||||
export class FormField extends React.PureComponent<Props, State> {
|
||||
static defaultProps = { labelOnLeft: false, blockWrap: true };
|
||||
|
||||
input: { current: ElementRef<any> };
|
||||
|
@ -60,6 +70,10 @@ export class FormField extends React.PureComponent<Props> {
|
|||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.input = React.createRef();
|
||||
|
||||
this.state = {
|
||||
drawerOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -69,6 +83,14 @@ export class FormField extends React.PureComponent<Props> {
|
|||
if (input && autoFocus) input.focus();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { showSelectors, slimInput } = this.props;
|
||||
const input = this.input.current;
|
||||
|
||||
// Opened selectors (emoji/sticker) -> blur input and hide keyboard
|
||||
if (slimInput && showSelectors && input) input.blur();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
uri,
|
||||
|
@ -85,15 +107,20 @@ export class FormField extends React.PureComponent<Props> {
|
|||
label,
|
||||
labelOnLeft,
|
||||
name,
|
||||
noEmojis,
|
||||
postfix,
|
||||
prefix,
|
||||
quickActionLabel,
|
||||
stretch,
|
||||
textAreaMaxLength,
|
||||
type,
|
||||
openEmoteMenu,
|
||||
slimInput,
|
||||
commentSelectorsProps,
|
||||
showSelectors,
|
||||
submitButtonRef,
|
||||
tipModalOpen,
|
||||
onSlimInputClick,
|
||||
quickActionHandler,
|
||||
setShowSelectors,
|
||||
render,
|
||||
handleTip,
|
||||
handleSubmit,
|
||||
|
@ -240,50 +267,58 @@ export class FormField extends React.PureComponent<Props> {
|
|||
case 'textarea':
|
||||
return (
|
||||
<fieldset-section>
|
||||
{(label || quickAction) && (
|
||||
<div className="form-field__two-column">
|
||||
<label htmlFor={name}>{label}</label>
|
||||
{countInfo}
|
||||
</div>
|
||||
)}
|
||||
<TextareaWrapper
|
||||
isDrawerOpen={Boolean(this.state.drawerOpen)}
|
||||
toggleDrawer={() => this.setState({ drawerOpen: !this.state.drawerOpen })}
|
||||
closeSelector={setShowSelectors ? () => setShowSelectors(false) : () => {}}
|
||||
commentSelectorsProps={commentSelectorsProps}
|
||||
showSelectors={Boolean(showSelectors)}
|
||||
slimInput={slimInput}
|
||||
tipModalOpen={tipModalOpen}
|
||||
onSlimInputClick={onSlimInputClick}
|
||||
>
|
||||
{(!slimInput || this.state.drawerOpen) && (label || quickAction) && (
|
||||
<div className="form-field__two-column">
|
||||
<label htmlFor={name}>{label}</label>
|
||||
{quickAction}
|
||||
{countInfo}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hideSuggestions ? (
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
) : (
|
||||
<React.Suspense fallback={null}>
|
||||
<TextareaWithSuggestions
|
||||
uri={uri}
|
||||
{hideSuggestions ? (
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
inputRef={this.input}
|
||||
isLivestream={isLivestream}
|
||||
handleEmojis={openEmoteMenu}
|
||||
handleTip={handleTip}
|
||||
handleSubmit={handleSubmit}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)}
|
||||
|
||||
{!noEmojis && openEmoteMenu && (
|
||||
<div className="form-field__textarea-info">
|
||||
<Button
|
||||
type="alt"
|
||||
className="button--file-action"
|
||||
title="Emotes"
|
||||
onClick={openEmoteMenu}
|
||||
icon={ICONS.EMOJI}
|
||||
iconSize={20}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<React.Suspense fallback={null}>
|
||||
<TextareaWithSuggestions
|
||||
uri={uri}
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
inputRef={this.input}
|
||||
isLivestream={isLivestream}
|
||||
toggleSelectors={setShowSelectors ? () => setShowSelectors(!showSelectors) : undefined}
|
||||
handleTip={handleTip}
|
||||
handleSubmit={() => {
|
||||
if (handleSubmit) handleSubmit();
|
||||
if (slimInput) this.setState({ drawerOpen: false });
|
||||
}}
|
||||
claimIsMine={commentSelectorsProps && commentSelectorsProps.claimIsMine}
|
||||
{...inputProps}
|
||||
handlePreventClick={
|
||||
!this.state.drawerOpen ? () => this.setState({ drawerOpen: true }) : undefined
|
||||
}
|
||||
autoFocus={this.state.drawerOpen}
|
||||
submitButtonRef={submitButtonRef}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)}
|
||||
</TextareaWrapper>
|
||||
</fieldset-section>
|
||||
);
|
||||
default:
|
||||
|
@ -321,3 +356,65 @@ export class FormField extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
export default FormField;
|
||||
|
||||
type TextareaWrapperProps = {
|
||||
slimInput?: boolean,
|
||||
children: Node,
|
||||
isDrawerOpen: boolean,
|
||||
showSelectors?: boolean,
|
||||
commentSelectorsProps?: any,
|
||||
tipModalOpen?: boolean,
|
||||
onSlimInputClick?: () => void,
|
||||
toggleDrawer: () => void,
|
||||
closeSelector?: () => void,
|
||||
};
|
||||
|
||||
function TextareaWrapper(wrapperProps: TextareaWrapperProps) {
|
||||
const {
|
||||
children,
|
||||
slimInput,
|
||||
isDrawerOpen,
|
||||
commentSelectorsProps,
|
||||
showSelectors,
|
||||
tipModalOpen,
|
||||
onSlimInputClick,
|
||||
toggleDrawer,
|
||||
closeSelector,
|
||||
} = wrapperProps;
|
||||
|
||||
function handleCloseAll() {
|
||||
toggleDrawer();
|
||||
if (closeSelector) closeSelector();
|
||||
if (onSlimInputClick) onSlimInputClick();
|
||||
}
|
||||
|
||||
return slimInput ? (
|
||||
!isDrawerOpen ? (
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => {
|
||||
toggleDrawer();
|
||||
if (onSlimInputClick) onSlimInputClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<Drawer
|
||||
className="comment-create--drawer"
|
||||
anchor="bottom"
|
||||
open
|
||||
onClose={handleCloseAll}
|
||||
// The Modal tries to enforce focus when open and doesn't allow clicking or changing any
|
||||
// other input boxes, so in this case it is disabled when trying to type in a custom tip
|
||||
ModalProps={{ disableEnforceFocus: tipModalOpen }}
|
||||
>
|
||||
{children}
|
||||
|
||||
{showSelectors && <CommentSelectors closeSelector={closeSelector} {...commentSelectorsProps} />}
|
||||
</Drawer>
|
||||
)
|
||||
) : (
|
||||
<>{children}</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { Global } from '@emotion/react';
|
|||
// $FlowFixMe
|
||||
import { grey } from '@mui/material/colors';
|
||||
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
|
@ -42,6 +41,7 @@ type Props = {
|
|||
superchatsHidden?: boolean,
|
||||
customViewMode?: string,
|
||||
theme: string,
|
||||
setCustomViewMode?: (any) => void,
|
||||
doCommentList: (string, string, number, number) => void,
|
||||
doResolveUris: (Array<string>, boolean) => void,
|
||||
doSuperChatList: (string) => void,
|
||||
|
@ -60,6 +60,7 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
superchatsHidden,
|
||||
customViewMode,
|
||||
theme,
|
||||
setCustomViewMode,
|
||||
doCommentList,
|
||||
doResolveUris,
|
||||
doSuperChatList,
|
||||
|
@ -67,11 +68,18 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
|
||||
const isMobile = useIsMobile() && !isPopoutWindow;
|
||||
|
||||
const discussionElement = document.querySelector('.livestream__comments');
|
||||
const webElement = document.querySelector('.livestream__comments');
|
||||
const mobileElement = document.querySelector('.livestream__comments--mobile');
|
||||
const discussionElement = isMobile ? mobileElement : webElement;
|
||||
const allCommentsElem = document.querySelectorAll('.livestream__comment');
|
||||
const lastCommentElem = allCommentsElem && allCommentsElem[allCommentsElem.length - 1];
|
||||
const minScrollPos =
|
||||
discussionElement && lastCommentElem && discussionElement.scrollHeight - lastCommentElem.offsetHeight;
|
||||
const minOffset = discussionElement && minScrollPos && discussionElement.scrollHeight - minScrollPos;
|
||||
|
||||
const restoreScrollPos = React.useCallback(() => {
|
||||
if (discussionElement) discussionElement.scrollTop = 0;
|
||||
}, [discussionElement]);
|
||||
if (discussionElement) discussionElement.scrollTop = !isMobile ? 0 : discussionElement.scrollHeight;
|
||||
}, [discussionElement, isMobile]);
|
||||
|
||||
const commentsRef = React.createRef();
|
||||
|
||||
|
@ -79,12 +87,12 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
const [scrollPos, setScrollPos] = React.useState(0);
|
||||
const [showPinned, setShowPinned] = React.useState(true);
|
||||
const [resolvingSuperChats, setResolvingSuperChats] = React.useState(false);
|
||||
const [mention, setMention] = React.useState();
|
||||
const [openedPopoutWindow, setPopoutWindow] = React.useState(undefined);
|
||||
const [chatHidden, setChatHidden] = React.useState(false);
|
||||
const [didInitialScroll, setDidInitialScroll] = React.useState(false);
|
||||
const [bottomScrollTop, setBottomScrollTop] = React.useState(0);
|
||||
|
||||
const quickMention =
|
||||
mention && formatLbryUrlForWeb(mention).substring(1, formatLbryUrlForWeb(mention).indexOf(':') + 3);
|
||||
const recentScrollPos = isMobile ? (bottomScrollTop > 0 && minOffset ? bottomScrollTop - minOffset : 0) : 0;
|
||||
const claimId = claim && claim.claim_id;
|
||||
const commentsToDisplay = viewMode === VIEW_MODES.CHAT ? commentsByChronologicalOrder : superChatsByAmount;
|
||||
const commentsLength = commentsToDisplay && commentsToDisplay.length;
|
||||
|
@ -100,6 +108,7 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
}
|
||||
}
|
||||
setViewMode(VIEW_MODES.SUPERCHAT);
|
||||
if (setCustomViewMode) setCustomViewMode(VIEW_MODES.SUPERCHAT);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -115,12 +124,26 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
}
|
||||
}, [claimId, uri, doCommentList, doSuperChatList]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
isMobile &&
|
||||
discussionElement &&
|
||||
viewMode === VIEW_MODES.CHAT &&
|
||||
!didInitialScroll &&
|
||||
discussionElement.scrollTop < discussionElement.scrollHeight
|
||||
) {
|
||||
discussionElement.scrollTop = discussionElement.scrollHeight;
|
||||
setDidInitialScroll(true);
|
||||
setBottomScrollTop(discussionElement.scrollTop);
|
||||
}
|
||||
}, [didInitialScroll, discussionElement, isMobile, viewMode]);
|
||||
|
||||
// Register scroll handler (TODO: Should throttle/debounce)
|
||||
React.useEffect(() => {
|
||||
function handleScroll() {
|
||||
if (discussionElement) {
|
||||
const scrollTop = discussionElement.scrollTop;
|
||||
if (scrollTop !== scrollPos) {
|
||||
if (!scrollPos || scrollTop !== scrollPos) {
|
||||
setScrollPos(scrollTop);
|
||||
}
|
||||
}
|
||||
|
@ -136,12 +159,13 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
React.useEffect(() => {
|
||||
if (discussionElement && commentsLength > 0) {
|
||||
// Only update comment scroll if the user hasn't scrolled up to view old comments
|
||||
if (scrollPos >= 0) {
|
||||
// $FlowFixMe
|
||||
if (scrollPos && (!isMobile || recentScrollPos) && scrollPos >= recentScrollPos) {
|
||||
// +ve scrollPos: not scrolled (Usually, there'll be a few pixels beyond 0).
|
||||
// -ve scrollPos: user scrolled.
|
||||
const timer = setTimeout(() => {
|
||||
// Use a timer here to ensure we reset after the new comment has been rendered.
|
||||
discussionElement.scrollTop = 0;
|
||||
discussionElement.scrollTop = !isMobile ? 0 : discussionElement.scrollHeight + 999;
|
||||
}, COMMENT_SCROLL_TIMEOUT);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
|
@ -283,7 +307,6 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
comment={pinnedComment}
|
||||
key={pinnedComment.comment_id}
|
||||
uri={uri}
|
||||
pushMention={setMention}
|
||||
handleDismissPin={() => setShowPinned(false)}
|
||||
isMobile
|
||||
/>
|
||||
|
@ -292,12 +315,7 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
) : (
|
||||
showPinned && (
|
||||
<div className="livestream-pinned__wrapper">
|
||||
<LivestreamComment
|
||||
comment={pinnedComment}
|
||||
key={pinnedComment.comment_id}
|
||||
uri={uri}
|
||||
pushMention={setMention}
|
||||
/>
|
||||
<LivestreamComment comment={pinnedComment} key={pinnedComment.comment_id} uri={uri} />
|
||||
|
||||
<Button
|
||||
title={__('Dismiss pinned comment')}
|
||||
|
@ -316,23 +334,18 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<LivestreamComments
|
||||
uri={uri}
|
||||
commentsToDisplay={commentsToDisplay}
|
||||
pushMention={setMention}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<LivestreamComments uri={uri} commentsToDisplay={commentsToDisplay} isMobile={isMobile} />
|
||||
)}
|
||||
|
||||
{scrollPos < 0 && (
|
||||
{scrollPos && (!isMobile || recentScrollPos) && scrollPos < recentScrollPos && viewMode === VIEW_MODES.CHAT ? (
|
||||
<Button
|
||||
button="secondary"
|
||||
className="livestreamComments__scrollToRecent"
|
||||
className="livestream-comments__scroll-to-recent"
|
||||
label={viewMode === VIEW_MODES.CHAT ? __('Recent Comments') : __('Recent Tips')}
|
||||
onClick={restoreScrollPos}
|
||||
iconRight={ICONS.DOWN}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<div className="livestream__comment-create">
|
||||
<CommentCreate
|
||||
|
@ -341,8 +354,13 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
embed={embed}
|
||||
uri={uri}
|
||||
onDoneReplying={restoreScrollPos}
|
||||
pushedMention={quickMention}
|
||||
setPushedMention={setMention}
|
||||
onSlimInputClick={
|
||||
scrollPos &&
|
||||
recentScrollPos &&
|
||||
scrollPos >= recentScrollPos &&
|
||||
viewMode === VIEW_MODES.CHAT &&
|
||||
restoreScrollPos
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -46,6 +46,8 @@ export default function LivestreamComment(props: Props) {
|
|||
timestamp,
|
||||
} = comment;
|
||||
|
||||
const commentRef = React.useRef();
|
||||
|
||||
const [hasUserMention, setUserMention] = React.useState(false);
|
||||
|
||||
const isStreamer = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||
|
@ -54,12 +56,24 @@ export default function LivestreamComment(props: Props) {
|
|||
const isSticker = Boolean(stickerUrlFromMessage);
|
||||
const timePosted = timestamp * 1000;
|
||||
const commentIsMine = comment.channel_id && isMyComment(comment.channel_id);
|
||||
const discussionElement = document.querySelector('.livestream__comments--mobile');
|
||||
const currentComment = commentRef && commentRef.current;
|
||||
const minScrollPos =
|
||||
discussionElement && currentComment && discussionElement.scrollHeight - currentComment.offsetHeight;
|
||||
|
||||
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
|
||||
function isMyComment(channelId: string) {
|
||||
return myChannelIds ? myChannelIds.includes(channelId) : false;
|
||||
}
|
||||
|
||||
// For every new <LivestreamComment /> component that is rendered on mobile view,
|
||||
// keep the scroll at the bottom (newest)
|
||||
React.useEffect(() => {
|
||||
if (isMobile && discussionElement && minScrollPos && discussionElement.scrollTop >= minScrollPos) {
|
||||
discussionElement.scrollTop = discussionElement.scrollHeight;
|
||||
}
|
||||
}, [discussionElement, isMobile, minScrollPos]);
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classnames('livestream__comment', {
|
||||
|
@ -68,6 +82,7 @@ export default function LivestreamComment(props: Props) {
|
|||
'livestream__comment--mentioned': hasUserMention,
|
||||
'livestream__comment--mobile': isMobile,
|
||||
})}
|
||||
ref={commentRef}
|
||||
>
|
||||
{supportAmount > 0 && (
|
||||
<div className="livestreamComment__superchatBanner">
|
||||
|
|
|
@ -44,8 +44,8 @@ export default function LivestreamComments(props: Props) {
|
|||
|
||||
/* top to bottom comment display */
|
||||
if (!fetchingComments && commentsToDisplay && commentsToDisplay.length > 0) {
|
||||
return (
|
||||
<div className="livestream__comments">
|
||||
return isMobile ? (
|
||||
<div className="livestream__comments--mobile">
|
||||
{commentsToDisplay
|
||||
.slice(0)
|
||||
.reverse()
|
||||
|
@ -55,10 +55,16 @@ export default function LivestreamComments(props: Props) {
|
|||
key={comment.comment_id}
|
||||
uri={uri}
|
||||
forceUpdate={forceUpdate}
|
||||
isMobile={isMobile}
|
||||
isMobile
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="livestream__comments">
|
||||
{commentsToDisplay.map((comment) => (
|
||||
<LivestreamComment comment={comment} key={comment.comment_id} uri={uri} forceUpdate={forceUpdate} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -130,6 +130,7 @@ export default function LivestreamLayout(props: Props) {
|
|||
hideHeader
|
||||
superchatsHidden={superchatsHidden}
|
||||
customViewMode={chatViewMode}
|
||||
setCustomViewMode={(mode) => setChatViewMode(mode)}
|
||||
/>
|
||||
</SwipeableDrawer>
|
||||
|
||||
|
@ -155,7 +156,7 @@ const ChatModeSelector = (chatSelectorProps: any) => {
|
|||
<Menu>
|
||||
<MenuButton>
|
||||
<span className="swipeable-drawer__title-menu">
|
||||
{chatViewMode === VIEW_MODES.CHAT ? __('Live Chat') : __('Super Chats')}
|
||||
{chatViewMode === VIEW_MODES.CHAT ? __('Live Chat') : __('HyperChats')}
|
||||
<Icon icon={ICONS.DOWN} />
|
||||
</span>
|
||||
</MenuButton>
|
||||
|
|
|
@ -11,13 +11,27 @@ type Props = {
|
|||
messageValue: string,
|
||||
inputDefaultProps: any,
|
||||
inputRef: any,
|
||||
handleEmojis: () => any,
|
||||
submitButtonRef?: any,
|
||||
claimIsMine?: boolean,
|
||||
toggleSelectors: () => any,
|
||||
handleTip: (isLBC: boolean) => void,
|
||||
handleSubmit: () => any,
|
||||
handlePreventClick?: () => void,
|
||||
};
|
||||
|
||||
const TextareaSuggestionsInput = (props: Props) => {
|
||||
const { params, messageValue, inputRef, inputDefaultProps, handleEmojis, handleTip, handleSubmit } = props;
|
||||
const {
|
||||
params,
|
||||
messageValue,
|
||||
inputRef,
|
||||
inputDefaultProps,
|
||||
submitButtonRef,
|
||||
claimIsMine,
|
||||
toggleSelectors,
|
||||
handleTip,
|
||||
handleSubmit,
|
||||
handlePreventClick,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
|
@ -26,15 +40,42 @@ const TextareaSuggestionsInput = (props: Props) => {
|
|||
const autocompleteProps = { InputProps, disabled, fullWidth, id, inputProps };
|
||||
|
||||
if (isMobile) {
|
||||
InputProps.startAdornment = <Button icon={ICONS.STICKER} onClick={handleEmojis} />;
|
||||
InputProps.startAdornment = (
|
||||
<Button
|
||||
icon={ICONS.STICKER}
|
||||
onClick={() => {
|
||||
if (handlePreventClick) handlePreventClick();
|
||||
toggleSelectors();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
InputProps.endAdornment = (
|
||||
<>
|
||||
<Button icon={ICONS.LBC} onClick={() => handleTip(true)} />
|
||||
<Button icon={ICONS.FINANCE} onClick={() => handleTip(false)} />
|
||||
{!claimIsMine && (
|
||||
<Button
|
||||
disabled={!messageValue || messageValue.length === 0}
|
||||
icon={ICONS.LBC}
|
||||
onClick={() => handleTip(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Zoom in={messageValue && messageValue.length > 0} mountOnEnter unmountOnExit>
|
||||
{!claimIsMine && (
|
||||
<Button
|
||||
disabled={!messageValue || messageValue.length === 0}
|
||||
icon={ICONS.FINANCE}
|
||||
onClick={() => handleTip(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Zoom in={messageValue ? messageValue.length > 0 : undefined} mountOnEnter unmountOnExit>
|
||||
<div>
|
||||
<Button button="primary" icon={ICONS.SUBMIT} iconColor="red" onClick={() => handleSubmit()} />
|
||||
<Button
|
||||
ref={submitButtonRef}
|
||||
button="primary"
|
||||
icon={ICONS.SUBMIT}
|
||||
iconColor="red"
|
||||
onClick={() => handleSubmit()}
|
||||
/>
|
||||
</div>
|
||||
</Zoom>
|
||||
</>
|
||||
|
|
|
@ -56,14 +56,18 @@ type Props = {
|
|||
type?: string,
|
||||
uri?: string,
|
||||
value: any,
|
||||
autoFocus?: boolean,
|
||||
submitButtonRef?: any,
|
||||
claimIsMine?: boolean,
|
||||
doResolveUris: (uris: Array<string>, cache: boolean) => void,
|
||||
doSetMentionSearchResults: (query: string, uris: Array<string>) => void,
|
||||
onBlur: (any) => any,
|
||||
onChange: (any) => any,
|
||||
onFocus: (any) => any,
|
||||
handleEmojis: () => any,
|
||||
toggleSelectors: () => any,
|
||||
handleTip: (isLBC: boolean) => any,
|
||||
handleSubmit: () => any,
|
||||
handlePreventClick?: () => void,
|
||||
};
|
||||
|
||||
export default function TextareaWithSuggestions(props: Props) {
|
||||
|
@ -85,14 +89,18 @@ export default function TextareaWithSuggestions(props: Props) {
|
|||
searchQuery,
|
||||
type,
|
||||
value: messageValue,
|
||||
autoFocus,
|
||||
submitButtonRef,
|
||||
claimIsMine,
|
||||
doResolveUris,
|
||||
doSetMentionSearchResults,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
handleEmojis,
|
||||
toggleSelectors,
|
||||
handleTip,
|
||||
handleSubmit,
|
||||
handlePreventClick,
|
||||
} = props;
|
||||
|
||||
const inputDefaultProps = { className, placeholder, maxLength, type, disabled };
|
||||
|
@ -290,6 +298,13 @@ export default function TextareaWithSuggestions(props: Props) {
|
|||
/** Effects **/
|
||||
/** ------- **/
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!autoFocus) return;
|
||||
|
||||
const inputElement = inputRef && inputRef.current;
|
||||
if (inputElement) inputElement.focus();
|
||||
}, [autoFocus, inputRef]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isMention) return;
|
||||
|
||||
|
@ -400,9 +415,12 @@ export default function TextareaWithSuggestions(props: Props) {
|
|||
messageValue={messageValue}
|
||||
inputRef={inputRef}
|
||||
inputDefaultProps={inputDefaultProps}
|
||||
handleEmojis={handleEmojis}
|
||||
toggleSelectors={toggleSelectors}
|
||||
handleTip={handleTip}
|
||||
handleSubmit={handleSubmit}
|
||||
handlePreventClick={handlePreventClick}
|
||||
submitButtonRef={submitButtonRef}
|
||||
claimIsMine={claimIsMine}
|
||||
/>
|
||||
)}
|
||||
renderOption={(optionProps, option) => (
|
||||
|
|
|
@ -44,6 +44,7 @@ type Props = {
|
|||
uri: string,
|
||||
isTipOnly?: boolean,
|
||||
hasSelectedTab?: string,
|
||||
customText?: string,
|
||||
doHideModal: () => void,
|
||||
doSendCashTip: (TipParams, boolean, UserParams, string, ?string) => string,
|
||||
doSendTip: (SupportParams, boolean) => void, // function that comes from lbry-redux
|
||||
|
@ -69,6 +70,7 @@ export default function WalletSendTip(props: Props) {
|
|||
uri,
|
||||
isTipOnly,
|
||||
hasSelectedTab,
|
||||
customText,
|
||||
doHideModal,
|
||||
doSendCashTip,
|
||||
doSendTip,
|
||||
|
@ -328,7 +330,7 @@ export default function WalletSendTip(props: Props) {
|
|||
button="primary"
|
||||
type="submit"
|
||||
disabled={fetchingChannels || isPending || tipError || !tipAmount || disableSubmitButton}
|
||||
label={buildButtonText()}
|
||||
label={customText || buildButtonText()}
|
||||
/>
|
||||
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
||||
</div>
|
||||
|
|
|
@ -71,7 +71,6 @@ function WalletTipAmountSelector(props: Props) {
|
|||
|
||||
// if it's fiat but there's no card saved OR the creator can't receive fiat tips
|
||||
const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip);
|
||||
if (setDisableSubmitButton) setDisableSubmitButton(shouldDisableFiatSelectors);
|
||||
|
||||
// setup variables for tip API
|
||||
const channelClaimId = claim ? (claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id) : undefined;
|
||||
|
@ -103,6 +102,10 @@ function WalletTipAmountSelector(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (setDisableSubmitButton) setDisableSubmitButton(shouldDisableFiatSelectors);
|
||||
}, [setDisableSubmitButton, shouldDisableFiatSelectors]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (setConvertedAmount && exchangeRate && (!convertedAmount || convertedAmount !== amount * exchangeRate)) {
|
||||
setConvertedAmount(amount * exchangeRate);
|
||||
|
|
|
@ -185,7 +185,7 @@ export const TECHNOLOGY = 'Technology';
|
|||
export const EMOJI = 'Emoji';
|
||||
export const STICKER = 'Sticker';
|
||||
export const EDUCATION = 'Education';
|
||||
export const POP_CULTURE = 'PopCulture';
|
||||
export const POP_CULTURE = 'Pop Culture';
|
||||
export const ODYSEE_LOGO = 'OdyseeLogo';
|
||||
export const ODYSEE_WHITE_TEXT = 'OdyseeLogoWhiteText';
|
||||
export const ODYSEE_DARK_TEXT = 'OdyseeLogoDarkText';
|
||||
|
|
|
@ -9,13 +9,14 @@ type Props = {
|
|||
isSupport: boolean,
|
||||
isTipOnly?: boolean,
|
||||
hasSelectedTab?: string,
|
||||
customText?: string,
|
||||
doHideModal: () => void,
|
||||
setAmount?: (number) => void,
|
||||
};
|
||||
|
||||
class ModalSendTip extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { uri, claimIsMine, isTipOnly, hasSelectedTab, doHideModal, setAmount } = this.props;
|
||||
const { uri, claimIsMine, isTipOnly, hasSelectedTab, customText, doHideModal, setAmount } = this.props;
|
||||
|
||||
return (
|
||||
<Modal onAborted={doHideModal} isOpen type="card">
|
||||
|
@ -25,6 +26,7 @@ class ModalSendTip extends React.PureComponent<Props> {
|
|||
onCancel={doHideModal}
|
||||
isTipOnly={isTipOnly}
|
||||
hasSelectedTab={hasSelectedTab}
|
||||
customText={customText}
|
||||
setAmount={setAmount}
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
@ -72,7 +72,8 @@ export default function FilePage(props: Props) {
|
|||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [showComments, setShowComments] = React.useState(undefined);
|
||||
// Auto-open the drawer on Mobile view if there is a linked comment
|
||||
const [showComments, setShowComments] = React.useState(linkedCommentId);
|
||||
|
||||
const cost = costInfo ? costInfo.cost : null;
|
||||
const hasFileInfo = fileInfo !== undefined;
|
||||
|
@ -186,11 +187,7 @@ export default function FilePage(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
const commentsListElement = commentsDisabled ? (
|
||||
<Empty text={__('The creator of this content has disabled comments.')} />
|
||||
) : (
|
||||
<CommentsList uri={uri} linkedCommentId={linkedCommentId} />
|
||||
);
|
||||
const commentsListProps = { uri, linkedCommentId };
|
||||
|
||||
return (
|
||||
<Page className="file-page" filePage isMarkdown={isMarkdown}>
|
||||
|
@ -216,20 +213,22 @@ export default function FilePage(props: Props) {
|
|||
{RENDER_MODES.FLOATING_MODES.includes(renderMode) && <FileTitleSection uri={uri} />}
|
||||
|
||||
<React.Suspense fallback={null}>
|
||||
{isMobile ? (
|
||||
{commentsDisabled ? (
|
||||
<Empty text={__('The creator of this content has disabled comments.')} />
|
||||
) : isMobile ? (
|
||||
<>
|
||||
<SwipeableDrawer
|
||||
open={Boolean(showComments)}
|
||||
toggleDrawer={() => setShowComments(!showComments)}
|
||||
title={commentsListTitle}
|
||||
>
|
||||
{commentsListElement}
|
||||
<CommentsList {...commentsListProps} />
|
||||
</SwipeableDrawer>
|
||||
|
||||
<DrawerExpandButton label={commentsListTitle} toggleDrawer={() => setShowComments(!showComments)} />
|
||||
</>
|
||||
) : (
|
||||
commentsListElement
|
||||
<CommentsList {...commentsListProps} />
|
||||
)}
|
||||
</React.Suspense>
|
||||
</section>
|
||||
|
|
|
@ -14,6 +14,44 @@
|
|||
overflow: visible;
|
||||
margin-bottom: var(--spacing-m);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
|
||||
.card__main-actions {
|
||||
margin-top: 0px !important;
|
||||
|
||||
.comment__sort {
|
||||
margin: 0px !important;
|
||||
display: inline;
|
||||
|
||||
button {
|
||||
padding: var(--spacing-xxs);
|
||||
|
||||
span {
|
||||
font-size: var(--font-xxsmall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .button--alt {
|
||||
padding: var(--spacing-xxs) var(--spacing-s);
|
||||
}
|
||||
|
||||
.comment__sort + button {
|
||||
margin: 0px var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.button + .commentCreate {
|
||||
margin-top: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card--comments-list {
|
||||
@extend .card--enable-overflow;
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
|
@ -362,7 +400,7 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.media__subtitle--centered::before {
|
||||
span + .media__subtitle--centered::before {
|
||||
content: '•';
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
@ -491,3 +529,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ReactModalPortal {
|
||||
.button--close {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,10 +21,16 @@ $thumbnailWidthSmall: 1rem;
|
|||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
fieldset-section + .section {
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
.commentCreate + .empty__wrap {
|
||||
.empty__wrap {
|
||||
p {
|
||||
font-size: var(--font-small);
|
||||
text-align: center;
|
||||
|
@ -32,6 +38,16 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment-create--drawer {
|
||||
.MuiPaper-root {
|
||||
background-color: var(--color-background) !important;
|
||||
|
||||
span {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commentCreate--reply {
|
||||
margin-top: var(--spacing-m);
|
||||
position: relative;
|
||||
|
@ -71,6 +87,15 @@ $thumbnailWidthSmall: 1rem;
|
|||
fieldset-section {
|
||||
font-size: var(--font-xxsmall);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: var(--font-xxsmall);
|
||||
}
|
||||
|
||||
select {
|
||||
height: 1rem;
|
||||
margin: var(--spacing-xxs) 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,12 +111,25 @@ $thumbnailWidthSmall: 1rem;
|
|||
margin-right: var(--spacing-m);
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: var(--spacing-xs);
|
||||
|
||||
span {
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commentCreate__minAmountNotice {
|
||||
.comment-create__min-amount-notice {
|
||||
.icon {
|
||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
margin: 0px;
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
}
|
||||
|
||||
.commentCreate__stickerPreview {
|
||||
|
@ -131,4 +169,13 @@ $thumbnailWidthSmall: 1rem;
|
|||
margin-left: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: var(--spacing-xs);
|
||||
height: 7rem;
|
||||
|
||||
span {
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
119
ui/scss/component/_comment-selectors.scss
Normal file
119
ui/scss/component/_comment-selectors.scss
Normal file
|
@ -0,0 +1,119 @@
|
|||
@import '../init/breakpoints';
|
||||
|
||||
$emote-item-size--small: 2.5rem;
|
||||
$emote-item-size--big: 3rem;
|
||||
$sticker-item-size: 5rem;
|
||||
|
||||
// -- EMOJIS --
|
||||
.selector-menu {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
max-height: 25vh;
|
||||
padding: var(--spacing-xs);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
max-height: 30vh;
|
||||
padding-top: var(--spacing-s);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emote-selector__items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, $emote-item-size--small);
|
||||
justify-items: center;
|
||||
justify-content: space-evenly;
|
||||
|
||||
button {
|
||||
margin: 0px !important;
|
||||
padding: var(--spacing-xs);
|
||||
height: unset;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
grid-template-columns: repeat(auto-fit, $emote-item-size--big);
|
||||
}
|
||||
}
|
||||
|
||||
// -- STICKERS --
|
||||
.selector-menu--stickers {
|
||||
@extend .selector-menu;
|
||||
padding-top: 0px;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sticker-selector__items {
|
||||
@extend .emote-selector__items;
|
||||
grid-template-columns: repeat(auto-fit, $sticker-item-size);
|
||||
|
||||
.button--file-action {
|
||||
overflow: hidden;
|
||||
margin: unset;
|
||||
padding: var(--spacing-xs);
|
||||
height: unset;
|
||||
|
||||
.sticker-item--priced {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.superChat--light {
|
||||
position: absolute;
|
||||
display: inline;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sticker-selector__row-title {
|
||||
font-size: var(--font-small);
|
||||
padding-left: var(--spacing-xxs);
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
background-color: var(--color-tabs-background);
|
||||
z-index: 1;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
}
|
|
@ -499,6 +499,10 @@ $thumbnailWidthSmall: 1.8rem;
|
|||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
height: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.emote {
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
@import '../init/breakpoints';
|
||||
|
||||
.emoteSelector {
|
||||
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
|
||||
.emoteSelector__list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
max-height: 25vh;
|
||||
padding: var(--spacing-s);
|
||||
|
||||
.emoteSelector__listRowItems {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.button--file-action {
|
||||
margin: var(--spacing-xxs);
|
||||
padding: var(--spacing-xs);
|
||||
|
||||
.button__content {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
|
||||
span {
|
||||
margin: auto;
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -122,6 +122,16 @@ $recent-msg-button__height: 2rem;
|
|||
}
|
||||
|
||||
.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__comments--mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: var(--font-small);
|
||||
|
@ -131,10 +141,9 @@ $recent-msg-button__height: 2rem;
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.livestreamComments__scrollToRecent {
|
||||
.livestream-comments__scroll-to-recent {
|
||||
margin-top: -$recent-msg-button__height;
|
||||
align-self: center;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-size: var(--font-xsmall);
|
||||
padding: var(--spacing-xxs) var(--spacing-s);
|
||||
opacity: 0.9;
|
||||
|
@ -142,6 +151,14 @@ $recent-msg-button__height: 2rem;
|
|||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
bottom: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
|
||||
.livestream__comment-create {
|
||||
|
@ -150,7 +167,7 @@ $recent-msg-button__height: 2rem;
|
|||
margin-top: auto;
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: var(--spacing-xxs);
|
||||
padding: 0px;
|
||||
|
||||
span,
|
||||
select,
|
||||
|
@ -186,7 +203,7 @@ $recent-msg-button__height: 2rem;
|
|||
.livestream-superchats__wrapper--mobile {
|
||||
@extend .livestream-superchats__wrapper;
|
||||
|
||||
z-index: 1300;
|
||||
z-index: 9999999;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
padding: 0px;
|
||||
|
@ -224,6 +241,7 @@ $recent-msg-button__height: 2rem;
|
|||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -268,24 +286,15 @@ $recent-msg-button__height: 2rem;
|
|||
border: 1px solid var(--color-border);
|
||||
|
||||
.livestream__comment {
|
||||
overflow: unset;
|
||||
overflow: hidden;
|
||||
|
||||
.livestreamComment__body {
|
||||
margin: 0px;
|
||||
width: 100%;
|
||||
overflow: scroll;
|
||||
|
||||
.markdown-preview {
|
||||
p,
|
||||
.button__label {
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
a {
|
||||
pointer-events: none;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -378,6 +387,7 @@ $recent-msg-button__height: 2rem;
|
|||
|
||||
span {
|
||||
font-size: var(--font-xxsmall);
|
||||
color: var(--color-text-subtitle);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -127,6 +127,12 @@
|
|||
flex: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
section + .empty__wrap {
|
||||
margin: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
@import '../init/breakpoints';
|
||||
|
||||
.stickerSelector {
|
||||
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: var(--spacing-m);
|
||||
|
||||
.stickerSelector__header {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: var(--spacing-s);
|
||||
margin-bottom: 0;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xxs);
|
||||
|
||||
.stickerSelector__headerTitle {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation__wrapper {
|
||||
height: unset;
|
||||
border-left: 1px solid var(--color-border);
|
||||
|
||||
.navigation-links {
|
||||
li {
|
||||
.button {
|
||||
padding: unset;
|
||||
|
||||
.button__content {
|
||||
justify-content: unset;
|
||||
flex-direction: unset;
|
||||
width: unset;
|
||||
|
||||
.button__label {
|
||||
font-size: var(--font-small);
|
||||
margin: 0 var(--spacing-s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stickerSelector__list {
|
||||
display: flex;
|
||||
|
||||
.stickerSelector__listBody {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
max-height: 25vh;
|
||||
padding: var(--spacing-s);
|
||||
|
||||
.stickerSelector__listBody-rowItems {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
.button--file-action {
|
||||
width: 5.5rem;
|
||||
height: 6rem;
|
||||
overflow: hidden;
|
||||
margin: unset;
|
||||
padding: var(--spacing-xxs);
|
||||
|
||||
.stickerItem--paid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.superChat--light {
|
||||
position: absolute;
|
||||
display: inline;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-xsmall) {
|
||||
width: 3.5rem;
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,16 +6,47 @@
|
|||
font-size: inherit !important;
|
||||
color: var(--color-text) !important;
|
||||
|
||||
.MuiOutlinedInput-notchedOutline {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.create__comment {
|
||||
min-height: calc(var(--height-input) * 1.5) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-create__auth {
|
||||
.MuiAutocomplete-root {
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-create--drawer {
|
||||
.MuiPaper-root {
|
||||
.form-field__two-column,
|
||||
.MuiOutlinedInput-root {
|
||||
padding: 0px var(--spacing-xxs) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.livestream__comment-create {
|
||||
.MuiOutlinedInput-root {
|
||||
padding: 0px var(--spacing-xxs) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.MuiOutlinedInput-notchedOutline {
|
||||
visibility: none;
|
||||
}
|
||||
|
||||
.comment-create--drawer .MuiOutlinedInput-notchedOutline {
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.card__main-actions .commentCreate .MuiOutlinedInput-notchedOutline {
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: var(--border-radius) !important;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
.MuiOutlinedInput-input {
|
||||
padding: 0px var(--spacing-xxs);
|
||||
|
@ -25,19 +56,15 @@
|
|||
font-size: var(--font-xsmall) !important;
|
||||
flex-wrap: nowrap !important;
|
||||
color: var(--color-text) !important;
|
||||
padding: 0px 9px !important;
|
||||
padding: 0px !important;
|
||||
|
||||
textarea {
|
||||
border: none;
|
||||
margin: 9px 0px;
|
||||
}
|
||||
|
||||
button:not(:first-of-type):not(:last-of-type) {
|
||||
margin: 0px var(--spacing-xxs);
|
||||
}
|
||||
|
||||
button + div {
|
||||
margin-left: var(--spacing-xxs);
|
||||
button {
|
||||
padding: var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.button--primary {
|
||||
|
@ -130,3 +157,11 @@
|
|||
.MuiAutocomplete-loading {
|
||||
color: var(--color-text) !important;
|
||||
}
|
||||
|
||||
.MuiPaper-root {
|
||||
.form-field__two-column {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,6 +167,23 @@
|
|||
padding: var(--spacing-s) var(--spacing-m);
|
||||
height: var(--button-height);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
margin-top: var(--spacing-xxs);
|
||||
|
||||
button {
|
||||
height: 2rem;
|
||||
padding: 0px var(--spacing-s);
|
||||
|
||||
.button__content {
|
||||
height: unset;
|
||||
|
||||
span {
|
||||
font-size: var(--font-xxsmall);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section__actions--centered {
|
||||
|
|
|
@ -14,6 +14,45 @@
|
|||
}
|
||||
}
|
||||
|
||||
.commentCreate {
|
||||
.tabs {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: var(--spacing-m);
|
||||
|
||||
.selector-menu {
|
||||
border: 0 !important;
|
||||
|
||||
button {
|
||||
margin: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs__list--comment-selector {
|
||||
@extend .tabs__list--channel-page;
|
||||
padding-left: var(--spacing-m) !important;
|
||||
border: 0 !important;
|
||||
border-bottom: 1px solid var(--color-border) !important;
|
||||
border-radius: 0 !important;
|
||||
margin-bottom: 0px !important;
|
||||
|
||||
+ .tab__divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
position: sticky;
|
||||
padding: 0px;
|
||||
height: 3rem !important;
|
||||
|
||||
button {
|
||||
font-size: var(--font-xxsmall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs__list--channel-page {
|
||||
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-xl));
|
||||
padding-right: var(--spacing-m);
|
||||
|
|
Loading…
Reference in a new issue