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:
Rafael 2022-02-04 17:59:11 -03:00 committed by Thomas Zarebczan
parent eef6691557
commit c90efc2078
34 changed files with 1214 additions and 696 deletions

View file

@ -21,6 +21,7 @@
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Expires" content="0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="preload" href="/public/font/v1/300.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/public/font/v1/300i.woff" as="font" type="font/woff" crossorigin />

View file

@ -213,17 +213,27 @@ function Comment(props: Props) {
replace(`${pathname}?${urlParams.toString()}`);
}
const linkedCommentRef = React.useCallback((node) => {
const linkedCommentRef = React.useCallback(
(node) => {
if (node !== null && window.pendingLinkedCommentScroll) {
const ROUGH_HEADER_HEIGHT = 125; // @see: --header-height
delete window.pendingLinkedCommentScroll;
window.scrollTo({
top: node.getBoundingClientRect().top + window.scrollY - ROUGH_HEADER_HEIGHT,
const elem = isMobile ? document.querySelector('.MuiPaper-root .card--enable-overflow') : window;
if (elem) {
// $FlowFixMe
elem.scrollTo({
// $FlowFixMe
top: node.getBoundingClientRect().top + elem.scrollY - (isMobile ? 0 : ROUGH_HEADER_HEIGHT),
left: 0,
behavior: 'smooth',
});
}
}, []);
}
},
[isMobile]
);
return (
<li
@ -313,6 +323,7 @@ function Comment(props: Props) {
charCount={charCount}
onChange={handleEditMessageChanged}
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
handleSubmit={handleSubmit}
/>
<div className="section__actions section__actions--no-margin">
<Button

View 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;
};

View file

@ -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>
);
}

View 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>
)}
</>
);
};

View 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}
/>
);
};

View file

@ -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>
);
}

View 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)}
/>
)
);
};

View file

