Skip to content

Commit

Permalink
Add Emote Selector and Emote Comment creation ability
Browse files Browse the repository at this point in the history
  • Loading branch information
saltrafael committed Oct 25, 2021
1 parent cb8e790 commit 9112fea
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 8 deletions.
64 changes: 64 additions & 0 deletions ui/component/commentCreate/emote-selector.jsx
@@ -0,0 +1,64 @@
// @flow
import React from 'react';
import emoji from 'emoji-dictionary';
import Button from 'component/button';
import OptimizedImage from 'component/optimizedImage';
import * as EMOTES from 'constants/emotes';

const OLD_QUICK_EMOJIS = [
emoji.getUnicode('rocket'),
emoji.getUnicode('jeans'),
emoji.getUnicode('fire'),
emoji.getUnicode('heart'),
emoji.getUnicode('open_mouth'),
];

type Props = { commentValue: string, setCommentValue: (string) => void };

export default function EmoteSelector(props: Props) {
const { commentValue, setCommentValue } = props;

function addEmoteToComment(emote: string) {
setCommentValue(
commentValue + (commentValue && commentValue.charAt(commentValue.length - 1) !== ' ' ? ` ${emote} ` : `${emote} `)
);
}

return (
<div className="emote__selector">
<div className="emotes-list">
<div className="emotes-list--row">
<div className="emotes-list--row-title">{__('Old')}</div>
<div className="emotes-list--row-items">
{OLD_QUICK_EMOJIS.map((emoji) => (
<Button
key={emoji}
label={emoji}
button="alt"
className="button--file-action"
onClick={() => addEmoteToComment(emoji)}
/>
))}
</div>
</div>

<div className="emotes-list--row">
<div className="emotes-list--row-title">{__('Global')}</div>
<div className="emotes-list--row-items">
{Object.keys(EMOTES).map((emote) => (
<Button
key={String(emote)}
title={`:${emote.toLowerCase()}:`}
button="alt"
className="button--file-action"
onClick={() => addEmoteToComment(`:${emote.toLowerCase()}:`)}
>
<OptimizedImage src={String(EMOTES[emote])} />
</Button>
))}
</div>
</div>
</div>
</div>
);
}
18 changes: 11 additions & 7 deletions ui/component/commentCreate/view.jsx
Expand Up @@ -16,6 +16,7 @@ import CreditAmount from 'component/common/credit-amount';
import Empty from 'component/common/empty';
import I18nMessage from 'component/i18nMessage';
import Icon from 'component/common/icon';
import EmoteSelector from './emote-selector';
import React from 'react';
import SelectChannel from 'component/selectChannel';
import type { ElementRef } from 'react';
Expand Down Expand Up @@ -106,6 +107,7 @@ export function CommentCreate(props: Props) {
const [deletedComment, setDeletedComment] = React.useState(false);
const [pauseQuickSend, setPauseQuickSend] = React.useState(false);
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState();
const [showEmotes, setShowEmotes] = React.useState(false);

const selectedMentionIndex =
commentValue.indexOf('@', selectionIndex) === selectionIndex
Expand Down Expand Up @@ -205,12 +207,6 @@ export function CommentCreate(props: Props) {
window.removeEventListener('keydown', altEnterListener);
}

function handleSubmit() {
if (activeChannelClaim && commentValue.length) {
handleCreateComment();
}
}

function handleSupportComment() {
if (!activeChannelClaim) {
return;
Expand Down Expand Up @@ -509,13 +505,14 @@ export function CommentCreate(props: Props) {

return (
<Form
onSubmit={handleSubmit}
className={classnames('comment__create', {
'comment__create--reply': isReply,
'comment__create--nested-reply': isNested,
'comment__create--bottom': bottom,
})}
>
{showEmotes && <EmoteSelector commentValue={commentValue} setCommentValue={setCommentValue} />}

{!advancedEditor && (
<ChannelMentionSuggestions
uri={uri}
Expand Down Expand Up @@ -544,6 +541,7 @@ export function CommentCreate(props: Props) {
!SIMPLE_SITE && (isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor'))
}
quickActionHandler={() => !SIMPLE_SITE && setAdvancedEditor(!advancedEditor)}
openEmoteMenu={() => setShowEmotes(!showEmotes)}
onFocus={onTextareaFocus}
onBlur={onTextareaBlur}
placeholder={__('Say something about this...')}
Expand Down Expand Up @@ -601,6 +599,12 @@ export function CommentCreate(props: Props) {
: __('Comment --[button to submit something]--')
}
requiresAuth={IS_WEB}
onClick={() => {
if (activeChannelClaim && commentValue.length) {
handleCreateComment();
setShowEmotes(false);
}
}}
/>
)}
{!supportDisabled && !claimIsMine && (
Expand Down
11 changes: 11 additions & 0 deletions ui/component/common/markdown-preview.jsx
Expand Up @@ -12,10 +12,14 @@ import MarkdownLink from 'component/markdownLink';
import defaultSchema from 'hast-util-sanitize/lib/github.json';
import { formattedLinks, inlineLinks } from 'util/remark-lbry';
import { formattedTimestamp, inlineTimestamp } from 'util/remark-timestamp';
import { formattedEmote, inlineEmote } from 'util/remark-emote';
import ZoomableImage from 'component/zoomableImage';
import { CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS, SIMPLE_SITE } from 'config';
import Button from 'component/button';
import * as ICONS from 'constants/icons';
import OptimizedImage from 'component/optimizedImage';

const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;

type SimpleTextProps = {
children?: React.Node,
Expand Down Expand Up @@ -94,10 +98,15 @@ const SimpleLink = (props: SimpleLinkProps) => {

const SimpleImageLink = (props: ImageLinkProps) => {
const { src, title, alt, helpText } = props;

if (!src) {
return null;
}

if (title && RE_EMOTE.test(title)) {
return <OptimizedImage title={title} src={src} />;
}

return (
<Button
button="link"
Expand Down Expand Up @@ -248,6 +257,8 @@ const MarkdownPreview = (props: MarkdownProps) => {
.use(disableTimestamps || isMarkdownPost ? null : inlineTimestamp)
.use(disableTimestamps || isMarkdownPost ? null : formattedTimestamp)
// Emojis
.use(inlineEmote)
.use(formattedEmote)
.use(remarkEmoji)
// Render new lines without needing spaces.
.use(remarkBreaks)
Expand Down
6 changes: 5 additions & 1 deletion ui/component/optimizedImage/view.jsx
Expand Up @@ -101,8 +101,12 @@ function OptimizedImage(props: Props) {
<img
ref={ref}
{...imgProps}
style={{ display: 'none' }}
src={optimizedSrc}
onLoad={() => adjustOptimizationIfNeeded(ref.current, objectFit, src)}
onLoad={() => {
ref.current.style.display = 'inline';
adjustOptimizationIfNeeded(ref.current, objectFit, src);
}}
/>
);
}
Expand Down
38 changes: 38 additions & 0 deletions ui/scss/component/_comments.scss
Expand Up @@ -48,6 +48,44 @@ $thumbnailWidthSmall: 1rem;
}
}

.emote__selector {
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);
}

.emotes-list {
display: flex;
flex-wrap: wrap;
overflow-y: scroll;
overflow-x: hidden;
max-height: 25vh;
padding: var(--spacing-s);

.emotes-list--row-items {
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-top: var(--spacing-xxs);
}
}
}
}
}

.comment__create--reply {
margin-top: var(--spacing-m);
position: relative;
Expand Down
112 changes: 112 additions & 0 deletions ui/util/remark-emote.js
@@ -0,0 +1,112 @@
import * 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 (Object.keys(EMOTES).some((emote) => str.replaceAll(':', '').toUpperCase() === emote)) {
// 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;

node.type = 'image';
node.url = EMOTES[emoteStr.replaceAll(':', '').toUpperCase()];
node.title = emoteStr;
node.children = [{ type: 'text', value: emoteStr }];
}
};

const transform = (tree) => visit(tree, [EMOTE_NODE_TYPE], transformer);

export const formattedEmote = () => transform;

0 comments on commit 9112fea

Please sign in to comment.