Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add emoji #388

Merged
merged 4 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
539 changes: 524 additions & 15 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@
"@zero-tech/zapp-daos": "^0.4.0",
"@zero-tech/zapp-nfts": "^0.7.5",
"@zero-tech/zapp-staking": "0.4.8",
"@zero-tech/zui": "^0.12.0",
"@zero-tech/zui": "^0.15.0",
"classnames": "^2.3.1",
"emoji-mart": "^3.0.1",
"es6-promise-debounce": "^1.0.1",
"ethers": "^5.5.2",
"global": "^4.4.0",
"history": "^4.10.1",
"if-emoji": "^0.1.0",
"linkify-react": "^4.0.2",
"linkifyjs": "^4.0.2",
"lodash.get": "^4.4.2",
Expand Down
2 changes: 1 addition & 1 deletion src/components/chat-view-container/chat-view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { MediaType } from '../../store/messages';
import InvertedScroll from '../inverted-scroll';
import IndicatorMessage from '../indicator-message';
import { Lightbox } from '@zer0-os/zos-component-library';
import { MessageInput } from '../message-input';
import { MessageInput } from '../message-input/container';
import { Button as ConnectButton } from '../authentication/button';
import { IfAuthenticated } from '../authentication/if-authenticated';
import { Message } from '../message';
Expand Down
2 changes: 1 addition & 1 deletion src/components/chat-view-container/chat-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Lightbox } from '@zer0-os/zos-component-library';
import { provider as cloudinaryProvider } from '../../lib/cloudinary/provider';
import { User } from '../../store/authentication/types';
import { User as UserModel } from '../../store/channels/index';
import { MessageInput } from '../message-input';
import { MessageInput } from '../message-input/container';
import { IfAuthenticated } from '../authentication/if-authenticated';
import { Button as ConnectButton } from '../authentication/button';
import { Button as ComponentButton } from '@zer0-os/zos-component-library';
Expand Down
60 changes: 60 additions & 0 deletions src/components/message-input/container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { RefObject } from 'react';
import { User } from '../../store/channels';
import { UserForMention, Media } from './utils';
import { RootState } from '../../store';
import { connectContainer } from '../../store/redux-container';
import { ViewModes } from '../../shared-components/theme-engine';
import { MessageInput as MessageInputComponent } from './index';
import { ParentMessage } from '../../lib/chat/types';

export interface PublicProperties {
className?: string;
onSubmit: (message: string, mentionedUserIds: User['id'][], media: Media[]) => void;
initialValue?: string;
getUsersForMentions: (search: string) => Promise<UserForMention[]>;
renderAfterInput?: (value: string, mentionedUserIds: User['id'][]) => React.ReactNode;
onMessageInputRendered?: (textareaRef: RefObject<HTMLTextAreaElement>) => void;
id?: string;
reply?: null | ParentMessage;
onRemoveReply?: () => void;
}

export interface Properties extends PublicProperties {
viewMode: ViewModes;
}

export class Container extends React.Component<Properties> {
static mapState(state: RootState): Partial<Properties> {
const {
theme: {
value: { viewMode },
},
} = state;

return {
viewMode,
};
}

static mapActions(_props: Properties): Partial<Properties> {
return {};
}

render() {
return (
<MessageInputComponent
className={this.props.className}
initialValue={this.props.initialValue}
onSubmit={this.props.onSubmit}
getUsersForMentions={this.props.getUsersForMentions}
renderAfterInput={this.props.renderAfterInput}
onMessageInputRendered={this.props.onMessageInputRendered}
onRemoveReply={this.props.onRemoveReply}
viewMode={this.props.viewMode}
reply={this.props.reply}
/>
);
}
}

export const MessageInput = connectContainer<PublicProperties>(Container);
74 changes: 74 additions & 0 deletions src/components/message-input/emoji-picker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { shallow } from 'enzyme';
import React from 'react';
import { ViewModes } from '../../shared-components/theme-engine';
import { EmojiPicker, Properties } from './emoji-picker';
import { Picker } from 'emoji-mart';