@ -14,23 +14,17 @@ import * as KEYCODES from 'constants/keycodes';
import * as PAGES from 'constants/pages';
import * as MODALS from 'constants/modal_types';
import Button from 'component/button';
import ChannelThumbnail from 'component/channelThumbnail';
import classnames from 'classnames';
import CreditAmount from 'component/common/credit-amount';
import EmoteSelector from './emote-selector';
import CommentSelectors from './comment-selectors';
import Empty from 'component/common/empty';
import FilePrice from 'component/filePrice';
import I18nMessage from 'component/i18nMessage';
import Icon from 'component/common/icon';
import OptimizedImage from 'component/optimizedImage';
import React from 'react';
import SelectChannel from 'component/selectChannel';
import StickerSelector from './sticker-selector';
import type { ElementRef } from 'react';
import UriIndicator from 'component/uriIndicator';
import usePersistedState from 'effects/use-persisted-state';
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
import { useIsMobile } from 'effects/use-screensize';
import { StickerReviewBox, StickerActionButton } from './sticker-contents';
import { TipReviewBox, TipActionButton } from './tip-contents';
import { FormChannelSelector, HelpText } from './extra-contents';
import { getStripeEnvironment } from 'util/stripe';
const stripeEnvironment = getStripeEnvironment();
@ -59,6 +53,7 @@ type Props = {
supportDisabled: boolean,
uri: string,
disableInput?: boolean,
onSlimInputClick?: () => void,
createComment: (string, string, string, ?string, ?string, ?string, boolean) => Promise<any>,
doFetchCreatorSettings: (channelId: string) => Promise<any>,
doToast: ({ message: string }) => void,
@ -90,6 +85,7 @@ export function CommentCreate(props: Props) {
supportDisabled,
uri,
disableInput,
onSlimInputClick,
createComment,
doFetchCreatorSettings,
doToast,
@ -115,7 +111,7 @@ export function CommentCreate(props: Props) {
const [isSubmitting, setSubmitting] = React.useState(false);
const [commentFailure, setCommentFailure] = React.useState(false);
const [successTip, setSuccessTip] = React.useState({ txid: undefined, tipAmount: undefined });
const [isSupportComment, setIsSupportComment] = React.useState();
const [tipSelectorOpen, setTipSelector] = React.useState();
const [isReviewingSupportComment, setReviewingSupportComment] = React.useState();
const [isReviewingStickerComment, setReviewingStickerComment] = React.useState();
const [selectedSticker, setSelectedSticker] = React.useState();
@ -123,18 +119,19 @@ export function CommentCreate(props: Props) {
const [convertedAmount, setConvertedAmount] = React.useState();
const [commentValue, setCommentValue] = React.useState('');
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
const [stickerSelector, setStickerSelector] = React.useState();
const [activeTab, setActiveTab] = React.useState();
const [tipError, setTipError] = React.useState();
const [deletedComment, setDeletedComment] = React.useState(false);
const [showEmotes, setShowEmotes] = React.useState(false);
const [showSelectors, setShowSelectors] = React.useState(false);
const [disableReviewButton, setDisableReviewButton] = React.useState();
const [exchangeRate, setExchangeRate] = React.useState();
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(undefined);
const [tipModalOpen, setTipModalOpen] = React.useState(undefined);
const claimId = claim && claim.claim_id;
const charCount = commentValue ? commentValue.length : 0;
const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length || disableInput;
const hasNothingToSumbit = !commentValue.length && !selectedSticker;
const disabled = deletedComment || isSubmitting || isFetchingChannels || hasNothingToSumbit || disableInput;
const channelId = getChannelIdFromClaim(claim);
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
const minSuper = (channelSettings && channelSettings.min_tip_amount_super_chat) || 0;
@ -142,6 +139,7 @@ export function CommentCreate(props: Props) {
const minAmount = minTip || minSuper || 0;
const minAmountMet = minAmount === 0 || tipAmount >= minAmount;
const stickerPrice = selectedSticker && selectedSticker.price;
const tipSelectorError = tipError || disableReviewButton;
const minAmountRef = React.useRef(minAmount);
minAmountRef.current = minAmount;
@ -150,16 +148,52 @@ export function CommentCreate(props: Props) {
// Functions
// **************************************************************************
function addEmoteToComment(emote: string) {
setCommentValue(
commentValue + (commentValue && commentValue.charAt(commentValue.length - 1) !== ' ' ? ` ${emote} ` : `${emote} `)
);
const inputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input && formFieldRef.current.input;
if (inputRef && inputRef.current) inputRef.current.focus();
}
function handleSelectTipComment(tab: string) {
setActiveTab(tab);
setTipSelector(true);
}
function handleStickerComment() {
if (selectedSticker) setReviewingStickerComment(false);
setTipSelector(false);
setShowSelectors(!showSelectors);
}
function handleSelectSticker(sticker: any) {
// $FlowFixMe
setSelectedSticker(sticker);
setReviewingStickerComment(true);
setTipAmount(sticker.price || 0);
setStickerSelector(false);
setShowSelectors(false);
if (sticker.price && sticker.price > 0) {
setActiveTab(canReceiveFiatTip ? TAB_FIAT : TAB_LBC);
setIsSupportComment(true);
setTipSelector(true);
}
}
function handleCancelSticker() {
setReviewingStickerComment(false);
setSelectedSticker(null);
}
function handleCancelSupport() {
if (!isReviewingSupportComment) setTipSelector(false);
setReviewingSupportComment(false);
if (stickerPrice) {
setReviewingStickerComment(false);
setShowSelectors(false);
setSelectedSticker(null);
}
}
@ -250,7 +284,7 @@ export function CommentCreate(props: Props) {
setCommentValue('');
setReviewingSupportComment(false);
setIsSupportComment(false);
setTipSelector(false);
setCommentFailure(false);
setSubmitting(false);
});
@ -266,7 +300,6 @@ export function CommentCreate(props: Props) {
function handleCreateComment(txid, payment_intent_id, environment) {
if (isSubmitting || disableInput) return;
setShowEmotes(false);
setSubmitting(true);
const stickerValue = selectedSticker && buildValidSticker(selectedSticker.name);
@ -279,7 +312,7 @@ export function CommentCreate(props: Props) {
if (res && res.signature) {
if (!stickerValue) setCommentValue('');
setReviewingSupportComment(false);
setIsSupportComment(false);
setTipSelector(false);
setCommentFailure(false);
if (onDoneReplying) {
@ -299,6 +332,19 @@ export function CommentCreate(props: Props) {
});
}
function handleSubmitSticker() {
if (isReviewingSupportComment) {
handleSupportComment();
} else {
handleCreateComment();
}
setSelectedSticker(null);
setReviewingStickerComment(false);
setShowSelectors(false);
setTipSelector(false);
}
// **************************************************************************
// Effects
// **************************************************************************
@ -327,7 +373,7 @@ export function CommentCreate(props: Props) {
// Stickers: Check if creator has a tip account saved (on selector so that if a paid sticker is selected,
// it defaults to LBC tip instead of USD)
React.useEffect(() => {
if (!stripeEnvironment || !stickerSelector || canReceiveFiatTip !== undefined) return;
if (!stripeEnvironment || !showSelectors || canReceiveFiatTip !== undefined) return;
const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
@ -350,7 +396,7 @@ export function CommentCreate(props: Props) {
}
})
.catch(() => {});
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, showSelectors]);
// Handle keyboard shortcut comment creation
React.useEffect(() => {
@ -384,14 +430,6 @@ export function CommentCreate(props: Props) {
// Render
// **************************************************************************
const getActionButton = (
title: string,
label?: string,
icon: string,
handleClick: () => void,
disabled?: boolean
) => <Button title={title} label={label} button="alt" icon={icon} onClick={handleClick} disabled={disabled} />;
if (channelSettings && !channelSettings.comments_enabled) {
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
}
@ -400,6 +438,7 @@ export function CommentCreate(props: Props) {
return (
<div
role="button"
className="comment-create__auth"
onClick={() => {
if (embed) {
window.open(`https://odysee.com/$/${PAGES.AUTH}?redirect=/$/${PAGES.LIVESTREAM}`);
@ -422,6 +461,18 @@ export function CommentCreate(props: Props) {
);
}
const commentSelectorsProps = { claimIsMine, addEmoteToComment, handleSelectSticker };
const submitButtonProps = { button: 'primary', type: 'submit', requiresAuth: true };
const actionButtonProps = { button: 'alt', isReviewingStickerComment };
const tipButtonProps = {
...actionButtonProps,
disabled: !commentValue.length && !selectedSticker,
tipSelectorOpen,
activeTab,
onClick: handleSelectTipComment,
};
const cancelButtonProps = { button: 'link', label: __('Cancel') };
return (
<Form
onSubmit={() => {}}
@ -431,50 +482,29 @@ export function CommentCreate(props: Props) {
'commentCreate--bottom': bottom,
})}
>
{/* Input Box/Preview Box */}
{stickerSelector ? (
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
<div className="commentCreate__stickerPreview">
<div className="commentCreate__stickerPreviewInfo">
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
<UriIndicator uri={activeChannelClaim.canonical_url} link />
</div>
<div className="commentCreate__stickerPreviewImage">
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
</div>
{selectedSticker.price && exchangeRate && (
<FilePrice
customPrices={{ priceFiat: selectedSticker.price, priceLBC: selectedSticker.price / exchangeRate }}
isFiat
{selectedSticker ? (
activeChannelClaim && (
<StickerReviewBox
activeChannelUrl={activeChannelClaim.canonical_url}
src={selectedSticker.url}
price={selectedSticker.price || 0}
exchangeRate={exchangeRate}
/>
)}
</div>
) : isReviewingSupportComment && activeChannelClaim ? (
<div className="commentCreate__supportCommentPreview">
<CreditAmount
amount={tipAmount}
className="commentCreate__supportCommentPreviewAmount"
isFiat={activeTab === TAB_FIAT}
size={activeTab === TAB_LBC ? 18 : 2}
)
) : isReviewingSupportComment ? (
activeChannelClaim &&
activeTab && (
<TipReviewBox
activeChannelUrl={activeChannelClaim.canonical_url}
tipAmount={tipAmount}
activeTab={activeTab}
message={commentValue}
/>
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
<div className="commentCreate__supportCommentBody">
<UriIndicator uri={activeChannelClaim.canonical_url} link />
<div>{commentValue}</div>
</div>
</div>
)
) : (
<>
{showEmotes && (
<EmoteSelector
commentValue={commentValue}
setCommentValue={setCommentValue}
closeSelector={() => setShowEmotes(false)}
/>
{!isMobile && showSelectors && (
<CommentSelectors {...commentSelectorsProps} closeSelector={() => setShowSelectors(false)} />
)}
<FormField
@ -483,30 +513,31 @@ export function CommentCreate(props: Props) {
className={isReply ? 'create__reply' : 'create__comment'}
disabled={isFetchingChannels || disableInput}
isLivestream={isLivestream}
label={
<div className="comment-create__label-wrapper">
<span className="comment-create__label">
{(isReply ? __('Replying as') : isLivestream ? __('Chat as') : __('Comment as')) + ' '}
</span>
<SelectChannel tiny />
</div>
}
label={<FormChannelSelector isReply={Boolean(isReply)} isLivestream={Boolean(isLivestream)} />}
name={isReply ? 'create__reply' : 'create__comment'}
onChange={(e) => setCommentValue(SIMPLE_SITE || !advancedEditor || isReply ? e.target.value : e)}
openEmoteMenu={() => setShowEmotes(!showEmotes)}
handleTip={(isLBC) =>
handleTip={(isLBC) => {
setActiveTab(isLBC ? TAB_LBC : TAB_FIAT);
setTipModalOpen(true);
doOpenModal(MODALS.SEND_TIP, {
uri,
isTipOnly: true,
hasSelectedTab: isLBC ? TAB_LBC : TAB_FIAT,
customText: __('Preview Comment Tip'),
setAmount: (amount) => {
setTipAmount(amount);
setReviewingSupportComment(true);
},
})
}
});
}}
handleSubmit={handleCreateComment}
noEmojis={isMobile}
slimInput={isMobile}
onSlimInputClick={onSlimInputClick}
commentSelectorsProps={commentSelectorsProps}
submitButtonRef={buttonRef}
setShowSelectors={setShowSelectors}
showSelectors={showSelectors}
tipModalOpen={tipModalOpen}
placeholder={__('Say something about this...')}
quickActionHandler={!SIMPLE_SITE ? () => setAdvancedEditor(!advancedEditor) : undefined}
quickActionLabel={
@ -521,7 +552,7 @@ export function CommentCreate(props: Props) {
</>
)}
{!isMobile && (isSupportComment || (isReviewingStickerComment && stickerPrice)) && (
{(!isMobile || isReviewingStickerComment) && (tipSelectorOpen || (isReviewingStickerComment && stickerPrice)) && (
<WalletTipAmountSelector
activeTab={activeTab}
amount={tipAmount}
@ -538,14 +569,13 @@ export function CommentCreate(props: Props) {
/>
)}
{/* Bottom Action Buttons */}
{!isMobile && (
<div className="section__actions section__actions--no-margin">
{(!isMobile || !isLivestream || isReviewingStickerComment || isReviewingSupportComment) && (
<div className="section__actions">
{/* Submit Button */}
{isReviewingSupportComment ? (
<Button
{...submitButtonProps}
autoFocus
button="primary"
disabled={disabled || !minAmountMet}
label={
isSubmitting
@ -556,40 +586,21 @@ export function CommentCreate(props: Props) {
}
onClick={handleSupportComment}
/>
) : isReviewingStickerComment && selectedSticker ? (
) : tipSelectorOpen ? (
<Button
button="primary"
label={__('Send')}
disabled={(isSupportComment && (tipError || disableReviewButton)) || disableInput}
onClick={() => {
if (isSupportComment) {
handleSupportComment();
} else {
handleCreateComment();
}
setSelectedSticker(null);
setReviewingStickerComment(false);
setStickerSelector(false);
setIsSupportComment(false);
}}
/>
) : isSupportComment ? (
<Button
disabled={disabled || tipError || disableReviewButton || !minAmountMet}
type="button"
button="primary"
{...submitButtonProps}
disabled={disabled || tipSelectorError || !minAmountMet}
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
label={__('Review')}
onClick={() => setReviewingSupportComment(true)}
requiresAuth
/>
) : (
(!isMobile || selectedSticker) &&
(!minTip || claimIsMine) && (
<Button
{...submitButtonProps}
ref={buttonRef}
button="primary"
disabled={disabled || stickerSelector}
type="submit"
disabled={disabled}
label={
isReply
? isSubmitting
@ -599,135 +610,36 @@ export function CommentCreate(props: Props) {
? __('Commenting...')
: __('Comment --[button to submit something]--')
}
requiresAuth
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
onClick={() => (selectedSticker ? handleSubmitSticker() : handleCreateComment())}
/>
)
)}
{/** Stickers/Support Buttons **/}
{!supportDisabled && !stickerSelector && (
{!isMobile && (
<>
{getActionButton(
__('Stickers'),
isReviewingStickerComment ? __('Different Sticker') : undefined,
ICONS.STICKER,
() => {
if (isReviewingStickerComment) setReviewingStickerComment(false);
setIsSupportComment(false);
setStickerSelector(true);
}
)}
<StickerActionButton {...actionButtonProps} icon={ICONS.STICKER} onClick={handleStickerComment} />
{!claimIsMine && (
{!supportDisabled && (
<>
{(!isSupportComment || activeTab !== TAB_LBC) &&
getActionButton(
__('Credits'),
isSupportComment ? __('Switch to Credits') : undefined,
ICONS.LBC,
() => {
setActiveTab(TAB_LBC);
<TipActionButton {...tipButtonProps} name={__('Credits')} icon={ICONS.LBC} tab={TAB_LBC} />
if (isMobile) {
doOpenModal(MODALS.SEND_TIP, {
uri,
isTipOnly: true,
hasSelectedTab: TAB_LBC,
setAmount: (amount) => {
setTipAmount(amount);
setReviewingSupportComment(true);
},
});
} else {
setIsSupportComment(true);
}
},
!commentValue.length
)}
{stripeEnvironment &&
(!isSupportComment || activeTab !== TAB_FIAT) &&
getActionButton(
__('Cash'),
isSupportComment ? __('Switch to Cash') : undefined,
ICONS.FINANCE,
() => {
setActiveTab(TAB_FIAT);
if (isMobile) {
doOpenModal(MODALS.SEND_TIP, {
uri,
isTipOnly: true,
hasSelectedTab: TAB_FIAT,
setAmount: (amount) => {
setTipAmount(amount);
setReviewingSupportComment(true);
},
});
} else {
setIsSupportComment(true);
}
},
!commentValue.length
{stripeEnvironment && (
<TipActionButton {...tipButtonProps} name={__('Cash')} icon={ICONS.FINANCE} tab={TAB_FIAT} />
)}
</>
)}
</>
)}
{/* Cancel Button */}
{(isSupportComment ||
isReviewingSupportComment ||
stickerSelector ||
isReviewingStickerComment ||
(isReply && !minTip)) && (
<Button
disabled={isSupportComment && isSubmitting}
button="link"
label={__('Cancel')}
onClick={() => {
if (isSupportComment || isReviewingSupportComment) {
if (!isReviewingSupportComment) setIsSupportComment(false);
setReviewingSupportComment(false);
if (stickerPrice) {
setReviewingStickerComment(false);
setStickerSelector(false);
setSelectedSticker(null);
}
} else if (stickerSelector || isReviewingStickerComment) {
setReviewingStickerComment(false);
setStickerSelector(false);
setSelectedSticker(null);
} else if (isReply && !minTip && onCancelReplying) {
onCancelReplying();
}
}}
/>
{tipSelectorOpen || isReviewingSupportComment ? (
<Button {...cancelButtonProps} disabled={isSubmitting} onClick={handleCancelSupport} />
) : isReviewingStickerComment ? (
<Button {...cancelButtonProps} onClick={handleCancelSticker} />
) : (
onCancelReplying && <Button {...cancelButtonProps} onClick={onCancelReplying} />
)}
{/* Help Text */}
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
{!!minAmount && (
<div className="help--notice commentCreate__minAmountNotice">
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
</I18nMessage>
<Icon
customTooltipText={
minTip
? __('This channel requires a minimum tip for each comment.')
: minSuper
? __('This channel requires a minimum amount for HyperChats to be visible.')
: ''
}
className="icon--help"
icon={ICONS.HELP}
tooltip
size={16}
/>
</div>
)}
<HelpText deletedComment={deletedComment} minAmount={minAmount} minSuper={minSuper} minTip={minTip} />
</div>
)}
</Form>

View file

@ -9,6 +9,7 @@ import * as MODALS from 'constants/modal_types';
import ChannelThumbnail from 'component/channelThumbnail';
import Icon from 'component/common/icon';
import React from 'react';
import { useIsMobile } from 'effects/use-screensize';
type Props = {
uri: ?string,
@ -67,6 +68,8 @@ function CommentMenuList(props: Props) {
handleDismissPin,
} = props;
const isMobile = useIsMobile();
const {
location: { pathname, search },
} = useHistory();
@ -253,7 +256,7 @@ function CommentMenuList(props: Props) {
</MenuItem>
)}
{isPinned && isLiveComment && (
{isPinned && isLiveComment && isMobile && (
<MenuItem className="comment__menu-option menu__link" onSelect={handleDismissPin}>
<Icon aria-hidden icon={ICONS.DISMISS_ALL} />
{__('Dismiss Pin')}

View file

@ -269,37 +269,17 @@ function CommentList(props: Props) {
/>
));
const sortButton = (label, icon, sortOption) => (
<Button
button="alt"
label={label}
icon={icon}
iconSize={18}
onClick={() => changeSort(sortOption)}
className={classnames(`button-toggle`, {
'button-toggle--active': sort === sortOption,
})}
/>
);
const actionButtonsProps = { totalComments, sort, changeSort, setPage };
return (
<Card
className="card--enable-overflow"
title={!isMobile && title}
titleActions={
<>
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
<span className="comment__sort">
{sortButton(__('Best'), ICONS.BEST, SORT_BY.POPULARITY)}
{sortButton(__('Controversial'), ICONS.CONTROVERSIAL, SORT_BY.CONTROVERSY)}
{sortButton(__('New'), ICONS.NEW, SORT_BY.NEWEST)}
</span>
)}
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
</>
}
titleActions={<CommentActionButtons {...actionButtonsProps} />}
actions={
<>
{isMobile && <CommentActionButtons {...actionButtonsProps} />}
<CommentCreate uri={uri} />
{channelSettings && channelSettings.comments_enabled && !isFetchingComments && !totalComments && (
@ -349,3 +329,55 @@ function CommentList(props: Props) {
}
export default CommentList;
type ActionButtonsProps = {
totalComments: number,
sort: string,
changeSort: (string) => void,
setPage: (number) => void,
};
const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
const { totalComments, sort, changeSort, setPage } = actionButtonsProps;
const sortButtonProps = { activeSort: sort, changeSort };
return (
<>
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
<span className="comment__sort">
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
<SortButton
{...sortButtonProps}
label={__('Controversial')}
icon={ICONS.CONTROVERSIAL}
sortOption={SORT_BY.CONTROVERSY}
/>
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
</span>
)}
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
</>
);
};
type SortButtonProps = {
activeSort: string,
sortOption: string,
changeSort: (string) => void,
};
const SortButton = (sortButtonProps: SortButtonProps) => {
const { activeSort, sortOption, changeSort, ...buttonProps } = sortButtonProps;
return (
<Button
{...buttonProps}
className={classnames(`button-toggle`, { 'button-toggle--active': activeSort === sortOption })}
button="alt"
iconSize={18}
onClick={() => changeSort(sortOption)}
/>
);
};

View file

@ -3,13 +3,14 @@ import 'easymde/dist/easymde.min.css';
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
import { lazyImport } from 'util/lazyImport';
import * as ICONS from 'constants/icons';
import Button from 'component/button';
import MarkdownPreview from 'component/common/markdown-preview';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import SimpleMDE from 'react-simplemde-editor';
import type { ElementRef, Node } from 'react';
import Drawer from '@mui/material/Drawer';
import CommentSelectors from 'component/commentCreate/comment-selectors';
// prettier-ignore
const TextareaWithSuggestions = lazyImport(() => import('component/textareaWithSuggestions' /* webpackChunkName: "suggestions" */));
@ -33,7 +34,6 @@ type Props = {
max?: number,
min?: number,
name: string,
noEmojis?: boolean,
placeholder?: string | number,
postfix?: string,
prefix?: string,
@ -44,15 +44,25 @@ type Props = {
textAreaMaxLength?: number,
type?: string,
value?: string | number,
slimInput?: boolean,
commentSelectorsProps?: any,
showSelectors?: boolean,
submitButtonRef?: any,
tipModalOpen?: boolean,
onSlimInputClick?: () => void,
onChange?: (any) => any,
openEmoteMenu?: () => void,
setShowSelectors?: (boolean) => void,
quickActionHandler?: (any) => any,
render?: () => React$Node,
handleTip?: (isLBC: boolean) => any,
handleSubmit?: () => any,
};
export class FormField extends React.PureComponent<Props> {
type State = {
drawerOpen: boolean,
};
export class FormField extends React.PureComponent<Props, State> {
static defaultProps = { labelOnLeft: false, blockWrap: true };
input: { current: ElementRef<any> };
@ -60,6 +70,10 @@ export class FormField extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
this.input = React.createRef();
this.state = {
drawerOpen: false,
};
}
componentDidMount() {
@ -69,6 +83,14 @@ export class FormField extends React.PureComponent<Props> {
if (input && autoFocus) input.focus();
}
componentDidUpdate() {
const { showSelectors, slimInput } = this.props;
const input = this.input.current;
// Opened selectors (emoji/sticker) -> blur input and hide keyboard
if (slimInput && showSelectors && input) input.blur();
}
render() {
const {
uri,
@ -85,15 +107,20 @@ export class FormField extends React.PureComponent<Props> {
label,
labelOnLeft,
name,
noEmojis,
postfix,
prefix,
quickActionLabel,
stretch,
textAreaMaxLength,
type,
openEmoteMenu,
slimInput,
commentSelectorsProps,
showSelectors,
submitButtonRef,
tipModalOpen,
onSlimInputClick,
quickActionHandler,
setShowSelectors,
render,
handleTip,
handleSubmit,
@ -240,9 +267,20 @@ export class FormField extends React.PureComponent<Props> {
case 'textarea':
return (
<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">
<label htmlFor={name}>{label}</label>
{quickAction}
{countInfo}
</div>
)}
@ -264,26 +302,23 @@ export class FormField extends React.PureComponent<Props> {
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
inputRef={this.input}
isLivestream={isLivestream}
handleEmojis={openEmoteMenu}
toggleSelectors={setShowSelectors ? () => setShowSelectors(!showSelectors) : undefined}
handleTip={handleTip}
handleSubmit={handleSubmit}
handleSubmit={() => {
if (handleSubmit) handleSubmit();
if (slimInput) this.setState({ drawerOpen: false });
}}
claimIsMine={commentSelectorsProps && commentSelectorsProps.claimIsMine}
{...inputProps}
handlePreventClick={
!this.state.drawerOpen ? () => this.setState({ drawerOpen: true }) : undefined
}
autoFocus={this.state.drawerOpen}
submitButtonRef={submitButtonRef}
/>
</React.Suspense>
)}
{!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>
)}
</TextareaWrapper>
</fieldset-section>
);
default:
@ -321,3 +356,65 @@ export class FormField extends React.PureComponent<Props> {
}
export default FormField;
type TextareaWrapperProps = {
slimInput?: boolean,
children: Node,
isDrawerOpen: boolean,
showSelectors?: boolean,
commentSelectorsProps?: any,
tipModalOpen?: boolean,
onSlimInputClick?: () => void,
toggleDrawer: () => void,
closeSelector?: () => void,
};
function TextareaWrapper(wrapperProps: TextareaWrapperProps) {
const {
children,
slimInput,
isDrawerOpen,
commentSelectorsProps,
showSelectors,
tipModalOpen,
onSlimInputClick,
toggleDrawer,
closeSelector,
} = wrapperProps;
function handleCloseAll() {
toggleDrawer();
if (closeSelector) closeSelector();
if (onSlimInputClick) onSlimInputClick();
}
return slimInput ? (
!isDrawerOpen ? (
<div
role="button"
onClick={() => {
toggleDrawer();
if (onSlimInputClick) onSlimInputClick();
}}
>
{children}
</div>
) : (
<Drawer
className="comment-create--drawer"
anchor="bottom"
open
onClose={handleCloseAll}
// The Modal tries to enforce focus when open and doesn't allow clicking or changing any
// other input boxes, so in this case it is disabled when trying to type in a custom tip
ModalProps={{ disableEnforceFocus: tipModalOpen }}
>
{children}
{showSelectors && <CommentSelectors closeSelector={closeSelector} {...commentSelectorsProps} />}
</Drawer>
)
) : (
<>{children}</>
);
}

View file

@ -6,7 +6,6 @@ import { Global } from '@emotion/react';
// $FlowFixMe
import { grey } from '@mui/material/colors';
import { formatLbryUrlForWeb } from 'util/url';
import { useIsMobile } from 'effects/use-screensize';
import * as ICONS from 'constants/icons';
import Button from 'component/button';
@ -42,6 +41,7 @@ type Props = {
superchatsHidden?: boolean,
customViewMode?: string,
theme: string,
setCustomViewMode?: (any) => void,
doCommentList: (string, string, number, number) => void,
doResolveUris: (Array<string>, boolean) => void,
doSuperChatList: (string) => void,
@ -60,6 +60,7 @@ export default function LivestreamChatLayout(props: Props) {
superchatsHidden,
customViewMode,
theme,
setCustomViewMode,
doCommentList,
doResolveUris,
doSuperChatList,
@ -67,11 +68,18 @@ export default function LivestreamChatLayout(props: Props) {
const isMobile = useIsMobile() && !isPopoutWindow;
const discussionElement = document.querySelector('.livestream__comments');
const webElement = document.querySelector('.livestream__comments');
const mobileElement = document.querySelector('.livestream__comments--mobile');
const discussionElement = isMobile ? mobileElement : webElement;
const allCommentsElem = document.querySelectorAll('.livestream__comment');
const lastCommentElem = allCommentsElem && allCommentsElem[allCommentsElem.length - 1];
const minScrollPos =
discussionElement && lastCommentElem && discussionElement.scrollHeight - lastCommentElem.offsetHeight;
const minOffset = discussionElement && minScrollPos && discussionElement.scrollHeight - minScrollPos;
const restoreScrollPos = React.useCallback(() => {
if (discussionElement) discussionElement.scrollTop = 0;
}, [discussionElement]);
if (discussionElement) discussionElement.scrollTop = !isMobile ? 0 : discussionElement.scrollHeight;
}, [discussionElement, isMobile]);
const commentsRef = React.createRef();
@ -79,12 +87,12 @@ export default function LivestreamChatLayout(props: Props) {
const [scrollPos, setScrollPos] = React.useState(0);
const [showPinned, setShowPinned] = React.useState(true);
const [resolvingSuperChats, setResolvingSuperChats] = React.useState(false);
const [mention, setMention] = React.useState();
const [openedPopoutWindow, setPopoutWindow] = React.useState(undefined);
const [chatHidden, setChatHidden] = React.useState(false);
const [didInitialScroll, setDidInitialScroll] = React.useState(false);
const [bottomScrollTop, setBottomScrollTop] = React.useState(0);
const quickMention =
mention && formatLbryUrlForWeb(mention).substring(1, formatLbryUrlForWeb(mention).indexOf(':') + 3);
const recentScrollPos = isMobile ? (bottomScrollTop > 0 && minOffset ? bottomScrollTop - minOffset : 0) : 0;
const claimId = claim && claim.claim_id;
const commentsToDisplay = viewMode === VIEW_MODES.CHAT ? commentsByChronologicalOrder : superChatsByAmount;
const commentsLength = commentsToDisplay && commentsToDisplay.length;
@ -100,6 +108,7 @@ export default function LivestreamChatLayout(props: Props) {
}
}
setViewMode(VIEW_MODES.SUPERCHAT);
if (setCustomViewMode) setCustomViewMode(VIEW_MODES.SUPERCHAT);
}
React.useEffect(() => {
@ -115,12 +124,26 @@ export default function LivestreamChatLayout(props: Props) {
}
}, [claimId, uri, doCommentList, doSuperChatList]);
React.useEffect(() => {
if (
isMobile &&
discussionElement &&
viewMode === VIEW_MODES.CHAT &&
!didInitialScroll &&
discussionElement.scrollTop < discussionElement.scrollHeight
) {
discussionElement.scrollTop = discussionElement.scrollHeight;
setDidInitialScroll(true);
setBottomScrollTop(discussionElement.scrollTop);
}
}, [didInitialScroll, discussionElement, isMobile, viewMode]);
// Register scroll handler (TODO: Should throttle/debounce)
React.useEffect(() => {
function handleScroll() {
if (discussionElement) {
const scrollTop = discussionElement.scrollTop;
if (scrollTop !== scrollPos) {
if (!scrollPos || scrollTop !== scrollPos) {
setScrollPos(scrollTop);
}
}
@ -136,12 +159,13 @@ export default function LivestreamChatLayout(props: Props) {
React.useEffect(() => {
if (discussionElement && commentsLength > 0) {
// Only update comment scroll if the user hasn't scrolled up to view old comments
if (scrollPos >= 0) {
// $FlowFixMe
if (scrollPos && (!isMobile || recentScrollPos) && scrollPos >= recentScrollPos) {
// +ve scrollPos: not scrolled (Usually, there'll be a few pixels beyond 0).
// -ve scrollPos: user scrolled.
const timer = setTimeout(() => {
// Use a timer here to ensure we reset after the new comment has been rendered.
discussionElement.scrollTop = 0;
discussionElement.scrollTop = !isMobile ? 0 : discussionElement.scrollHeight + 999;
}, COMMENT_SCROLL_TIMEOUT);
return () => clearTimeout(timer);
}
@ -283,7 +307,6 @@ export default function LivestreamChatLayout(props: Props) {
comment={pinnedComment}
key={pinnedComment.comment_id}
uri={uri}
pushMention={setMention}
handleDismissPin={() => setShowPinned(false)}
isMobile
/>
@ -292,12 +315,7 @@ export default function LivestreamChatLayout(props: Props) {
) : (
showPinned && (
<div className="livestream-pinned__wrapper">
<LivestreamComment
comment={pinnedComment}
key={pinnedComment.comment_id}
uri={uri}
pushMention={setMention}
/>
<LivestreamComment comment={pinnedComment} key={pinnedComment.comment_id} uri={uri} />
<Button
title={__('Dismiss pinned comment')}
@ -316,23 +334,18 @@ export default function LivestreamChatLayout(props: Props) {
<Spinner />
</div>
) : (
<LivestreamComments
uri={uri}
commentsToDisplay={commentsToDisplay}
pushMention={setMention}
isMobile={isMobile}
/>
<LivestreamComments uri={uri} commentsToDisplay={commentsToDisplay} isMobile={isMobile} />
)}
{scrollPos < 0 && (
{scrollPos && (!isMobile || recentScrollPos) && scrollPos < recentScrollPos && viewMode === VIEW_MODES.CHAT ? (
<Button
button="secondary"
className="livestreamComments__scrollToRecent"
className="livestream-comments__scroll-to-recent"
label={viewMode === VIEW_MODES.CHAT ? __('Recent Comments') : __('Recent Tips')}
onClick={restoreScrollPos}
iconRight={ICONS.DOWN}
/>
)}
) : null}
<div className="livestream__comment-create">
<CommentCreate
@ -341,8 +354,13 @@ export default function LivestreamChatLayout(props: Props) {
embed={embed}
uri={uri}
onDoneReplying={restoreScrollPos}
pushedMention={quickMention}
setPushedMention={setMention}
onSlimInputClick={
scrollPos &&
recentScrollPos &&
scrollPos >= recentScrollPos &&
viewMode === VIEW_MODES.CHAT &&
restoreScrollPos
}
/>
</div>
</div>

View file

@ -46,6 +46,8 @@ export default function LivestreamComment(props: Props) {
timestamp,
} = comment;
const commentRef = React.useRef();
const [hasUserMention, setUserMention] = React.useState(false);
const isStreamer = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
@ -54,12 +56,24 @@ export default function LivestreamComment(props: Props) {
const isSticker = Boolean(stickerUrlFromMessage);
const timePosted = timestamp * 1000;
const commentIsMine = comment.channel_id && isMyComment(comment.channel_id);
const discussionElement = document.querySelector('.livestream__comments--mobile');
const currentComment = commentRef && commentRef.current;
const minScrollPos =
discussionElement && currentComment && discussionElement.scrollHeight - currentComment.offsetHeight;
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
function isMyComment(channelId: string) {
return myChannelIds ? myChannelIds.includes(channelId) : false;
}
// For every new <LivestreamComment /> component that is rendered on mobile view,
// keep the scroll at the bottom (newest)
React.useEffect(() => {
if (isMobile && discussionElement && minScrollPos && discussionElement.scrollTop >= minScrollPos) {
discussionElement.scrollTop = discussionElement.scrollHeight;
}
}, [discussionElement, isMobile, minScrollPos]);
return (
<li
className={classnames('livestream__comment', {
@ -68,6 +82,7 @@ export default function LivestreamComment(props: Props) {
'livestream__comment--mentioned': hasUserMention,
'livestream__comment--mobile': isMobile,
})}
ref={commentRef}
>
{supportAmount > 0 && (
<div className="livestreamComment__superchatBanner">

View file

@ -44,8 +44,8 @@ export default function LivestreamComments(props: Props) {
/* top to bottom comment display */
if (!fetchingComments && commentsToDisplay && commentsToDisplay.length > 0) {
return (
<div className="livestream__comments">
return isMobile ? (
<div className="livestream__comments--mobile">
{commentsToDisplay
.slice(0)
.reverse()
@ -55,10 +55,16 @@ export default function LivestreamComments(props: Props) {
key={comment.comment_id}
uri={uri}
forceUpdate={forceUpdate}
isMobile={isMobile}
isMobile
/>
))}
</div>
) : (
<div className="livestream__comments">
{commentsToDisplay.map((comment) => (
<LivestreamComment comment={comment} key={comment.comment_id} uri={uri} forceUpdate={forceUpdate} />
))}
</div>
);
}

View file

@ -130,6 +130,7 @@ export default function LivestreamLayout(props: Props) {
hideHeader
superchatsHidden={superchatsHidden}
customViewMode={chatViewMode}
setCustomViewMode={(mode) => setChatViewMode(mode)}
/>
</SwipeableDrawer>
@ -155,7 +156,7 @@ const ChatModeSelector = (chatSelectorProps: any) => {
<Menu>
<MenuButton>
<span className="swipeable-drawer__title-menu">
{chatViewMode === VIEW_MODES.CHAT ? __('Live Chat') : __('Super Chats')}
{chatViewMode === VIEW_MODES.CHAT ? __('Live Chat') : __('HyperChats')}
<Icon icon={ICONS.DOWN} />
</span>
</MenuButton>

View file

@ -11,13 +11,27 @@ type Props = {
messageValue: string,
inputDefaultProps: any,
inputRef: any,
handleEmojis: () => any,
submitButtonRef?: any,
claimIsMine?: boolean,
toggleSelectors: () => any,
handleTip: (isLBC: boolean) => void,
handleSubmit: () => any,
handlePreventClick?: () => void,
};
const TextareaSuggestionsInput = (props: Props) => {
const { params, messageValue, inputRef, inputDefaultProps, handleEmojis, handleTip, handleSubmit } = props;
const {
params,
messageValue,
inputRef,
inputDefaultProps,
submitButtonRef,
claimIsMine,
toggleSelectors,
handleTip,
handleSubmit,
handlePreventClick,
} = props;
const isMobile = useIsMobile();
@ -26,15 +40,42 @@ const TextareaSuggestionsInput = (props: Props) => {
const autocompleteProps = { InputProps, disabled, fullWidth, id, inputProps };
if (isMobile) {
InputProps.startAdornment = <Button icon={ICONS.STICKER} onClick={handleEmojis} />;
InputProps.startAdornment = (
<Button
icon={ICONS.STICKER}
onClick={() => {
if (handlePreventClick) handlePreventClick();
toggleSelectors();
}}
/>
);
InputProps.endAdornment = (
<>
<Button icon={ICONS.LBC} onClick={() => handleTip(true)} />
<Button icon={ICONS.FINANCE} onClick={() => handleTip(false)} />
{!claimIsMine && (
<Button
disabled={!messageValue || messageValue.length === 0}
icon={ICONS.LBC}
onClick={() => handleTip(true)}
/>
)}
<Zoom in={messageValue && messageValue.length > 0} mountOnEnter unmountOnExit>
{!claimIsMine && (
<Button
disabled={!messageValue || messageValue.length === 0}
icon={ICONS.FINANCE}
onClick={() => handleTip(false)}
/>
)}
<Zoom in={messageValue ? messageValue.length > 0 : undefined} mountOnEnter unmountOnExit>
<div>
<Button button="primary" icon={ICONS.SUBMIT} iconColor="red" onClick={() => handleSubmit()} />
<Button
ref={submitButtonRef}
button="primary"
icon={ICONS.SUBMIT}
iconColor="red"
onClick={() => handleSubmit()}
/>
</div>
</Zoom>
</>

View file

@ -56,14 +56,18 @@ type Props = {
type?: string,
uri?: string,
value: any,
autoFocus?: boolean,
submitButtonRef?: any,
claimIsMine?: boolean,
doResolveUris: (uris: Array<string>, cache: boolean) => void,
doSetMentionSearchResults: (query: string, uris: Array<string>) => void,
onBlur: (any) => any,
onChange: (any) => any,
onFocus: (any) => any,
handleEmojis: () => any,
toggleSelectors: () => any,
handleTip: (isLBC: boolean) => any,
handleSubmit: () => any,
handlePreventClick?: () => void,
};
export default function TextareaWithSuggestions(props: Props) {
@ -85,14 +89,18 @@ export default function TextareaWithSuggestions(props: Props) {
searchQuery,
type,
value: messageValue,
autoFocus,
submitButtonRef,
claimIsMine,
doResolveUris,
doSetMentionSearchResults,
onBlur,
onChange,
onFocus,
handleEmojis,
toggleSelectors,
handleTip,
handleSubmit,
handlePreventClick,
} = props;
const inputDefaultProps = { className, placeholder, maxLength, type, disabled };
@ -290,6 +298,13 @@ export default function TextareaWithSuggestions(props: Props) {
/** Effects **/
/** ------- **/
React.useEffect(() => {
if (!autoFocus) return;
const inputElement = inputRef && inputRef.current;
if (inputElement) inputElement.focus();
}, [autoFocus, inputRef]);
React.useEffect(() => {
if (!isMention) return;
@ -400,9 +415,12 @@ export default function TextareaWithSuggestions(props: Props) {
messageValue={messageValue}
inputRef={inputRef}
inputDefaultProps={inputDefaultProps}
handleEmojis={handleEmojis}
toggleSelectors={toggleSelectors}
handleTip={handleTip}
handleSubmit={handleSubmit}
handlePreventClick={handlePreventClick}
submitButtonRef={submitButtonRef}
claimIsMine={claimIsMine}
/>
)}
renderOption={(optionProps, option) => (

View file

@ -44,6 +44,7 @@ type Props = {
uri: string,
isTipOnly?: boolean,
hasSelectedTab?: string,
customText?: string,
doHideModal: () => void,
doSendCashTip: (TipParams, boolean, UserParams, string, ?string) => string,
doSendTip: (SupportParams, boolean) => void, // function that comes from lbry-redux
@ -69,6 +70,7 @@ export default function WalletSendTip(props: Props) {
uri,
isTipOnly,
hasSelectedTab,
customText,
doHideModal,
doSendCashTip,
doSendTip,
@ -328,7 +330,7 @@ export default function WalletSendTip(props: Props) {
button="primary"
type="submit"
disabled={fetchingChannels || isPending || tipError || !tipAmount || disableSubmitButton}
label={buildButtonText()}
label={customText || buildButtonText()}
/>
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
</div>

View file

@ -71,7 +71,6 @@ function WalletTipAmountSelector(props: Props) {
// if it's fiat but there's no card saved OR the creator can't receive fiat tips
const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip);
if (setDisableSubmitButton) setDisableSubmitButton(shouldDisableFiatSelectors);
// setup variables for tip API
const channelClaimId = claim ? (claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id) : undefined;
@ -103,6 +102,10 @@ function WalletTipAmountSelector(props: Props) {
}
}
React.useEffect(() => {
if (setDisableSubmitButton) setDisableSubmitButton(shouldDisableFiatSelectors);
}, [setDisableSubmitButton, shouldDisableFiatSelectors]);
React.useEffect(() => {
if (setConvertedAmount && exchangeRate && (!convertedAmount || convertedAmount !== amount * exchangeRate)) {
setConvertedAmount(amount * exchangeRate);

View file

@ -185,7 +185,7 @@ export const TECHNOLOGY = 'Technology';
export const EMOJI = 'Emoji';
export const STICKER = 'Sticker';
export const EDUCATION = 'Education';
export const POP_CULTURE = 'PopCulture';
export const POP_CULTURE = 'Pop Culture';
export const ODYSEE_LOGO = 'OdyseeLogo';
export const ODYSEE_WHITE_TEXT = 'OdyseeLogoWhiteText';
export const ODYSEE_DARK_TEXT = 'OdyseeLogoDarkText';

View file

@ -9,13 +9,14 @@ type Props = {
isSupport: boolean,
isTipOnly?: boolean,
hasSelectedTab?: string,
customText?: string,
doHideModal: () => void,
setAmount?: (number) => void,
};
class ModalSendTip extends React.PureComponent<Props> {
render() {
const { uri, claimIsMine, isTipOnly, hasSelectedTab, doHideModal, setAmount } = this.props;
const { uri, claimIsMine, isTipOnly, hasSelectedTab, customText, doHideModal, setAmount } = this.props;
return (
<Modal onAborted={doHideModal} isOpen type="card">
@ -25,6 +26,7 @@ class ModalSendTip extends React.PureComponent<Props> {
onCancel={doHideModal}
isTipOnly={isTipOnly}
hasSelectedTab={hasSelectedTab}
customText={customText}
setAmount={setAmount}
/>
</Modal>

View file

@ -72,7 +72,8 @@ export default function FilePage(props: Props) {
const isMobile = useIsMobile();
const [showComments, setShowComments] = React.useState(undefined);
// Auto-open the drawer on Mobile view if there is a linked comment
const [showComments, setShowComments] = React.useState(linkedCommentId);
const cost = costInfo ? costInfo.cost : null;
const hasFileInfo = fileInfo !== undefined;
@ -186,11 +187,7 @@ export default function FilePage(props: Props) {
);
}
const commentsListElement = commentsDisabled ? (
<Empty text={__('The creator of this content has disabled comments.')} />
) : (
<CommentsList uri={uri} linkedCommentId={linkedCommentId} />
);
const commentsListProps = { uri, linkedCommentId };
return (
<Page className="file-page" filePage isMarkdown={isMarkdown}>
@ -216,20 +213,22 @@ export default function FilePage(props: Props) {
{RENDER_MODES.FLOATING_MODES.includes(renderMode) && <FileTitleSection uri={uri} />}
<React.Suspense fallback={null}>
{isMobile ? (
{commentsDisabled ? (
<Empty text={__('The creator of this content has disabled comments.')} />
) : isMobile ? (
<>
<SwipeableDrawer
open={Boolean(showComments)}
toggleDrawer={() => setShowComments(!showComments)}
title={commentsListTitle}
>
{commentsListElement}
<CommentsList {...commentsListProps} />
</SwipeableDrawer>
<DrawerExpandButton label={commentsListTitle} toggleDrawer={() => setShowComments(!showComments)} />
</>
) : (
commentsListElement
<CommentsList {...commentsListProps} />
)}
</React.Suspense>
</section>

View file

@ -14,6 +14,44 @@
overflow: visible;
margin-bottom: var(--spacing-m);
@media (max-width: $breakpoint-small) {
overflow-y: scroll;
height: 100%;
.card__main-actions {
margin-top: 0px !important;
.comment__sort {
margin: 0px !important;
display: inline;
button {
padding: var(--spacing-xxs);
span {
font-size: var(--font-xxsmall);
}
}
}
> .button--alt {
padding: var(--spacing-xxs) var(--spacing-s);
}
.comment__sort + button {
margin: 0px var(--spacing-xxs);
}
.button + .commentCreate {
margin-top: var(--spacing-xxs);
}
}
}
}
.card--comments-list {
@extend .card--enable-overflow;
@media (max-width: $breakpoint-small) {
overflow-y: scroll;
height: 100%;
@ -362,7 +400,7 @@
margin: 0;
}
.media__subtitle--centered::before {
span + .media__subtitle--centered::before {
content: '';
margin: 0 5px;
}
@ -491,3 +529,10 @@
}
}
}
.ReactModalPortal {
.button--close {
top: 0;
right: 0;
}
}

View file

@ -21,10 +21,16 @@ $thumbnailWidthSmall: 1rem;
justify-content: space-between;
align-items: flex-end;
}
@media (min-width: $breakpoint-small) {
fieldset-section + .section {
margin-top: var(--spacing-m);
}
}
}
@media (max-width: $breakpoint-small) {
.commentCreate + .empty__wrap {
.empty__wrap {
p {
font-size: var(--font-small);
text-align: center;
@ -32,6 +38,16 @@ $thumbnailWidthSmall: 1rem;
}
}
.comment-create--drawer {
.MuiPaper-root {
background-color: var(--color-background) !important;
span {
color: var(--color-text);
}
}
}
.commentCreate--reply {
margin-top: var(--spacing-m);
position: relative;
@ -71,6 +87,15 @@ $thumbnailWidthSmall: 1rem;
fieldset-section {
font-size: var(--font-xxsmall);
}
span {
font-size: var(--font-xxsmall);
}
select {
height: 1rem;
margin: var(--spacing-xxs) 0px;
}
}
}
@ -86,12 +111,25 @@ $thumbnailWidthSmall: 1rem;
margin-right: var(--spacing-m);
font-size: var(--font-large);
}
@media (max-width: $breakpoint-small) {
padding: var(--spacing-xs);
span {
font-size: var(--font-xsmall);
}
}
}
.commentCreate__minAmountNotice {
.comment-create__min-amount-notice {
.icon {
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
}
@media (max-width: $breakpoint-small) {
margin: 0px;
font-size: var(--font-xsmall);
}
}
.commentCreate__stickerPreview {
@ -131,4 +169,13 @@ $thumbnailWidthSmall: 1rem;
margin-left: var(--spacing-xxs);
}
}
@media (max-width: $breakpoint-small) {
padding: var(--spacing-xs);
height: 7rem;
span {
font-size: var(--font-xsmall);
}
}
}

View 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);
}
}

View file

@ -499,6 +499,10 @@ $thumbnailWidthSmall: 1.8rem;
max-width: 100%;
max-height: 100%;
}
@media (max-width: $breakpoint-small) {
height: 5rem;
}
}
.emote {

View file

@ -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);
}
}
}
}
}

View file

@ -122,6 +122,16 @@ $recent-msg-button__height: 2rem;
}
.livestream__comments {
display: flex;
flex-direction: column-reverse;
font-size: var(--font-small);
overflow-y: scroll;
overflow-x: visible;
padding-top: var(--spacing-s);
width: 100%;
}
.livestream__comments--mobile {
display: flex;
flex-direction: column;
font-size: var(--font-small);
@ -131,10 +141,9 @@ $recent-msg-button__height: 2rem;
width: 100%;
}
.livestreamComments__scrollToRecent {
.livestream-comments__scroll-to-recent {
margin-top: -$recent-msg-button__height;
align-self: center;
margin-bottom: var(--spacing-xs);
font-size: var(--font-xsmall);
padding: var(--spacing-xxs) var(--spacing-s);
opacity: 0.9;
@ -142,6 +151,14 @@ $recent-msg-button__height: 2rem;
&:hover {
opacity: 1;
}
@media (min-width: $breakpoint-small) {
margin-bottom: var(--spacing-xs);
}
@media (max-width: $breakpoint-small) {
bottom: var(--spacing-xxs);
}
}
.livestream__comment-create {
@ -150,7 +167,7 @@ $recent-msg-button__height: 2rem;
margin-top: auto;
@media (max-width: $breakpoint-small) {
padding: var(--spacing-xxs);
padding: 0px;
span,
select,
@ -186,7 +203,7 @@ $recent-msg-button__height: 2rem;
.livestream-superchats__wrapper--mobile {
@extend .livestream-superchats__wrapper;
z-index: 1300;
z-index: 9999999;
width: 100%;
background-color: transparent;
padding: 0px;
@ -224,6 +241,7 @@ $recent-msg-button__height: 2rem;
left: 0;
right: 0;
width: 100%;
z-index: 1;
}
}
}
@ -268,24 +286,15 @@ $recent-msg-button__height: 2rem;
border: 1px solid var(--color-border);
.livestream__comment {
overflow: unset;
overflow: hidden;
.livestreamComment__body {
margin: 0px;
width: 100%;
overflow: scroll;
.markdown-preview {
p,
.button__label {
white-space: nowrap !important;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
}
a {
pointer-events: none;
}
&::-webkit-scrollbar {
width: 0 !important;
}
}
}
@ -378,6 +387,7 @@ $recent-msg-button__height: 2rem;
span {
font-size: var(--font-xxsmall);
color: var(--color-text-subtitle);
}
}

View file

@ -127,6 +127,12 @@
flex: 1;
max-width: 100%;
}
@media (max-width: $breakpoint-medium) {
section + .empty__wrap {
margin: var(--spacing-m);
}
}
}
@keyframes fadeIn {

View file

@ -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;
}
}
}
}
}

View file

@ -6,16 +6,47 @@
font-size: inherit !important;
color: var(--color-text) !important;
.MuiOutlinedInput-notchedOutline {
visibility: hidden;
}
.create__comment {
min-height: calc(var(--height-input) * 1.5) !important;
}
}
}
.comment-create__auth {
.MuiAutocomplete-root {
margin-bottom: var(--spacing-m);
}
}
.comment-create--drawer {
.MuiPaper-root {
.form-field__two-column,
.MuiOutlinedInput-root {
padding: 0px var(--spacing-xxs) !important;
}
}
}
.livestream__comment-create {
.MuiOutlinedInput-root {
padding: 0px var(--spacing-xxs) !important;
}
}
.MuiOutlinedInput-notchedOutline {
visibility: none;
}
.comment-create--drawer .MuiOutlinedInput-notchedOutline {
border: 1px solid var(--color-border) !important;
border-radius: 0 !important;
}
.card__main-actions .commentCreate .MuiOutlinedInput-notchedOutline {
border: 1px solid var(--color-border) !important;
border-radius: var(--border-radius) !important;
}
@media (max-width: $breakpoint-small) {
.MuiOutlinedInput-input {
padding: 0px var(--spacing-xxs);
@ -25,19 +56,15 @@
font-size: var(--font-xsmall) !important;
flex-wrap: nowrap !important;
color: var(--color-text) !important;
padding: 0px 9px !important;
padding: 0px !important;
textarea {
border: none;
margin: 9px 0px;
}
button:not(:first-of-type):not(:last-of-type) {
margin: 0px var(--spacing-xxs);
}
button + div {
margin-left: var(--spacing-xxs);
button {
padding: var(--spacing-xxs);
}
.button--primary {
@ -130,3 +157,11 @@
.MuiAutocomplete-loading {
color: var(--color-text) !important;
}
.MuiPaper-root {
.form-field__two-column {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
}

View file

@ -167,6 +167,23 @@
padding: var(--spacing-s) var(--spacing-m);
height: var(--button-height);
}
@media (max-width: $breakpoint-small) {
margin-top: var(--spacing-xxs);
button {
height: 2rem;
padding: 0px var(--spacing-s);
.button__content {
height: unset;
span {
font-size: var(--font-xxsmall);
}
}
}
}
}
.section__actions--centered {

View file

@ -14,6 +14,45 @@
}
}
.commentCreate {
.tabs {
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
margin-bottom: var(--spacing-m);
.selector-menu {
border: 0 !important;
button {
margin: var(--spacing-xxs);
}
}
}
}
.tabs__list--comment-selector {
@extend .tabs__list--channel-page;
padding-left: var(--spacing-m) !important;
border: 0 !important;
border-bottom: 1px solid var(--color-border) !important;
border-radius: 0 !important;
margin-bottom: 0px !important;
+ .tab__divider {
display: none;
}
@media (max-width: $breakpoint-small) {
position: sticky;
padding: 0px;
height: 3rem !important;
button {
font-size: var(--font-xxsmall);
}
}
}
.tabs__list--channel-page {
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-xl));
padding-right: var(--spacing-m);