Bringing in emotes, stickers, and refactors from ody #7435

Merged
jessopb merged 8 commits from reactorMentions into master 2022-01-24 17:07:09 +01:00
17 changed files with 1458 additions and 323 deletions
Showing only changes of commit 5c4cfdd4d8 - Show all commits

View file

@ -2239,5 +2239,85 @@
"A channel is required to repost on LBRY": "A channel is required to repost on LBRY", "A channel is required to repost on LBRY": "A channel is required to repost on LBRY",
"Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.", "Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.",
"Admin": "Admin", "Admin": "Admin",
":ALIEN:": ":ALIEN:",
":ANGRY_2:": ":ANGRY_2:",
":ANGRY_3:": ":ANGRY_3:",
":ANGRY_4:": ":ANGRY_4:",
":ANGRY_1:": ":ANGRY_1:",
":BLIND:": ":BLIND:",
":BLOCK:": ":BLOCK:",
":BOMB:": ":BOMB:",
":BRAIN_CHIP:": ":BRAIN_CHIP:",
":CONFIRM:": ":CONFIRM:",
":CONFUSED_1:": ":CONFUSED_1:",
":CONFUSED_2:": ":CONFUSED_2:",
":COOKING_SOMETHING_NICE:": ":COOKING_SOMETHING_NICE:",
":CRY_2:": ":CRY_2:",
":CRY_3:": ":CRY_3:",
":CRY_4:": ":CRY_4:",
":CRY_5:": ":CRY_5:",
":CRY_1:": ":CRY_1:",
":SPACE_DOGE:": ":SPACE_DOGE:",
":DONUT:": ":DONUT:",
":EGGPLANT_WITH_CONDOM:": ":EGGPLANT_WITH_CONDOM:",
":EGGPLANT:": ":EGGPLANT:",
":FIRE_UP:": ":FIRE_UP:",
":FLAT_EARTH:": ":FLAT_EARTH:",
":FLYING_SAUCER:": ":FLYING_SAUCER:",
":HEART_CHOPPER:": ":HEART_CHOPPER:",
":HYPER_TROLL:": ":HYPER_TROLL:",
":ICE_CREAM:": ":ICE_CREAM:",
":IDK:": ":IDK:",
":ILLUMINATI_1:": ":ILLUMINATI_1:",
":ILLUMINATI_2:": ":ILLUMINATI_2:",
":KISS_2:": ":KISS_2:",
":KISS_1:": ":KISS_1:",
":LASER_GUN:": ":LASER_GUN:",
":LAUGHING_2:": ":LAUGHING_2:",
":LAUGHING_1:": ":LAUGHING_1:",
":LOLLIPOP:": ":LOLLIPOP:",
":LOVE_2:": ":LOVE_2:",
":LOVE_1:": ":LOVE_1:",
":MONSTER:": ":MONSTER:",
":MUSHROOM:": ":MUSHROOM:",
":NAIL_IT:": ":NAIL_IT:",
":NO:": ":NO:",
":OUCH:": ":OUCH:",
":PREACE:": ":PREACE:",
":PIZZA:": ":PIZZA:",
":RABBIT_HOLE:": ":RABBIT_HOLE:",
":RAINBOW_PUKE_1:": ":RAINBOW_PUKE_1:",
":RAINBOW_PUKE_2:": ":RAINBOW_PUKE_2:",
":SPACE_RESITAS:": ":SPACE_RESITAS:",
":ROCK:": ":ROCK:",
":SAD:": ":SAD:",
":SALTY:": ":SALTY:",
":SCARY:": ":SCARY:",
":SLEEP:": ":SLEEP:",
":SLIME_DOWN:": ":SLIME_DOWN:",
":SMELLY_SOCKS:": ":SMELLY_SOCKS:",
":SMILE_2:": ":SMILE_2:",
":SMILE_1:": ":SMILE_1:",
":SPACE_CHAD:": ":SPACE_CHAD:",
":SPACE_JULIAN:": ":SPACE_JULIAN:",
":SPACE_TOM:": ":SPACE_TOM:",
":SPACE_WOJAK_1:": ":SPACE_WOJAK_1:",
":SPOCK:": ":SPOCK:",
":STAR:": ":STAR:",
":SUNNY_DAY:": ":SUNNY_DAY:",
":SUPRISED:": ":SUPRISED:",
":SWEET:": ":SWEET:",
":THINKING_1:": ":THINKING_1:",
":THINKING_2:": ":THINKING_2:",
":THUMB_DOWN:": ":THUMB_DOWN:",
":THUMB_UP_1:": ":THUMB_UP_1:",
":THUMB_UP_2:": ":THUMB_UP_2:",
":TINFOIL_HAT:": ":TINFOIL_HAT:",
":TROLL_KING:": ":TROLL_KING:",
":UFO:": ":UFO:",
":WAITING:": ":WAITING:",
":WHAT:": ":WHAT:",
":WOODOO_DOLL:": ":WOODOO_DOLL:",
"Global Emotes": "Global Emotes",
"--end--": "--end--" "--end--": "--end--"
} }

View file

@ -0,0 +1,68 @@
// @flow
import 'scss/component/_emote-selector.scss';
import { EMOTES_24px 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__listRowTitle">{__('Global Emotes')}</div>
<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.toLowerCase();
return (
<Button
key={String(emote)}
title={emoteName}
button="alt"
className="button--file-action"
onClick={() => addEmoteToComment(emoteName)}
>
<OptimizedImage src={emote.url} waitLoad />
</Button>
);
})}
</div>
</div>
</div>
</div>
);
}

View file

