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="Pragma" content="no-cache" />
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
<meta http-equiv="Expires" content="0" />
|
<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/300.woff" as="font" type="font/woff" crossorigin />
|
||||||
<link rel="preload" href="/public/font/v1/300i.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()}`);
|
replace(`${pathname}?${urlParams.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkedCommentRef = React.useCallback((node) => {
|
const linkedCommentRef = React.useCallback(
|
||||||
|
(node) => {
|
||||||
if (node !== null && window.pendingLinkedCommentScroll) {
|
if (node !== null && window.pendingLinkedCommentScroll) {
|
||||||
const ROUGH_HEADER_HEIGHT = 125; // @see: --header-height
|
const ROUGH_HEADER_HEIGHT = 125; // @see: --header-height
|
||||||
delete window.pendingLinkedCommentScroll;
|
delete window.pendingLinkedCommentScroll;
|
||||||
window.scrollTo({
|
|
||||||
top: node.getBoundingClientRect().top + window.scrollY - ROUGH_HEADER_HEIGHT,
|
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,
|
left: 0,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}
|
||||||
|
},
|
||||||
|
[isMobile]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
@ -313,6 +323,7 @@ function Comment(props: Props) {
|
||||||
charCount={charCount}
|
charCount={charCount}
|
||||||
onChange={handleEditMessageChanged}
|
onChange={handleEditMessageChanged}
|
||||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
<div className="section__actions section__actions--no-margin">
|
<div className="section__actions section__actions--no-margin">
|
||||||
<Button
|
<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 PAGES from 'constants/pages';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CommentSelectors from './comment-selectors';
|
||||||
import EmoteSelector from './emote-selector';
|
|
||||||
import Empty from 'component/common/empty';
|
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 React from 'react';
|
||||||
import SelectChannel from 'component/selectChannel';
|
|
||||||
import StickerSelector from './sticker-selector';
|
|
||||||
import type { ElementRef } from 'react';
|
import type { ElementRef } from 'react';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
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';
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
const stripeEnvironment = getStripeEnvironment();
|
const stripeEnvironment = getStripeEnvironment();
|
||||||
|
@ -59,6 +53,7 @@ type Props = {
|
||||||
supportDisabled: boolean,
|
supportDisabled: boolean,
|
||||||
uri: string,
|
uri: string,
|
||||||
disableInput?: boolean,
|
disableInput?: boolean,
|
||||||
|
onSlimInputClick?: () => void,
|
||||||
createComment: (string, string, string, ?string, ?string, ?string, boolean) => Promise<any>,
|
createComment: (string, string, string, ?string, ?string, ?string, boolean) => Promise<any>,
|
||||||
doFetchCreatorSettings: (channelId: string) => Promise<any>,
|
doFetchCreatorSettings: (channelId: string) => Promise<any>,
|
||||||
doToast: ({ message: string }) => void,
|
doToast: ({ message: string }) => void,
|
||||||
|
@ -90,6 +85,7 @@ export function CommentCreate(props: Props) {
|
||||||
supportDisabled,
|
supportDisabled,
|
||||||
uri,
|
uri,
|
||||||
disableInput,
|
disableInput,
|
||||||
|
onSlimInputClick,
|
||||||
createComment,
|
createComment,
|
||||||
doFetchCreatorSettings,
|
doFetchCreatorSettings,
|
||||||
doToast,
|
doToast,
|
||||||
|
@ -115,7 +111,7 @@ export function CommentCreate(props: Props) {
|
||||||
const [isSubmitting, setSubmitting] = React.useState(false);
|
const [isSubmitting, setSubmitting] = React.useState(false);
|
||||||
const [commentFailure, setCommentFailure] = React.useState(false);
|
const [commentFailure, setCommentFailure] = React.useState(false);
|
||||||
const [successTip, setSuccessTip] = React.useState({ txid: undefined, tipAmount: undefined });
|
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 [isReviewingSupportComment, setReviewingSupportComment] = React.useState();
|
||||||
const [isReviewingStickerComment, setReviewingStickerComment] = React.useState();
|
const [isReviewingStickerComment, setReviewingStickerComment] = React.useState();
|
||||||
const [selectedSticker, setSelectedSticker] = React.useState();
|
const [selectedSticker, setSelectedSticker] = React.useState();
|
||||||
|
@ -123,18 +119,19 @@ export function CommentCreate(props: Props) {
|
||||||
const [convertedAmount, setConvertedAmount] = React.useState();
|
const [convertedAmount, setConvertedAmount] = React.useState();
|
||||||
const [commentValue, setCommentValue] = React.useState('');
|
const [commentValue, setCommentValue] = React.useState('');
|
||||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||||
const [stickerSelector, setStickerSelector] = React.useState();
|
|
||||||
const [activeTab, setActiveTab] = React.useState();
|
const [activeTab, setActiveTab] = React.useState();
|
||||||
const [tipError, setTipError] = React.useState();
|
const [tipError, setTipError] = React.useState();
|
||||||
const [deletedComment, setDeletedComment] = React.useState(false);
|
const [deletedComment, setDeletedComment] = React.useState(false);
|
||||||
const [showEmotes, setShowEmotes] = React.useState(false);
|
const [showSelectors, setShowSelectors] = React.useState(false);
|
||||||
const [disableReviewButton, setDisableReviewButton] = React.useState();
|
const [disableReviewButton, setDisableReviewButton] = React.useState();
|
||||||
const [exchangeRate, setExchangeRate] = React.useState();
|
const [exchangeRate, setExchangeRate] = React.useState();
|
||||||
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(undefined);
|
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(undefined);
|
||||||
|
const [tipModalOpen, setTipModalOpen] = React.useState(undefined);
|
||||||
|
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const charCount = commentValue ? commentValue.length : 0;
|
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 channelId = getChannelIdFromClaim(claim);
|
||||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||||
const minSuper = (channelSettings && channelSettings.min_tip_amount_super_chat) || 0;
|
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 minAmount = minTip || minSuper || 0;
|
||||||
const minAmountMet = minAmount === 0 || tipAmount >= minAmount;
|
const minAmountMet = minAmount === 0 || tipAmount >= minAmount;
|
||||||
const stickerPrice = selectedSticker && selectedSticker.price;
|
const stickerPrice = selectedSticker && selectedSticker.price;
|
||||||
|
const tipSelectorError = tipError || disableReviewButton;
|
||||||
|
|
||||||
const minAmountRef = React.useRef(minAmount);
|
const minAmountRef = React.useRef(minAmount);
|
||||||
minAmountRef.current = minAmount;
|
minAmountRef.current = minAmount;
|
||||||
|
@ -150,16 +148,52 @@ export function CommentCreate(props: Props) {
|
||||||
// Functions
|
// 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) {
|
function handleSelectSticker(sticker: any) {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
setSelectedSticker(sticker);
|
setSelectedSticker(sticker);
|
||||||
setReviewingStickerComment(true);
|
setReviewingStickerComment(true);
|
||||||
setTipAmount(sticker.price || 0);
|
setTipAmount(sticker.price || 0);
|
||||||
setStickerSelector(false);
|
setShowSelectors(false);
|
||||||
|
|
||||||
if (sticker.price && sticker.price > 0) {
|
if (sticker.price && sticker.price > 0) {
|
||||||
setActiveTab(canReceiveFiatTip ? TAB_FIAT : TAB_LBC);
|
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('');
|
setCommentValue('');
|
||||||
setReviewingSupportComment(false);
|
setReviewingSupportComment(false);
|
||||||
setIsSupportComment(false);
|
setTipSelector(false);
|
||||||
setCommentFailure(false);
|
setCommentFailure(false);
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
});
|
});
|
||||||
|
@ -266,7 +300,6 @@ export function CommentCreate(props: Props) {
|
||||||
function handleCreateComment(txid, payment_intent_id, environment) {
|
function handleCreateComment(txid, payment_intent_id, environment) {
|
||||||
if (isSubmitting || disableInput) return;
|
if (isSubmitting || disableInput) return;
|
||||||
|
|
||||||
setShowEmotes(false);
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
|
||||||
const stickerValue = selectedSticker && buildValidSticker(selectedSticker.name);
|
const stickerValue = selectedSticker && buildValidSticker(selectedSticker.name);
|
||||||
|
@ -279,7 +312,7 @@ export function CommentCreate(props: Props) {
|
||||||
if (res && res.signature) {
|
if (res && res.signature) {
|
||||||
if (!stickerValue) setCommentValue('');
|
if (!stickerValue) setCommentValue('');
|
||||||
setReviewingSupportComment(false);
|
setReviewingSupportComment(false);
|
||||||
setIsSupportComment(false);
|
setTipSelector(false);
|
||||||
setCommentFailure(false);
|
setCommentFailure(false);
|
||||||
|
|
||||||
if (onDoneReplying) {
|
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
|
// 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,
|
// 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)
|
// it defaults to LBC tip instead of USD)
|
||||||
React.useEffect(() => {
|
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 channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||||
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||||
|
@ -350,7 +396,7 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.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
|
// Handle keyboard shortcut comment creation
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -384,14 +430,6 @@ export function CommentCreate(props: Props) {
|
||||||
// Render
|
// 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) {
|
if (channelSettings && !channelSettings.comments_enabled) {
|
||||||
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
|
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
|
||||||
}
|
}
|
||||||
|
@ -400,6 +438,7 @@ export function CommentCreate(props: Props) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
className="comment-create__auth"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (embed) {
|
if (embed) {
|
||||||
window.open(`https://odysee.com/$/${PAGES.AUTH}?redirect=/$/${PAGES.LIVESTREAM}`);
|
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 (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={() => {}}
|
onSubmit={() => {}}
|
||||||
|
@ -431,50 +482,29 @@ export function CommentCreate(props: Props) {
|
||||||
'commentCreate--bottom': bottom,
|
'commentCreate--bottom': bottom,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Input Box/Preview Box */}
|
{selectedSticker ? (
|
||||||
{stickerSelector ? (
|
activeChannelClaim && (
|
||||||
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
<StickerReviewBox
|
||||||
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
activeChannelUrl={activeChannelClaim.canonical_url}
|
||||||
<div className="commentCreate__stickerPreview">
|
src={selectedSticker.url}
|
||||||
<div className="commentCreate__stickerPreviewInfo">
|
price={selectedSticker.price || 0}
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
exchangeRate={exchangeRate}
|
||||||
<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 ? (
|
||||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
activeChannelClaim &&
|
||||||
<div className="commentCreate__supportCommentPreview">
|
activeTab && (
|
||||||
<CreditAmount
|
<TipReviewBox
|
||||||
amount={tipAmount}
|
activeChannelUrl={activeChannelClaim.canonical_url}
|
||||||
className="commentCreate__supportCommentPreviewAmount"
|
tipAmount={tipAmount}
|
||||||
isFiat={activeTab === TAB_FIAT}
|
activeTab={activeTab}
|
||||||
size={activeTab === TAB_LBC ? 18 : 2}
|
message={commentValue}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
|
||||||
<div className="commentCreate__supportCommentBody">
|
|
||||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
|
||||||
<div>{commentValue}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{showEmotes && (
|
{!isMobile && showSelectors && (
|
||||||
<EmoteSelector
|
<CommentSelectors {...commentSelectorsProps} closeSelector={() => setShowSelectors(false)} />
|
||||||
commentValue={commentValue}
|
|
||||||
setCommentValue={setCommentValue}
|
|
||||||
closeSelector={() => setShowEmotes(false)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
|
@ -483,30 +513,31 @@ export function CommentCreate(props: Props) {
|
||||||
className={isReply ? 'create__reply' : 'create__comment'}
|
className={isReply ? 'create__reply' : 'create__comment'}
|
||||||
disabled={isFetchingChannels || disableInput}
|
disabled={isFetchingChannels || disableInput}
|
||||||
isLivestream={isLivestream}
|
isLivestream={isLivestream}
|
||||||
label={
|
label={<FormChannelSelector isReply={Boolean(isReply)} isLivestream={Boolean(isLivestream)} />}
|
||||||
<div className="comment-create__label-wrapper">
|
|
||||||
<span className="comment-create__label">
|
|
||||||
{(isReply ? __('Replying as') : isLivestream ? __('Chat as') : __('Comment as')) + ' '}
|
|
||||||
</span>
|
|
||||||
<SelectChannel tiny />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
name={isReply ? 'create__reply' : 'create__comment'}
|
name={isReply ? 'create__reply' : 'create__comment'}
|
||||||
onChange={(e) => setCommentValue(SIMPLE_SITE || !advancedEditor || isReply ? e.target.value : e)}
|
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, {
|
doOpenModal(MODALS.SEND_TIP, {
|
||||||
uri,
|
uri,
|
||||||
isTipOnly: true,
|
isTipOnly: true,
|
||||||
hasSelectedTab: isLBC ? TAB_LBC : TAB_FIAT,
|
hasSelectedTab: isLBC ? TAB_LBC : TAB_FIAT,
|
||||||
|
customText: __('Preview Comment Tip'),
|
||||||
setAmount: (amount) => {
|
setAmount: (amount) => {
|
||||||
setTipAmount(amount);
|
setTipAmount(amount);
|
||||||
setReviewingSupportComment(true);
|
setReviewingSupportComment(true);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}}
|
||||||
handleSubmit={handleCreateComment}
|
handleSubmit={handleCreateComment}
|
||||||
noEmojis={isMobile}
|
slimInput={isMobile}
|
||||||
|
onSlimInputClick={onSlimInputClick}
|
||||||
|
commentSelectorsProps={commentSelectorsProps}
|
||||||
|
submitButtonRef={buttonRef}
|
||||||
|
setShowSelectors={setShowSelectors}
|
||||||
|
showSelectors={showSelectors}
|
||||||
|
tipModalOpen={tipModalOpen}
|
||||||
placeholder={__('Say something about this...')}
|
placeholder={__('Say something about this...')}
|
||||||
quickActionHandler={!SIMPLE_SITE ? () => setAdvancedEditor(!advancedEditor) : undefined}
|
quickActionHandler={!SIMPLE_SITE ? () => setAdvancedEditor(!advancedEditor) : undefined}
|
||||||
quickActionLabel={
|
quickActionLabel={
|
||||||
|
@ -521,7 +552,7 @@ export function CommentCreate(props: Props) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isMobile && (isSupportComment || (isReviewingStickerComment && stickerPrice)) && (
|
{(!isMobile || isReviewingStickerComment) && (tipSelectorOpen || (isReviewingStickerComment && stickerPrice)) && (
|
||||||
<WalletTipAmountSelector
|
<WalletTipAmountSelector
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
amount={tipAmount}
|
amount={tipAmount}
|
||||||
|
@ -538,14 +569,13 @@ export function CommentCreate(props: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bottom Action Buttons */}
|
{(!isMobile || !isLivestream || isReviewingStickerComment || isReviewingSupportComment) && (
|
||||||
{!isMobile && (
|
<div className="section__actions">
|
||||||
<div className="section__actions section__actions--no-margin">
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
{isReviewingSupportComment ? (
|
{isReviewingSupportComment ? (
|
||||||
<Button
|
<Button
|
||||||
|
{...submitButtonProps}
|
||||||
autoFocus
|
autoFocus
|
||||||
button="primary"
|
|
||||||
disabled={disabled || !minAmountMet}
|
disabled={disabled || !minAmountMet}
|
||||||
label={
|
label={
|
||||||
isSubmitting
|
isSubmitting
|
||||||
|
@ -556,40 +586,21 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
onClick={handleSupportComment}
|
onClick={handleSupportComment}
|
||||||
/>
|
/>
|
||||||
) : isReviewingStickerComment && selectedSticker ? (
|
) : tipSelectorOpen ? (
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
{...submitButtonProps}
|
||||||
label={__('Send')}
|
disabled={disabled || tipSelectorError || !minAmountMet}
|
||||||
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"
|
|
||||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||||
label={__('Review')}
|
label={__('Review')}
|
||||||
onClick={() => setReviewingSupportComment(true)}
|
onClick={() => setReviewingSupportComment(true)}
|
||||||
requiresAuth
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
(!isMobile || selectedSticker) &&
|
||||||
(!minTip || claimIsMine) && (
|
(!minTip || claimIsMine) && (
|
||||||
<Button
|
<Button
|
||||||
|
{...submitButtonProps}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
button="primary"
|
disabled={disabled}
|
||||||
disabled={disabled || stickerSelector}
|
|
||||||
type="submit"
|
|
||||||
label={
|
label={
|
||||||
isReply
|
isReply
|
||||||
? isSubmitting
|
? isSubmitting
|
||||||
|
@ -599,135 +610,36 @@ export function CommentCreate(props: Props) {
|
||||||
? __('Commenting...')
|
? __('Commenting...')
|
||||||
: __('Comment --[button to submit something]--')
|
: __('Comment --[button to submit something]--')
|
||||||
}
|
}
|
||||||
requiresAuth
|
onClick={() => (selectedSticker ? handleSubmitSticker() : handleCreateComment())}
|
||||||
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/** Stickers/Support Buttons **/}
|
{!isMobile && (
|
||||||
{!supportDisabled && !stickerSelector && (
|
|
||||||
<>
|
<>
|
||||||
{getActionButton(
|
<StickerActionButton {...actionButtonProps} icon={ICONS.STICKER} onClick={handleStickerComment} />
|
||||||
__('Stickers'),
|
|
||||||
isReviewingStickerComment ? __('Different Sticker') : undefined,
|
|
||||||
ICONS.STICKER,
|
|
||||||
() => {
|
|
||||||
if (isReviewingStickerComment) setReviewingStickerComment(false);
|
|
||||||
setIsSupportComment(false);
|
|
||||||
setStickerSelector(true);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!claimIsMine && (
|
{!supportDisabled && (
|
||||||
<>
|
<>
|
||||||
{(!isSupportComment || activeTab !== TAB_LBC) &&
|
<TipActionButton {...tipButtonProps} name={__('Credits')} icon={ICONS.LBC} tab={TAB_LBC} />
|
||||||
getActionButton(
|
|
||||||
__('Credits'),
|
|
||||||
isSupportComment ? __('Switch to Credits') : undefined,
|
|
||||||
ICONS.LBC,
|
|
||||||
() => {
|
|
||||||
setActiveTab(TAB_LBC);
|
|
||||||
|
|
||||||
if (isMobile) {
|
{stripeEnvironment && (
|
||||||
doOpenModal(MODALS.SEND_TIP, {
|
<TipActionButton {...tipButtonProps} name={__('Cash')} icon={ICONS.FINANCE} tab={TAB_FIAT} />
|
||||||
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
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cancel Button */}
|
{tipSelectorOpen || isReviewingSupportComment ? (
|
||||||
{(isSupportComment ||
|
<Button {...cancelButtonProps} disabled={isSubmitting} onClick={handleCancelSupport} />
|
||||||
isReviewingSupportComment ||
|
) : isReviewingStickerComment ? (
|
||||||
stickerSelector ||
|
<Button {...cancelButtonProps} onClick={handleCancelSticker} />
|
||||||
isReviewingStickerComment ||
|
) : (
|
||||||
(isReply && !minTip)) && (
|
onCancelReplying && <Button {...cancelButtonProps} onClick={onCancelReplying} />
|
||||||
<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();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Help Text */}
|
<HelpText deletedComment={deletedComment} minAmount={minAmount} minSuper={minSuper} minTip={minTip} />
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import * as MODALS from 'constants/modal_types';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
|
@ -67,6 +68,8 @@ function CommentMenuList(props: Props) {
|
||||||
handleDismissPin,
|
handleDismissPin,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
location: { pathname, search },
|
location: { pathname, search },
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
|
@ -253,7 +256,7 @@ function CommentMenuList(props: Props) {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPinned && isLiveComment && (
|
{isPinned && isLiveComment && isMobile && (
|
||||||
<MenuItem className="comment__menu-option menu__link" onSelect={handleDismissPin}>
|
<MenuItem className="comment__menu-option menu__link" onSelect={handleDismissPin}>
|
||||||
<Icon aria-hidden icon={ICONS.DISMISS_ALL} />
|
<Icon aria-hidden icon={ICONS.DISMISS_ALL} />
|
||||||
{__('Dismiss Pin')}
|
{__('Dismiss Pin')}
|
||||||
|
|
|
@ -269,37 +269,17 @@ function CommentList(props: Props) {
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
const sortButton = (label, icon, sortOption) => (
|
const actionButtonsProps = { totalComments, sort, changeSort, setPage };
|
||||||
<Button
|
|
||||||
button="alt"
|
|
||||||
label={label}
|
|
||||||
icon={icon}
|
|
||||||
iconSize={18}
|
|
||||||
onClick={() => changeSort(sortOption)}
|
|
||||||
className={classnames(`button-toggle`, {
|
|
||||||
'button-toggle--active': sort === sortOption,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="card--enable-overflow"
|
className="card--enable-overflow"
|
||||||
title={!isMobile && title}
|
title={!isMobile && title}
|
||||||
titleActions={
|
titleActions={<CommentActionButtons {...actionButtonsProps} />}
|
||||||
<>
|
|
||||||
{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)} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
|
{isMobile && <CommentActionButtons {...actionButtonsProps} />}
|
||||||
|
|
||||||
<CommentCreate uri={uri} />
|
<CommentCreate uri={uri} />
|
||||||
|
|
||||||
{channelSettings && channelSettings.comments_enabled && !isFetchingComments && !totalComments && (
|
{channelSettings && channelSettings.comments_enabled && !isFetchingComments && !totalComments && (
|
||||||
|
@ -349,3 +329,55 @@ function CommentList(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CommentList;
|
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 { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||||
import { lazyImport } from 'util/lazyImport';
|
import { lazyImport } from 'util/lazyImport';
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOMServer from 'react-dom/server';
|
import ReactDOMServer from 'react-dom/server';
|
||||||
import SimpleMDE from 'react-simplemde-editor';
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
import type { ElementRef, Node } from 'react';
|
import type { ElementRef, Node } from 'react';
|
||||||
|
import Drawer from '@mui/material/Drawer';
|
||||||
|
import CommentSelectors from 'component/commentCreate/comment-selectors';
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const TextareaWithSuggestions = lazyImport(() => import('component/textareaWithSuggestions' /* webpackChunkName: "suggestions" */));
|
const TextareaWithSuggestions = lazyImport(() => import('component/textareaWithSuggestions' /* webpackChunkName: "suggestions" */));
|
||||||
|
@ -33,7 +34,6 @@ type Props = {
|
||||||
max?: number,
|
max?: number,
|
||||||
min?: number,
|
min?: number,
|
||||||
name: string,
|
name: string,
|
||||||
noEmojis?: boolean,
|
|
||||||
placeholder?: string | number,
|
placeholder?: string | number,
|
||||||
postfix?: string,
|
postfix?: string,
|
||||||
prefix?: string,
|
prefix?: string,
|
||||||
|
@ -44,15 +44,25 @@ type Props = {
|
||||||
textAreaMaxLength?: number,
|
textAreaMaxLength?: number,
|
||||||
type?: string,
|
type?: string,
|
||||||
value?: string | number,
|
value?: string | number,
|
||||||
|
slimInput?: boolean,
|
||||||
|
commentSelectorsProps?: any,
|
||||||
|
showSelectors?: boolean,
|
||||||
|
submitButtonRef?: any,
|
||||||
|
tipModalOpen?: boolean,
|
||||||
|
onSlimInputClick?: () => void,
|
||||||
onChange?: (any) => any,
|
onChange?: (any) => any,
|
||||||
openEmoteMenu?: () => void,
|
setShowSelectors?: (boolean) => void,
|
||||||
quickActionHandler?: (any) => any,
|
quickActionHandler?: (any) => any,
|
||||||
render?: () => React$Node,
|
render?: () => React$Node,
|
||||||
handleTip?: (isLBC: boolean) => any,
|
handleTip?: (isLBC: boolean) => any,
|
||||||
handleSubmit?: () => 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 };
|
static defaultProps = { labelOnLeft: false, blockWrap: true };
|
||||||
|
|
||||||
input: { current: ElementRef<any> };
|
input: { current: ElementRef<any> };
|
||||||
|
@ -60,6 +70,10 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.input = React.createRef();
|
this.input = React.createRef();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
drawerOpen: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -69,6 +83,14 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
if (input && autoFocus) input.focus();
|
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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
uri,
|
uri,
|
||||||
|
@ -85,15 +107,20 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
label,
|
label,
|
||||||
labelOnLeft,
|
labelOnLeft,
|
||||||
name,
|
name,
|
||||||
noEmojis,
|
|
||||||
postfix,
|
postfix,
|
||||||
prefix,
|
prefix,
|
||||||
quickActionLabel,
|
quickActionLabel,
|
||||||
stretch,
|
stretch,
|
||||||
textAreaMaxLength,
|
textAreaMaxLength,
|
||||||
type,
|
type,
|
||||||
openEmoteMenu,
|
slimInput,
|
||||||
|
commentSelectorsProps,
|
||||||
|
showSelectors,
|
||||||
|
submitButtonRef,
|
||||||
|
tipModalOpen,
|
||||||
|
onSlimInputClick,
|
||||||
quickActionHandler,
|
quickActionHandler,
|
||||||
|
setShowSelectors,
|
||||||
render,
|
render,
|
||||||
handleTip,
|
handleTip,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
@ -240,9 +267,20 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
return (
|
return (
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
{(label || quickAction) && (
|
<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">
|
<div className="form-field__two-column">
|
||||||
<label htmlFor={name}>{label}</label>
|
<label htmlFor={name}>{label}</label>
|
||||||
|
{quickAction}
|
||||||
{countInfo}
|
{countInfo}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -264,26 +302,23 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||||
inputRef={this.input}
|
inputRef={this.input}
|
||||||
isLivestream={isLivestream}
|
isLivestream={isLivestream}
|
||||||
handleEmojis={openEmoteMenu}
|
toggleSelectors={setShowSelectors ? () => setShowSelectors(!showSelectors) : undefined}
|
||||||
handleTip={handleTip}
|
handleTip={handleTip}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={() => {
|
||||||
|
if (handleSubmit) handleSubmit();
|
||||||
|
if (slimInput) this.setState({ drawerOpen: false });
|
||||||
|
}}
|
||||||
|
claimIsMine={commentSelectorsProps && commentSelectorsProps.claimIsMine}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
handlePreventClick={
|
||||||
|
!this.state.drawerOpen ? () => this.setState({ drawerOpen: true }) : undefined
|
||||||
|
}
|
||||||
|
autoFocus={this.state.drawerOpen}
|
||||||
|
submitButtonRef={submitButtonRef}
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
)}
|
)}
|
||||||
|
</TextareaWrapper>
|
||||||
{!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>
|
|
||||||
)}
|
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
@ -321,3 +356,65 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FormField;
|
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
|
// $FlowFixMe
|
||||||
import { grey } from '@mui/material/colors';
|
import { grey } from '@mui/material/colors';
|
||||||
|
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
@ -42,6 +41,7 @@ type Props = {
|
||||||
superchatsHidden?: boolean,
|
superchatsHidden?: boolean,
|
||||||
customViewMode?: string,
|
customViewMode?: string,
|
||||||
theme: string,
|
theme: string,
|
||||||
|
setCustomViewMode?: (any) => void,
|
||||||
doCommentList: (string, string, number, number) => void,
|
doCommentList: (string, string, number, number) => void,
|
||||||
doResolveUris: (Array<string>, boolean) => void,
|
doResolveUris: (Array<string>, boolean) => void,
|
||||||
doSuperChatList: (string) => void,
|
doSuperChatList: (string) => void,
|
||||||
|
@ -60,6 +60,7 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
superchatsHidden,
|
superchatsHidden,
|
||||||
customViewMode,
|
customViewMode,
|
||||||
theme,
|
theme,
|
||||||
|
setCustomViewMode,
|
||||||
doCommentList,
|
doCommentList,
|
||||||
doResolveUris,
|
doResolveUris,
|
||||||
doSuperChatList,
|
doSuperChatList,
|
||||||
|
@ -67,11 +68,18 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
|
|
||||||
const isMobile = useIsMobile() && !isPopoutWindow;
|
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(() => {
|
const restoreScrollPos = React.useCallback(() => {
|
||||||
if (discussionElement) discussionElement.scrollTop = 0;
|
if (discussionElement) discussionElement.scrollTop = !isMobile ? 0 : discussionElement.scrollHeight;
|
||||||
}, [discussionElement]);
|
}, [discussionElement, isMobile]);
|
||||||
|
|
||||||
const commentsRef = React.createRef();
|
const commentsRef = React.createRef();
|
||||||
|
|
||||||
|
@ -79,12 +87,12 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
const [scrollPos, setScrollPos] = React.useState(0);
|
const [scrollPos, setScrollPos] = React.useState(0);
|
||||||
const [showPinned, setShowPinned] = React.useState(true);
|
const [showPinned, setShowPinned] = React.useState(true);
|
||||||
const [resolvingSuperChats, setResolvingSuperChats] = React.useState(false);
|
const [resolvingSuperChats, setResolvingSuperChats] = React.useState(false);
|
||||||
const [mention, setMention] = React.useState();
|
|
||||||
const [openedPopoutWindow, setPopoutWindow] = React.useState(undefined);
|
const [openedPopoutWindow, setPopoutWindow] = React.useState(undefined);
|
||||||
const [chatHidden, setChatHidden] = React.useState(false);
|
const [chatHidden, setChatHidden] = React.useState(false);
|
||||||
|
const [didInitialScroll, setDidInitialScroll] = React.useState(false);
|
||||||
|
const [bottomScrollTop, setBottomScrollTop] = React.useState(0);
|
||||||
|
|
||||||
const quickMention =
|
const recentScrollPos = isMobile ? (bottomScrollTop > 0 && minOffset ? bottomScrollTop - minOffset : 0) : 0;
|
||||||
mention && formatLbryUrlForWeb(mention).substring(1, formatLbryUrlForWeb(mention).indexOf(':') + 3);
|
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const commentsToDisplay = viewMode === VIEW_MODES.CHAT ? commentsByChronologicalOrder : superChatsByAmount;
|
const commentsToDisplay = viewMode === VIEW_MODES.CHAT ? commentsByChronologicalOrder : superChatsByAmount;
|
||||||
const commentsLength = commentsToDisplay && commentsToDisplay.length;
|
const commentsLength = commentsToDisplay && commentsToDisplay.length;
|
||||||
|
@ -100,6 +108,7 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setViewMode(VIEW_MODES.SUPERCHAT);
|
setViewMode(VIEW_MODES.SUPERCHAT);
|
||||||
|
if (setCustomViewMode) setCustomViewMode(VIEW_MODES.SUPERCHAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -115,12 +124,26 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
}
|
}
|
||||||
}, [claimId, uri, doCommentList, doSuperChatList]);
|
}, [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)
|
// Register scroll handler (TODO: Should throttle/debounce)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (discussionElement) {
|
if (discussionElement) {
|
||||||
const scrollTop = discussionElement.scrollTop;
|
const scrollTop = discussionElement.scrollTop;
|
||||||
if (scrollTop !== scrollPos) {
|
if (!scrollPos || scrollTop !== scrollPos) {
|
||||||
setScrollPos(scrollTop);
|
setScrollPos(scrollTop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,12 +159,13 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (discussionElement && commentsLength > 0) {
|
if (discussionElement && commentsLength > 0) {
|
||||||
// Only update comment scroll if the user hasn't scrolled up to view old comments
|
// 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: not scrolled (Usually, there'll be a few pixels beyond 0).
|
||||||
// -ve scrollPos: user scrolled.
|
// -ve scrollPos: user scrolled.
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
// Use a timer here to ensure we reset after the new comment has been rendered.
|
// 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);
|
}, COMMENT_SCROLL_TIMEOUT);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
@ -283,7 +307,6 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
comment={pinnedComment}
|
comment={pinnedComment}
|
||||||
key={pinnedComment.comment_id}
|
key={pinnedComment.comment_id}
|
||||||
uri={uri}
|
uri={uri}
|
||||||
pushMention={setMention}
|
|
||||||
handleDismissPin={() => setShowPinned(false)}
|
handleDismissPin={() => setShowPinned(false)}
|
||||||
isMobile
|
isMobile
|
||||||
/>
|
/>
|
||||||
|
@ -292,12 +315,7 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
) : (
|
) : (
|
||||||
showPinned && (
|
showPinned && (
|
||||||
<div className="livestream-pinned__wrapper">
|
<div className="livestream-pinned__wrapper">
|
||||||
<LivestreamComment
|
<LivestreamComment comment={pinnedComment} key={pinnedComment.comment_id} uri={uri} />
|
||||||
comment={pinnedComment}
|
|
||||||
key={pinnedComment.comment_id}
|
|
||||||
uri={uri}
|
|
||||||
pushMention={setMention}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
title={__('Dismiss pinned comment')}
|
title={__('Dismiss pinned comment')}
|
||||||
|
@ -316,23 +334,18 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<LivestreamComments
|
<LivestreamComments uri={uri} commentsToDisplay={commentsToDisplay} isMobile={isMobile} />
|
||||||
uri={uri}
|
|
||||||
commentsToDisplay={commentsToDisplay}
|
|
||||||
pushMention={setMention}
|
|
||||||
isMobile={isMobile}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{scrollPos < 0 && (
|
{scrollPos && (!isMobile || recentScrollPos) && scrollPos < recentScrollPos && viewMode === VIEW_MODES.CHAT ? (
|
||||||
<Button
|
<Button
|
||||||
button="secondary"
|
button="secondary"
|
||||||
className="livestreamComments__scrollToRecent"
|
className="livestream-comments__scroll-to-recent"
|
||||||
label={viewMode === VIEW_MODES.CHAT ? __('Recent Comments') : __('Recent Tips')}
|
label={viewMode === VIEW_MODES.CHAT ? __('Recent Comments') : __('Recent Tips')}
|
||||||
onClick={restoreScrollPos}
|
onClick={restoreScrollPos}
|
||||||
iconRight={ICONS.DOWN}
|
iconRight={ICONS.DOWN}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
<div className="livestream__comment-create">
|
<div className="livestream__comment-create">
|
||||||
<CommentCreate
|
<CommentCreate
|
||||||
|
@ -341,8 +354,13 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
embed={embed}
|
embed={embed}
|
||||||
uri={uri}
|
uri={uri}
|
||||||
onDoneReplying={restoreScrollPos}
|
onDoneReplying={restoreScrollPos}
|
||||||
pushedMention={quickMention}
|
onSlimInputClick={
|
||||||
setPushedMention={setMention}
|
scrollPos &&
|
||||||
|
recentScrollPos &&
|
||||||
|
scrollPos >= recentScrollPos &&
|
||||||
|
viewMode === VIEW_MODES.CHAT &&
|
||||||
|
restoreScrollPos
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -46,6 +46,8 @@ export default function LivestreamComment(props: Props) {
|
||||||
timestamp,
|
timestamp,
|
||||||
} = comment;
|
} = comment;
|
||||||
|
|
||||||
|
const commentRef = React.useRef();
|
||||||
|
|
||||||
const [hasUserMention, setUserMention] = React.useState(false);
|
const [hasUserMention, setUserMention] = React.useState(false);
|
||||||
|
|
||||||
const isStreamer = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
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 isSticker = Boolean(stickerUrlFromMessage);
|
||||||
const timePosted = timestamp * 1000;
|
const timePosted = timestamp * 1000;
|
||||||
const commentIsMine = comment.channel_id && isMyComment(comment.channel_id);
|
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
|
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
|
||||||
function isMyComment(channelId: string) {
|
function isMyComment(channelId: string) {
|
||||||
return myChannelIds ? myChannelIds.includes(channelId) : false;
|
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 (
|
return (
|
||||||
<li
|
<li
|
||||||
className={classnames('livestream__comment', {
|
className={classnames('livestream__comment', {
|
||||||
|
@ -68,6 +82,7 @@ export default function LivestreamComment(props: Props) {
|
||||||
'livestream__comment--mentioned': hasUserMention,
|
'livestream__comment--mentioned': hasUserMention,
|
||||||
'livestream__comment--mobile': isMobile,
|
'livestream__comment--mobile': isMobile,
|
||||||
})}
|
})}
|
||||||
|
ref={commentRef}
|
||||||
>
|
>
|
||||||
{supportAmount > 0 && (
|
{supportAmount > 0 && (
|
||||||
<div className="livestreamComment__superchatBanner">
|
<div className="livestreamComment__superchatBanner">
|
||||||
|
|
|
@ -44,8 +44,8 @@ export default function LivestreamComments(props: Props) {
|
||||||
|
|
||||||
/* top to bottom comment display */
|
/* top to bottom comment display */
|
||||||
if (!fetchingComments && commentsToDisplay && commentsToDisplay.length > 0) {
|
if (!fetchingComments && commentsToDisplay && commentsToDisplay.length > 0) {
|
||||||
return (
|
return isMobile ? (
|
||||||
<div className="livestream__comments">
|
<div className="livestream__comments--mobile">
|
||||||
{commentsToDisplay
|
{commentsToDisplay
|
||||||
.slice(0)
|
.slice(0)
|
||||||
.reverse()
|
.reverse()
|
||||||
|
@ -55,10 +55,16 @@ export default function LivestreamComments(props: Props) {
|
||||||
key={comment.comment_id}
|
key={comment.comment_id}
|
||||||
uri={uri}
|
uri={uri}
|
||||||
forceUpdate={forceUpdate}
|
forceUpdate={forceUpdate}
|
||||||
isMobile={isMobile}
|
isMobile
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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
|
hideHeader
|
||||||
superchatsHidden={superchatsHidden}
|
superchatsHidden={superchatsHidden}
|
||||||
customViewMode={chatViewMode}
|
customViewMode={chatViewMode}
|
||||||
|
setCustomViewMode={(mode) => setChatViewMode(mode)}
|
||||||
/>
|
/>
|
||||||
</SwipeableDrawer>
|
</SwipeableDrawer>
|
||||||
|
|
||||||
|
@ -155,7 +156,7 @@ const ChatModeSelector = (chatSelectorProps: any) => {
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton>
|
<MenuButton>
|
||||||
<span className="swipeable-drawer__title-menu">
|
<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} />
|
<Icon icon={ICONS.DOWN} />
|
||||||
</span>
|
</span>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
|
|
|
@ -11,13 +11,27 @@ type Props = {
|
||||||
messageValue: string,
|
messageValue: string,
|
||||||
inputDefaultProps: any,
|
inputDefaultProps: any,
|
||||||
inputRef: any,
|
inputRef: any,
|
||||||
handleEmojis: () => any,
|
submitButtonRef?: any,
|
||||||
|
claimIsMine?: boolean,
|
||||||
|
toggleSelectors: () => any,
|
||||||
handleTip: (isLBC: boolean) => void,
|
handleTip: (isLBC: boolean) => void,
|
||||||
handleSubmit: () => any,
|
handleSubmit: () => any,
|
||||||
|
handlePreventClick?: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
const TextareaSuggestionsInput = (props: Props) => {
|
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();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
@ -26,15 +40,42 @@ const TextareaSuggestionsInput = (props: Props) => {
|
||||||
const autocompleteProps = { InputProps, disabled, fullWidth, id, inputProps };
|
const autocompleteProps = { InputProps, disabled, fullWidth, id, inputProps };
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
InputProps.startAdornment = <Button icon={ICONS.STICKER} onClick={handleEmojis} />;
|
InputProps.startAdornment = (
|
||||||
|
<Button
|
||||||
|
icon={ICONS.STICKER}
|
||||||
|
onClick={() => {
|
||||||
|
if (handlePreventClick) handlePreventClick();
|
||||||
|
toggleSelectors();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
InputProps.endAdornment = (
|
InputProps.endAdornment = (
|
||||||
<>
|
<>
|
||||||
<Button icon={ICONS.LBC} onClick={() => handleTip(true)} />
|
{!claimIsMine && (
|
||||||
<Button icon={ICONS.FINANCE} onClick={() => handleTip(false)} />
|
<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>
|
<div>
|
||||||
<Button button="primary" icon={ICONS.SUBMIT} iconColor="red" onClick={() => handleSubmit()} />
|
<Button
|
||||||
|
ref={submitButtonRef}
|
||||||
|
button="primary"
|
||||||
|
icon={ICONS.SUBMIT}
|
||||||
|
iconColor="red"
|
||||||
|
onClick={() => handleSubmit()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Zoom>
|
</Zoom>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -56,14 +56,18 @@ type Props = {
|
||||||
type?: string,
|
type?: string,
|
||||||
uri?: string,
|
uri?: string,
|
||||||
value: any,
|
value: any,
|
||||||
|
autoFocus?: boolean,
|
||||||
|
submitButtonRef?: any,
|
||||||
|
claimIsMine?: boolean,
|
||||||
doResolveUris: (uris: Array<string>, cache: boolean) => void,
|
doResolveUris: (uris: Array<string>, cache: boolean) => void,
|
||||||
doSetMentionSearchResults: (query: string, uris: Array<string>) => void,
|
doSetMentionSearchResults: (query: string, uris: Array<string>) => void,
|
||||||
onBlur: (any) => any,
|
onBlur: (any) => any,
|
||||||
onChange: (any) => any,
|
onChange: (any) => any,
|
||||||
onFocus: (any) => any,
|
onFocus: (any) => any,
|
||||||
handleEmojis: () => any,
|
toggleSelectors: () => any,
|
||||||
handleTip: (isLBC: boolean) => any,
|
handleTip: (isLBC: boolean) => any,
|
||||||
handleSubmit: () => any,
|
handleSubmit: () => any,
|
||||||
|
handlePreventClick?: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TextareaWithSuggestions(props: Props) {
|
export default function TextareaWithSuggestions(props: Props) {
|
||||||
|
@ -85,14 +89,18 @@ export default function TextareaWithSuggestions(props: Props) {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
type,
|
type,
|
||||||
value: messageValue,
|
value: messageValue,
|
||||||
|
autoFocus,
|
||||||
|
submitButtonRef,
|
||||||
|
claimIsMine,
|
||||||
doResolveUris,
|
doResolveUris,
|
||||||
doSetMentionSearchResults,
|
doSetMentionSearchResults,
|
||||||
onBlur,
|
onBlur,
|
||||||
onChange,
|
onChange,
|
||||||
onFocus,
|
onFocus,
|
||||||
handleEmojis,
|
toggleSelectors,
|
||||||
handleTip,
|
handleTip,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
handlePreventClick,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const inputDefaultProps = { className, placeholder, maxLength, type, disabled };
|
const inputDefaultProps = { className, placeholder, maxLength, type, disabled };
|
||||||
|
@ -290,6 +298,13 @@ export default function TextareaWithSuggestions(props: Props) {
|
||||||
/** Effects **/
|
/** Effects **/
|
||||||
/** ------- **/
|
/** ------- **/
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!autoFocus) return;
|
||||||
|
|
||||||
|
const inputElement = inputRef && inputRef.current;
|
||||||
|
if (inputElement) inputElement.focus();
|
||||||
|
}, [autoFocus, inputRef]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isMention) return;
|
if (!isMention) return;
|
||||||
|
|
||||||
|
@ -400,9 +415,12 @@ export default function TextareaWithSuggestions(props: Props) {
|
||||||
messageValue={messageValue}
|
messageValue={messageValue}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
inputDefaultProps={inputDefaultProps}
|
inputDefaultProps={inputDefaultProps}
|
||||||
handleEmojis={handleEmojis}
|
toggleSelectors={toggleSelectors}
|
||||||
handleTip={handleTip}
|
handleTip={handleTip}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
|
handlePreventClick={handlePreventClick}
|
||||||
|
submitButtonRef={submitButtonRef}
|
||||||
|
claimIsMine={claimIsMine}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderOption={(optionProps, option) => (
|
renderOption={(optionProps, option) => (
|
||||||
|
|
|
@ -44,6 +44,7 @@ type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
isTipOnly?: boolean,
|
isTipOnly?: boolean,
|
||||||
hasSelectedTab?: string,
|
hasSelectedTab?: string,
|
||||||
|
customText?: string,
|
||||||
doHideModal: () => void,
|
doHideModal: () => void,
|
||||||
doSendCashTip: (TipParams, boolean, UserParams, string, ?string) => string,
|
doSendCashTip: (TipParams, boolean, UserParams, string, ?string) => string,
|
||||||
doSendTip: (SupportParams, boolean) => void, // function that comes from lbry-redux
|
doSendTip: (SupportParams, boolean) => void, // function that comes from lbry-redux
|
||||||
|
@ -69,6 +70,7 @@ export default function WalletSendTip(props: Props) {
|
||||||
uri,
|
uri,
|
||||||
isTipOnly,
|
isTipOnly,
|
||||||
hasSelectedTab,
|
hasSelectedTab,
|
||||||
|
customText,
|
||||||
doHideModal,
|
doHideModal,
|
||||||
doSendCashTip,
|
doSendCashTip,
|
||||||
doSendTip,
|
doSendTip,
|
||||||
|
@ -328,7 +330,7 @@ export default function WalletSendTip(props: Props) {
|
||||||
button="primary"
|
button="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={fetchingChannels || isPending || tipError || !tipAmount || disableSubmitButton}
|
disabled={fetchingChannels || isPending || tipError || !tipAmount || disableSubmitButton}
|
||||||
label={buildButtonText()}
|
label={customText || buildButtonText()}
|
||||||
/>
|
/>
|
||||||
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
||||||
</div>
|
</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
|
// 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);
|
const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip);
|
||||||
if (setDisableSubmitButton) setDisableSubmitButton(shouldDisableFiatSelectors);
|
|
||||||
|
|
||||||
// setup variables for tip API
|
// setup variables for tip API
|
||||||
const channelClaimId = claim ? (claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id) : undefined;
|
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(() => {
|
React.useEffect(() => {
|
||||||
if (setConvertedAmount && exchangeRate && (!convertedAmount || convertedAmount !== amount * exchangeRate)) {
|
if (setConvertedAmount && exchangeRate && (!convertedAmount || convertedAmount !== amount * exchangeRate)) {
|
||||||
setConvertedAmount(amount * exchangeRate);
|
setConvertedAmount(amount * exchangeRate);
|
||||||
|
|
|
@ -185,7 +185,7 @@ export const TECHNOLOGY = 'Technology';
|
||||||
export const EMOJI = 'Emoji';
|
export const EMOJI = 'Emoji';
|
||||||
export const STICKER = 'Sticker';
|
export const STICKER = 'Sticker';
|
||||||
export const EDUCATION = 'Education';
|
export const EDUCATION = 'Education';
|
||||||
export const POP_CULTURE = 'PopCulture';
|
export const POP_CULTURE = 'Pop Culture';
|
||||||
export const ODYSEE_LOGO = 'OdyseeLogo';
|
export const ODYSEE_LOGO = 'OdyseeLogo';
|
||||||
export const ODYSEE_WHITE_TEXT = 'OdyseeLogoWhiteText';
|
export const ODYSEE_WHITE_TEXT = 'OdyseeLogoWhiteText';
|
||||||
export const ODYSEE_DARK_TEXT = 'OdyseeLogoDarkText';
|
export const ODYSEE_DARK_TEXT = 'OdyseeLogoDarkText';
|
||||||
|
|
|
@ -9,13 +9,14 @@ type Props = {
|
||||||
isSupport: boolean,
|
isSupport: boolean,
|
||||||
isTipOnly?: boolean,
|
isTipOnly?: boolean,
|
||||||
hasSelectedTab?: string,
|
hasSelectedTab?: string,
|
||||||
|
customText?: string,
|
||||||
doHideModal: () => void,
|
doHideModal: () => void,
|
||||||
setAmount?: (number) => void,
|
setAmount?: (number) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
class ModalSendTip extends React.PureComponent<Props> {
|
class ModalSendTip extends React.PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { uri, claimIsMine, isTipOnly, hasSelectedTab, doHideModal, setAmount } = this.props;
|
const { uri, claimIsMine, isTipOnly, hasSelectedTab, customText, doHideModal, setAmount } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onAborted={doHideModal} isOpen type="card">
|
<Modal onAborted={doHideModal} isOpen type="card">
|
||||||
|
@ -25,6 +26,7 @@ class ModalSendTip extends React.PureComponent<Props> {
|
||||||
onCancel={doHideModal}
|
onCancel={doHideModal}
|
||||||
isTipOnly={isTipOnly}
|
isTipOnly={isTipOnly}
|
||||||
hasSelectedTab={hasSelectedTab}
|
hasSelectedTab={hasSelectedTab}
|
||||||
|
customText={customText}
|
||||||
setAmount={setAmount}
|
setAmount={setAmount}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -72,7 +72,8 @@ export default function FilePage(props: Props) {
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
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 cost = costInfo ? costInfo.cost : null;
|
||||||
const hasFileInfo = fileInfo !== undefined;
|
const hasFileInfo = fileInfo !== undefined;
|
||||||
|
@ -186,11 +187,7 @@ export default function FilePage(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const commentsListElement = commentsDisabled ? (
|
const commentsListProps = { uri, linkedCommentId };
|
||||||
<Empty text={__('The creator of this content has disabled comments.')} />
|
|
||||||
) : (
|
|
||||||
<CommentsList uri={uri} linkedCommentId={linkedCommentId} />
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page className="file-page" filePage isMarkdown={isMarkdown}>
|
<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} />}
|
{RENDER_MODES.FLOATING_MODES.includes(renderMode) && <FileTitleSection uri={uri} />}
|
||||||
|
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
{isMobile ? (
|
{commentsDisabled ? (
|
||||||
|
<Empty text={__('The creator of this content has disabled comments.')} />
|
||||||
|
) : isMobile ? (
|
||||||
<>
|
<>
|
||||||
<SwipeableDrawer
|
<SwipeableDrawer
|
||||||
open={Boolean(showComments)}
|
open={Boolean(showComments)}
|
||||||
toggleDrawer={() => setShowComments(!showComments)}
|
toggleDrawer={() => setShowComments(!showComments)}
|
||||||
title={commentsListTitle}
|
title={commentsListTitle}
|
||||||
>
|
>
|
||||||
{commentsListElement}
|
<CommentsList {...commentsListProps} />
|
||||||
</SwipeableDrawer>
|
</SwipeableDrawer>
|
||||||
|
|
||||||
<DrawerExpandButton label={commentsListTitle} toggleDrawer={() => setShowComments(!showComments)} />
|
<DrawerExpandButton label={commentsListTitle} toggleDrawer={() => setShowComments(!showComments)} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
commentsListElement
|
<CommentsList {...commentsListProps} />
|
||||||
)}
|
)}
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -14,6 +14,44 @@
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
margin-bottom: var(--spacing-m);
|
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) {
|
@media (max-width: $breakpoint-small) {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -362,7 +400,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media__subtitle--centered::before {
|
span + .media__subtitle--centered::before {
|
||||||
content: '•';
|
content: '•';
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
|
@ -491,3 +529,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ReactModalPortal {
|
||||||
|
.button--close {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -21,10 +21,16 @@ $thumbnailWidthSmall: 1rem;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
fieldset-section + .section {
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
.commentCreate + .empty__wrap {
|
.empty__wrap {
|
||||||
p {
|
p {
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
text-align: center;
|
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 {
|
.commentCreate--reply {
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -71,6 +87,15 @@ $thumbnailWidthSmall: 1rem;
|
||||||
fieldset-section {
|
fieldset-section {
|
||||||
font-size: var(--font-xxsmall);
|
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);
|
margin-right: var(--spacing-m);
|
||||||
font-size: var(--font-large);
|
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 {
|
.icon {
|
||||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
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 {
|
.commentCreate__stickerPreview {
|
||||||
|
@ -131,4 +169,13 @@ $thumbnailWidthSmall: 1rem;
|
||||||
margin-left: var(--spacing-xxs);
|
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-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emote {
|
.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 {
|
.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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
|
@ -131,10 +141,9 @@ $recent-msg-button__height: 2rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestreamComments__scrollToRecent {
|
.livestream-comments__scroll-to-recent {
|
||||||
margin-top: -$recent-msg-button__height;
|
margin-top: -$recent-msg-button__height;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin-bottom: var(--spacing-xs);
|
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
padding: var(--spacing-xxs) var(--spacing-s);
|
padding: var(--spacing-xxs) var(--spacing-s);
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
|
@ -142,6 +151,14 @@ $recent-msg-button__height: 2rem;
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
bottom: var(--spacing-xxs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__comment-create {
|
.livestream__comment-create {
|
||||||
|
@ -150,7 +167,7 @@ $recent-msg-button__height: 2rem;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
padding: var(--spacing-xxs);
|
padding: 0px;
|
||||||
|
|
||||||
span,
|
span,
|
||||||
select,
|
select,
|
||||||
|
@ -186,7 +203,7 @@ $recent-msg-button__height: 2rem;
|
||||||
.livestream-superchats__wrapper--mobile {
|
.livestream-superchats__wrapper--mobile {
|
||||||
@extend .livestream-superchats__wrapper;
|
@extend .livestream-superchats__wrapper;
|
||||||
|
|
||||||
z-index: 1300;
|
z-index: 9999999;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
@ -224,6 +241,7 @@ $recent-msg-button__height: 2rem;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,24 +286,15 @@ $recent-msg-button__height: 2rem;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|
||||||
.livestream__comment {
|
.livestream__comment {
|
||||||
overflow: unset;
|
overflow: hidden;
|
||||||
|
|
||||||
.livestreamComment__body {
|
.livestreamComment__body {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
.markdown-preview {
|
&::-webkit-scrollbar {
|
||||||
p,
|
width: 0 !important;
|
||||||
.button__label {
|
|
||||||
white-space: nowrap !important;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -378,6 +387,7 @@ $recent-msg-button__height: 2rem;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: var(--font-xxsmall);
|
font-size: var(--font-xxsmall);
|
||||||
|
color: var(--color-text-subtitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -127,6 +127,12 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-medium) {
|
||||||
|
section + .empty__wrap {
|
||||||
|
margin: var(--spacing-m);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@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;
|
font-size: inherit !important;
|
||||||
color: var(--color-text) !important;
|
color: var(--color-text) !important;
|
||||||
|
|
||||||
.MuiOutlinedInput-notchedOutline {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create__comment {
|
.create__comment {
|
||||||
min-height: calc(var(--height-input) * 1.5) !important;
|
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) {
|
@media (max-width: $breakpoint-small) {
|
||||||
.MuiOutlinedInput-input {
|
.MuiOutlinedInput-input {
|
||||||
padding: 0px var(--spacing-xxs);
|
padding: 0px var(--spacing-xxs);
|
||||||
|
@ -25,19 +56,15 @@
|
||||||
font-size: var(--font-xsmall) !important;
|
font-size: var(--font-xsmall) !important;
|
||||||
flex-wrap: nowrap !important;
|
flex-wrap: nowrap !important;
|
||||||
color: var(--color-text) !important;
|
color: var(--color-text) !important;
|
||||||
padding: 0px 9px !important;
|
padding: 0px !important;
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
border: none;
|
border: none;
|
||||||
margin: 9px 0px;
|
margin: 9px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:not(:first-of-type):not(:last-of-type) {
|
button {
|
||||||
margin: 0px var(--spacing-xxs);
|
padding: var(--spacing-xxs);
|
||||||
}
|
|
||||||
|
|
||||||
button + div {
|
|
||||||
margin-left: var(--spacing-xxs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--primary {
|
.button--primary {
|
||||||
|
@ -130,3 +157,11 @@
|
||||||
.MuiAutocomplete-loading {
|
.MuiAutocomplete-loading {
|
||||||
color: var(--color-text) !important;
|
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);
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
height: var(--button-height);
|
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 {
|
.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 {
|
.tabs__list--channel-page {
|
||||||
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-xl));
|
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-xl));
|
||||||
padding-right: var(--spacing-m);
|
padding-right: var(--spacing-m);
|
||||||
|
|
Loading…
Reference in a new issue