describe('EmojiPicker', () => {
const subject = (props: Partial<Properties>) => {
const allProps: Properties = {
textareaRef: { current: null },
isOpen: true,
onOpen: jest.fn(),
onClose: jest.fn(),
value: null,
onSelect: jest.fn(),
viewMode: ViewModes.Dark,
...props,
};

return shallow(<EmojiPicker {...allProps} />);
};

const initialDocument = global.document;

beforeEach(() => {
// @ts-ignore
global.document = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
});

afterEach(() => {
global.document = initialDocument;
});

it('renders Picker', () => {
const wrapper = subject({});

expect(wrapper.find(Picker).exists()).toBeTrue();
});

it('attach mousedown listener to document', () => {
subject({});

expect(global.document.addEventListener).toHaveBeenCalledWith('mousedown', expect.any(Function));
});

it('removeEventListener mousedown listener to document', () => {
const wrapper = subject({});

(wrapper.instance() as any).componentWillUnmount();

expect(global.document.removeEventListener).toHaveBeenCalledWith('mousedown', expect.any(Function));
});

it('does not render', () => {
const wrapper = subject({ isOpen: false });

expect(wrapper.find(Picker).exists()).toBeFalsy();
});

it('passes props', () => {
const wrapper = subject({});

expect(wrapper.find(Picker).props()).toEqual(
expect.objectContaining({
theme: ViewModes.Dark,
emoji: 'mechanical_arm',
title: 'ZOS',
})
);
});
});
84 changes: 84 additions & 0 deletions src/components/message-input/emoji-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { RefObject } from 'react';
import { mentionsConfigs } from './mentions-config';
import { Picker } from 'emoji-mart';
import { ViewModes } from '../../shared-components/theme-engine';
import { mapPlainTextIndex } from './react-mentions-utils';

import 'emoji-mart/css/emoji-mart.css';

export interface Properties {
textareaRef: RefObject<HTMLTextAreaElement>;
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
value: string;
viewMode: ViewModes;
onSelect: (value: string) => void;
}

export class EmojiPicker extends React.Component<Properties> {
componentDidMount() {
document.addEventListener('mousedown', this.clickOutsideEmojiCheck);
}

componentWillUnmount() {
document.removeEventListener('mousedown', this.clickOutsideEmojiCheck);
}

clickOutsideEmojiCheck = (event: MouseEvent) => {
if (!this.props.isOpen) {
return;
}

const [emojiMart] = document.getElementsByClassName('emoji-mart');

if (emojiMart && event && event.target) {
if (!emojiMart.contains(event.target as Node)) {
this.props.onClose();
}
}
};

insertEmoji = (emoji: any) => {
const emojiToInsert = (emoji.native || emoji.colons) + ' ';

const selectionStart = this.props.textareaRef && this.props.textareaRef.current.selectionStart;
const position =
selectionStart != null ? mapPlainTextIndex(this.props.value, mentionsConfigs, selectionStart, 'START') : null;

const value = this.props.value;
const newValue =
position == null
? value + emojiToInsert
: [
value.slice(0, position),
emojiToInsert,
value.slice(position),
].join('');

this.props.onSelect(newValue);
};

get pickerViewMode() {
if (this.props.viewMode === ViewModes.Dark) {
return 'dark';
} else {
return 'light';
}
}

render() {
if (!this.props.isOpen) {
return null;
}

return (
<Picker
theme={this.pickerViewMode}
emoji='mechanical_arm'
title='ZOS'
onSelect={this.insertEmoji}
/>
);
}
}
16 changes: 16 additions & 0 deletions src/components/message-input/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Key } from '../../lib/keyboard-search';
import Dropzone from 'react-dropzone';
import { config } from '../../config';
import ReplyCard from '../reply-card/reply-card';
import { ViewModes } from '../../shared-components/theme-engine';
import { EmojiPicker } from './emoji-picker';

describe('MessageInput', () => {
const subject = (props: Partial<Properties>, child: any = <div />) => {
Expand All @@ -23,6 +25,7 @@ describe('MessageInput', () => {
addPasteListener: (_) => {},
removePasteListener: (_) => {},
},
viewMode: ViewModes.Dark,
...props,
};

Expand Down Expand Up @@ -156,6 +159,19 @@ describe('MessageInput', () => {
]);
});

describe('Emojis', () => {
const getEmojiPicker = () => {
const wrapper = subject({});
return wrapper.find(Dropzone).shallow().find(EmojiPicker);
};

it('renders', function () {
const emojiPicker = getEmojiPicker();

expect(emojiPicker.exists()).toBe(true);
});
});

async function userSearch(wrapper, search) {
const userMentionHandler = wrapper
.find(Dropzone)
Expand Down
Loading