@ -1,5 +1,6 @@
// @flow // @flow
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field'; import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
import 'scss/component/_comment-create.scss';
import { FormField, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import { getChannelIdFromClaim } from 'util/claim'; import { getChannelIdFromClaim } from 'util/claim';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
@ -12,6 +13,7 @@ import ChannelMentionSuggestions from 'component/channelMentionSuggestions';
import ChannelThumbnail from 'component/channelThumbnail'; import ChannelThumbnail from 'component/channelThumbnail';
import classnames from 'classnames'; import classnames from 'classnames';
import CreditAmount from 'component/common/credit-amount'; import CreditAmount from 'component/common/credit-amount';
import EmoteSelector from './emote-selector';
import Empty from 'component/common/empty'; import Empty from 'component/common/empty';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
@ -102,6 +104,7 @@ export function CommentCreate(props: Props) {
const [deletedComment, setDeletedComment] = React.useState(false); const [deletedComment, setDeletedComment] = React.useState(false);
const [pauseQuickSend, setPauseQuickSend] = React.useState(false); const [pauseQuickSend, setPauseQuickSend] = React.useState(false);
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState(); const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState();
const [showEmotes, setShowEmotes] = React.useState(false);
const selectedMentionIndex = const selectedMentionIndex =
commentValue.indexOf('@', selectionIndex) === selectionIndex commentValue.indexOf('@', selectionIndex) === selectionIndex
@ -199,12 +202,6 @@ export function CommentCreate(props: Props) {
window.removeEventListener('keydown', altEnterListener); window.removeEventListener('keydown', altEnterListener);
} }
function handleSubmit() {
if (activeChannelClaim && commentValue.length) {
handleCreateComment();
}
}
function handleSupportComment() { function handleSupportComment() {
if (!activeChannelClaim) { if (!activeChannelClaim) {
return; return;
@ -362,6 +359,7 @@ export function CommentCreate(props: Props) {
* @param {string} [environment] Optional environment for Stripe (test|live) * @param {string} [environment] Optional environment for Stripe (test|live)
*/ */
function handleCreateComment(txid, payment_intent_id, environment) { function handleCreateComment(txid, payment_intent_id, environment) {
setShowEmotes(false);
setIsSubmitting(true); setIsSubmitting(true);
createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment) createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment)
@ -494,13 +492,20 @@ export function CommentCreate(props: Props) {
return ( return (
<Form <Form
onSubmit={handleSubmit}
className={classnames('comment__create', { className={classnames('comment__create', {
'comment__create--reply': isReply, 'comment__create--reply': isReply,
'comment__create--nested-reply': isNested, 'comment__create--nested-reply': isNested,
'comment__create--bottom': bottom, 'comment__create--bottom': bottom,
})} })}
> >
{showEmotes && (
<EmoteSelector
commentValue={commentValue}
setCommentValue={setCommentValue}
closeSelector={() => setShowEmotes(false)}
/>
)}
{!advancedEditor && ( {!advancedEditor && (
<ChannelMentionSuggestions <ChannelMentionSuggestions
uri={uri} uri={uri}
@ -524,6 +529,7 @@ export function CommentCreate(props: Props) {
} }
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')} quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
quickActionHandler={() => setAdvancedEditor(!advancedEditor)} quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
openEmoteMenu={() => setShowEmotes(!showEmotes)}
onFocus={onTextareaFocus} onFocus={onTextareaFocus}
onBlur={onTextareaBlur} onBlur={onTextareaBlur}
placeholder={__('Say something about this...')} placeholder={__('Say something about this...')}
@ -579,6 +585,7 @@ export function CommentCreate(props: Props) {
? __('Commenting...') ? __('Commenting...')
: __('Comment --[button to submit something]--') : __('Comment --[button to submit something]--')
} }
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
/> />
)} )}
{!supportDisabled && !claimIsMine && ( {!supportDisabled && !claimIsMine && (

View file

@ -1,33 +1,23 @@
// @flow // @flow
import type { ElementRef, Node } from 'react'; import 'easymde/dist/easymde.min.css';
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
import * as ICONS from 'constants/icons';
import Button from 'component/button';
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 MarkdownPreview from 'component/common/markdown-preview'; import type { ElementRef, Node } from 'react';
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
import 'easymde/dist/easymde.min.css';
import Button from 'component/button';
import emoji from 'emoji-dictionary';
const QUICK_EMOJIS = [
emoji.getUnicode('rocket'),
emoji.getUnicode('jeans'),
emoji.getUnicode('fire'),
emoji.getUnicode('heart'),
emoji.getUnicode('open_mouth'),
];
type Props = { type Props = {
name: string, name: string,
label?: string | Node, label?: string | Node,
render?: () => React$Node,
prefix?: string, prefix?: string,
postfix?: string, postfix?: string,
error?: string | boolean, error?: string | boolean,
helper?: string | React$Node, helper?: string | React$Node,
type?: string, type?: string,
onChange?: (any) => any,
defaultValue?: string | number, defaultValue?: string | number,
placeholder?: string | number, placeholder?: string | number,
children?: React$Node, children?: React$Node,
@ -43,18 +33,17 @@ type Props = {
min?: number, min?: number,
max?: number, max?: number,
quickActionLabel?: string, quickActionLabel?: string,
quickActionHandler?: (any) => any,
disabled?: boolean, disabled?: boolean,
onChange: (any) => void,
value?: string | number, value?: string | number,
noEmojis?: boolean, noEmojis?: boolean,
render?: () => React$Node,
onChange?: (any) => any,
quickActionHandler?: (any) => any,
openEmoteMenu?: () => void,
}; };
export class FormField extends React.PureComponent<Props> { export class FormField extends React.PureComponent<Props> {
static defaultProps = { static defaultProps = { labelOnLeft: false, blockWrap: true };
labelOnLeft: false,
blockWrap: true,
};
input: { current: ElementRef<any> }; input: { current: ElementRef<any> };
@ -67,14 +56,11 @@ export class FormField extends React.PureComponent<Props> {
const { autoFocus } = this.props; const { autoFocus } = this.props;
const input = this.input.current; const input = this.input.current;
if (input && autoFocus) { if (input && autoFocus) input.focus();
input.focus();
}
} }
render() { render() {
const { const {
render,
label, label,
prefix, prefix,
postfix, postfix,
@ -92,11 +78,24 @@ export class FormField extends React.PureComponent<Props> {
charCount, charCount,
textAreaMaxLength, textAreaMaxLength,
quickActionLabel, quickActionLabel,
quickActionHandler,
noEmojis, noEmojis,
render,
quickActionHandler,
openEmoteMenu,
...inputProps ...inputProps
} = this.props; } = this.props;
const errorMessage = typeof error === 'object' ? error.message : error; const errorMessage = typeof error === 'object' ? error.message : error;
// Ideally, the character count should (and can) be appended to the
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
// to pass the current value to it's callback, nor query the current
// text length from the callback. So, we'll use our own widget.
const hasCharCount = charCount !== undefined && charCount >= 0;
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
);
const Wrapper = blockWrap const Wrapper = blockWrap
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section> ? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>; : ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
@ -108,32 +107,15 @@ export class FormField extends React.PureComponent<Props> {
</div> </div>
) : null; ) : null;
let input; const inputSimple = (type: string) => (
if (type) { <>
if (type === 'radio') { <input id={name} type={type} {...inputProps} />
input = (
<Wrapper>
<input id={name} type="radio" {...inputProps} />
<label htmlFor={name}>{label}</label> <label htmlFor={name}>{label}</label>
</Wrapper> </>
); );
} else if (type === 'checkbox') {
input = ( const inputSelect = (selectClass: string) => (
<div className="checkbox"> <fieldset-section class={selectClass}>
<input id={name} type="checkbox" {...inputProps} />
<label htmlFor={name}>{label}</label>
</div>
);
} else if (type === 'range') {
input = (
<div>
<input id={name} type="range" {...inputProps} />
<label htmlFor={name}>{label}</label>
</div>
);
} else if (type === 'select') {
input = (
<fieldset-section>
{(label || errorMessage) && ( {(label || errorMessage) && (
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label> <label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
)} )}
@ -142,21 +124,21 @@ export class FormField extends React.PureComponent<Props> {
</select> </select>
</fieldset-section> </fieldset-section>
); );
} else if (type === 'select-tiny') {
input = ( const input = () => {
<fieldset-section class="select--slim"> switch (type) {
{(label || errorMessage) && ( case 'radio':
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label> return <Wrapper>{inputSimple('radio')}</Wrapper>;
)} case 'checkbox':
<select id={name} {...inputProps}> return <div className="checkbox">{inputSimple('checkbox')}</div>;
{children} case 'range':
</select> return <div>{inputSimple('range')}</div>;
</fieldset-section> case 'select':
); return inputSelect('');
} else if (type === 'markdown') { case 'select-tiny':
const handleEvents = { return inputSelect('select--slim');
contextmenu: openEditorMenu, case 'markdown':
}; const handleEvents = { contextmenu: openEditorMenu };
const getInstance = (editor) => { const getInstance = (editor) => {
// SimpleMDE max char check // SimpleMDE max char check
@ -164,9 +146,9 @@ export class FormField extends React.PureComponent<Props> {
if (textAreaMaxLength && changes.update) { if (textAreaMaxLength && changes.update) {
var str = changes.text.join('\n'); var str = changes.text.join('\n');
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from)); var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
if (delta <= 0) {
return; if (delta <= 0) return;
}
delta = instance.getValue().length + delta - textAreaMaxLength; delta = instance.getValue().length + delta - textAreaMaxLength;
if (delta > 0) { if (delta > 0) {
str = str.substr(0, str.length - delta); str = str.substr(0, str.length - delta);
@ -208,22 +190,11 @@ export class FormField extends React.PureComponent<Props> {
}, 25); }, 25);
} }
} }
} catch (err) { } catch (e) {} // Do nothing (revert to original behavior)
// Do nothing (revert to original behavior)
}
}); });
}; };
// Ideally, the character count should (and can) be appended to the return (
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
// to pass the current value to it's callback, nor query the current
// text length from the callback. So, we'll use our own widget.
const hasCharCount = charCount !== undefined && charCount >= 0;
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
);
input = (
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}> <div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
<fieldset-section> <fieldset-section>
<div className="form-field__two-column"> <div className="form-field__two-column">
@ -251,18 +222,12 @@ export class FormField extends React.PureComponent<Props> {
</fieldset-section> </fieldset-section>
</div> </div>
); );
} else if (type === 'textarea') { case 'textarea':
const hasCharCount = charCount !== undefined && charCount >= 0; return (
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
<span className="comment__char-count">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
);
input = (
<fieldset-section> <fieldset-section>
{(label || quickAction) && ( {(label || quickAction) && (
<div className="form-field__two-column"> <div className="form-field__two-column">
<div>
<label htmlFor={name}>{label}</label> <label htmlFor={name}>{label}</label>
</div>
{quickAction} {quickAction}
</div> </div>
)} )}
@ -275,28 +240,20 @@ export class FormField extends React.PureComponent<Props> {
/> />
<div className="form-field__textarea-info"> <div className="form-field__textarea-info">
{!noEmojis && ( {!noEmojis && (
<div className="form-field__quick-emojis">
{QUICK_EMOJIS.map((emoji) => (
<Button <Button
key={emoji} type="alt"
disabled={inputProps.disabled} className="button--file-action"
type="button" title="Emotes"
className="button--emoji" onClick={openEmoteMenu}
label={emoji} icon={ICONS.EMOJI}
onClick={() => { iconSize={20}
inputProps.onChange({
target: { value: inputProps.value ? `${inputProps.value} ${emoji}` : emoji },
});
}}
/> />
))}
</div>
)} )}
{countInfo} {countInfo}
</div> </div>
</fieldset-section> </fieldset-section>
); );
} else { default:
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />; const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
const inner = inputButton ? ( const inner = inputButton ? (
<input-submit> <input-submit>
@ -307,8 +264,7 @@ export class FormField extends React.PureComponent<Props> {
inputElement inputElement
); );
input = ( return (
<React.Fragment>
<fieldset-section> <fieldset-section>
{(label || errorMessage) && ( {(label || errorMessage) && (
<label htmlFor={name}> <label htmlFor={name}>
@ -318,17 +274,15 @@ export class FormField extends React.PureComponent<Props> {
{prefix && <label htmlFor={name}>{prefix}</label>} {prefix && <label htmlFor={name}>{prefix}</label>}
{inner} {inner}
</fieldset-section> </fieldset-section>
</React.Fragment>
); );
} }
} };
return ( return (
<React.Fragment> <>
{input} {type && input()}
{helper && <div className="form-field__help">{helper}</div>} {helper && <div className="form-field__help">{helper}</div>}
</React.Fragment> </>
); );
} }
} }

View file

@ -2014,4 +2014,12 @@ export const icons = {
<line x1="19" y1="20.5" x2="20" y2="20.5" /> <line x1="19" y1="20.5" x2="20" y2="20.5" />
</g> </g>
), ),
[ICONS.EMOJI]: buildIcon(
<g>
<circle cx="12" cy="12" r="10" />
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
<line x1="9" y1="9" x2="9.01" y2="9" />
<line x1="15" y1="9" x2="15.01" y2="9" />
</g>
),
}; };

View file

@ -10,13 +10,18 @@ import remarkFrontMatter from 'remark-frontmatter';
import reactRenderer from 'remark-react'; import reactRenderer from 'remark-react';
import MarkdownLink from 'component/markdownLink'; import MarkdownLink from 'component/markdownLink';
import defaultSchema from 'hast-util-sanitize/lib/github.json'; import defaultSchema from 'hast-util-sanitize/lib/github.json';
import { formatedLinks, inlineLinks } from 'util/remark-lbry'; import { formattedLinks, inlineLinks } from 'util/remark-lbry';
import { formattedTimestamp, inlineTimestamp } from 'util/remark-timestamp'; import { formattedTimestamp, inlineTimestamp } from 'util/remark-timestamp';
import { formattedEmote, inlineEmote } from 'util/remark-emote';
import ZoomableImage from 'component/zoomableImage'; import ZoomableImage from 'component/zoomableImage';
import { CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS } from 'config'; import { CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS } from 'config';
import Button from 'component/button'; import Button from 'component/button';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import { parse } from 'node-html-parser'; import { parse } from 'node-html-parser';
import OptimizedImage from 'component/optimizedImage';
const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;
type SimpleTextProps = { type SimpleTextProps = {
children?: React.Node, children?: React.Node,
}; };
@ -42,6 +47,7 @@ type MarkdownProps = {
className?: string, className?: string,
parentCommentId?: string, parentCommentId?: string,
isMarkdownPost?: boolean, isMarkdownPost?: boolean,
disableTimestamps?: boolean,
stakedLevel?: number, stakedLevel?: number,
}; };
@ -93,10 +99,15 @@ const SimpleLink = (props: SimpleLinkProps) => {
const SimpleImageLink = (props: ImageLinkProps) => { const SimpleImageLink = (props: ImageLinkProps) => {
const { src, title, alt, helpText } = props; const { src, title, alt, helpText } = props;
if (!src) { if (!src) {
return null; return null;
} }
if (title && RE_EMOTE.test(title) && src.includes('static.odycdn.com/emoticons')) {
return <OptimizedImage title={title} src={src} />;
}
return ( return (
<Button <Button
button="link" button="link"
@ -132,9 +143,20 @@ function isStakeEnoughForPreview(stakedLevel) {
// **************************************************************************** // ****************************************************************************
const MarkdownPreview = (props: MarkdownProps) => { const MarkdownPreview = (props: MarkdownProps) => {
const { content, strip, simpleLinks, noDataStore, className, parentCommentId, isMarkdownPost, stakedLevel } = props; const {
content,
strip,
simpleLinks,
noDataStore,
className,
parentCommentId,
isMarkdownPost,
disableTimestamps,
stakedLevel,
} = props;
const strippedContent = content const strippedContent = content
? content.replace(REPLACE_REGEX, (iframeHtml, y, iframeSrc) => { ? content.replace(REPLACE_REGEX, (iframeHtml) => {
// Let the browser try to create an iframe to see if the markup is valid // Let the browser try to create an iframe to see if the markup is valid
let lbrySrc; let lbrySrc;
try { try {
@ -152,6 +174,10 @@ const MarkdownPreview = (props: MarkdownProps) => {
}) })
: ''; : '';
const initialQuote = strippedContent.split(' ').find((word) => word.length > 0 || word.charAt(0) === '>');
let stripQuote;
if (initialQuote && initialQuote.charAt(0) === '>') stripQuote = true;
const remarkOptions: Object = { const remarkOptions: Object = {
sanitize: schema, sanitize: schema,
fragment: React.Fragment, fragment: React.Fragment,
@ -183,10 +209,22 @@ const MarkdownPreview = (props: MarkdownProps) => {
}; };
// Strip all content and just render text // Strip all content and just render text
if (strip) { if (strip || stripQuote) {
// Remove new lines and extra space // Remove new lines and extra space
remarkOptions.remarkReactComponents.p = SimpleText; remarkOptions.remarkReactComponents.p = SimpleText;
return ( return stripQuote ? (
<span dir="auto" className="markdown-preview">
<blockquote>
{
remark()
.use(remarkStrip)
.use(remarkFrontMatter, ['yaml'])
.use(reactRenderer, remarkOptions)
.processSync(content).contents
}
</blockquote>
</span>
) : (
<span dir="auto" className="markdown-preview"> <span dir="auto" className="markdown-preview">
{ {
remark() remark()
@ -206,11 +244,13 @@ const MarkdownPreview = (props: MarkdownProps) => {
.use(remarkAttr, remarkAttrOpts) .use(remarkAttr, remarkAttrOpts)
// Remark plugins for lbry urls // Remark plugins for lbry urls
// Note: The order is important // Note: The order is important
.use(formatedLinks) .use(formattedLinks)
.use(inlineLinks) .use(inlineLinks)
.use(isMarkdownPost ? null : inlineTimestamp) .use(disableTimestamps || isMarkdownPost ? null : inlineTimestamp)
.use(isMarkdownPost ? null : formattedTimestamp) .use(disableTimestamps || isMarkdownPost ? null : formattedTimestamp)
// Emojis // Emojis
.use(inlineEmote)
.use(formattedEmote)
.use(remarkEmoji) .use(remarkEmoji)
// Render new lines without needing spaces. // Render new lines without needing spaces.
.use(remarkBreaks) .use(remarkBreaks)

View file

@ -10,10 +10,11 @@ function scaleToDevicePixelRatio(value: number, window: any) {
type Props = { type Props = {
src: string, src: string,
objectFit?: string, objectFit?: string,
waitLoad?: boolean,
}; };
function OptimizedImage(props: Props) { function OptimizedImage(props: Props) {
const { objectFit, src, ...imgProps } = props; const { objectFit, src, waitLoad, ...imgProps } = props;
const [optimizedSrc, setOptimizedSrc] = React.useState(''); const [optimizedSrc, setOptimizedSrc] = React.useState('');
const ref = React.useRef<any>(); const ref = React.useRef<any>();
@ -101,8 +102,12 @@ function OptimizedImage(props: Props) {
<img <img
ref={ref} ref={ref}
{...imgProps} {...imgProps}
style={{ visibility: waitLoad ? 'hidden' : 'visible' }}
src={optimizedSrc} src={optimizedSrc}
onLoad={() => adjustOptimizationIfNeeded(ref.current, objectFit, src)} onLoad={() => {
if (waitLoad) ref.current.style.visibility = 'visible';
adjustOptimizationIfNeeded(ref.current, objectFit, src);
}}
/> />
); );
} }

96
ui/constants/emotes.js Normal file
View file

@ -0,0 +1,96 @@
// @flow
const buildCDNUrl = (path: string) => `https://static.odycdn.com/emoticons/${path}`;
const buildEmote = (name: string, path: string) => ({
name: __(`:${name}:`),
url: buildCDNUrl(path),
});
const getEmotes = (px: string, multiplier: string) => [
buildEmote('ALIEN', `${px}/Alien${multiplier}.png`),
buildEmote('ANGRY_2', `${px}/angry%202${multiplier}.png`),
buildEmote('ANGRY_3', `${px}/angry%203${multiplier}.png`),
buildEmote('ANGRY_4', `${px}/angry%204${multiplier}.png`),
buildEmote('ANGRY_1', `${px}/angry${multiplier}.png`),
buildEmote('BLIND', `${px}/blind${multiplier}.png`),
buildEmote('BLOCK', `${px}/block${multiplier}.png`),
buildEmote('BOMB', `${px}/bomb${multiplier}.png`),
buildEmote('BRAIN_CHIP', `${px}/Brain%20chip${multiplier}.png`),
buildEmote('CONFIRM', `${px}/CONFIRM${multiplier}.png`),
buildEmote('CONFUSED_1', `${px}/confused-1${multiplier}.png`),
buildEmote('CONFUSED_2', `${px}/confused${multiplier}.png`),
buildEmote('COOKING_SOMETHING_NICE', `${px}/cooking%20something%20nice${multiplier}.png`),
buildEmote('CRY_2', `${px}/cry%202${multiplier}.png`),
buildEmote('CRY_3', `${px}/cry%203${multiplier}.png`),
buildEmote('CRY_4', `${px}/cry%204${multiplier}.png`),
buildEmote('CRY_5', `${px}/cry%205${multiplier}.png`),
buildEmote('CRY_1', `${px}/cry${multiplier}.png`),
buildEmote('SPACE_DOGE', `${px}/doge${multiplier}.png`),
buildEmote('DONUT', `${px}/donut${multiplier}.png`),
buildEmote('EGGPLANT_WITH_CONDOM', `${px}/eggplant%20with%20condom${multiplier}.png`),
buildEmote('EGGPLANT', `${px}/eggplant${multiplier}.png`),
buildEmote('FIRE_UP', `${px}/fire%20up${multiplier}.png`),
buildEmote('FLAT_EARTH', `${px}/Flat%20earth${multiplier}.png`),
buildEmote('FLYING_SAUCER', `${px}/Flying%20saucer${multiplier}.png`),
buildEmote('HEART_CHOPPER', `${px}/heart%20chopper${multiplier}.png`),
buildEmote('HYPER_TROLL', `${px}/HyperTroll${multiplier}.png`),
buildEmote('ICE_CREAM', `${px}/ice%20cream${multiplier}.png`),
buildEmote('IDK', `${px}/IDK${multiplier}.png`),
buildEmote('ILLUMINATI_1', `${px}/Illuminati-1${multiplier}.png`),
buildEmote('ILLUMINATI_2', `${px}/Illuminati${multiplier}.png`),
buildEmote('KISS_2', `${px}/kiss%202${multiplier}.png`),
buildEmote('KISS_1', `${px}/kiss${multiplier}.png`),
buildEmote('LASER_GUN', `${px}/laser%20gun${multiplier}.png`),
buildEmote('LAUGHING_2', `${px}/Laughing 2${multiplier}.png`),
buildEmote('LAUGHING_1', `${px}/Laughing${multiplier}.png`),
buildEmote('LOLLIPOP', `${px}/Lollipop${multiplier}.png`),
buildEmote('LOVE_2', `${px}/Love%202${multiplier}.png`),
buildEmote('LOVE_1', `${px}/Love${multiplier}.png`),
buildEmote('MONSTER', `${px}/Monster${multiplier}.png`),
buildEmote('MUSHROOM', `${px}/mushroom${multiplier}.png`),
buildEmote('NAIL_IT', `${px}/Nail%20It${multiplier}.png`),
buildEmote('NO', `${px}/NO${multiplier}.png`),
buildEmote('OUCH', `${px}/ouch${multiplier}.png`),
buildEmote('PREACE', `${px}/peace${multiplier}.png`),
buildEmote('PIZZA', `${px}/pizza${multiplier}.png`),
buildEmote('RABBIT_HOLE', `${px}/rabbit%20hole${multiplier}.png`),
buildEmote('RAINBOW_PUKE_1', `${px}/rainbow%20puke-1${multiplier}.png`),
buildEmote('RAINBOW_PUKE_2', `${px}/rainbow%20puke${multiplier}.png`),
buildEmote('SPACE_RESITAS', `${px}/resitas${multiplier}.png`),
buildEmote('ROCK', `${px}/ROCK${multiplier}.png`),
buildEmote('SAD', `${px}/sad${multiplier}.png`),
buildEmote('SALTY', `${px}/salty${multiplier}.png`),
buildEmote('SCARY', `${px}/scary${multiplier}.png`),
buildEmote('SLEEP', `${px}/Sleep${multiplier}.png`),
buildEmote('SLIME_DOWN', `${px}/slime%20down${multiplier}.png`),
buildEmote('SMELLY_SOCKS', `${px}/smelly%20socks${multiplier}.png`),
buildEmote('SMILE_2', `${px}/smile%202${multiplier}.png`),
buildEmote('SMILE_1', `${px}/smile${multiplier}.png`),
buildEmote('SPACE_CHAD', `${px}/space%20chad${multiplier}.png`),
buildEmote('SPACE_JULIAN', `${px}/Space%20Julian${multiplier}.png`),
buildEmote('SPACE_TOM', `${px}/space%20Tom${multiplier}.png`),
buildEmote('SPACE_WOJAK_1', `${px}/space%20wojak-1${multiplier}.png`),
buildEmote('ANGRY_3', `${px}/space%20wojak${multiplier}.png`),
buildEmote('SPOCK', `${px}/SPOCK${multiplier}.png`),
buildEmote('STAR', `${px}/Star${multiplier}.png`),
buildEmote('SUNNY_DAY', `${px}/sunny%20day${multiplier}.png`),
buildEmote('SUPRISED', `${px}/surprised${multiplier}.png`),
buildEmote('SWEET', `${px}/sweet${multiplier}.png`),
buildEmote('THINKING_1', `${px}/thinking-1${multiplier}.png`),
buildEmote('THINKING_2', `${px}/thinking${multiplier}.png`),
buildEmote('THUMB_DOWN', `${px}/thumb%20down${multiplier}.png`),
buildEmote('THUMB_UP_1', `${px}/thumb%20up-1${multiplier}.png`),
buildEmote('THUMB_UP_2', `${px}/thumb%20up${multiplier}.png`),
buildEmote('TINFOIL_HAT', `${px}/tin%20hat${multiplier}.png`),
buildEmote('TROLL_KING', `${px}/Troll%20king${multiplier}.png`),
buildEmote('UFO', `${px}/ufo${multiplier}.png`),
buildEmote('WAITING', `${px}/waiting${multiplier}.png`),
buildEmote('WHAT', `${px}/what_${multiplier}.png`),
buildEmote('WOODOO_DOLL', `${px}/woodo%20doll${multiplier}.png`),
];
export const EMOTES_24px = getEmotes('24%20px', '');
export const EMOTES_36px = getEmotes('36px', '@1.5x');
export const EMOTES_48px = getEmotes('48%20px', '@2x');
export const EMOTES_72px = getEmotes('72%20px', '@3x');

View file

@ -178,3 +178,4 @@ export const LIFE = 'Life';
export const ARTISTS = 'Artists'; export const ARTISTS = 'Artists';
export const MYSTERIES = 'Mysteries'; export const MYSTERIES = 'Mysteries';
export const TECHNOLOGY = 'Technology'; export const TECHNOLOGY = 'Technology';
export const EMOJI = 'Emoji';

View file

@ -549,12 +549,6 @@
.button--highlighted { .button--highlighted {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
} }
.button--emoji {
font-size: 1.1rem;
border-radius: 3rem;
}
.button__content { .button__content {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -0,0 +1,82 @@
@import '../init/vars';
$thumbnailWidth: 1.5rem;
$thumbnailWidthSmall: 1rem;
.comment__create {
font-size: var(--font-small);
position: relative;
fieldset-section,
.form-field--SimpleMDE {
margin-top: 0;
}
.form-field__two-column {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
}
.comment__create--reply {
margin-top: var(--spacing-m);
position: relative;
}
.content_comment {
position: relative;
}
.comment__create--nested-reply {
margin-top: var(--spacing-s);
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
@media (min-width: $breakpoint-small) {
margin-left: calc((#{$thumbnailWidth} + var(--spacing-m)) * 2 + var(--spacing-m) + 4px);
}
}
.comment__create--bottom {
padding-bottom: 0;
}
.comment-new__label-wrapper {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: baseline;
flex-wrap: wrap;
width: 100%;
@media (min-width: $breakpoint-small) {
fieldset-section {
max-width: 10rem;
}
}
}
.comment-new__label {
white-space: nowrap;
margin-right: var(--spacing-xs);
}
.comment__sc-preview {
display: flex;
align-items: center;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
padding: var(--spacing-s);
margin: var(--spacing-s) 0;
}
.comment__sc-preview-amount {
margin-right: var(--spacing-m);
font-size: var(--font-large);
}
.comment--min-amount-notice {
.icon {
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
}
}

View file

@ -32,29 +32,6 @@ $thumbnailWidthSmall: 1rem;
} }
} }
.comment__create {
font-size: var(--font-small);
position: relative;
fieldset-section,
.form-field--SimpleMDE {
margin-top: 0;
}
.form-field__two-column {
column-count: 2;
}
}
.comment__create--reply {
margin-top: var(--spacing-m);
position: relative;
}
.comment__create--bottom {
padding-bottom: 0;
}
.comment { .comment {
width: 100%; width: 100%;
display: flex; display: flex;
@ -90,10 +67,6 @@ $thumbnailWidthSmall: 1rem;
} }
} }
.content_comment {
position: relative;
}
.comment__thumbnail-wrapper { .comment__thumbnail-wrapper {
flex: 0; flex: 0;
margin-top: var(--spacing-xxs); margin-top: var(--spacing-xxs);
@ -136,24 +109,10 @@ $thumbnailWidthSmall: 1rem;
opacity: 0.6; opacity: 0.6;
} }
.comment__sc-preview {
display: flex;
align-items: center;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
padding: var(--spacing-s);
margin: var(--spacing-s) 0;
}
.comment__edit-input { .comment__edit-input {
margin-top: var(--spacing-xxs); margin-top: var(--spacing-xxs);
} }
.comment__sc-preview-amount {
margin-right: var(--spacing-m);
font-size: var(--font-large);
}
.comment__threadline { .comment__threadline {
@extend .button--alt; @extend .button--alt;
height: auto; height: auto;
@ -173,26 +132,6 @@ $thumbnailWidthSmall: 1rem;
} }
} }
.comment-new__label-wrapper {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: baseline;
flex-wrap: wrap;
width: 100%;
@media (min-width: $breakpoint-small) {
fieldset-section {
max-width: 10rem;
}
}
}
.comment-new__label {
white-space: nowrap;
margin-right: var(--spacing-xs);
}
.comment--highlighted { .comment--highlighted {
background: var(--color-comment-highlighted); background: var(--color-comment-highlighted);
box-shadow: 0 0 0 5px var(--color-comment-highlighted); box-shadow: 0 0 0 5px var(--color-comment-highlighted);
@ -429,8 +368,7 @@ $thumbnailWidthSmall: 1rem;
@extend .comment__action; @extend .comment__action;
} }
.comment__action--nested, .comment__action--nested {
.comment__create--nested-reply {
margin-top: var(--spacing-s); margin-top: var(--spacing-s);
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px); margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
@ -485,12 +423,6 @@ $thumbnailWidthSmall: 1rem;
opacity: 0.5; opacity: 0.5;
} }
.comment--min-amount-notice {
.icon {
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
}
}
.comments-own { .comments-own {
.section__actions { .section__actions {
align-items: flex-start; align-items: flex-start;

View file

@ -0,0 +1,40 @@
@import '../init/vars';
.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

@ -448,9 +448,7 @@ fieldset-group {
} }
.form-field__quick-action { .form-field__quick-action {
float: right;
font-size: var(--font-xsmall); font-size: var(--font-xsmall);
margin-top: 2.5%;
} }
.form-field__textarea-info { .form-field__textarea-info {
@ -462,12 +460,6 @@ fieldset-group {
margin-bottom: var(--spacing-s); margin-bottom: var(--spacing-s);
} }
.form-field__quick-emojis {
> *:not(:last-child) {
margin-right: var(--spacing-s);
}
}
fieldset-section { fieldset-section {
.form-field__internal-option { .form-field__internal-option {
margin-top: var(--spacing-s); margin-top: var(--spacing-s);

125
ui/util/remark-emote.js Normal file
View file

@ -0,0 +1,125 @@
import { EMOTES_24px as EMOTES } from 'constants/emotes';
import visit from 'unist-util-visit';
const EMOTE_NODE_TYPE = 'emote';
const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;
// ***************************************************************************
// Tokenize emote
// ***************************************************************************
function findNextEmote(value, fromIndex, strictlyFromIndex) {
let begin = 0;
while (begin < value.length) {
const match = value.substring(begin).match(RE_EMOTE);
if (!match) return null;
match.index += begin;
if (strictlyFromIndex && match.index !== fromIndex) {
if (match.index > fromIndex) {
// Already gone past desired index. Skip the rest.
return null;
} else {
// Next match might fit 'fromIndex'.
begin = match.index + match[0].length;
continue;
}
}
if (fromIndex > 0 && fromIndex > match.index && fromIndex < match.index + match[0].length) {
// Skip previously-rejected word
// This assumes that a non-zero 'fromIndex' means that a previous lookup has failed.
begin = match.index + match[0].length;
continue;
}
const str = match[0];
if (EMOTES.some(({ name }) => str.toUpperCase() === name)) {
// Profit!
return { text: str, index: match.index };
}
if (strictlyFromIndex && match.index >= fromIndex) {
return null; // Since it failed and we've gone past the desired index, skip the rest.
}
begin = match.index + match[0].length;
}
return null;
}
function locateEmote(value, fromIndex) {
const emote = findNextEmote(value, fromIndex, false);
return emote ? emote.index : -1;
}
// Generate 'emote' markdown node
const createEmoteNode = (text) => ({
type: EMOTE_NODE_TYPE,
value: text,
children: [{ type: 'text', value: text }],
});
// Generate a markdown image from emote
function tokenizeEmote(eat, value, silent) {
if (silent) return true;
const emote = findNextEmote(value, 0, true);
if (emote) {
try {
const text = emote.text;
return eat(text)(createEmoteNode(text));
} catch (e) {}
}
}
tokenizeEmote.locator = locateEmote;
export function inlineEmote() {
const Parser = this.Parser;
const tokenizers = Parser.prototype.inlineTokenizers;
const methods = Parser.prototype.inlineMethods;
// Add an inline tokenizer (defined in the following example).
tokenizers.emote = tokenizeEmote;
// Run it just before `text`.
methods.splice(methods.indexOf('text'), 0, 'emote');
}
// ***************************************************************************
// Format emote
// ***************************************************************************
const transformer = (node, index, parent) => {
if (node.type === EMOTE_NODE_TYPE && parent && parent.type === 'paragraph') {
const emoteStr = node.value;
const emote = EMOTES.find(({ name }) => emoteStr.toUpperCase() === name);
node.type = 'image';
node.url = emote.url;
node.title = emoteStr;
node.children = [{ type: 'text', value: emoteStr }];
if (!node.data || !node.data.hProperties) {
// Create new node data
node.data = {
hProperties: { emote: true },
};
} else if (node.data.hProperties) {
// Don't overwrite current attributes
node.data.hProperties = {
emote: true,
...node.data.hProperties,
};
}
}
};
const transform = (tree) => visit(tree, [EMOTE_NODE_TYPE], transformer);
export const formattedEmote = () => transform;

View file

@ -149,7 +149,7 @@ const transform = (tree) => {
visit(tree, ['link'], visitor); visit(tree, ['link'], visitor);
}; };
export const formatedLinks = () => transform; export const formattedLinks = () => transform;
// Main module // Main module
export function inlineLinks() { export function inlineLinks() {

View file

@ -0,0 +1,711 @@
@import '../init/mixins';
input,
textarea,
select,
.date-picker-input {
height: var(--height-input);
border-radius: var(--border-radius);
border: 1px solid;
color: var(--color-input);
border-color: var(--color-input-border);
background-color: var(--color-input-bg);
padding-right: var(--spacing-s);
padding-left: var(--spacing-s);
&:focus {
@include focus;
}
&::placeholder {
color: var(--color-input-placeholder);
opacity: 0.4;
}
&:disabled {
opacity: 0.4;
& + label {
opacity: 0.4;
}
}
&[type='range'] {
height: auto;
height: 0.5rem;
background-color: var(--color-secondary);
}
}
checkbox-element,
radio-element,
select {
cursor: pointer;
}
select {
background-image: var(--select-toggle-background);
background-position: 99% center;
background-repeat: no-repeat;
background-size: 1rem;
padding-right: var(--spacing-l);
padding-left: var(--spacing-s);
font-weight: bold;
}
fieldset-group {
display: flex;
flex-direction: row;
justify-content: space-between;
&.fieldset-group--smushed {
fieldset-section + fieldset-section {
margin-top: 0;
}
}
}
fieldset-section,
fieldset-group,
form,
.checkbox,
.radio,
.form-field--SimpleMDE,
.form-field__help {
+ fieldset-section,
+ fieldset-group,
+ form,
+ .checkbox,
+ .radio,
+ .form-field--SimpleMDE {
margin-top: var(--spacing-l);
}
+ .form-field__help {
margin-top: var(--spacing-s);
}
&:last-child {
margin-bottom: 0;
}
input,
select {
width: 100%;
}
}
fieldset-section,
.checkbox,
.radio {
display: flex;
flex-direction: column;
}
label {
font-size: var(--font-small);
color: var(--color-input-label);
display: inline-block;
margin-bottom: 0.1rem;
.icon__lbc {
margin-bottom: 4px;
}
}
input-submit {
display: flex;
& > *:first-child,
& > *:nth-child(2) {
margin: 0;
}
& > *:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
}
& > *:nth-child(2) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border: 1px solid var(--color-border);
}
}
.checkbox,
.radio {
position: relative;
input[type='checkbox'],
input[type='radio'] {
height: var(--height-checkbox);
width: var(--height-checkbox);
position: absolute;
border: none;
left: 0;
padding: 0;
background-color: transparent;
&:disabled + label {
cursor: default;
pointer-events: none;
}
}
label {
position: relative;
display: inline-block;
margin: 0;
font-size: var(--font-base);
padding-left: calc(var(--height-checkbox) + var(--spacing-s));
min-height: var(--height-checkbox);
&::before {
background-color: var(--color-input-toggle-bg);
}
&:hover {
&::before {
background-color: var(--color-input-toggle-bg-hover);
}
}
}
label::before,
label::after {
position: absolute;
content: '';
}
// Hide the checkmark by default
input[type='checkbox'] + label::after,
input[type='radio'] + label::after {
content: none;
}
// Unhide on the checked state
input[type='checkbox']:checked + label::after,
input[type='radio']:checked + label::after {
content: '';
}
input[type='checkbox']:focus + label::before,
input[type='radio']:focus + label::before {
@include focus;
}
}
.checkbox {
// Outer box of the fake checkbox
label::before {
height: var(--height-checkbox);
width: var(--height-checkbox);
border: 1px solid var(--color-input-border);
border-radius: var(--border-radius);
left: 0px;
top: -1px;
}
// Checkmark of the fake checkbox
label::after {
height: 6px;
width: 12px;
border-left: 2px solid;
border-bottom: 2px solid;
border-color: var(--color-input-toggle);
border-left-color: var(--color-input-toggle);
transform: rotate(-45deg);
left: 6px;
top: 6px;
}
}
.radio {
input[type='radio'] {
border-radius: 50%;
}
// Outer box of the fake radio
label::before {
height: var(--height-radio);
width: var(--height-radio);
border: 1px solid var(--color-input-border);
border-radius: calc(var(--height-radio) * 0.5);
left: 0px;
top: -1px;
}
// Checkmark of the fake radio
label::after {
height: 12px;
width: 12px;
border-radius: 50%;
background-color: var(--color-primary);
left: 6px;
top: 5px;
}
}
.range__label {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: var(--spacing-m);
> * {
width: 33%;
text-align: center;
&:first-of-type {
text-align: left;
}
&:last-of-type {
text-align: right;
}
}
}
.fieldset-group {
@extend fieldset-group;
}
.fieldset-section {
@extend fieldset-section;
}
.input-submit {
@extend input-submit;
}
input-submit {
align-items: center;
input {
z-index: 2;
}
}
input[type='number'] {
width: 8rem;
}
fieldset-group {
+ fieldset-group {
margin-top: var(--spacing-s);
}
&.fieldset-group--smushed {
justify-content: flex-start;
fieldset-section {
width: auto;
margin: 0;
&:first-child {
input,
select {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
&:nth-of-type(2) {
input,
select {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
label {
margin-left: var(--spacing-s);
}
}
}
&.fieldgroup--paginate {
padding-bottom: var(--spacing-l);
margin-top: var(--spacing-l);
align-items: flex-end;
justify-content: center;
}
}
// This is a special case where the prefix appears "inside" the input
// It would be way simpler to just use position: absolute and give it a width
// but the width can change when we use it for the name prefix
// lbry:// {input}, lbry://@short {input}, @lbry://longername {input}
// The spacing/alignment isn't very robust and will probably need to be changed
// if we use this in more places
&.fieldset-group--disabled-prefix {
align-items: flex-end;
label {
min-height: 18px;
white-space: nowrap;
// Set width 0 and overflow visible so the label can act as if it's the input label and not a random text node in a side by side div
overflow: visible;
width: 0;
}
fieldset-section:first-child {
max-width: 40%;
.form-field__prefix {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
padding: 0.5rem;
height: var(--height-input);
border: 1px solid;
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
border-color: var(--color-input-border);
border-right-color: var(--color-input-prefix-border);
color: var(--color-text);
background-color: var(--color-input-prefix-bg);
}
}
fieldset-section:last-child {
width: 100%;
label {
// Overwrite the input's label to wrap instead. This is usually
// an error message, which could be long in other languages.
width: 100%;
white-space: normal;
}
input {
border-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-color: var(--color-input-border);
padding-left: var(--spacing-xs);
}
}
}
}
.form-field--copyable {
padding: 0.2rem 0.75rem;
text-overflow: ellipsis;
user-select: text;
cursor: default;
}
.form-field--short {
width: 100%;
@media (min-width: $breakpoint-small) {
width: 25em;
}
}
.form-field--price-amount {
max-width: 6em;
}
.form-field--price-amount--auto {
width: auto;
min-width: 100%;
}
.form-field--address {
min-width: 18em;
@media (max-width: $breakpoint-xxsmall) {
min-width: 10em;
}
}
.form-field__help {
@extend .help;
}
.form-field__help + .checkbox,
.form-field__help + .radio {
margin-top: var(--spacing-l);
}
.form-field__conjuction {
padding-top: 1rem;
}
.form-field__two-column {
@media (min-width: $breakpoint-small) {
column-count: 2;
}
}
.form-field__quick-action {
font-size: var(--font-xsmall);
}
.form-field__textarea-info {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
margin-top: var(--spacing-xxs);
margin-bottom: var(--spacing-s);
}
.form-field__quick-emojis {
> *:not(:last-child) {
margin-right: var(--spacing-s);
}
}
fieldset-section {
.form-field__internal-option {
margin-top: var(--spacing-s);
margin-left: 2.2rem;
&:first-of-type {
margin-top: var(--spacing-s); // Extra specificity needed here since _section.scss is applied after this file
}
}
.select--slim {
margin-bottom: var(--spacing-xxs);
@media (min-width: $breakpoint-small) {
max-width: none;
}
select {
max-height: 1.5rem !important;
padding: 0 var(--spacing-xs);
padding-right: var(--spacing-l);
}
}
}
#automatic_dark_mode_range_start,
#automatic_dark_mode_range_end {
min-width: 6em;
}
.date-picker-input {
font-weight: bold;
.react-datetime-picker__wrapper {
border: 0;
}
}
.form-field-date-picker {
margin-bottom: var(--spacing-l);
label {
display: block;
}
.controls {
display: flex;
.date-picker-input,
.button--link {
margin-right: var(--spacing-m);
}
}
.react-datetime-picker__button {
svg {
stroke: var(--color-text);
}
}
.react-datetime-picker__button:enabled:hover .react-datetime-picker__button__icon,
.react-datetime-picker__button:enabled:focus .react-datetime-picker__button__icon {
stroke: var(--color-primary);
}
.react-date-picker__calendar {
z-index: 1000;
}
.react-calendar {
width: 350px;
max-width: 100%;
background: var(--color-card-background);
border: 1px solid #a0a096;
font-family: inherit;
line-height: 1;
}
.react-calendar--doubleView {
width: 700px;
}
.react-calendar--doubleView .react-calendar__viewContainer {
display: flex;
margin: -0.5em;
}
.react-calendar--doubleView .react-calendar__viewContainer > * {
width: 50%;
margin: 0.5em;
}
.react-calendar,
.react-calendar *,
.react-calendar *:before,
.react-calendar *:after {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding: 2px 1px;
}
.react-calendar button {
margin: 0;
border: 0;
outline: none;
}
.react-calendar button:enabled:hover {
cursor: pointer;
}
.react-calendar__navigation {
height: 44px;
margin-bottom: 1em;
color: var(--color-text);
}
.react-calendar__navigation__label {
color: var(--color-text);
}
.react-calendar__navigation button {
min-width: 44px;
background: none;
color: var(--color-text);
}
.react-calendar__navigation button:enabled:hover,
.react-calendar__navigation button:enabled:focus {
background: var(--color-button-alt-bg-hover);
}
.react-calendar__navigation button[disabled] {
color: var(--color-text);
}
.react-calendar__month-view__weekdays {
text-align: center;
text-transform: uppercase;
font-weight: bold;
font-size: 0.75em;
color: var(--color-text-alt);
}
.react-calendar__month-view__weekdays__weekday {
padding: 0.5em;
}
.react-calendar__month-view__weekNumbers {
font-weight: bold;
}
.react-calendar__month-view__weekNumbers .react-calendar__tile {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75em;
padding: calc(0.75em / 0.75) calc(0.5em / 0.75);
}
.react-calendar__month-view__days__day,
.react-calendar__month-view__days__day--weekend {
color: var(--color-text);
font-weight: normal;
}
.react-calendar__month-view__days__day--neighboringMonth {
color: var(--color-gray-5);
}
.react-calendar__year-view .react-calendar__tile,
.react-calendar__decade-view .react-calendar__tile,
.react-calendar__century-view .react-calendar__tile {
padding: 2em 0.5em;
}
.react-calendar__tile {
max-width: 100%;
text-align: center;
padding: 0.75em 0.5em;
background: none;
border-radius: var(--border-radius);
color: var(--color-text);
}
.react-calendar__tile:enabled:hover,
.react-calendar__tile:enabled:focus {
background: var(--color-button-alt-bg-hover);
}
.react-calendar__tile--now {
background: var(--color-button-secondary-bg);
}
.react-calendar__tile--now:enabled:hover,
.react-calendar__tile--now:enabled:focus {
background: var(--color-button-secondary-bg-hover);
}
.react-calendar__tile--hasActive {
color: var(--color-button-primary-text);
background: var(--color-button-primary-bg);
}
.react-calendar__tile--hasActive:enabled:hover,
.react-calendar__tile--hasActive:enabled:focus {
background: var(--color-button-primary-bg-hover);
}
.react-calendar__tile--active {
color: var(--color-button-primary-text);
background: var(--color-button-primary-bg);
}
.react-calendar__tile--active:enabled:hover,
.react-calendar__tile--active:enabled:focus {
background: var(--color-button-primary-bg-hover);
}
.react-calendar--selectRange .react-calendar__tile--hover {
background-color: #e6e6e6;
}
.react-datetime-picker__inputGroup__amPm {
background: var(--color-input-bg);
}
.react-datetime-picker__inputGroup__leadingZero {
// Not perfect, but good enough for our standard zoom levels.
margin-bottom: 1px;
}
.react-datetime-picker__inputGroup__input--hasLeadingZero {
margin-left: -0.54em;
padding-left: calc(1px + 0.54em);
}
.react-calendar__month-view__days__day--neighboringMonth {
color: var(--color-gray-5);
}
}
.form-field-calendar {
border-radius: var(--border-radius);
border: 1px solid var(--color-border);
margin-left: calc(var(--spacing-xs) * -1);
margin-bottom: var(--spacing-xs);
animation: menu-animate-in var(--animation-duration) var(--animation-style);
box-shadow: 3px 3px rgba(0, 0, 0, 0.1);
}