feat: upgraded emoji-mart and added theme support

This commit is contained in:
William Grant 2022-09-30 13:55:38 +10:00
parent dd58d29450
commit c6af1a7468
11 changed files with 109 additions and 3000 deletions

View File

@ -83,7 +83,8 @@
"rebuild-curve25519-js": "cd node_modules/curve25519-js && yarn install && yarn build && cd ../../" "rebuild-curve25519-js": "cd node_modules/curve25519-js && yarn install && yarn build && cd ../../"
}, },
"dependencies": { "dependencies": {
"@emoji-mart/data": "1.0.2", "@emoji-mart/data": "^1.0.6",
"@emoji-mart/react": "^1.0.1",
"@reduxjs/toolkit": "^1.4.0", "@reduxjs/toolkit": "^1.4.0",
"abort-controller": "3.0.0", "abort-controller": "3.0.0",
"auto-bind": "^4.0.0", "auto-bind": "^4.0.0",
@ -104,7 +105,7 @@
"electron-is-dev": "^1.1.0", "electron-is-dev": "^1.1.0",
"electron-localshortcut": "^3.2.1", "electron-localshortcut": "^3.2.1",
"electron-updater": "^4.2.2", "electron-updater": "^4.2.2",
"emoji-mart": "5.1.0", "emoji-mart": "^5.2.2",
"filesize": "3.6.1", "filesize": "3.6.1",
"firstline": "1.2.1", "firstline": "1.2.1",
"fs-extra": "9.0.0", "fs-extra": "9.0.0",

File diff suppressed because one or more lines are too long

View File

@ -170,77 +170,6 @@
} }
} }
.session-emoji-panel {
position: absolute;
bottom: 68px;
right: 0px;
padding: var(--margins-lg);
z-index: 5;
opacity: 0;
visibility: hidden;
transition: var(--default-duration);
button:focus {
outline: none;
}
&.show {
opacity: 1;
visibility: visible;
}
& > section.emoji-mart {
font-family: $session-font-default;
font-size: $session-font-sm;
background-color: var(--color-cell-background);
border: 1px solid var(--color-session-border);
border-radius: 8px;
padding-bottom: var(--margins-sm);
.emoji-mart-category-label {
top: -2px;
span {
font-family: $session-font-default;
padding-top: var(--margins-sm);
background-color: var(--color-cell-background);
}
}
.emoji-mart-scroll {
height: 340px;
}
.emoji-mart-category .emoji-mart-emoji span {
cursor: pointer;
}
.emoji-mart-bar:last-child {
border: none;
.emoji-mart-preview {
display: none;
}
}
&:after {
content: '';
position: absolute;
top: calc(100% - 40px);
left: calc(100% - 79px);
width: 22px;
height: 22px;
background-color: var(--color-cell-background);
transform: rotate(45deg);
border-radius: 3px;
transform: scaleY(1.4) rotate(45deg);
border: 0.7px solid var(--color-session-border);
clip-path: polygon(100% 100%, 7.2px 100%, 100% 7.2px);
}
}
}
.send-message-button { .send-message-button {
animation: fadein var(--default-duration); animation: fadein var(--default-duration);

View File

@ -1,17 +1,23 @@
import React, { forwardRef, MutableRefObject, useEffect } from 'react'; import React, { forwardRef, useEffect, useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import styled from 'styled-components'; import styled from 'styled-components';
import data from '@emoji-mart/data';
// @ts-ignore // @ts-ignore
import { Picker } from '../../../node_modules/emoji-mart/dist/index.cjs'; import Picker from '@emoji-mart/react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { getTheme } from '../../state/selectors/theme'; import { getTheme } from '../../state/selectors/theme';
import { noop } from 'lodash';
import { loadEmojiPanelI18n } from '../../util/i18n';
import { FixedBaseEmoji, FixedPickerProps } from '../../types/Reaction'; import { FixedBaseEmoji, FixedPickerProps } from '../../types/Reaction';
import { ThemeStateType } from '../../themes/colors.js'; import { COLORS, PrimaryColorStateType, THEMES, ThemeStateType } from '../../themes/colors.js';
import { hexColorToRGB } from '../../util/hexColorToRGB';
import { getPrimaryColor } from '../../state/selectors/primaryColor';
import { i18nEmojiData } from '../../util/emoji';
export const StyledEmojiPanel = styled.div<{ isModal: boolean; theme: ThemeStateType }>` export const StyledEmojiPanel = styled.div<{
isModal: boolean;
primaryColor: PrimaryColorStateType;
theme: ThemeStateType;
panelBackgroundRGB: string;
panelTextRGB: string;
}>`
padding: var(--margins-lg); padding: var(--margins-lg);
z-index: 5; z-index: 5;
opacity: 0; opacity: 0;
@ -29,41 +35,25 @@ export const StyledEmojiPanel = styled.div<{ isModal: boolean; theme: ThemeState
} }
em-emoji-picker { em-emoji-picker {
background-color: var(--color-cell-background); ${props => props.panelBackgroundRGB && `background-color: rgb(${props.panelBackgroundRGB})`};
border: 1px solid var(--color-session-border); border: 1px solid var(--border-color);
padding-bottom: var(--margins-sm); padding-bottom: var(--margins-sm);
--shadow: none;
--border-radius: 8px;
--color-border: var(--color-session-border);
--font-family: var(--font-default); --font-family: var(--font-default);
--font-size: var(--font-size-sm); --font-size: var(--font-size-sm);
--rgb-accent: 0, 247, 130; // Constants.UI.COLORS.GREEN --shadow: none;
--border-radius: 8px;
${props => { --color-border: var(--border-color);
switch (props.theme) { --color-border-over: var(--border-color);
case 'ocean-dark': --background-rgb: ${props => props.panelBackgroundRGB};
// TODO Theming --rgb-background: ${props => props.panelBackgroundRGB};
return ``; --rgb-color: ${props => props.panelTextRGB};
case 'ocean-light': --rgb-input: ${props => props.panelBackgroundRGB};
// TODO Theming --rgb-accent: ${props =>
return ``; hexColorToRGB(
case 'classic-dark': props.primaryColor
return ` ? (COLORS.PRIMARY as any)[`${props.primaryColor.toUpperCase()}`]
--background-rgb: 27, 27, 27; // var(--color-cell-background) : COLORS.PRIMARY.GREEN
--rgb-background: 27, 27, 27; )};
--rgb-color: 255, 255, 255; // var(--color-text)
--rgb-input: 27, 27, 27;
`;
case 'classic-light':
default:
return `
--background-rgb: 249, 249, 249; // var(--color-cell-background)
--rgb-background: 249, 249, 249;
--rgb-color: 0, 0, 0; // var(--color-text)
--rgb-input: 249, 249, 249;
`;
}
}}
${props => ${props =>
!props.isModal && !props.isModal &&
@ -75,14 +65,14 @@ export const StyledEmojiPanel = styled.div<{ isModal: boolean; theme: ThemeState
left: calc(100% - 79px); left: calc(100% - 79px);
width: 22px; width: 22px;
height: 22px; height: 22px;
background-color: var(--color-cell-background);
transform: rotate(45deg); transform: rotate(45deg);
border-radius: 3px; border-radius: 3px;
transform: scaleY(1.4) rotate(45deg); transform: scaleY(1.4) rotate(45deg);
border: 0.7px solid var(--color-session-border); border: 0.7px solid var(--border-color);
clip-path: polygon(100% 100%, 7.2px 100%, 100% 7.2px); clip-path: polygon(100% 100%, 7.2px 100%, 100% 7.2px);
${props.panelBackgroundRGB && `background-color: rgb(${props.panelBackgroundRGB})`};
} }
`} `};
} }
`; `;
@ -90,6 +80,7 @@ type Props = {
onEmojiClicked: (emoji: FixedBaseEmoji) => void; onEmojiClicked: (emoji: FixedBaseEmoji) => void;
show: boolean; show: boolean;
isModal?: boolean; isModal?: boolean;
// NOTE Currently this doesn't work but we have a PR waiting to be merged to resolve this. William Grant 30/09/2022
onKeyDown?: (event: any) => void; onKeyDown?: (event: any) => void;
}; };
@ -102,45 +93,51 @@ const pickerProps: FixedPickerProps = {
export const SessionEmojiPanel = forwardRef<HTMLDivElement, Props>((props: Props, ref) => { export const SessionEmojiPanel = forwardRef<HTMLDivElement, Props>((props: Props, ref) => {
const { onEmojiClicked, show, isModal = false, onKeyDown } = props; const { onEmojiClicked, show, isModal = false, onKeyDown } = props;
const primaryColor = useSelector(getPrimaryColor);
const theme = useSelector(getTheme); const theme = useSelector(getTheme);
const emojiPanelTheme = theme.includes('light') ? 'light' : 'dark';
const pickerRef = ref as MutableRefObject<HTMLDivElement>; const [panelBackgroundRGB, setPanelBackgroundRGB] = useState('');
const [panelTextRGB, setPanelTextRGB] = useState('');
useEffect(() => { useEffect(() => {
let isCancelled = false; switch (theme) {
if (pickerRef.current !== null) { case 'ocean-dark':
if (pickerRef.current.children.length === 0) { setPanelBackgroundRGB(hexColorToRGB(THEMES.OCEAN_DARK.COLOR1));
loadEmojiPanelI18n() setPanelTextRGB(hexColorToRGB(THEMES.OCEAN_DARK.COLOR6));
.then(async i18n => { break;
if (isCancelled) { case 'ocean-light':
return; setPanelBackgroundRGB(hexColorToRGB(THEMES.OCEAN_LIGHT.COLOR7!));
} setPanelTextRGB(hexColorToRGB(THEMES.OCEAN_LIGHT.COLOR1));
// tslint:disable-next-line: no-unused-expression break;
new Picker({ case 'classic-dark':
data, setPanelBackgroundRGB(hexColorToRGB(THEMES.CLASSIC_DARK.COLOR1));
ref, setPanelTextRGB(hexColorToRGB(THEMES.CLASSIC_DARK.COLOR6));
i18n, break;
theme: emojiPanelTheme, case 'classic-light':
onEmojiSelect: onEmojiClicked, default:
onKeyDown, setPanelBackgroundRGB(hexColorToRGB(THEMES.CLASSIC_LIGHT.COLOR6));
...pickerProps, setPanelTextRGB(hexColorToRGB(THEMES.CLASSIC_LIGHT.COLOR0));
}); break;
})
.catch(noop);
}
} }
}, [theme]);
return () => {
isCancelled = true;
};
}, [data, pickerProps]);
return ( return (
<StyledEmojiPanel <StyledEmojiPanel
isModal={isModal} isModal={isModal}
primaryColor={primaryColor}
theme={theme} theme={theme}
panelBackgroundRGB={panelBackgroundRGB}
panelTextRGB={panelTextRGB}
className={classNames(show && 'show')} className={classNames(show && 'show')}
ref={ref} ref={ref}
/> >
<Picker
theme={theme.includes('light') ? 'light' : 'dark'}
i18n={i18nEmojiData}
onEmojiSelect={onEmojiClicked}
onKeyDown={onKeyDown}
{...pickerProps}
/>
</StyledEmojiPanel>
); );
}); });

View File

@ -2,7 +2,7 @@ import React from 'react';
import { SuggestionDataItem } from 'react-mentions'; import { SuggestionDataItem } from 'react-mentions';
import styled from 'styled-components'; import styled from 'styled-components';
// @ts-ignore // @ts-ignore
import { SearchIndex } from '../../../../node_modules/emoji-mart/dist/index.cjs'; import { SearchIndex } from 'emoji-mart';
import { searchSync } from '../../../util/emoji.js'; import { searchSync } from '../../../util/emoji.js';
const EmojiQuickResult = styled.span` const EmojiQuickResult = styled.span`

View File

@ -109,7 +109,7 @@ export const MessageContextMenu = (props: Props) => {
const emojiPanelRef = useRef<HTMLDivElement>(null); const emojiPanelRef = useRef<HTMLDivElement>(null);
const [showEmojiPanel, setShowEmojiPanel] = useState(false); const [showEmojiPanel, setShowEmojiPanel] = useState(false);
// emoji-mart v5.1 default dimensions // emoji-mart v5.2.2 default dimensions
const emojiPanelWidth = 354; const emojiPanelWidth = 354;
const emojiPanelHeight = 435; const emojiPanelHeight = 435;

View File

@ -21,7 +21,6 @@ import { OpenGroupData } from '../data/opengroups';
import { loadKnownBlindedKeys } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys'; import { loadKnownBlindedKeys } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import nativeEmojiData from '@emoji-mart/data'; import nativeEmojiData from '@emoji-mart/data';
import { initialiseEmojiData } from '../util/emoji'; import { initialiseEmojiData } from '../util/emoji';
import { loadEmojiPanelI18n } from '../util/i18n';
// tslint:disable: max-classes-per-file // tslint:disable: max-classes-per-file
// Globally disable drag and drop // Globally disable drag and drop
@ -176,7 +175,7 @@ Storage.onready(async () => {
await window.Events.setThemeSetting(newThemeSetting); await window.Events.setThemeSetting(newThemeSetting);
try { try {
initialiseEmojiData(nativeEmojiData); await initialiseEmojiData(nativeEmojiData);
await AttachmentDownloads.initAttachmentPaths(); await AttachmentDownloads.initAttachmentPaths();
await Promise.all([ await Promise.all([
@ -184,7 +183,6 @@ Storage.onready(async () => {
BlockedNumberController.load(), BlockedNumberController.load(),
OpenGroupData.opengroupRoomsLoad(), OpenGroupData.opengroupRoomsLoad(),
loadKnownBlindedKeys(), loadKnownBlindedKeys(),
loadEmojiPanelI18n(),
]); ]);
} catch (error) { } catch (error) {
window.log.error( window.log.error(

View File

@ -1,8 +1,13 @@
import { AbortSignal } from 'abort-controller'; import { AbortSignal } from 'abort-controller';
import { getEmojiDataFromNative } from 'emoji-mart';
import { Data } from '../../../../data/data'; import { Data } from '../../../../data/data';
import { ConversationModel } from '../../../../models/conversation'; import { ConversationModel } from '../../../../models/conversation';
import { Action, OpenGroupReactionResponse, Reaction } from '../../../../types/Reaction'; import {
import { getEmojiDataFromNative } from '../../../../util/emoji'; Action,
FixedBaseEmoji,
OpenGroupReactionResponse,
Reaction,
} from '../../../../types/Reaction';
import { Reactions } from '../../../../util/reactions'; import { Reactions } from '../../../../util/reactions';
import { OnionSending } from '../../../onions/onionSend'; import { OnionSending } from '../../../onions/onionSend';
import { UserUtils } from '../../../utils'; import { UserUtils } from '../../../utils';
@ -68,7 +73,9 @@ export const sendSogsReactionOnionV4 = async (
// The SOGS endpoint supports any text input so we need to make sure we are sending a valid unicode emoji // The SOGS endpoint supports any text input so we need to make sure we are sending a valid unicode emoji
// for an invalid input we use https://emojipedia.org/frame-with-an-x/ as a replacement since it cannot rendered as an emoji but is valid unicode // for an invalid input we use https://emojipedia.org/frame-with-an-x/ as a replacement since it cannot rendered as an emoji but is valid unicode
const emoji = getEmojiDataFromNative(reaction.emoji) ? reaction.emoji : '🖾'; // NOTE emoji-mart v5.2.2 types for getEmojiDataFromNative are broken
// @ts-ignore
const emoji = (getEmojiDataFromNative(reaction.emoji) as FixedBaseEmoji) ? reaction.emoji : '🖾';
const endpoint = `/room/${room}/reaction/${reaction.id}/${emoji}`; const endpoint = `/room/${room}/reaction/${reaction.id}/${emoji}`;
const method = reaction.action === Action.REACT ? 'PUT' : 'DELETE'; const method = reaction.action === Action.REACT ? 'PUT' : 'DELETE';
const serverPubkey = allValidRoomInfos[0].serverPublicKey; const serverPubkey = allValidRoomInfos[0].serverPublicKey;

View File

@ -1,4 +1,4 @@
import { EmojiSet } from 'emoji-mart'; import { EmojiSet, PartialI18n } from 'emoji-mart';
export const reactionLimit: number = 6; export const reactionLimit: number = 6;
@ -76,6 +76,7 @@ export interface FixedPickerProps {
noResultsEmoji?: string | undefined; noResultsEmoji?: string | undefined;
previewPosition?: 'bottom' | 'top' | 'none' | undefined; previewPosition?: 'bottom' | 'top' | 'none' | undefined;
skinTonePosition?: 'preview' | 'search' | 'none'; skinTonePosition?: 'preview' | 'search' | 'none';
i18n?: PartialI18n | undefined;
onEmojiSelect?: (emoji: FixedBaseEmoji) => void; onEmojiSelect?: (emoji: FixedBaseEmoji) => void;
onClickOutside?: () => void; onClickOutside?: () => void;
onKeyDown?: (event: any) => void; onKeyDown?: (event: any) => void;
@ -83,7 +84,6 @@ export interface FixedPickerProps {
getImageURL?: () => void; getImageURL?: () => void;
getSpritesheetURL?: () => void; getSpritesheetURL?: () => void;
// Below here I'm currently unsure of usage // Below here I'm currently unsure of usage
// i18n?: PartialI18n | undefined;
// style?: React.CSSProperties | undefined; // style?: React.CSSProperties | undefined;
// color?: string | undefined; // color?: string | undefined;
// skin?: EmojiSkin | undefined; // skin?: EmojiSkin | undefined;

View File

@ -1,4 +1,7 @@
import { FixedBaseEmoji, NativeEmojiData } from '../types/Reaction'; import { FixedBaseEmoji, NativeEmojiData } from '../types/Reaction';
// @ts-ignore
import { init, PartialI18n } from 'emoji-mart';
import { loadEmojiPanelI18n } from './i18n';
export type SizeClassType = 'default' | 'small' | 'medium' | 'large' | 'jumbo'; export type SizeClassType = 'default' | 'small' | 'medium' | 'large' | 'jumbo';
@ -40,8 +43,9 @@ export function getEmojiSizeClass(str: string): SizeClassType {
} }
export let nativeEmojiData: NativeEmojiData | null = null; export let nativeEmojiData: NativeEmojiData | null = null;
export let i18nEmojiData: PartialI18n | null = null;
export function initialiseEmojiData(data: any) { export async function initialiseEmojiData(data: any): Promise<void> {
const ariaLabels: Record<string, string> = {}; const ariaLabels: Record<string, string> = {};
Object.entries(data.emojis).forEach(([key, value]: [string, any]) => { Object.entries(data.emojis).forEach(([key, value]: [string, any]) => {
value.search = `,${[ value.search = `,${[
@ -73,6 +77,11 @@ export function initialiseEmojiData(data: any) {
data.ariaLabels = ariaLabels; data.ariaLabels = ariaLabels;
nativeEmojiData = data; nativeEmojiData = data;
i18nEmojiData = await loadEmojiPanelI18n();
// Data needs to be initialised once per page load for the emoji components
// See https://github.com/missive/emoji-mart#%EF%B8%8F%EF%B8%8F-headless-search
init({ data, i18n: i18nEmojiData });
} }
// Synchronous version of Emoji Mart's SearchIndex.search() // Synchronous version of Emoji Mart's SearchIndex.search()
@ -148,23 +157,3 @@ export function searchSync(query: string, args?: any): Array<any> {
} }
return results; return results;
} }
// No longer exists on emoji-mart v5.1
export function getEmojiDataFromNative(nativeString: string): FixedBaseEmoji | null {
if (!nativeEmojiData) {
return null;
}
const matches = Object.values(nativeEmojiData.emojis).filter((emoji: any) => {
const skinMatches = (emoji as FixedBaseEmoji).skins.filter((skin: any) => {
return skin.native === nativeString;
});
return skinMatches.length > 0;
});
if (matches.length === 0) {
return null;
}
return matches[0] as FixedBaseEmoji;
}

View File

@ -645,10 +645,15 @@
global-agent "^3.0.0" global-agent "^3.0.0"
global-tunnel-ng "^2.7.1" global-tunnel-ng "^2.7.1"
"@emoji-mart/data@1.0.2": "@emoji-mart/data@^1.0.6":
version "1.0.2" version "1.0.6"
resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.0.2.tgz#2b94c5b5f2c79611c12238438dad9516576a09ab" resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.0.6.tgz#68f71be5e023653a3f9f73f4beadfd50848e2131"
integrity sha512-+ZdzBM4llDJJvjuCEsdOYVoSlNA16MMmxKG3oF5LARkwhx6N5clr6phzneWV1qIwJsywqwG7NaBjH8DV6yzjcA== integrity sha512-8wu3ec/kLCB0Y3K+pOKyY6Ob+xtQu3XhZvntdrpOTUQZ/PO6FW5PpFw7RE1kQ/up1fsVSJBl5mZ8Gs4SPwTYeg==
"@emoji-mart/react@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@emoji-mart/react/-/react-1.0.1.tgz#46b6a2e92faf16fa9b7f9471f137fa2e3d1e8ac3"
integrity sha512-ALhLD96BOL5w+a4NI5NpmfqfF1aVjjj2qJE0dLst/OhjBfVmpteWNgn/h8LZy9ulU6AnbeS+13KnPFzDjCvRRw==
"@emotion/is-prop-valid@^0.8.8": "@emotion/is-prop-valid@^0.8.8":
version "0.8.8" version "0.8.8"
@ -3823,10 +3828,10 @@ elliptic@^6.5.3:
minimalistic-assert "^1.0.1" minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1" minimalistic-crypto-utils "^1.0.1"
emoji-mart@5.1.0: emoji-mart@^5.2.2:
version "5.1.0" version "5.2.2"
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.1.0.tgz#8a36a872e1297747342d1385bd7b7141ac2f4365" resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.2.2.tgz#1c093ffc19554dd6edfcfeec9aca43ff38bcc16e"
integrity sha512-ytXgeemyw4FormPQqWd35Vh06ZSnQFhVUqW51kASZzzjhQOPSGtiN3VCC7vDq94Pkxmsbet+Gps/qj5N90mEnw== integrity sha512-BvcrX+Ps9MxSVEjnvxupclU3MBD6WVC4WZOY26csfC6oFdaWpFhdrzeVNVBmCLPOmzY1SE0aAsqZJRNVbZ1yhQ==
emoji-regex@^8.0.0: emoji-regex@^8.0.0:
version "8.0.0" version "8.0.0"