diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java index 35574f109..127e03ee4 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -75,6 +75,8 @@ public class MainApplication extends NavigationApplication implements INotificat @Override public NativeModule getModule(String name, ReactApplicationContext reactContext) { switch (name) { + case "MattermostManaged": + return MattermostManagedModule.getInstance(reactContext); case "MattermostShare": return new ShareModule(instance, reactContext); case "NotificationPreferences": @@ -90,6 +92,7 @@ public class MainApplication extends NavigationApplication implements INotificat public ReactModuleInfoProvider getReactModuleInfoProvider() { return () -> { Map map = new HashMap<>(); + map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false)); map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false)); map.put("NotificationPreferences", new ReactModuleInfo("NotificationPreferences", "com.mattermost.rnbeta.NotificationPreferencesModule", false, false, false, false, false)); map.put("RNTextInputReset", new ReactModuleInfo("RNTextInputReset", "com.mattermost.rnbeta.RNTextInputResetModule", false, false, false, false, false)); diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java new file mode 100644 index 000000000..b74bde4d7 --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java @@ -0,0 +1,53 @@ +package com.mattermost.rnbeta; + +import android.app.Activity; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; + +public class MattermostManagedModule extends ReactContextBaseJavaModule { + private static MattermostManagedModule instance; + + private static final String TAG = MattermostManagedModule.class.getSimpleName(); + + private MattermostManagedModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + public static MattermostManagedModule getInstance(ReactApplicationContext reactContext) { + if (instance == null) { + instance = new MattermostManagedModule(reactContext); + } + + return instance; + } + + public static MattermostManagedModule getInstance() { + return instance; + } + + @Override + @NonNull + public String getName() { + return "MattermostManaged"; + } + + @ReactMethod + public void isRunningInSplitView(final Promise promise) { + WritableMap result = Arguments.createMap(); + Activity current = getCurrentActivity(); + if (current != null) { + result.putBoolean("isSplitView", current.isInMultiWindowMode()); + } else { + result.putBoolean("isSplitView", false); + } + + promise.resolve(result); + } +} diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts new file mode 100644 index 000000000..39b47ec0b --- /dev/null +++ b/app/actions/local/channel.ts @@ -0,0 +1,228 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Model} from '@nozbe/watermelondb'; + +import {fetchChannelByName, fetchMyChannelsForTeam, joinChannel, markChannelAsViewed} from '@actions/remote/channel'; +import {fetchPostsForChannel} from '@actions/remote/post'; +import {fetchRolesIfNeeded} from '@actions/remote/role'; +import {addUserToTeam, fetchTeamByName, removeUserFromTeam} from '@actions/remote/team'; +import {General} from '@constants'; +import DatabaseManager from '@database/manager'; +import {privateChannelJoinPrompt} from '@helpers/api/channel'; +import {prepareMyChannelsForTeam, queryMyChannel} from '@queries/servers/channel'; +import {addChannelToTeamHistory, prepareMyTeams, queryMyTeamById, queryTeamById, queryTeamByName} from '@queries/servers/team'; +import {queryCommonSystemValues, setCurrentChannelId, setCurrentTeamAndChannelId} from '@queries/servers/system'; +import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url'; + +import type {IntlShape} from 'react-intl'; + +import type ChannelModel from '@typings/database/models/servers/channel'; +import type MyChannelModel from '@typings/database/models/servers/my_channel'; +import type MyTeamModel from '@typings/database/models/servers/my_team'; +import type TeamModel from '@typings/database/models/servers/team'; + +export const switchToChannel = async (serverUrl: string, channelId: string) => { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (!database) { + return {error: `${serverUrl} database not found`}; + } + + try { + const dt = Date.now(); + const system = await queryCommonSystemValues(database); + const member = await queryMyChannel(database, channelId); + + if (member) { + fetchPostsForChannel(serverUrl, channelId); + + const channel: ChannelModel = await member.channel.fetch(); + const {operator} = DatabaseManager.serverDatabases[serverUrl]; + const result = await setCurrentChannelId(operator, channelId); + + let previousChannelId: string | undefined; + if (system.currentChannelId !== channelId) { + previousChannelId = system.currentChannelId; + await addChannelToTeamHistory(operator, system.currentTeamId, channelId, false); + } + await markChannelAsViewed(serverUrl, channelId, previousChannelId, true); + + if (!result.error) { + console.log('channel switch to', channel?.displayName, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line + } + } + } catch (error) { + return {error}; + } + + return {error: undefined}; +}; + +export const switchToChannelByName = async (serverUrl: string, channelName: string, teamName: string, errorHandler: (intl: IntlShape) => void, intl: IntlShape) => { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (!database) { + return {error: `${serverUrl} database not found`}; + } + + try { + let myChannel: MyChannelModel | ChannelMembership | undefined; + let team: TeamModel | Team | undefined; + let myTeam: MyTeamModel | TeamMembership | undefined; + let unreads: TeamUnread | undefined; + let name = teamName; + const roles: string [] = []; + const system = await queryCommonSystemValues(database); + const currentTeam = await queryTeamById(database, system.currentTeamId); + + if (name === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) { + name = currentTeam!.name; + } else { + team = await queryTeamByName(database, teamName); + } + + if (!team) { + const fetchTeam = await fetchTeamByName(serverUrl, name, true); + if (fetchTeam.error) { + errorHandler(intl); + return {error: fetchTeam.error}; + } + + team = fetchTeam.team!; + } + + let joinedNewTeam = false; + myTeam = await queryMyTeamById(database, team.id); + if (!myTeam) { + const added = await addUserToTeam(serverUrl, team.id, system.currentUserId, true); + if (added.error) { + errorHandler(intl); + return {error: added.error}; + } + myTeam = added.member!; + roles.push(...myTeam.roles.split(' ')); + unreads = added.unreads!; + joinedNewTeam = true; + } + + if (!myTeam) { + errorHandler(intl); + return {error: 'Could not fetch team member'}; + } + + let isArchived = false; + const chReq = await fetchChannelByName(serverUrl, team.id, channelName); + if (chReq.error) { + errorHandler(intl); + return {error: chReq.error}; + } + const channel = chReq.channel; + if (!channel) { + errorHandler(intl); + return {error: 'Could not fetch channel'}; + } + + isArchived = channel.delete_at > 0; + if (isArchived && system.config.ExperimentalViewArchivedChannels !== 'true') { + errorHandler(intl); + return {error: 'Channel is archived'}; + } + + myChannel = await queryMyChannel(database, channel.id); + + if (!myChannel) { + if (channel.type === General.PRIVATE_CHANNEL) { + const displayName = channel.display_name; + const {join} = await privateChannelJoinPrompt(displayName, intl); + if (!join) { + if (joinedNewTeam) { + await removeUserFromTeam(serverUrl, team.id, system.currentUserId, true); + } + errorHandler(intl); + return {error: 'Refused to join Private channel'}; + } + console.log('joining channel', displayName, channel.id); //eslint-disable-line + const result = await joinChannel(serverUrl, system.currentUserId, team.id, channel.id, undefined, true); + if (result.error || !result.channel) { + if (joinedNewTeam) { + await removeUserFromTeam(serverUrl, team.id, system.currentUserId, true); + } + + errorHandler(intl); + return {error: result.error}; + } + + myChannel = result.member!; + roles.push(...myChannel.roles.split(' ')); + } + } + + if (!myChannel) { + errorHandler(intl); + return {error: 'could not fetch channel member'}; + } + + const modelPromises: Array> = []; + const {operator} = DatabaseManager.serverDatabases[serverUrl]; + if (!(team instanceof Model)) { + const prepT = prepareMyTeams(operator, [team], [(myTeam as TeamMembership)], [unreads!]); + if (prepT) { + modelPromises.push(...prepT); + } + } else if (!(myTeam instanceof Model)) { + const mt: MyTeam[] = [{ + id: myTeam.team_id, + roles: myTeam.roles, + is_unread: unreads!.msg_count > 0, + mentions_count: unreads!.mention_count, + }]; + modelPromises.push( + operator.handleMyTeam({myTeams: mt, prepareRecordsOnly: true}), + operator.handleTeamMemberships({teamMemberships: [myTeam], prepareRecordsOnly: true}), + ); + } + + if (!(myChannel instanceof Model)) { + const prepCh = await prepareMyChannelsForTeam(operator, team.id, [channel], [myChannel]); + if (prepCh) { + modelPromises.push(...prepCh); + } + } + + let teamId; + if (team.id !== system.currentTeamId) { + teamId = team.id; + } + + let channelId; + if (channel.id !== system.currentChannelId) { + channelId = channel.id; + } + + if (modelPromises.length) { + const models = await Promise.all(modelPromises); + await operator.batchRecords(models.flat()); + } + + if (channelId) { + fetchPostsForChannel(serverUrl, channelId); + } + + if (teamId) { + fetchMyChannelsForTeam(serverUrl, teamId, true, 0, false, true); + } + + await setCurrentTeamAndChannelId(operator, teamId, channelId); + if (teamId && channelId) { + await addChannelToTeamHistory(operator, teamId, channelId, false); + } + + if (roles.length) { + fetchRolesIfNeeded(serverUrl, roles); + } + + return {error: undefined}; + } catch (error) { + errorHandler(intl); + return {error}; + } +}; diff --git a/app/actions/local/permalink.ts b/app/actions/local/permalink.ts new file mode 100644 index 000000000..e475a0a9b --- /dev/null +++ b/app/actions/local/permalink.ts @@ -0,0 +1,82 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Keyboard} from 'react-native'; + +import type {IntlShape} from 'react-intl'; + +import {fetchMyChannelsForTeam} from '@actions/remote/channel'; +import DatabaseManager from '@database/manager'; +import {queryCommonSystemValues} from '@queries/servers/system'; +import {queryTeamById, queryTeamByName} from '@queries/servers/team'; +import {dismissAllModals, showModalOverCurrentContext} from '@screens/navigation'; +import {permalinkBadTeam} from '@utils/draft'; +import {changeOpacity} from '@utils/theme'; +import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url'; + +import type TeamModel from '@typings/database/models/servers/team'; + +let showingPermalink = false; + +export const showPermalink = async (serverUrl: string, teamName: string, postId: string, intl: IntlShape, openAsPermalink = true) => { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (!database) { + return {error: `${serverUrl} database not found`}; + } + + try { + let name = teamName; + let team: TeamModel | undefined; + const system = await queryCommonSystemValues(database); + if (!name || name === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) { + team = await queryTeamById(database, system.currentTeamId); + if (team) { + name = team.name; + } + } + + if (!team) { + team = await queryTeamByName(database, name); + if (!team) { + permalinkBadTeam(intl); + return {error: 'Bad Permalink team'}; + } + } + + if (team.id !== system.currentTeamId) { + const result = await fetchMyChannelsForTeam(serverUrl, team.id, true, 0, false, true); + if (result.error) { + return {error: result.error}; + } + } + + Keyboard.dismiss(); + if (showingPermalink) { + await dismissAllModals(); + } + + const screen = 'Permalink'; + const passProps = { + isPermalink: openAsPermalink, + teamName, + postId, + }; + + const options = { + layout: { + componentBackgroundColor: changeOpacity('#000', 0.2), + }, + }; + + showingPermalink = true; + showModalOverCurrentContext(screen, passProps, options); + + return {error: undefined}; + } catch (error) { + return {error}; + } +}; + +export const closePermalink = () => { + showingPermalink = false; +}; diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index e9108e7b8..782f13a64 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -4,11 +4,12 @@ import {General} from '@constants'; import DatabaseManager from '@database/manager'; import NetworkManager from '@init/network_manager'; -import {prepareMyChannelsForTeam} from '@queries/servers/channel'; +import {prepareMyChannelsForTeam, queryMyChannel} from '@queries/servers/channel'; import {displayGroupMessageName, displayUsername} from '@utils/user'; import type {Model} from '@nozbe/watermelondb'; +import {fetchRolesIfNeeded} from './role'; import {forceLogoutIfNecessary} from './session'; import {fetchProfilesPerChannels} from './user'; @@ -18,6 +19,31 @@ export type MyChannelsRequest = { error?: never; } +export const fetchChannelByName = async (serverUrl: string, teamId: string, channelName: string, fetchOnly = false) => { + let client; + try { + client = NetworkManager.getClient(serverUrl); + } catch (error) { + return {error}; + } + + try { + const channel = await client.getChannelByName(teamId, channelName, true); + + if (!fetchOnly) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (operator) { + await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); + } + } + + return {channel}; + } catch (error) { + forceLogoutIfNecessary(serverUrl, error); + return {error}; + } +}; + export const fetchMyChannelsForTeam = async (serverUrl: string, teamId: string, includeDeleted = true, since = 0, fetchOnly = false, excludeDirect = false): Promise => { let client; try { @@ -107,3 +133,119 @@ export const fetchMissingSidebarInfo = async (serverUrl: string, directChannels: operator.handleChannel({channels: directChannels, prepareRecordsOnly: false}); } }; + +export const joinChannel = async (serverUrl: string, userId: string, teamId: string, channelId?: string, channelName?: string, fetchOnly = false) => { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return {error: `${serverUrl} database not found`}; + } + + let client; + try { + client = NetworkManager.getClient(serverUrl); + } catch (error) { + return {error}; + } + + let member: ChannelMembership | undefined; + let channel: Channel | undefined; + try { + if (channelId) { + member = await client.addToChannel(userId, channelId); + channel = await client.getChannel(channelId); + } else if (channelName) { + channel = await client.getChannelByName(teamId, channelName, true); + if ([General.GM_CHANNEL, General.DM_CHANNEL].includes(channel.type)) { + member = await client.getChannelMember(channel.id, userId); + } else { + member = await client.addToChannel(userId, channel.id); + } + } + } catch (error) { + forceLogoutIfNecessary(serverUrl, error); + return {error}; + } + + try { + if (channel && member && !fetchOnly) { + fetchRolesIfNeeded(serverUrl, member.roles.split(' ')); + + const modelPromises: Array> = []; + const prepare = await prepareMyChannelsForTeam(operator, teamId, [channel], [member]); + if (prepare) { + modelPromises.push(...prepare); + } + if (modelPromises.length) { + const models = await Promise.all(modelPromises); + const flattenedModels = models.flat() as Model[]; + if (flattenedModels?.length > 0) { + try { + await operator.batchRecords(flattenedModels); + } catch { + // eslint-disable-next-line no-console + console.log('FAILED TO BATCH CHANNELS'); + } + } + } + } + } catch (error) { + return {error}; + } + + return {channel, member}; +}; + +export const markChannelAsViewed = async (serverUrl: string, channelId: string, previousChannelId = '', markOnServer = true) => { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (!database) { + return {error: `${serverUrl} database not found`}; + } + + const member = await queryMyChannel(database, channelId); + const prevMember = await queryMyChannel(database, previousChannelId); + if (markOnServer) { + try { + const client = NetworkManager.getClient(serverUrl); + client.viewMyChannel(channelId, prevMember?.manuallyUnread ? '' : previousChannelId).catch(() => { + // do nothing just adding the handler to avoid the warning + }); + } catch (error) { + return {error}; + } + } + + const models = []; + const lastViewedAt = Date.now(); + if (member) { + member.prepareUpdate((m) => { + m.messageCount = 0; + m.mentionsCount = 0; + m.manuallyUnread = false; + m.lastViewedAt = lastViewedAt; + }); + + models.push(member); + } + + if (prevMember && !prevMember.manuallyUnread) { + prevMember.prepareUpdate((m) => { + m.messageCount = 0; + m.mentionsCount = 0; + m.manuallyUnread = false; + m.lastViewedAt = lastViewedAt; + }); + + models.push(prevMember); + } + + try { + if (models.length) { + const {operator} = DatabaseManager.serverDatabases[serverUrl]; + await operator.batchRecords(models); + } + + return {data: true}; + } catch (error) { + return {error}; + } +}; diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index c682b217e..f98c5695d 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -6,11 +6,14 @@ import {Model} from '@nozbe/watermelondb'; import DatabaseManager from '@database/manager'; import NetworkManager from '@init/network_manager'; import {queryWebSocketLastDisconnected} from '@queries/servers/system'; -import {prepareMyTeams} from '@queries/servers/team'; +import {prepareMyTeams, queryMyTeamById} from '@queries/servers/team'; import {fetchMyChannelsForTeam} from './channel'; import {fetchPostsForUnreadChannels} from './post'; +import {fetchRolesIfNeeded} from './role'; import {forceLogoutIfNecessary} from './session'; +import TeamModel from '@typings/database/models/servers/team'; +import TeamMembershipModel from '@typings/database/models/servers/team_membership'; export type MyTeamsRequest = { teams?: Team[]; @@ -19,6 +22,50 @@ export type MyTeamsRequest = { error?: never; } +export const addUserToTeam = async (serverUrl: string, teamId: string, userId: string, fetchOnly = false) => { + let client; + try { + client = NetworkManager.getClient(serverUrl); + } catch (error) { + return {error}; + } + + try { + const member = await client.addToTeam(teamId, userId); + const unreads = await client.getTeamUnreads(teamId); + + if (!fetchOnly) { + fetchRolesIfNeeded(serverUrl, member.roles.split(' ')); + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (operator) { + const myTeams: MyTeam[] = [{ + id: member.team_id, + roles: member.roles, + is_unread: unreads.msg_count > 0, + mentions_count: unreads.mention_count, + }]; + + const models = await Promise.all([ + operator.handleMyTeam({myTeams, prepareRecordsOnly: true}), + operator.handleTeamMemberships({teamMemberships: [member], prepareRecordsOnly: true}), + ]); + + if (models.length) { + const flattenedModels = models.flat() as Model[]; + if (flattenedModels?.length > 0) { + await operator.batchRecords(flattenedModels); + } + } + } + } + + return {member, unreads}; + } catch (error) { + forceLogoutIfNecessary(serverUrl, error); + return {error}; + } +}; + export const fetchMyTeams = async (serverUrl: string, fetchOnly = false): Promise => { let client; try { @@ -78,3 +125,71 @@ export const fetchTeamsChannelsAndUnreadPosts = async (serverUrl: string, teams: return {error: undefined}; }; + +export const fetchTeamByName = async (serverUrl: string, teamName: string, fetchOnly = false) => { + let client; + try { + client = NetworkManager.getClient(serverUrl); + } catch (error) { + return {error}; + } + + try { + const team = await client.getTeamByName(teamName); + + if (!fetchOnly) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (operator) { + const model = await operator.handleTeam({teams: [team], prepareRecordsOnly: true}); + if (model) { + await operator.batchRecords(model); + } + } + } + + return {team}; + } catch (error) { + forceLogoutIfNecessary(serverUrl, error); + return {error}; + } +}; + +export const removeUserFromTeam = async (serverUrl: string, teamId: string, userId: string, fetchOnly = false) => { + let client; + try { + client = NetworkManager.getClient(serverUrl); + } catch (error) { + return {error}; + } + + try { + await client.removeFromTeam(teamId, userId); + + if (!fetchOnly && DatabaseManager.serverDatabases[serverUrl]) { + const {operator, database} = DatabaseManager.serverDatabases[serverUrl]; + const myTeam = await queryMyTeamById(database, teamId); + const models: Model[] = []; + if (myTeam) { + const team = await myTeam.team.fetch() as TeamModel; + const members: TeamMembershipModel[] = await team.members.fetch(); + const member = members.find((m) => m.userId === userId); + + myTeam.prepareDestroyPermanently(); + models.push(myTeam); + if (member) { + member.prepareDestroyPermanently(); + models.push(member); + } + + if (models.length) { + await operator.batchRecords(models); + } + } + } + + return {error: undefined}; + } catch (error) { + forceLogoutIfNecessary(serverUrl, error); + return {error}; + } +}; diff --git a/app/client/rest/emojis.ts b/app/client/rest/emojis.ts index a946f02d6..5cd032ec5 100644 --- a/app/client/rest/emojis.ts +++ b/app/client/rest/emojis.ts @@ -6,11 +6,9 @@ import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; export interface ClientEmojisMix { - createCustomEmoji: (emoji: CustomEmoji, imageData: any) => Promise; getCustomEmoji: (id: string) => Promise; getCustomEmojiByName: (name: string) => Promise; getCustomEmojis: (page?: number, perPage?: number, sort?: string) => Promise; - deleteCustomEmoji: (emojiId: string) => Promise; getSystemEmojiImageUrl: (filename: string) => string; getCustomEmojiImageUrl: (id: string) => string; searchCustomEmoji: (term: string, options?: Record) => Promise; @@ -18,31 +16,6 @@ export interface ClientEmojisMix { } const ClientEmojis = (superclass: any) => class extends superclass { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - createCustomEmoji = async (emoji: CustomEmoji, imageData: any) => { - this.analytics.trackAPI('api_emoji_custom_add'); - - // FIXME: Multipart upload with client - // const formData = new FormData(); - // formData.append('image', imageData); - // formData.append('emoji', JSON.stringify(emoji)); - // const request: any = { - // method: 'post', - // body: formData, - // }; - - // if (formData.getBoundary) { - // request.headers = { - // 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, - // }; - // } - - // return this.doFetch( - // `${this.getEmojisRoute()}`, - // request, - // ); - }; - getCustomEmoji = async (id: string) => { return this.doFetch( `${this.getEmojisRoute()}/${id}`, @@ -64,21 +37,12 @@ const ClientEmojis = (superclass: any) => class extends superclass { ); }; - deleteCustomEmoji = async (emojiId: string) => { - this.analytics.trackAPI('api_emoji_custom_delete'); - - return this.doFetch( - `${this.getEmojiRoute(emojiId)}`, - {method: 'delete'}, - ); - }; - getSystemEmojiImageUrl = (filename: string) => { - return `${this.url}/static/emoji/${filename}.png`; + return `${this.apiClient.baseUrl}static/emoji/${filename}.png`; }; getCustomEmojiImageUrl = (id: string) => { - return `${this.getEmojiRoute(id)}/image`; + return `${this.apiClient.baseUrl}${this.getEmojiRoute(id)}/image`; }; searchCustomEmoji = async (term: string, options = {}) => { diff --git a/app/client/rest/files.ts b/app/client/rest/files.ts index 7bb120f08..080ff71c9 100644 --- a/app/client/rest/files.ts +++ b/app/client/rest/files.ts @@ -10,7 +10,7 @@ export interface ClientFilesMix { const ClientFiles = (superclass: any) => class extends superclass { getFileUrl(fileId: string, timestamp: number) { - let url = `${this.getFileRoute(fileId)}`; + let url = `${this.apiClient.baseUrl}${this.getFileRoute(fileId)}`; if (timestamp) { url += `?${timestamp}`; } @@ -19,7 +19,7 @@ const ClientFiles = (superclass: any) => class extends superclass { } getFileThumbnailUrl(fileId: string, timestamp: number) { - let url = `${this.getFileRoute(fileId)}/thumbnail`; + let url = `${this.apiClient.baseUrl}${this.getFileRoute(fileId)}/thumbnail`; if (timestamp) { url += `?${timestamp}`; } @@ -28,7 +28,7 @@ const ClientFiles = (superclass: any) => class extends superclass { } getFilePreviewUrl(fileId: string, timestamp: number) { - let url = `${this.getFileRoute(fileId)}/preview`; + let url = `${this.apiClient.baseUrl}${this.getFileRoute(fileId)}/preview`; if (timestamp) { url += `?${timestamp}`; } diff --git a/app/client/rest/teams.ts b/app/client/rest/teams.ts index 25b18d1b9..80849a834 100644 --- a/app/client/rest/teams.ts +++ b/app/client/rest/teams.ts @@ -17,6 +17,7 @@ export interface ClientTeamsMix { getTeamsForUser: (userId: string) => Promise; getMyTeamMembers: () => Promise; getMyTeamUnreads: () => Promise; + getTeamUnreads: (teamId: string) => Promise; getTeamMembers: (teamId: string, page?: number, perPage?: number) => Promise; getTeamMember: (teamId: string, userId: string) => Promise; addToTeam: (teamId: string, userId: string) => Promise; @@ -114,6 +115,13 @@ const ClientTeams = (superclass: any) => class extends superclass { ); }; + getTeamUnreads = async (teamId: string) => { + return this.doFetch( + `${this.getUserRoute('me')}/${this.getTeamRoute(teamId)}/unread`, + {method: 'get'}, + ); + } + getTeamMembers = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { return this.doFetch( `${this.getTeamMembersRoute(teamId)}${buildQueryString({page, per_page: perPage})}`, diff --git a/app/components/emoji/index.tsx b/app/components/emoji/index.tsx new file mode 100644 index 000000000..b824cffc1 --- /dev/null +++ b/app/components/emoji/index.tsx @@ -0,0 +1,167 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Q} from '@nozbe/watermelondb'; +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; +import React from 'react'; +import { + Platform, + StyleProp, + StyleSheet, + Text, + TextStyle, + View, +} from 'react-native'; +import FastImage, {ImageStyle} from 'react-native-fast-image'; +import {of} from 'rxjs'; + +import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; +import {useServerUrl} from '@context/server_url'; +import NetworkManager from '@init/network_manager'; +import {EmojiIndicesByAlias, Emojis} from '@utils/emoji'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type SystemModel from '@typings/database/models/servers/system'; +import CustomEmojiModel from '@typings/database/models/servers/custom_emoji'; + +const assetImages = new Map([['mattermost.png', require('@assets/images/emojis/mattermost.png')]]); + +type Props = { + emojiName: string; + displayTextOnly?: boolean; + literal?: string; + size?: number; + textStyle?: StyleProp; + customEmojiStyle?: StyleProp; + customEmojis: CustomEmojiModel[]; + testID?: string; +} + +const Emoji = (props: Props) => { + const { + customEmojis, + customEmojiStyle, + displayTextOnly, + emojiName, + literal = '', + testID, + textStyle, + } = props; + const serverUrl = useServerUrl(); + let assetImage = ''; + let unicode; + let imageUrl = ''; + + if (EmojiIndicesByAlias.has(emojiName)) { + const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)!]; + if (emoji.category === 'custom') { + assetImage = emoji.fileName; + } else { + unicode = emoji.image; + } + } else { + const custom = customEmojis.find((ce) => ce.name === emojiName); + if (custom) { + try { + const client = NetworkManager.getClient(serverUrl); + imageUrl = client.getCustomEmojiImageUrl(custom.id); + } catch { + // do nothing + } + } + } + + let size = props.size; + let fontSize = size; + if (!size && textStyle) { + const flatten = StyleSheet.flatten(textStyle); + fontSize = flatten.fontSize; + size = fontSize; + } + + if (displayTextOnly || (!imageUrl && !assetImage && !unicode)) { + return ( + + {literal} + ); + } + + const width = size; + const height = size; + + if (unicode && !imageUrl) { + const codeArray = unicode.split('-'); + const code = codeArray.reduce((acc: string, c: string) => { + return acc + String.fromCodePoint(parseInt(c, 16)); + }, ''); + + return ( + + {code} + + ); + } + + if (assetImage) { + const key = Platform.OS === 'android' ? (`${assetImage}-${height}-${width}`) : null; + + const image = assetImages.get(assetImage); + if (!image) { + return null; + } + return ( + + + + ); + } + + if (!imageUrl) { + return null; + } + + // Android can't change the size of an image after its first render, so + // force a new image to be rendered when the size changes + const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null; + + return ( + + + + ); +}; + +const withSystemIds = withObservables([], ({database}: WithDatabaseArgs) => ({ + config: database.get(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG), +})); + +const withCustomEmojis = withObservables(['config', 'emojiName'], ({config, database, emojiName}: WithDatabaseArgs & {config: SystemModel; emojiName: string}) => { + const cfg: ClientConfig = config.value; + const displayTextOnly = cfg.EnableCustomEmoji !== 'true'; + + return { + displayTextOnly: of(displayTextOnly), + customEmojis: database.get(MM_TABLES.SERVER.CUSTOM_EMOJI).query(Q.where('name', emojiName)).observe(), + }; +}); + +export default withDatabase(withSystemIds(withCustomEmojis(Emoji))); diff --git a/app/components/jumbo_emoji/index.tsx b/app/components/jumbo_emoji/index.tsx new file mode 100644 index 000000000..be541d7fd --- /dev/null +++ b/app/components/jumbo_emoji/index.tsx @@ -0,0 +1,140 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Node, Parser} from 'commonmark'; +import Renderer from 'commonmark-react-renderer'; +import React, {ReactElement, useRef} from 'react'; +import {Platform, StyleProp, StyleSheet, Text, TextStyle, View} from 'react-native'; + +import Emoji from '@components/emoji'; +import FormattedText from '@components/formatted_text'; +import {useTheme} from '@context/theme'; +import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme'; + +type JumboEmojiProps = { + baseTextStyle: StyleProp; + isEdited?: boolean; + value: string; +} + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + // Android has trouble giving text transparency depending on how it's nested, + // so we calculate the resulting colour manually + const editedOpacity = Platform.select({ + ios: 0.3, + android: 1.0, + }); + const editedColor = Platform.select({ + ios: theme.centerChannelColor, + android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3), + }); + + return { + block: { + alignItems: 'flex-start', + flexDirection: 'row', + flexWrap: 'wrap', + }, + editedIndicatorText: { + color: editedColor, + opacity: editedOpacity, + }, + jumboEmoji: { + fontSize: 50, + lineHeight: 60, + }, + }; +}); + +const JumboEmoji = ({baseTextStyle, isEdited, value}: JumboEmojiProps) => { + const theme = useTheme(); + const style = getStyleSheet(theme); + + const renderEmoji = ({emojiName, literal}: {context: string[]; emojiName: string; literal: string}) => { + const flat = StyleSheet.flatten(style.jumboEmoji); + const size = flat.lineHeight - flat.fontSize; + + return ( + + ); + }; + + const renderParagraph = ({children}: {children: ReactElement}) => { + return ( + {children} + ); + }; + + const renderText = ({literal}: {literal: string}) => { + return {literal}; + }; + + const renderNewLine = () => { + return {'\n'}; + }; + + const renderEditedIndicator = ({context}: {context: string[]}) => { + let spacer = ''; + if (context[0] === 'paragraph') { + spacer = ' '; + } + + const styles = [ + baseTextStyle, + style.editedIndicatorText, + ]; + + return ( + + {spacer} + + + ); + }; + + const createRenderer = () => { + const renderers: any = { + editedIndicator: renderEditedIndicator, + emoji: renderEmoji, + paragraph: renderParagraph, + document: renderParagraph, + text: renderText, + hardbreak: renderNewLine, + softBreak: renderNewLine, + }; + + return new Renderer({ + renderers, + renderParagraphsInLists: true, + }); + }; + + const parser = useRef(new Parser()).current; + const renderer = useRef(createRenderer()).current; + const ast = parser.parse(value.replace(/\n*$/, '')); + + if (isEdited) { + const editIndicatorNode = new Node('edited_indicator'); + if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) { + ast.lastChild.appendChild(editIndicatorNode); + } else { + const node = new Node('paragraph'); + node.appendChild(editIndicatorNode); + + ast.appendChild(node); + } + } + + return renderer.render(ast) as ReactElement; +}; + +export default JumboEmoji; diff --git a/app/components/markdown/at_mention/index.tsx b/app/components/markdown/at_mention/index.tsx new file mode 100644 index 000000000..0b268b142 --- /dev/null +++ b/app/components/markdown/at_mention/index.tsx @@ -0,0 +1,275 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useManagedConfig} from '@mattermost/react-native-emm'; +import {Database, Q} from '@nozbe/watermelondb'; +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; +import Clipboard from '@react-native-community/clipboard'; +import React, {useCallback, useMemo} from 'react'; +import {useIntl} from 'react-intl'; +import {DeviceEventEmitter, GestureResponderEvent, StyleProp, StyleSheet, Text, TextStyle, View} from 'react-native'; +import {of} from 'rxjs'; + +import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; +import {useTheme} from '@context/theme'; +import {Navigation, Preferences} from '@constants'; +import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; +import UserModel from '@database/models/server/user'; +import {getTeammateNameDisplaySetting} from '@helpers/api/preference'; +import CompassIcon from '@components/compass_icon'; +import {showModal, showModalOverCurrentContext} from '@screens/navigation'; +import {displayUsername, getUserMentionKeys, getUsersByUsername} from '@utils/user'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type PreferenceModel from '@typings/database/models/servers/preference'; +import type SystemModel from '@typings/database/models/servers/system'; +import type UserModelType from '@typings/database/models/servers/user'; + +type AtMentionProps = { + database: Database; + groupsByName?: Record; + isSearchResult?: boolean; + mentionKeys: Array<{key: string }>; + mentionName: string; + mentionStyle: TextStyle; + onPostPress?: (e: GestureResponderEvent) => void; + teammateNameDisplay: string; + textStyle: StyleProp; + users: UserModelType[]; +} + +type AtMentionArgs = { + config: SystemModel; + license: SystemModel; + preferences: PreferenceModel[]; + mentionName: string; +} + +const {SERVER: {PREFERENCE, SYSTEM, USER}} = MM_TABLES; + +const style = StyleSheet.create({ + bottomSheet: { + flex: 1, + }, +}); + +const AtMention = ({ + database, + groupsByName, + isSearchResult, + mentionName, + mentionKeys, + mentionStyle, + onPostPress, + teammateNameDisplay, + textStyle, + users, +}: AtMentionProps) => { + const intl = useIntl(); + const managedConfig = useManagedConfig(); + const theme = useTheme(); + const user = useMemo(() => { + const usersByUsername = getUsersByUsername(users); + let mn = mentionName.toLowerCase(); + + while (mn.length > 0) { + if (usersByUsername[mn]) { + return usersByUsername[mn]; + } + + // Repeatedly trim off trailing punctuation in case this is at the end of a sentence + if ((/[._-]$/).test(mn)) { + mn = mn.substring(0, mn.length - 1); + } else { + break; + } + } + + // @ts-expect-error: The model constructor is hidden within WDB type definition + return new UserModel(database.get(USER), {username: ''}); + }, [users, mentionName]); + const userMentionKeys = useMemo(() => { + if (mentionKeys) { + return mentionKeys; + } + return getUserMentionKeys(user); + }, [user, mentionKeys]); + + const getGroupFromMentionName = () => { + const mentionNameTrimmed = mentionName.toLowerCase().replace(/[._-]*$/, ''); + return groupsByName?.[mentionNameTrimmed]; + }; + + const goToUserProfile = useCallback(() => { + const screen = 'UserProfile'; + const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); + const passProps = { + userId: user.id, + }; + + const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor); + + const options = { + topBar: { + leftButtons: [{ + id: 'close-settings', + icon: closeButton, + testID: 'close.settings.button', + }], + }, + }; + + showModal(screen, title, passProps, options); + }, [user]); + + const handleLongPress = useCallback(() => { + if (managedConfig?.copyAndPasteProtection !== 'true') { + const renderContent = () => { + return ( + + { + DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL); + let username = mentionName; + if (user.username) { + username = user.username; + } + + Clipboard.setString(`@${username}`); + }} + testID='at_mention.bottom_sheet.copy_mention' + text={intl.formatMessage({id: 'mobile.mention.copy_mention', defaultMessage: 'Copy Mention'})} + /> + { + DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL); + }} + testID='at_mention.bottom_sheet.cancel' + text={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})} + /> + + ); + }; + + showModalOverCurrentContext('BottomSheet', { + renderContent, + snapPoints: [3 * ITEM_HEIGHT, 10], + }); + } + }, [managedConfig]); + + const mentionTextStyle = []; + + let backgroundColor; + let canPress = false; + let highlighted; + let isMention = false; + let mention; + let onLongPress; + let onPress; + let suffix; + let suffixElement; + let styleText; + + if (textStyle) { + backgroundColor = theme.mentionHighlightBg; + styleText = textStyle; + } + + if (user?.username) { + suffix = mentionName.substring(user.username.length); + highlighted = userMentionKeys.some((item) => item.key.includes(user.username)); + mention = displayUsername(user, user.locale, teammateNameDisplay); + isMention = true; + canPress = true; + } else { + // TODO: Add group functionality + const group = getGroupFromMentionName(); + if (group?.allow_reference) { + highlighted = userMentionKeys.some((item) => item.key === `@${group.name}`); + isMention = true; + mention = group.name; + suffix = mentionName.substring(group.name.length); + } else { + const pattern = new RegExp(/\b(all|channel|here)(?:\.\B|_\b|\b)/, 'i'); + const mentionMatch = pattern.exec(mentionName); + + if (mentionMatch) { + mention = mentionMatch.length > 1 ? mentionMatch[1] : mentionMatch[0]; + suffix = mentionName.replace(mention, ''); + isMention = true; + highlighted = true; + } else { + mention = mentionName; + } + } + } + + if (canPress) { + onLongPress = handleLongPress; + onPress = isSearchResult ? onPostPress : goToUserProfile; + } + + if (suffix) { + const suffixStyle = {...StyleSheet.flatten(styleText), color: theme.centerChannelColor}; + suffixElement = ( + + {suffix} + + ); + } + + if (isMention) { + mentionTextStyle.push(mentionStyle); + } + + if (highlighted) { + mentionTextStyle.push({backgroundColor, color: theme.mentionHighlightLink}); + } + + return ( + + + {'@' + mention} + + {suffixElement} + + ); +}; + +const withPreferences = withObservables([], ({database}: WithDatabaseArgs) => ({ + preferences: database.get(PREFERENCE).query(Q.where('category', Preferences.CATEGORY_DISPLAY_SETTINGS)).observe(), + config: database.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG), + license: database.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.LICENSE), +})); + +const withAtMention = withObservables(['mentionName', 'preferences', 'config', 'license'], ({database, mentionName, preferences, config, license}: WithDatabaseArgs & AtMentionArgs) => { + let mn = mentionName.toLowerCase(); + if ((/[._-]$/).test(mn)) { + mn = mn.substring(0, mn.length - 1); + } + + const teammateNameDisplay = of(getTeammateNameDisplaySetting(preferences, config.value, license.value)); + + return { + teammateNameDisplay, + users: database.get(USER).query( + Q.where('username', Q.like( + `%${Q.sanitizeLikeString(mn)}%`, + )), + ).observeWithColumns(['username']), + }; +}); + +export default withDatabase(withPreferences(withAtMention(AtMention))); diff --git a/app/components/markdown/channel_mention/index.tsx b/app/components/markdown/channel_mention/index.tsx new file mode 100644 index 000000000..14cef47a3 --- /dev/null +++ b/app/components/markdown/channel_mention/index.tsx @@ -0,0 +1,134 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Q} from '@nozbe/watermelondb'; +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; +import React, {useCallback} from 'react'; +import {useIntl} from 'react-intl'; +import {StyleProp, Text, TextStyle} from 'react-native'; + +import {switchToChannel} from '@actions/local/channel'; +import {joinChannel} from '@actions/remote/channel'; +import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; +import {useServerUrl} from '@context/server_url'; +import {t} from '@i18n'; +import {dismissAllModals, popToRoot} from '@screens/navigation'; +import {alertErrorWithFallback} from '@utils/draft'; +import {preventDoubleTap} from '@utils/tap'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type ChannelModelType from '@typings/database/models/servers/channel'; +import type SystemModel from '@typings/database/models/servers/system'; +import type TeamModelType from '@typings/database/models/servers/team'; + +export type ChannelMentions = Record; + +type ChannelMentionProps = { + channelMentions?: ChannelMentions; + channelName: string; + channels: ChannelModelType[]; + currentTeamId: SystemModel; + currentUserId: SystemModel; + linkStyle: StyleProp; + team: TeamModelType; + textStyle: StyleProp; +} + +function getChannelFromChannelName(name: string, channels: ChannelModelType[], channelMentions: ChannelMentions = {}, teamName: string) { + const channelsByName = channelMentions; + let channelName = name; + + channels.forEach((c) => { + channelsByName[c.name] = { + id: c.id, + display_name: c.displayName, + name: c.name, + team_name: teamName, + }; + }); + + while (channelName.length > 0) { + if (channelsByName[channelName]) { + return channelsByName[channelName]; + } + + // Repeatedly trim off trailing punctuation in case this is at the end of a sentence + if ((/[_-]$/).test(channelName)) { + channelName = channelName.substring(0, channelName.length - 1); + } else { + break; + } + } + + return null; +} + +const ChannelMention = ({ + channelMentions, channelName, channels, currentTeamId, currentUserId, + linkStyle, team, textStyle, +}: ChannelMentionProps) => { + const intl = useIntl(); + const serverUrl = useServerUrl(); + const channel = getChannelFromChannelName(channelName, channels, channelMentions, team.name); + + const handlePress = useCallback(preventDoubleTap(async () => { + let c = channel; + + if (!c?.id && c?.display_name) { + const result = await joinChannel(serverUrl, currentUserId.value, currentTeamId.value, undefined, channelName); + if (result.error || !result.channel) { + const joinFailedMessage = { + id: t('mobile.join_channel.error'), + defaultMessage: "We couldn't join the channel {displayName}. Please check your connection and try again.", + }; + alertErrorWithFallback(intl, result.error || {}, joinFailedMessage, {displayName: c.display_name}); + } else if (result.channel) { + c = { + ...c, + id: result.channel.id, + name: result.channel.name, + }; + } + } + + if (c?.id) { + switchToChannel(serverUrl, c.id); + await dismissAllModals(); + await popToRoot(); + } + }), [channel?.display_name, channel?.id]); + + if (!channel) { + return {`~${channelName}`}; + } + + let suffix; + if (channel.name) { + suffix = channelName.substring(channel.name.length); + } + + return ( + + + {`~${channel.display_name}`} + + {suffix} + + ); +}; + +const withSystemIds = withObservables([], ({database}: WithDatabaseArgs) => ({ + currentTeamId: database.get(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID), + currentUserId: database.get(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID), +})); + +const withChannelsForTeam = withObservables(['currentTeamId'], ({database, currentTeamId}: WithDatabaseArgs & {currentTeamId: SystemModel}) => ({ + channels: database.get(MM_TABLES.SERVER.CHANNEL).query(Q.where('team_id', currentTeamId.value)).observeWithColumns(['display_name']), + team: database.get(MM_TABLES.SERVER.TEAM).findAndObserve(currentTeamId.value), +})); + +export default withDatabase(withSystemIds(withChannelsForTeam(ChannelMention))); diff --git a/app/components/markdown/hashtag/index.tsx b/app/components/markdown/hashtag/index.tsx new file mode 100644 index 000000000..fba1484d2 --- /dev/null +++ b/app/components/markdown/hashtag/index.tsx @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Text, TextStyle} from 'react-native'; + +import {popToRoot, showSearchModal, dismissAllModals} from '@screens/navigation'; + +type HashtagProps = { + hashtag: string; + linkStyle: TextStyle; +}; + +const Hashtag = ({hashtag, linkStyle}: HashtagProps) => { + const handlePress = async () => { + // Close thread view, permalink view, etc + await dismissAllModals(); + await popToRoot(); + + showSearchModal('#' + hashtag); + }; + + return ( + + {`#${hashtag}`} + + ); +}; + +export default Hashtag; diff --git a/app/components/markdown/index.tsx b/app/components/markdown/index.tsx new file mode 100644 index 000000000..50c0042d6 --- /dev/null +++ b/app/components/markdown/index.tsx @@ -0,0 +1,513 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Parser, Node} from 'commonmark'; +import Renderer from 'commonmark-react-renderer'; +import React, {PureComponent, ReactElement} from 'react'; +import {GestureResponderEvent, Platform, StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle} from 'react-native'; + +import Emoji from '@components/emoji'; +import FormattedText from '@components/formatted_text'; +import Hashtag from '@components/markdown/hashtag'; +import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme'; +import {getScheme} from '@utils/url'; + +import AtMention from './at_mention'; +import ChannelMention, {ChannelMentions} from './channel_mention'; +import MarkdownBlockQuote from './markdown_block_quote'; +import MarkdownCodeBlock from './markdown_code_block'; +import MarkdownImage from './markdown_image'; +import MarkdownLink from './markdown_link'; +import MarkdownList from './markdown_list'; +import MarkdownListItem from './markdown_list_item'; +import MarkdownTable from './markdown_table'; +import MarkdownTableImage from './markdown_table_image'; +import MarkdownTableRow, {MarkdownTableRowProps} from './markdown_table_row'; +import MarkdownTableCell, {MarkdownTableCellProps} from './markdown_table_cell'; +import {addListItemIndices, combineTextNodes, highlightMentions, pullOutImages} from './transform'; + +type MarkdownProps = { + autolinkedUrlSchemes?: string[]; + baseTextStyle: StyleProp; + blockStyles: { + adjacentParagraph: ViewStyle; + horizontalRule: ViewStyle; + quoteBlockIcon: TextStyle; + }; + channelMentions?: ChannelMentions; + disableAtMentions?: boolean; + disableChannelLink?: boolean; + disableGallery?: boolean; + disableHashtags?: boolean; + imagesMetadata?: Record; + isEdited?: boolean; + isReplyPost?: boolean; + isSearchResult?: boolean; + mentionKeys?: UserMentionKey[]; + minimumHashtagLength?: number; + onPostPress?: (event: GestureResponderEvent) => void; + postId?: string; + textStyles: { + [key: string]: TextStyle; + }; + theme: Theme; + value: string | number; +} + +class Markdown extends PureComponent { + static defaultProps = { + textStyles: {}, + blockStyles: {}, + disableHashtags: false, + disableAtMentions: false, + disableChannelLink: false, + disableGallery: false, + value: '', + minimumHashtagLength: 3, + mentionKeys: [], + }; + + private parser: Parser; + private renderer: Renderer.Renderer; + + constructor(props: MarkdownProps) { + super(props); + + this.parser = this.createParser(); + this.renderer = this.createRenderer(); + } + + createParser = () => { + return new Parser({ + urlFilter: this.urlFilter, + minimumHashtagLength: this.props.minimumHashtagLength, + }); + }; + + urlFilter = (url: string) => { + const scheme = getScheme(url); + return !scheme || this.props.autolinkedUrlSchemes?.indexOf(scheme) !== -1; + }; + + createRenderer = () => { + const renderers: any = { + text: this.renderText, + + emph: Renderer.forwardChildren, + strong: Renderer.forwardChildren, + del: Renderer.forwardChildren, + code: this.renderCodeSpan, + link: this.renderLink, + image: this.renderImage, + atMention: this.renderAtMention, + channelLink: this.renderChannelLink, + emoji: this.renderEmoji, + hashtag: this.renderHashtag, + + paragraph: this.renderParagraph, + heading: this.renderHeading, + codeBlock: this.renderCodeBlock, + blockQuote: this.renderBlockQuote, + + list: this.renderList, + item: this.renderListItem, + + hardBreak: this.renderHardBreak, + thematicBreak: this.renderThematicBreak, + softBreak: this.renderSoftBreak, + + htmlBlock: this.renderHtml, + htmlInline: this.renderHtml, + + table: this.renderTable, + table_row: this.renderTableRow, + table_cell: this.renderTableCell, + + mention_highlight: Renderer.forwardChildren, + + editedIndicator: this.renderEditedIndicator, + }; + + return new Renderer({ + renderers, + renderParagraphsInLists: true, + getExtraPropsForNode: this.getExtraPropsForNode, + }); + }; + + getExtraPropsForNode = (node: any) => { + const extraProps: Record = { + continue: node.continue, + index: node.index, + }; + + if (node.type === 'image') { + extraProps.reactChildren = node.react.children; + extraProps.linkDestination = node.linkDestination; + } + + return extraProps; + }; + + computeTextStyle = (baseStyle: StyleProp, context: any) => { + const {textStyles} = this.props; + type TextType = keyof typeof textStyles; + const contextStyles: TextStyle[] = context.map((type: any) => textStyles[type as TextType]).filter((f: any) => f !== undefined); + return contextStyles.length ? concatStyles(baseStyle, contextStyles) : baseStyle; + }; + + renderText = ({context, literal}: any) => { + if (context.indexOf('image') !== -1) { + // If this text is displayed, it will be styled by the image component + return ( + + {literal} + + ); + } + + // Construct the text style based off of the parents of this node since RN's inheritance is limited + const style = this.computeTextStyle(this.props.baseTextStyle, context); + + return ( + + {literal} + + ); + }; + + renderCodeSpan = ({context, literal}: {context: any; literal: any}) => { + const {baseTextStyle, textStyles: {code}} = this.props; + return {literal}; + }; + + renderImage = ({linkDestination, context, src}: {linkDestination?: string; context: string[]; src: string}) => { + if (!this.props.imagesMetadata) { + return null; + } + + if (context.indexOf('table') !== -1) { + // We have enough problems rendering images as is, so just render a link inside of a table + return ( + + ); + } + + return ( + + ); + }; + + renderAtMention = ({context, mentionName}: {context: string[]; mentionName: string}) => { + if (this.props.disableAtMentions) { + return this.renderText({context, literal: `@${mentionName}`}); + } + + const style = getStyleSheet(this.props.theme); + + return ( + + ); + }; + + renderChannelLink = ({context, channelName}: {context: string[]; channelName: string}) => { + if (this.props.disableChannelLink) { + return this.renderText({context, literal: `~${channelName}`}); + } + + return ( + + ); + }; + + renderEmoji = ({context, emojiName, literal}: {context: string[]; emojiName: string; literal: string}) => { + let customEmojiStyle; + + if (Platform.OS === 'android') { + const flat = StyleSheet.flatten(this.props.baseTextStyle); + + if (flat) { + const size = Math.abs(((flat.lineHeight || 0) - (flat.fontSize || 0))) / 2; + if (size > 0) { + customEmojiStyle = {marginRight: size, top: size}; + } + } + } + return ( + + ); + }; + + renderHashtag = ({context, hashtag}: {context: string[]; hashtag: string}) => { + if (this.props.disableHashtags) { + return this.renderText({context, literal: `#${hashtag}`}); + } + + return ( + + ); + }; + + renderParagraph = ({children, first}: {children: ReactElement[]; first: boolean}) => { + if (!children || children.length === 0) { + return null; + } + + const style = getStyleSheet(this.props.theme); + const blockStyle = [style.block]; + if (!first) { + blockStyle.push(this.props.blockStyles.adjacentParagraph); + } + + return ( + + + {children} + + + ); + }; + + renderHeading = ({children, level}: {children: ReactElement; level: string}) => { + const {textStyles} = this.props; + const containerStyle = [ + getStyleSheet(this.props.theme).block, + textStyles[`heading${level}`], + ]; + const textStyle = textStyles[`heading${level}Text`]; + return ( + + + {children} + + + ); + }; + + renderCodeBlock = (props: any) => { + // These sometimes include a trailing newline + const content = props.literal.replace(/\n$/, ''); + + return ( + + ); + }; + + renderBlockQuote = ({children, ...otherProps}: any) => { + return ( + + {children} + + ); + }; + + renderList = ({children, start, tight, type}: any) => { + return ( + + {children} + + ); + }; + + renderListItem = ({children, context, ...otherProps}: any) => { + const level = context.filter((type: string) => type === 'list').length; + + return ( + + {children} + + ); + }; + + renderHardBreak = () => { + return {'\n'}; + }; + + renderThematicBreak = () => { + return ( + + ); + }; + + renderSoftBreak = () => { + return {'\n'}; + }; + + renderHtml = (props: any) => { + let rendered = this.renderText(props); + + if (props.isBlock) { + const style = getStyleSheet(this.props.theme); + + rendered = ( + + {rendered} + + ); + } + + return rendered; + }; + + renderTable = ({children, numColumns}: {children: ReactElement; numColumns: number}) => { + return ( + + {children} + + ); + }; + + renderTableRow = (args: MarkdownTableRowProps) => { + return ; + }; + + renderTableCell = (args: MarkdownTableCellProps) => { + return ; + }; + + renderLink = ({children, href}: {children: ReactElement; href: string}) => { + return ( + + {children} + + ); + }; + + renderEditedIndicator = ({context}: {context: string[]}) => { + let spacer = ''; + if (context[0] === 'paragraph') { + spacer = ' '; + } + + const style = getStyleSheet(this.props.theme); + const styles = [ + this.props.baseTextStyle, + style.editedIndicatorText, + ]; + + return ( + + {spacer} + + + ); + }; + + render() { + let ast = this.parser.parse(this.props.value.toString()); + + ast = combineTextNodes(ast); + ast = addListItemIndices(ast); + ast = pullOutImages(ast); + if (this.props.mentionKeys) { + ast = highlightMentions(ast, this.props.mentionKeys); + } + + if (this.props.isEdited) { + const editIndicatorNode = new Node('edited_indicator'); + if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) { + ast.lastChild.appendChild(editIndicatorNode); + } else { + const node = new Node('paragraph'); + node.appendChild(editIndicatorNode); + + ast.appendChild(node); + } + } + + return this.renderer.render(ast); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + // Android has trouble giving text transparency depending on how it's nested, + // so we calculate the resulting colour manually + const editedOpacity = Platform.select({ + ios: 0.3, + android: 1.0, + }); + const editedColor = Platform.select({ + ios: theme.centerChannelColor, + android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3), + }); + + return { + block: { + alignItems: 'flex-start', + flexDirection: 'row', + flexWrap: 'wrap', + }, + editedIndicatorText: { + color: editedColor, + opacity: editedOpacity, + }, + atMentionOpacity: { + opacity: 1, + }, + }; +}); + +export default Markdown; diff --git a/app/components/markdown/markdown_block_quote/index.tsx b/app/components/markdown/markdown_block_quote/index.tsx new file mode 100644 index 000000000..88141bbe8 --- /dev/null +++ b/app/components/markdown/markdown_block_quote/index.tsx @@ -0,0 +1,49 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {ReactNode} from 'react'; + +import {StyleSheet, TextStyle, View, ViewStyle} from 'react-native'; + +import CompassIcon from '@components/compass_icon'; + +type MarkdownBlockQuoteProps = { + continueBlock?: boolean; + iconStyle: ViewStyle | TextStyle; + children: ReactNode | ReactNode[]; +}; + +const style = StyleSheet.create({ + container: { + alignItems: 'flex-start', + flexDirection: 'row', + }, + childContainer: { + flex: 1, + }, + icon: { + width: 23, + }, +}); + +const MarkdownBlockQuote = ({children, continueBlock, iconStyle}: MarkdownBlockQuoteProps) => { + return ( + + {!continueBlock && ( + + + + )} + {children} + + ); +}; + +export default MarkdownBlockQuote; diff --git a/app/components/markdown/markdown_code_block/index.tsx b/app/components/markdown/markdown_code_block/index.tsx new file mode 100644 index 000000000..f8154caca --- /dev/null +++ b/app/components/markdown/markdown_code_block/index.tsx @@ -0,0 +1,250 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useManagedConfig} from '@mattermost/react-native-emm'; +import Clipboard from '@react-native-community/clipboard'; +import React, {useCallback} from 'react'; +import {useIntl} from 'react-intl'; +import {DeviceEventEmitter, Keyboard, StyleSheet, Text, TextStyle, View} from 'react-native'; + +import FormattedText from '@components/formatted_text'; +import {useTheme} from '@context/theme'; +import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {Navigation} from '@constants'; +import {goToScreen, showModalOverCurrentContext} from '@screens/navigation'; +import {getDisplayNameForLanguage} from '@utils/markdown'; +import {preventDoubleTap} from '@utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +type MarkdownCodeBlockProps = { + language: string; + content: string; + textStyle: TextStyle; +}; + +const MAX_LINES = 4; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + bottomSheet: { + flex: 1, + }, + container: { + borderColor: changeOpacity(theme.centerChannelColor, 0.15), + borderRadius: 3, + borderWidth: StyleSheet.hairlineWidth, + flexDirection: 'row', + }, + lineNumbers: { + alignItems: 'center', + backgroundColor: changeOpacity(theme.centerChannelColor, 0.05), + borderRightColor: changeOpacity(theme.centerChannelColor, 0.15), + borderRightWidth: StyleSheet.hairlineWidth, + flexDirection: 'column', + justifyContent: 'flex-start', + paddingVertical: 4, + width: 21, + }, + lineNumbersText: { + color: changeOpacity(theme.centerChannelColor, 0.5), + fontSize: 12, + lineHeight: 18, + }, + rightColumn: { + flexDirection: 'column', + flex: 1, + paddingHorizontal: 6, + paddingVertical: 4, + }, + code: { + flexDirection: 'row', + overflow: 'scroll', // Doesn't actually cause a scrollbar, but stops text from wrapping + }, + codeText: { + color: changeOpacity(theme.centerChannelColor, 0.65), + fontSize: 12, + lineHeight: 18, + }, + plusMoreLinesText: { + color: changeOpacity(theme.centerChannelColor, 0.4), + fontSize: 11, + marginTop: 2, + }, + language: { + alignItems: 'center', + backgroundColor: theme.sidebarHeaderBg, + justifyContent: 'center', + opacity: 0.8, + padding: 6, + position: 'absolute', + right: 0, + top: 0, + }, + languageText: { + color: theme.sidebarHeaderTextColor, + fontSize: 12, + }, + }; +}); + +const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBlockProps) => { + const intl = useIntl(); + const managedConfig = useManagedConfig(); + const theme = useTheme(); + const style = getStyleSheet(theme); + + const handlePress = preventDoubleTap(() => { + const screen = 'Code'; + const passProps = { + content, + }; + + const languageDisplayName = getDisplayNameForLanguage(language); + let title: string; + if (languageDisplayName) { + title = intl.formatMessage( + { + id: 'mobile.routes.code', + defaultMessage: '{language} Code', + }, + { + language: languageDisplayName, + }, + ); + } else { + title = intl.formatMessage({ + id: 'mobile.routes.code.noLanguage', + defaultMessage: 'Code', + }); + } + + Keyboard.dismiss(); + requestAnimationFrame(() => { + goToScreen(screen, title, passProps); + }); + }); + + const handleLongPress = useCallback(() => { + if (managedConfig?.copyAndPasteProtection !== 'true') { + const renderContent = () => { + return ( + + { + DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL); + Clipboard.setString(content); + }} + testID='at_mention.bottom_sheet.copy_code' + text={intl.formatMessage({id: 'mobile.markdown.code.copy_code', defaultMessage: 'Copy Code'})} + /> + { + DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL); + }} + testID='at_mention.bottom_sheet.cancel' + text={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})} + /> + + ); + }; + + showModalOverCurrentContext('BottomSheet', { + renderContent, + snapPoints: [3 * ITEM_HEIGHT, 10], + }); + } + }, [managedConfig]); + + const trimContent = (text: string) => { + const lines = text.split('\n'); + const numberOfLines = lines.length; + + if (numberOfLines > MAX_LINES) { + return { + content: lines.slice(0, MAX_LINES).join('\n'), + numberOfLines, + }; + } + + return { + content: text, + numberOfLines, + }; + }; + + const renderLanguageBlock = () => { + if (language) { + const languageDisplayName = getDisplayNameForLanguage(language); + + if (languageDisplayName) { + return ( + + + {languageDisplayName} + + + ); + } + } + return null; + }; + + const {content: codeContent, numberOfLines} = trimContent(content); + + const getLineNumbers = () => { + let lineNumbers = '1'; + for (let i = 1; i < Math.min(numberOfLines, MAX_LINES); i++) { + const line = (i + 1).toString(); + lineNumbers += '\n' + line; + } + return lineNumbers; + }; + + const renderPlusMoreLines = () => { + if (numberOfLines > MAX_LINES) { + return ( + + ); + } + return null; + }; + + return ( + + + + {getLineNumbers()} + + + + + {codeContent} + + + {renderPlusMoreLines()} + + {renderLanguageBlock()} + + + ); +}; + +export default MarkdownCodeBlock; diff --git a/app/components/markdown/markdown_image/index.tsx b/app/components/markdown/markdown_image/index.tsx new file mode 100644 index 000000000..3ffa47d0c --- /dev/null +++ b/app/components/markdown/markdown_image/index.tsx @@ -0,0 +1,225 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useManagedConfig} from '@mattermost/react-native-emm'; +import Clipboard from '@react-native-community/clipboard'; +import React, {useCallback, useRef, useState} from 'react'; +import {useIntl} from 'react-intl'; +import {Alert, DeviceEventEmitter, Platform, StyleProp, StyleSheet, Text, TextStyle, View} from 'react-native'; +import parseUrl from 'url-parse'; + +import CompassIcon from '@components/compass_icon'; +import FormattedText from '@components/formatted_text'; +import ProgressiveImage from '@components/progressive_image'; +import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {Navigation} from '@constants'; +import {useServerUrl} from '@context/server_url'; +import {usePermanentSidebar, useSplitView} from '@hooks/device'; +import {showModalOverCurrentContext} from '@screens/navigation'; +import {openGallerWithMockFile} from '@utils/gallery'; +import {generateId} from '@utils/general'; +import {calculateDimensions, getViewPortWidth, isGifTooLarge} from '@utils/images'; +import {normalizeProtocol, tryOpenURL} from '@utils/url'; + +type MarkdownImageProps = { + disabled?: boolean; + errorTextStyle: StyleProp; + imagesMetadata?: Record; + isReplyPost?: boolean; + linkDestination?: string; + postId: string; + source: string; +} + +const ANDROID_MAX_HEIGHT = 4096; +const ANDROID_MAX_WIDTH = 4096; + +const style = StyleSheet.create({ + bottomSheet: { + flex: 1, + }, + brokenImageIcon: { + width: 24, + height: 24, + }, + container: { + marginBottom: 5, + }, +}); + +const MarkdownImage = ({ + disabled, errorTextStyle, imagesMetadata, isReplyPost = false, + linkDestination, postId, source, +}: MarkdownImageProps) => { + const intl = useIntl(); + const isSplitView = useSplitView(); + const managedConfig = useManagedConfig(); + const permanentSidebar = usePermanentSidebar(); + const genericFileId = useRef(generateId()).current; + const metadata = imagesMetadata?.[source] || Object.values(imagesMetadata || {})[0]; + const [failed, setFailed] = useState(isGifTooLarge(metadata)); + const originalSize = {width: metadata?.width || 0, height: metadata?.height || 0}; + const serverUrl = useServerUrl(); + + let uri = source; + if (uri.startsWith('/')) { + uri = serverUrl + uri; + } + + const link = decodeURIComponent(uri); + let filename = parseUrl(link.substr(link.lastIndexOf('/'))).pathname.replace('/', ''); + let extension = filename.split('.').pop(); + if (extension === filename) { + const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.')); + filename = `${filename}${ext}`; + extension = ext; + } + + const fileInfo = { + id: genericFileId, + name: filename, + extension, + has_preview_image: true, + post_id: postId, + uri: link, + width: originalSize.width, + height: originalSize.height, + }; + + const {height, width} = calculateDimensions(fileInfo.height, fileInfo.width, getViewPortWidth(isReplyPost, (permanentSidebar && !isSplitView))); + + const handleLinkPress = useCallback(() => { + if (linkDestination) { + const url = normalizeProtocol(linkDestination); + + const onError = () => { + Alert.alert( + intl.formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + intl.formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + + tryOpenURL(url, onError); + } + }, [linkDestination]); + + const handleLinkLongPress = useCallback(() => { + if (managedConfig?.copyAndPasteProtection !== 'true') { + const renderContent = () => { + return ( + + { + DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL); + Clipboard.setString(linkDestination || source); + }} + testID='at_mention.bottom_sheet.copy_url' + text={intl.formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'})} + /> + { + DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL); + }} + testID='at_mention.bottom_sheet.cancel' + text={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})} + /> + + ); + }; + + showModalOverCurrentContext('BottomSheet', { + renderContent, + snapPoints: [3 * ITEM_HEIGHT, 10], + }); + } + }, [managedConfig]); + + const handlePreviewImage = useCallback(() => { + openGallerWithMockFile(fileInfo.uri, postId, fileInfo.height, fileInfo.width, fileInfo.id); + }, []); + + const handleOnError = useCallback(() => { + setFailed(true); + }, []); + + if (failed) { + return ( + + ); + } + + let image; + if (height && width) { + if (Platform.OS === 'android' && (height > ANDROID_MAX_HEIGHT || width > ANDROID_MAX_WIDTH)) { + // Android has a cap on the max image size that can be displayed + image = ( + + + {' '} + + ); + } else { + image = ( + + + + ); + } + } + + if (image && linkDestination && !disabled) { + image = ( + + {image} + + ); + } + + return ( + + {image} + + ); +}; + +export default MarkdownImage; diff --git a/app/components/markdown/markdown_link/index.tsx b/app/components/markdown/markdown_link/index.tsx new file mode 100644 index 000000000..144419a1a --- /dev/null +++ b/app/components/markdown/markdown_link/index.tsx @@ -0,0 +1,184 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useManagedConfig} from '@mattermost/react-native-emm'; +import Clipboard from '@react-native-community/clipboard'; +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; +import React, {Children, ReactElement, useCallback} from 'react'; +import {useIntl} from 'react-intl'; +import {Alert, DeviceEventEmitter, StyleSheet, Text, View} from 'react-native'; +import {of} from 'rxjs'; +import urlParse from 'url-parse'; + +import {switchToChannelByName} from '@actions/local/channel'; +import {showPermalink} from '@actions/local/permalink'; +import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; +import {Navigation} from '@constants'; +import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; +import DeepLinkTypes from '@constants/deep_linking'; +import {useServerUrl} from '@context/server_url'; +import {dismissAllModals, popToRoot, showModalOverCurrentContext} from '@screens/navigation'; +import {errorBadChannel} from '@utils/draft'; +import {matchDeepLink, normalizeProtocol, tryOpenURL} from '@utils/url'; +import {preventDoubleTap} from '@utils/tap'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type SystemModel from '@typings/database/models/servers/system'; +import type {DeepLinkChannel, DeepLinkPermalink, DeepLinkWithData} from '@typings/launch'; + +type MarkdownLinkProps = { + children: ReactElement; + experimentalNormalizeMarkdownLinks: string; + href: string; + siteURL: string; +} + +const style = StyleSheet.create({ + bottomSheet: { + flex: 1, + }, +}); + +const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteURL}: MarkdownLinkProps) => { + const intl = useIntl(); + const managedConfig = useManagedConfig(); + const serverUrl = useServerUrl(); + + const {formatMessage} = intl; + + const handlePress = preventDoubleTap(async () => { + const url = normalizeProtocol(href); + + if (!url) { + return; + } + + const match: DeepLinkWithData | null = matchDeepLink(url, serverUrl, siteURL); + + if (match && match.data?.teamName) { + if (match.type === DeepLinkTypes.CHANNEL) { + const result = await switchToChannelByName(serverUrl, (match?.data as DeepLinkChannel).channelName, match.data?.teamName, errorBadChannel, intl); + if (!result.error) { + await dismissAllModals(); + await popToRoot(); + } + } else if (match.type === DeepLinkTypes.PERMALINK) { + showPermalink(serverUrl, match.data.teamName, (match.data as DeepLinkPermalink).postId, intl); + } + } else { + const onError = () => { + Alert.alert( + formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + + tryOpenURL(url, onError); + } + }); + + const parseLinkLiteral = (literal: string) => { + let nextLiteral = literal; + + const WWW_REGEX = /\b^(?:www.)/i; + if (nextLiteral.match(WWW_REGEX)) { + nextLiteral = literal.replace(WWW_REGEX, 'www.'); + } + + const parsed = urlParse(nextLiteral, {}); + + return parsed.href; + }; + + const parseChildren = () => { + return Children.map(children, (child: ReactElement) => { + if (!child.props.literal || typeof child.props.literal !== 'string' || (child.props.context && child.props.context.length && !child.props.context.includes('link'))) { + return child; + } + + const {props, ...otherChildProps} = child; + // eslint-disable-next-line react/prop-types + const {literal, ...otherProps} = props; + + const nextProps = { + literal: parseLinkLiteral(literal), + ...otherProps, + }; + + return { + props: nextProps, + ...otherChildProps, + }; + }); + }; + + const handleLongPress = useCallback(() => { + if (managedConfig?.copyAndPasteProtection !== 'true') { + const renderContent = () => { + return ( + + { + DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL); + Clipboard.setString(href); + }} + testID='at_mention.bottom_sheet.copy_url' + text={intl.formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'})} + /> + { + DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL); + }} + testID='at_mention.bottom_sheet.cancel' + text={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})} + /> + + ); + }; + + showModalOverCurrentContext('BottomSheet', { + renderContent, + snapPoints: [3 * ITEM_HEIGHT, 10], + }); + } + }, [managedConfig]); + + const renderChildren = experimentalNormalizeMarkdownLinks ? parseChildren() : children; + + return ( + + {renderChildren} + + ); +}; + +const withSystemIds = withObservables([], ({database}: WithDatabaseArgs) => ({ + config: database.get(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG), +})); + +const withConfigValues = withObservables(['config'], ({config}: {config: SystemModel}) => { + const cfg: ClientConfig = config.value; + + return { + experimentalNormalizeMarkdownLinks: of(cfg.ExperimentalNormalizeMarkdownLinks), + siteURL: of(cfg.SiteURL), + }; +}); + +export default withDatabase(withSystemIds(withConfigValues(MarkdownLink))); diff --git a/app/components/markdown/markdown_list/index.tsx b/app/components/markdown/markdown_list/index.tsx new file mode 100644 index 000000000..4a61a2f5d --- /dev/null +++ b/app/components/markdown/markdown_list/index.tsx @@ -0,0 +1,35 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {ReactElement} from 'react'; + +type MarkdownList = { + children: ReactElement[]; + ordered: boolean; + start: number; + tight: boolean; +}; + +const MarkdownList = ({start = 1, tight, ordered, children}: MarkdownList) => { + let bulletWidth = 15; + if (ordered) { + const lastNumber = (start + children.length) - 1; + bulletWidth = (9 * lastNumber.toString().length) + 7; + } + + const childrenElements = React.Children.map(children, (child) => { + return React.cloneElement(child, { + bulletWidth, + ordered, + tight, + }); + }); + + return ( + <> + {childrenElements} + + ); +}; + +export default MarkdownList; diff --git a/app/components/markdown/markdown_list_item/index.tsx b/app/components/markdown/markdown_list_item/index.tsx new file mode 100644 index 000000000..ef427b043 --- /dev/null +++ b/app/components/markdown/markdown_list_item/index.tsx @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {ReactNode} from 'react'; +import {StyleSheet, Text, TextStyle, View} from 'react-native'; + +type MarkdownListItemProps = { + bulletStyle: TextStyle; + bulletWidth: number; + children: ReactNode | ReactNode[]; + continue: boolean; + index: number; + ordered: boolean; + level: number; +} + +const style = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'flex-start', + }, + bullet: { + alignItems: 'flex-end', + marginRight: 5, + }, + contents: { + flex: 1, + }, +}); + +const MarkdownListItem = ({index, level, bulletWidth, bulletStyle, children, continue: doContinue, ordered}: MarkdownListItemProps) => { + let bullet; + if (doContinue) { + bullet = ''; + } else if (ordered) { + bullet = index + '.'; + } else if (level % 2 === 0) { + bullet = '◦'; + } else { + bullet = '•'; + } + + return ( + + + + {bullet} + + + + {children} + + + ); +}; + +export default MarkdownListItem; diff --git a/app/components/markdown/markdown_table/index.tsx b/app/components/markdown/markdown_table/index.tsx new file mode 100644 index 000000000..97a69179f --- /dev/null +++ b/app/components/markdown/markdown_table/index.tsx @@ -0,0 +1,362 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent, ReactNode} from 'react'; +import {injectIntl, IntlShape} from 'react-intl'; +import {Dimensions, LayoutChangeEvent, Platform, ScaledSize, ScrollView, View} from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; + +import CompassIcon from '@components/compass_icon'; +import {CELL_MAX_WIDTH, CELL_MIN_WIDTH} from '@components/markdown/markdown_table_cell'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import DeviceTypes from '@constants/device'; +import {goToScreen} from '@screens/navigation'; +import {preventDoubleTap} from '@utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +const MAX_HEIGHT = 300; +const MAX_PREVIEW_COLUMNS = 5; + +type MarkdownTableState = { + cellWidth: number; + containerWidth: number; + contentHeight: number; + maxPreviewColumns: number; +} + +type MarkdownTableInputProps = { + children: ReactNode; + numColumns: number; +} + +type MarkdownTableProps = MarkdownTableInputProps & { + intl: IntlShape; + theme: Theme; +} + +class MarkdownTable extends PureComponent { + private rowsSliced: boolean | undefined; + private colsSliced: boolean | undefined; + + state = { + containerWidth: 0, + contentHeight: 0, + cellWidth: 0, + maxPreviewColumns: 0, + }; + + componentDidMount() { + Dimensions.addEventListener('change', this.setMaxPreviewColumns); + + const window = Dimensions.get('window'); + this.setMaxPreviewColumns({window}); + } + + componentWillUnmount() { + Dimensions.removeEventListener('change', this.setMaxPreviewColumns); + } + + setMaxPreviewColumns = ({window}: {window: ScaledSize}) => { + const maxPreviewColumns = Math.floor(window.width / CELL_MIN_WIDTH); + this.setState({maxPreviewColumns}); + } + + getTableWidth = (isFullView = false) => { + const maxPreviewColumns = this.state.maxPreviewColumns || MAX_PREVIEW_COLUMNS; + const columns = Math.min(this.props.numColumns, maxPreviewColumns); + + return (isFullView || columns === 1) ? columns * CELL_MAX_WIDTH : columns * CELL_MIN_WIDTH; + }; + + handlePress = preventDoubleTap(() => { + const {intl} = this.context; + const screen = 'Table'; + const title = intl.formatMessage({ + id: 'mobile.routes.table', + defaultMessage: 'Table', + }); + const passProps = { + renderRows: this.renderRows, + tableWidth: this.getTableWidth(true), + renderAsFlex: this.shouldRenderAsFlex(true), + }; + + goToScreen(screen, title, passProps); + }); + + handleContainerLayout = (e: LayoutChangeEvent) => { + this.setState({ + containerWidth: e.nativeEvent.layout.width, + }); + }; + + handleContentSizeChange = (contentWidth: number, contentHeight: number) => { + this.setState({ + contentHeight, + }); + }; + + renderPreviewRows = (isFullView = false) => { + return this.renderRows(isFullView, true); + } + + shouldRenderAsFlex = (isFullView = false) => { + const {numColumns} = this.props; + const {height, width} = Dimensions.get('window'); + const isLandscape = width > height; + + // render as flex in the channel screen, only for mobile phones on the portrait mode, + // and if tables have 2 ~ 4 columns + if (!isFullView && numColumns > 1 && numColumns < 4 && !DeviceTypes.IS_TABLET) { + return true; + } + + // render a 4 column table as flex when in landscape mode only + // otherwise it should expand beyond the device's full width + if (!isFullView && isLandscape && numColumns === 4) { + return true; + } + + // render as flex in full table screen, only for mobile phones on portrait mode, + // and if tables have 3 or 4 columns + if (isFullView && numColumns >= 3 && numColumns <= 4 && !DeviceTypes.IS_TABLET) { + return true; + } + + return false; + } + + getTableStyle = (isFullView: boolean) => { + const {theme} = this.props; + const style = getStyleSheet(theme); + const tableStyle = [style.table]; + + const renderAsFlex = this.shouldRenderAsFlex(isFullView); + if (renderAsFlex) { + tableStyle.push(style.displayFlex); + return tableStyle; + } + + tableStyle.push({width: this.getTableWidth(isFullView)}); + return tableStyle; + } + + renderRows = (isFullView = false, isPreview = false) => { + const tableStyle = this.getTableStyle(isFullView); + + let rows = React.Children.toArray(this.props.children) as React.ReactElement[]; + if (isPreview) { + const {maxPreviewColumns} = this.state; + const prevRowLength = rows.length; + const prevColLength = React.Children.toArray(rows[0].props.children).length; + + rows = rows.slice(0, maxPreviewColumns).map((row) => { + const children = React.Children.toArray(row.props.children).slice(0, maxPreviewColumns); + return { + ...row, + props: { + ...row.props, + children, + }, + }; + }); + + if (!rows.length) { + return null; + } + + this.rowsSliced = prevRowLength > rows.length; + this.colsSliced = prevColLength > React.Children.toArray(rows[0].props.children).length; + } + + // Add an extra prop to the last row of the table so that it knows not to render a bottom border + // since the container should be rendering that + rows[rows.length - 1] = React.cloneElement(rows[rows.length - 1], { + isLastRow: true, + }); + + // Add an extra prop to the first row of the table so that it can have a different background color + rows[0] = React.cloneElement(rows[0], { + isFirstRow: true, + }); + + return ( + + {rows} + + ); + } + + render() { + const {containerWidth, contentHeight} = this.state; + const {theme} = this.props; + const style = getStyleSheet(theme); + const tableWidth = this.getTableWidth(); + const renderAsFlex = this.shouldRenderAsFlex(); + const previewRows = this.renderPreviewRows(); + + let leftOffset; + if (renderAsFlex || tableWidth > containerWidth) { + leftOffset = containerWidth - 20; + } else { + leftOffset = tableWidth - 20; + } + let expandButtonOffset = leftOffset; + if (Platform.OS === 'android') { + expandButtonOffset -= 10; + } + + // Renders when the columns were sliced, or the table width exceeds the container, + // or if the columns exceed maximum allowed for previews + let moreRight = null; + if (this.colsSliced || + (containerWidth && tableWidth > containerWidth && !renderAsFlex) || + (this.props.numColumns > MAX_PREVIEW_COLUMNS)) { + moreRight = ( + + ); + } + + let moreBelow = null; + if (this.rowsSliced || contentHeight > MAX_HEIGHT) { + const width = renderAsFlex ? '100%' : Math.min(tableWidth, containerWidth); + + moreBelow = ( + + ); + } + + let expandButton = null; + if (expandButtonOffset > 0) { + expandButton = ( + + + + + + + + ); + } + + return ( + + + {previewRows} + + {moreRight} + {moreBelow} + {expandButton} + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + maxHeight: MAX_HEIGHT, + }, + expandButton: { + height: 34, + width: 34, + }, + iconContainer: { + maxWidth: '100%', + alignItems: 'flex-end', + paddingTop: 8, + paddingBottom: 4, + ...Platform.select({ + ios: { + paddingRight: 14, + }, + }), + }, + iconButton: { + backgroundColor: theme.centerChannelBg, + marginTop: -32, + marginRight: -6, + borderWidth: 1, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 50, + borderColor: changeOpacity(theme.centerChannelColor, 0.2), + width: 34, + height: 34, + }, + icon: { + fontSize: 14, + color: theme.linkColor, + ...Platform.select({ + ios: { + fontSize: 13, + }, + }), + }, + displayFlex: { + flex: 1, + }, + table: { + width: '100%', + borderColor: changeOpacity(theme.centerChannelColor, 0.2), + borderWidth: 1, + }, + tablePadding: { + paddingRight: 10, + }, + moreBelow: { + bottom: Platform.select({ + ios: 34, + android: 33.75, + }), + height: 20, + position: 'absolute', + left: 0, + borderColor: changeOpacity(theme.centerChannelColor, 0.2), + }, + moreRight: { + maxHeight: MAX_HEIGHT, + position: 'absolute', + top: 0, + width: 20, + borderColor: changeOpacity(theme.centerChannelColor, 0.2), + borderRightWidth: 1, + }, + }; +}); + +export default injectIntl(MarkdownTable); diff --git a/app/components/markdown/markdown_table_cell/index.tsx b/app/components/markdown/markdown_table_cell/index.tsx new file mode 100644 index 000000000..bc05528ab --- /dev/null +++ b/app/components/markdown/markdown_table_cell/index.tsx @@ -0,0 +1,62 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {ReactNode} from 'react'; +import {View} from 'react-native'; + +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +export type MarkdownTableCellProps = { + align: 'left' | 'center' | 'right'; + children: ReactNode; + isLastCell: boolean; +}; + +export const CELL_MIN_WIDTH = 96; +export const CELL_MAX_WIDTH = 192; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + cell: { + flex: 1, + borderColor: changeOpacity(theme.centerChannelColor, 0.2), + justifyContent: 'flex-start', + padding: 8, + }, + cellRightBorder: { + borderRightWidth: 1, + }, + alignCenter: { + alignItems: 'center', + }, + alignRight: { + alignItems: 'flex-end', + }, + }; +}); + +const MarkdownTableCell = ({isLastCell, align, children}: MarkdownTableCellProps) => { + const theme = useTheme(); + const style = getStyleSheet(theme); + + const cellStyle = [style.cell]; + if (!isLastCell) { + cellStyle.push(style.cellRightBorder); + } + + let textStyle = null; + if (align === 'center') { + textStyle = style.alignCenter; + } else if (align === 'right') { + textStyle = style.alignRight; + } + + return ( + + {children} + + ); +}; + +export default MarkdownTableCell; diff --git a/app/components/markdown/markdown_table_image/index.tsx b/app/components/markdown/markdown_table_image/index.tsx new file mode 100644 index 000000000..3ce4a66cd --- /dev/null +++ b/app/components/markdown/markdown_table_image/index.tsx @@ -0,0 +1,122 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useServerUrl} from '@context/server_url'; +import {generateId} from '@utils/general'; +import React, {memo, useCallback, useRef, useState} from 'react'; +import {StyleSheet, View} from 'react-native'; +import parseUrl from 'url-parse'; + +import CompassIcon from '@components/compass_icon'; +import ProgressiveImage from '@components/progressive_image'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {openGallerWithMockFile} from '@utils/gallery'; +import {calculateDimensions, isGifTooLarge} from '@utils/images'; + +type MarkdownTableImageProps = { + disabled?: boolean; + imagesMetadata: Record; + postId: string; + serverURL?: string; + source: string; +} + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + flex: 1, + }, +}); + +const MarkTableImage = ({disabled, imagesMetadata, postId, serverURL, source}: MarkdownTableImageProps) => { + const metadata = imagesMetadata[source]; + const fileId = useRef(generateId()).current; + const [failed, setFailed] = useState(isGifTooLarge(metadata)); + const currentServerUrl = useServerUrl(); + + const getImageSource = () => { + let uri = source; + let server = serverURL; + + if (!serverURL) { + server = currentServerUrl; + } + + if (uri.startsWith('/')) { + uri = server + uri; + } + + return uri; + }; + + const getFileInfo = () => { + const {height, width} = metadata; + const link = decodeURIComponent(getImageSource()); + let filename = parseUrl(link.substr(link.lastIndexOf('/'))).pathname.replace('/', ''); + let extension = filename.split('.').pop(); + + if (extension === filename) { + const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.')); + filename = `${filename}${ext}`; + extension = ext; + } + + return { + id: fileId, + name: filename, + extension, + has_preview_image: true, + post_id: postId, + uri: link, + width, + height, + }; + }; + + const handlePreviewImage = useCallback(() => { + const file = getFileInfo() as FileInfo; + if (!file?.uri) { + return; + } + openGallerWithMockFile(file.uri, file.post_id, file.height, file.width, file.id); + }, []); + + const onLoadFailed = useCallback(() => { + setFailed(true); + }, []); + + let image; + if (failed) { + image = ( + + ); + } else { + const {height, width} = calculateDimensions(metadata.height, metadata.width, 100, 100); + image = ( + + + + ); + } + + return ( + + {image} + + ); +}; + +export default memo(MarkTableImage); diff --git a/app/components/markdown/markdown_table_row/index.tsx b/app/components/markdown/markdown_table_row/index.tsx new file mode 100644 index 000000000..3f78c39f8 --- /dev/null +++ b/app/components/markdown/markdown_table_row/index.tsx @@ -0,0 +1,55 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {ReactElement, ReactNode} from 'react'; +import {View} from 'react-native'; + +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +export type MarkdownTableRowProps = { + isFirstRow: boolean; + isLastRow: boolean; + children: ReactNode; +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + row: { + flex: 1, + flexDirection: 'row', + }, + rowTopBackground: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), + }, + rowBottomBorder: { + borderColor: changeOpacity(theme.centerChannelColor, 0.2), + borderBottomWidth: 1, + }, + }; +}); + +const MarkdownTableRow = ({isFirstRow, isLastRow, children}: MarkdownTableRowProps) => { + const theme = useTheme(); + const style = getStyleSheet(theme); + + const rowStyle = [style.row]; + if (!isLastRow) { + rowStyle.push(style.rowBottomBorder); + } + + if (isFirstRow) { + rowStyle.push(style.rowTopBackground); + } + + // Add an extra prop to the last cell so that it knows not to render a right border since the container + // will handle that + const renderChildren = React.Children.toArray(children) as ReactElement[]; + renderChildren[renderChildren.length - 1] = React.cloneElement(renderChildren[renderChildren.length - 1], { + isLastCell: true, + }); + + return {renderChildren}; +}; + +export default MarkdownTableRow; diff --git a/app/components/markdown/transform.ts b/app/components/markdown/transform.ts new file mode 100644 index 000000000..a476134eb --- /dev/null +++ b/app/components/markdown/transform.ts @@ -0,0 +1,252 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Node, NodeType} from 'commonmark'; + +import {escapeRegex} from '@utils/markdown'; + +/* eslint-disable no-underscore-dangle */ + +const cjkPattern = /[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf\uac00-\ud7a3]/; + +// Combines adjacent text nodes into a single text node to make further transformation easier +export function combineTextNodes(ast: any) { + const walker = ast.walker(); + + let e; + while ((e = walker.next())) { + if (!e.entering) { + continue; + } + + const node = e.node; + + if (node.type !== 'text') { + continue; + } + + while (node._next && node._next.type === 'text') { + const next = node._next; + + node.literal += next.literal; + + node._next = next._next; + if (node._next) { + node._next._prev = node; + } + + if (node._parent._lastChild === next) { + node._parent._lastChild = node; + } + } + + // Resume parsing after the current node since otherwise the walker would continue to parse any old text nodes + // that have been merged into this one + walker.resumeAt(node, false); + } + + return ast; +} + +// Add indices to the items of every list +export function addListItemIndices(ast: any) { + const walker = ast.walker(); + + let e; + while ((e = walker.next())) { + if (e.entering) { + const node = e.node; + + if (node.type === 'list') { + let i = node.listStart == null ? 1 : node.listStart; // List indices match what would be displayed in the UI + + for (let child = node.firstChild; child; child = child.next) { + child.index = i; + + i += 1; + } + } + } + } + + return ast; +} + +// Take all images that aren't inside of tables and move them to be children of the root document node. +// When this happens, their parent nodes are split into two, if necessary, with the version that follows +// the image having its "continue" field set to true to indicate that things like bullet points don't +// need to be rendered. +export function pullOutImages(ast: any) { + const walker = ast.walker(); + + let e; + while ((e = walker.next())) { + if (!e.entering) { + continue; + } + + const node = e.node; + + // Skip tables because we'll render images inside of those as links + if (node.type === 'table') { + walker.resumeAt(node, false); + continue; + } + + if (node.type === 'image' && node.parent?.type !== 'document') { + pullOutImage(node); + } + } + + return ast; +} + +function pullOutImage(image: any) { + const parent = image.parent; + + if (parent?.type === 'link') { + image.linkDestination = parent.destination; + } +} + +export function highlightMentions(ast: Node, mentionKeys: UserMentionKey[]) { + const walker = ast.walker(); + + let e; + while ((e = walker.next())) { + if (!e.entering) { + continue; + } + + const node = e.node; + + if (node.type === 'text' && node.literal) { + const {index, mention} = getFirstMention(node.literal, mentionKeys); + + if (index === -1 || !mention) { + continue; + } + + const mentionNode = highlightTextNode(node, index, index + mention.key.length, 'mention_highlight'); + + // Resume processing on the next node after the mention node which may include any remaining text + // that was part of this one + walker.resumeAt(mentionNode, false); + } else if (node.type === 'at_mention') { + const matches = mentionKeys.some((mention) => { + const mentionName = '@' + node.mentionName; + const flags = mention.caseSensitive ? '' : 'i'; + const pattern = new RegExp(`${escapeRegex(mention.key)}\\.?`, flags); + + return pattern.test(mentionName); + }); + + if (!matches) { + continue; + } + + const wrapper = new Node('mention_highlight'); + wrapNode(wrapper, node); + + // Skip processing the wrapper to prevent checking this node again as its child + walker.resumeAt(wrapper, false); + } + } + + return ast; +} + +// Given a string and an array of mention keys, returns the first mention that appears and its index. +export function getFirstMention(str: string, mentionKeys: UserMentionKey[]) { + let firstMention = null; + let firstMentionIndex = -1; + + for (const mention of mentionKeys) { + if (mention.key.trim() === '') { + continue; + } + + const flags = mention.caseSensitive ? '' : 'i'; + let pattern; + if (cjkPattern.test(mention.key)) { + pattern = new RegExp(`${escapeRegex(mention.key)}`, flags); + } else { + pattern = new RegExp(`\\b${escapeRegex(mention.key)}_*\\b`, flags); + } + + const match = pattern.exec(str); + if (!match || match[0] === '') { + continue; + } + + if (firstMentionIndex === -1 || match.index < firstMentionIndex) { + firstMentionIndex = match.index; + firstMention = mention; + } + } + + return { + index: firstMentionIndex, + mention: firstMention, + }; +} + +// Given a text node, start/end indices, and a highlight node type, splits it into up to three nodes: +// the text before the highlight (if any exists), the highlighted text, and the text after the highlight +// the end of the highlight (if any exists). Returns a node containing the highlighted text. +export function highlightTextNode(node: Node, start: number, end: number, type: NodeType) { + const literal = node.literal; + node.literal = literal!.substring(start, end); + + // Start by wrapping the node and then re-insert any non-highlighted code around it + const highlighted = new Node(type); + wrapNode(highlighted, node); + + if (start !== 0) { + const before = new Node('text'); + before.literal = literal!.substring(0, start); + + highlighted.insertBefore(before); + } + + if (end !== literal!.length) { + const after = new Node('text'); + after.literal = literal!.substring(end); + + highlighted.insertAfter(after); + } + + return highlighted; +} + +// Wraps a given node in another node of the given type. The wrapper will take the place of +// the node in the AST relative to its parents and siblings, and it will have the node as +// its only child. +function wrapNode(wrapper: any, node: any) { + // Set parent and update parent's children if necessary + wrapper._parent = node._parent; + if (node._parent._firstChild === node) { + node._parent._firstChild = wrapper; + } + if (node._parent._lastChild === node) { + node._parent._lastChild = wrapper; + } + + // Set siblings and update those if necessary + wrapper._prev = node._prev; + node._prev = null; + if (wrapper._prev) { + wrapper._prev._next = wrapper; + } + + wrapper._next = node._next; + node._next = null; + if (wrapper._next) { + wrapper._next._prev = wrapper; + } + + // Make node a child of wrapper + wrapper._firstChild = node; + wrapper._lastChild = node; + node._parent = wrapper; +} diff --git a/app/components/progressive_image/index.tsx b/app/components/progressive_image/index.tsx new file mode 100644 index 000000000..e674ff1e7 --- /dev/null +++ b/app/components/progressive_image/index.tsx @@ -0,0 +1,161 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {ReactNode, useEffect, useRef, useState} from 'react'; +import {Animated, ImageBackground, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'; +import FastImage, {ImageStyle, ResizeMode, Source} from 'react-native-fast-image'; + +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import Thumbnail from './thumbnail'; + +const AnimatedImageBackground = Animated.createAnimatedComponent(ImageBackground); +const AnimatedFastImage = Animated.createAnimatedComponent(FastImage); + +type ProgressiveImageProps = { + children?: ReactNode | ReactNode[]; + defaultSource?: Source; // this should be provided by the component + id: string; + imageStyle?: StyleProp; + imageUri?: string; + inViewPort?: boolean; + isBackgroundImage?: boolean; + onError: () => void; + resizeMode?: ResizeMode; + style?: ViewStyle; + thumbnailUri?: string; + tintDefaultSource?: boolean; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + defaultImageContainer: { + alignItems: 'center', + justifyContent: 'center', + backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), + }, + defaultImageTint: { + flex: 1, + tintColor: changeOpacity(theme.centerChannelColor, 0.2), + }, + }; +}); + +const ProgressiveImage = ({ + children, defaultSource, id, imageStyle, imageUri, inViewPort, isBackgroundImage, onError, resizeMode = 'contain', + style = {}, thumbnailUri, tintDefaultSource, +}: ProgressiveImageProps) => { + const intensity = useRef(new Animated.Value(0)).current; + const [showHighResImage, setShowHighResImage] = useState(false); + const theme = useTheme(); + const styles = getStyleSheet(theme); + + const onLoadImageEnd = () => { + Animated.timing(intensity, { + duration: 300, + toValue: 100, + useNativeDriver: true, + }).start(); + }; + + useEffect(() => { + if (inViewPort) { + setShowHighResImage(true); + } + }, [inViewPort]); + + if (isBackgroundImage && imageUri) { + return ( + + + {children} + + + ); + } + + if (defaultSource) { + return ( + + + + ); + } + + const opacity = intensity.interpolate({ + inputRange: [20, 100], + outputRange: [0.2, 1], + }); + + const defaultOpacity = intensity.interpolate({inputRange: [0, 100], outputRange: [0.5, 0]}); + + const containerStyle = {backgroundColor: changeOpacity(theme.centerChannelColor, Number(defaultOpacity))}; + + let image; + if (thumbnailUri) { + if (showHighResImage && imageUri) { + image = ( + + ); + } + } else if (imageUri) { + image = ( + + ); + } + + return ( + + ), + ]} + /> + {image} + + ); +}; + +export default ProgressiveImage; diff --git a/app/components/progressive_image/thumbnail.tsx b/app/components/progressive_image/thumbnail.tsx new file mode 100644 index 000000000..91789cf45 --- /dev/null +++ b/app/components/progressive_image/thumbnail.tsx @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Animated, StyleProp, StyleSheet} from 'react-native'; +import FastImage, {ImageStyle, Source} from 'react-native-fast-image'; + +const AnimatedFastImage = Animated.createAnimatedComponent(FastImage); + +type ThumbnailProps = { + onError: () => void; + opacity?: number | Animated.AnimatedInterpolation | Animated.AnimatedValue; + source?: Source; + style: StyleProp; +} + +const Thumbnail = ({onError, opacity, style, source}: ThumbnailProps) => { + if (source?.uri) { + return ( + + ); + } + + const tintColor = StyleSheet.flatten(style).tintColor; + + return ( + + ); +}; + +export default Thumbnail; diff --git a/app/components/slide_up_panel_item/index.tsx b/app/components/slide_up_panel_item/index.tsx new file mode 100644 index 000000000..7fe1c0be8 --- /dev/null +++ b/app/components/slide_up_panel_item/index.tsx @@ -0,0 +1,132 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback} from 'react'; +import {StyleProp, Text, View, ViewStyle} from 'react-native'; +import FastImage, {ImageStyle, Source} from 'react-native-fast-image'; + +import CompassIcon from '@components/compass_icon'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {useTheme} from '@context/theme'; +import {preventDoubleTap} from '@utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {isValidUrl} from '@utils/url'; + +type SlideUpPanelProps = { + destructive?: boolean; + icon?: string | Source; + onPress: () => void; + testID?: string; + text: string; +} + +export const ITEM_HEIGHT = 51; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + height: ITEM_HEIGHT, + width: '100%', + }, + destructive: { + color: '#D0021B', + }, + row: { + flex: 1, + flexDirection: 'row', + }, + iconContainer: { + alignItems: 'center', + height: 50, + justifyContent: 'center', + width: 60, + }, + noIconContainer: { + height: 50, + width: 18, + }, + icon: { + color: changeOpacity(theme.centerChannelColor, 0.64), + }, + textContainer: { + justifyContent: 'center', + flex: 1, + height: 50, + marginRight: 5, + }, + text: { + color: theme.centerChannelColor, + fontSize: 16, + lineHeight: 19, + opacity: 0.9, + letterSpacing: -0.45, + }, + footer: { + marginHorizontal: 17, + borderBottomWidth: 0.5, + borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2), + }, + }; +}); + +const SlideUpPanelItem = ({destructive, icon, onPress, testID, text}: SlideUpPanelProps) => { + const theme = useTheme(); + const handleOnPress = useCallback(preventDoubleTap(onPress, 500), []); + const style = getStyleSheet(theme); + + let image; + let iconStyle: StyleProp = [style.iconContainer]; + if (icon) { + const imageStyle: StyleProp = [style.icon]; + if (destructive) { + imageStyle.push(style.destructive); + } + if (typeof icon === 'object') { + if (icon.uri && isValidUrl(icon.uri)) { + imageStyle.push({width: 24, height: 24}); + image = ( + + ); + } else { + iconStyle = [style.noIconContainer]; + } + } else { + image = ( + + ); + } + } + + return ( + + + + {Boolean(image) && + {image} + } + + {text} + + + + + + ); +}; + +export default SlideUpPanelItem; diff --git a/app/components/touchable_with_feedback/index.tsx b/app/components/touchable_with_feedback/index.tsx new file mode 100644 index 000000000..b52915114 --- /dev/null +++ b/app/components/touchable_with_feedback/index.tsx @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import TouchableWithFeedback from './touchable_with_feedback'; + +export default TouchableWithFeedback; diff --git a/app/components/touchable_with_feedback/touchable_with_feedback.android.tsx b/app/components/touchable_with_feedback/touchable_with_feedback.android.tsx new file mode 100644 index 000000000..bebea779f --- /dev/null +++ b/app/components/touchable_with_feedback/touchable_with_feedback.android.tsx @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable new-cap */ + +import React, {memo} from 'react'; +import {TouchableOpacity, TouchableWithoutFeedback, View, StyleProp, ViewStyle} from 'react-native'; +import {TouchableNativeFeedback} from 'react-native-gesture-handler'; + +type TouchableProps = { + testID: string; + children: React.ReactNode | React.ReactNode[]; + underlayColor: string; + type: 'native' | 'opacity' | 'none'; + style?: StyleProp; + [x: string]: any; +} + +const TouchableWithFeedbackAndroid = ({testID, children, underlayColor, type = 'native', ...props}: TouchableProps) => { + switch (type) { + case 'native': + return ( + + + {children} + + + ); + case 'opacity': + return ( + + {children} + + ); + case 'none': + return ( + + {children} + + ); + } + + return null; +}; + +export default memo(TouchableWithFeedbackAndroid); diff --git a/app/components/touchable_with_feedback/touchable_with_feedback.ios.tsx b/app/components/touchable_with_feedback/touchable_with_feedback.ios.tsx new file mode 100644 index 000000000..82c097375 --- /dev/null +++ b/app/components/touchable_with_feedback/touchable_with_feedback.ios.tsx @@ -0,0 +1,59 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {memo} from 'react'; +import {PanResponder, TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback, View} from 'react-native'; + +type TouchableProps = { + cancelTouchOnPanning: boolean; + children: React.ReactNode | React.ReactNode[]; + testID: string; + type: 'native' | 'opacity' | 'none'; + [x: string]: any; +} + +const TouchableWithFeedbackIOS = ({testID, children, type = 'native', cancelTouchOnPanning, ...props}: TouchableProps) => { + const panResponder = React.useRef(PanResponder.create({ + onMoveShouldSetPanResponderCapture: (evt, gestureState) => { + return cancelTouchOnPanning && (gestureState.dx >= 5 || gestureState.dy >= 5 || gestureState.vx > 5); + }, + })); + + switch (type) { + case 'native': + return ( + + + {children} + + + ); + case 'opacity': + return ( + + {children} + + ); + case 'none': + return ( + + {children} + + ); + } + + return null; +}; + +export default memo(TouchableWithFeedbackIOS); diff --git a/app/constants/deep_linking.ts b/app/constants/deep_linking.ts new file mode 100644 index 000000000..31988cb4d --- /dev/null +++ b/app/constants/deep_linking.ts @@ -0,0 +1,10 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export default { + CHANNEL: 'channel', + DMCHANNEL: 'dmchannel', + GROUPCHANNEL: 'groupchannel', + PERMALINK: 'permalink', + OTHER: 'other', +}; diff --git a/app/constants/device.ts b/app/constants/device.ts index d99480133..1b8e0e22a 100644 --- a/app/constants/device.ts +++ b/app/constants/device.ts @@ -21,7 +21,7 @@ export default { IMAGES_PATH: `${FileSystem.cacheDirectory}/Images`, IS_IPHONE_WITH_INSETS: Platform.OS === 'ios' && DeviceInfo.hasNotch(), IS_TABLET: DeviceInfo.isTablet(), - PERMANENT_SIDEBAR_SETTINGS: '@PERMANENT_SIDEBAR_SETTINGS', + PERMANENT_SIDEBAR_SETTINGS: 'PERMANENT_SIDEBAR_SETTINGS', PUSH_NOTIFY_ANDROID_REACT_NATIVE: 'android_rn', PUSH_NOTIFY_APPLE_REACT_NATIVE: 'apple_rn', TABLET_WIDTH: 250, diff --git a/app/constants/index.ts b/app/constants/index.ts index 7a005e484..c5a778561 100644 --- a/app/constants/index.ts +++ b/app/constants/index.ts @@ -4,6 +4,7 @@ import ActionType from './action_type'; import Attachment from './attachment'; import Database from './database'; +import DeepLink from './deep_linking'; import Device from './device'; import Files from './files'; import General from './general'; @@ -12,8 +13,8 @@ import Navigation from './navigation'; import Network from './network'; import Permissions from './permissions'; import Preferences from './preferences'; -import Screens from './screens'; import SSO, {REDIRECT_URL_SCHEME, REDIRECT_URL_SCHEME_DEV} from './sso'; +import Screens from './screens'; import View, {Upgrade} from './view'; import WebsocketEvents from './websocket'; @@ -21,6 +22,7 @@ export { ActionType, Attachment, Database, + DeepLink, Device, Files, General, @@ -31,8 +33,8 @@ export { Preferences, REDIRECT_URL_SCHEME, REDIRECT_URL_SCHEME_DEV, - Screens, SSO, + Screens, Upgrade, View, WebsocketEvents, diff --git a/app/context/theme/index.tsx b/app/context/theme/index.tsx index 00fa8742d..36d3c49bf 100644 --- a/app/context/theme/index.tsx +++ b/app/context/theme/index.tsx @@ -8,6 +8,8 @@ import {Appearance} from 'react-native'; import {Preferences} from '@constants'; import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; +import EphemeralStore from '@store/ephemeral_store'; +import {setNavigationStackStyles} from '@utils/theme'; import type Database from '@nozbe/watermelondb/Database'; import type {PreferenceModel, SystemModel} from '@database/models/server'; @@ -42,6 +44,10 @@ const ThemeProvider = ({currentTeamId, children, themes}: Props) => { if (teamTheme?.value) { try { const theme = JSON.parse(teamTheme.value) as Theme; + EphemeralStore.theme = theme; + requestAnimationFrame(() => { + setNavigationStackStyles(theme); + }); return theme; } catch { // no theme change @@ -62,7 +68,7 @@ const ThemeProvider = ({currentTeamId, children, themes}: Props) => { }; export function withTheme(Component: ComponentType): ComponentType { - return function ServerUrlComponent(props) { + return function ThemeComponent(props) { return ( {(theme: Theme) => ( diff --git a/app/database/models/server/my_channel.ts b/app/database/models/server/my_channel.ts index 41dd4d33e..c6fe73ba4 100644 --- a/app/database/models/server/my_channel.ts +++ b/app/database/models/server/my_channel.ts @@ -24,6 +24,9 @@ export default class MyChannelModel extends Model { /** last_viewed_at : The timestamp showing the user's last viewed post on this channel */ @field('last_viewed_at') lastViewedAt!: number; + /** manually_unread : Determine if the user marked a post as unread */ + @field('manually_unread') manuallyUnread!: boolean; + /** mentions_count : The number of mentions on this channel */ @field('mentions_count') mentionsCount!: number; diff --git a/app/database/operator/server_data_operator/handlers/channel.test.ts b/app/database/operator/server_data_operator/handlers/channel.test.ts index 47852f760..6afce2506 100644 --- a/app/database/operator/server_data_operator/handlers/channel.test.ts +++ b/app/database/operator/server_data_operator/handlers/channel.test.ts @@ -142,6 +142,25 @@ describe('*** Operator: Channel Handlers tests ***', () => { expect.assertions(2); const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords'); + const channels: Channel[] = [{ + id: 'c', + name: 'channel', + display_name: 'Channel', + type: 'O', + create_at: 1, + update_at: 1, + delete_at: 0, + team_id: '123', + header: '', + purpose: '', + last_post_at: 2, + creator_id: 'me', + total_msg_count: 20, + extra_update_at: 0, + shared: false, + scheme_id: null, + group_constrained: false, + }]; const myChannels: ChannelMembership[] = [ { id: 'c', @@ -164,6 +183,7 @@ describe('*** Operator: Channel Handlers tests ***', () => { ]; await operator.handleMyChannel({ + channels, myChannels, prepareRecordsOnly: false, }); diff --git a/app/database/operator/server_data_operator/handlers/channel.ts b/app/database/operator/server_data_operator/handlers/channel.ts index 81d90aeee..4d2a1347f 100644 --- a/app/database/operator/server_data_operator/handlers/channel.ts +++ b/app/database/operator/server_data_operator/handlers/channel.ts @@ -34,7 +34,7 @@ export interface ChannelHandlerMix { handleChannel: ({channels, prepareRecordsOnly}: HandleChannelArgs) => Promise; handleMyChannelSettings: ({settings, prepareRecordsOnly}: HandleMyChannelSettingsArgs) => Promise; handleChannelInfo: ({channelInfos, prepareRecordsOnly}: HandleChannelInfoArgs) => Promise; - handleMyChannel: ({myChannels, prepareRecordsOnly}: HandleMyChannelArgs) => Promise; + handleMyChannel: ({channels, myChannels, prepareRecordsOnly}: HandleMyChannelArgs) => Promise; } const ChannelHandler = (superclass: any) => class extends superclass { @@ -130,13 +130,20 @@ const ChannelHandler = (superclass: any) => class extends superclass { * @throws DataOperatorException * @returns {Promise} */ - handleMyChannel = ({myChannels, prepareRecordsOnly = true}: HandleMyChannelArgs): Promise => { + handleMyChannel = ({channels, myChannels, prepareRecordsOnly = true}: HandleMyChannelArgs): Promise => { if (!myChannels.length) { throw new DataOperatorException( 'An empty "myChannels" array has been passed to the handleMyChannel method', ); } + myChannels.forEach((my) => { + const channel = channels.find((c) => c.id === my.channel_id); + if (channel) { + my.msg_count = Math.max(0, channel.total_msg_count - my.msg_count); + } + }); + const createOrUpdateRawValues = getUniqueRawsBy({ raws: myChannels, key: 'id', diff --git a/app/database/operator/server_data_operator/handlers/posts_in_channel.ts b/app/database/operator/server_data_operator/handlers/posts_in_channel.ts index fc1389fc3..003da757c 100644 --- a/app/database/operator/server_data_operator/handlers/posts_in_channel.ts +++ b/app/database/operator/server_data_operator/handlers/posts_in_channel.ts @@ -74,12 +74,10 @@ const PostsInChannelHandler = (superclass: any) => class extends superclass { const latest = lastPost.create_at; // Find the records in the PostsInChannel table that have a matching channel_id - // const chunks = (await database.collections.get(POSTS_IN_CHANNEL).query(Q.where('channel_id', channelId)).fetch()) as PostsInChannel[]; - const chunks = (await retrieveRecords({ - database: this.database, - tableName: POSTS_IN_CHANNEL, - condition: (Q.where('id', channelId), Q.experimentalSortBy('latest', Q.desc)), - })) as PostsInChannelModel[]; + const chunks = (await this.database.get(POSTS_IN_CHANNEL).query( + Q.where('id', channelId), + Q.experimentalSortBy('latest', Q.desc), + ).fetch()) as PostsInChannelModel[]; // chunk length 0; then it's a new chunk to be added to the PostsInChannel table if (chunks.length === 0) { diff --git a/app/database/operator/server_data_operator/handlers/team.ts b/app/database/operator/server_data_operator/handlers/team.ts index af11bae25..bb84772ca 100644 --- a/app/database/operator/server_data_operator/handlers/team.ts +++ b/app/database/operator/server_data_operator/handlers/team.ts @@ -201,7 +201,7 @@ const TeamHandler = (superclass: any) => class extends superclass { ); } - const createOrUpdateRawValues = getUniqueRawsBy({raws: myTeams, key: 'team_id'}); + const createOrUpdateRawValues = getUniqueRawsBy({raws: myTeams, key: 'id'}); return this.handleRecords({ fieldName: 'id', diff --git a/app/database/schema/server/table_schemas/my_channel.ts b/app/database/schema/server/table_schemas/my_channel.ts index 2c5d30026..cea26920c 100644 --- a/app/database/schema/server/table_schemas/my_channel.ts +++ b/app/database/schema/server/table_schemas/my_channel.ts @@ -12,6 +12,7 @@ export default tableSchema({ columns: [ {name: 'last_post_at', type: 'number'}, {name: 'last_viewed_at', type: 'number'}, + {name: 'manually_unread', type: 'boolean'}, {name: 'mentions_count', type: 'number'}, {name: 'message_count', type: 'number'}, {name: 'roles', type: 'string'}, diff --git a/app/database/schema/server/test.ts b/app/database/schema/server/test.ts index 3b6549508..9baddaacf 100644 --- a/app/database/schema/server/test.ts +++ b/app/database/schema/server/test.ts @@ -112,6 +112,7 @@ describe('*** Test schema for SERVER database ***', () => { columns: { last_post_at: {name: 'last_post_at', type: 'number'}, last_viewed_at: {name: 'last_viewed_at', type: 'number'}, + manually_unread: {name: 'manually_unread', type: 'boolean'}, mentions_count: {name: 'mentions_count', type: 'number'}, message_count: {name: 'message_count', type: 'number'}, roles: {name: 'roles', type: 'string'}, @@ -119,6 +120,7 @@ describe('*** Test schema for SERVER database ***', () => { columnArray: [ {name: 'last_post_at', type: 'number'}, {name: 'last_viewed_at', type: 'number'}, + {name: 'manually_unread', type: 'boolean'}, {name: 'mentions_count', type: 'number'}, {name: 'message_count', type: 'number'}, {name: 'roles', type: 'string'}, diff --git a/app/helpers/api/channel.ts b/app/helpers/api/channel.ts new file mode 100644 index 000000000..0c9298add --- /dev/null +++ b/app/helpers/api/channel.ts @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Alert} from 'react-native'; + +import type {IntlShape} from 'react-intl'; + +export function privateChannelJoinPrompt(displayName: string, intl: IntlShape): Promise<{join: boolean}> { + return new Promise((resolve) => { + Alert.alert( + intl.formatMessage({ + id: 'permalink.show_dialog_warn.title', + defaultMessage: 'Join private channel', + }), + intl.formatMessage({ + id: 'permalink.show_dialog_warn.description', + defaultMessage: 'You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?', + }, { + channel: displayName, + }), + [ + { + text: intl.formatMessage({ + id: 'permalink.show_dialog_warn.cancel', + defaultMessage: 'Cancel', + }), + onPress: async () => { + resolve({ + join: false, + }); + }, + }, + { + text: intl.formatMessage({ + id: 'permalink.show_dialog_warn.join', + defaultMessage: 'Join', + }), + onPress: async () => { + resolve({ + join: true, + }); + }, + }, + ], + ); + }); +} diff --git a/app/helpers/api/preference.ts b/app/helpers/api/preference.ts index f1b450e16..49b168f49 100644 --- a/app/helpers/api/preference.ts +++ b/app/helpers/api/preference.ts @@ -3,6 +3,8 @@ import {General, Preferences} from '@constants'; +import type PreferenceModel from '@typings/database/models/servers/preference'; + export function getPreferenceValue(preferences: PreferenceType[], category: string, name: string, defaultValue: unknown = '') { const pref = preferences.find((p) => p.category === category && p.name === name); @@ -27,9 +29,9 @@ export function getPreferenceAsInt(preferences: PreferenceType[], category: stri return defaultValue; } -export function getTeammateNameDisplaySetting(preferences: PreferenceType[], config?: ClientConfig, license?: ClientLicense) { +export function getTeammateNameDisplaySetting(preferences: PreferenceType[] | PreferenceModel[], config?: ClientConfig, license?: ClientLicense) { const useAdminTeammateNameDisplaySetting = license?.LockTeammateNameDisplay === 'true' && config?.LockTeammateNameDisplay === 'true'; - const preference = getPreferenceValue(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT, '') as string; + const preference = getPreferenceValue(preferences as PreferenceType[], Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT, '') as string; if (preference && !useAdminTeammateNameDisplaySetting) { return preference; } else if (config?.TeammateNameDisplay) { diff --git a/app/hooks/device.ts b/app/hooks/device.ts new file mode 100644 index 000000000..cb10a2075 --- /dev/null +++ b/app/hooks/device.ts @@ -0,0 +1,59 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useEffect, useState} from 'react'; +import {DeviceEventEmitter, NativeModules, useWindowDimensions} from 'react-native'; + +import {Device} from '@constants'; +import {MM_TABLES} from '@constants/database'; +import DatabaseManager from '@database/manager'; + +import type GlobalModel from '@typings/database/models/app/global'; + +const {MattermostManaged} = NativeModules; +const isRunningInSplitView = MattermostManaged.isRunningInSplitView; + +export function usePermanentSidebar() { + const [permanentSidebar, setPermanentSidebar] = useState(Device.IS_TABLET); + + useEffect(() => { + const handlePermanentSidebar = async () => { + if (Device.IS_TABLET) { + const database = DatabaseManager.appDatabase?.database; + if (database) { + try { + const enabled = await database.get(MM_TABLES.APP.GLOBAL).find(Device.PERMANENT_SIDEBAR_SETTINGS) as GlobalModel; + setPermanentSidebar(enabled.value === 'true'); + } catch { + setPermanentSidebar(false); + } + } + } + }; + + handlePermanentSidebar(); + + const listener = DeviceEventEmitter.addListener(Device.PERMANENT_SIDEBAR_SETTINGS, handlePermanentSidebar); + + return () => { + listener.remove(); + }; + }, []); + + return permanentSidebar; +} + +export function useSplitView() { + const [isSplitView, setIsSplitView] = useState(false); + const dimensions = useWindowDimensions(); + + useEffect(() => { + if (Device.IS_TABLET) { + isRunningInSplitView().then((result: {isSplitView: boolean}) => { + setIsSplitView(result.isSplitView); + }); + } + }, [dimensions]); + + return isSplitView; +} diff --git a/app/init/global_event_handler.ts b/app/init/global_event_handler.ts index 11a3a27bf..84234ecb1 100644 --- a/app/init/global_event_handler.ts +++ b/app/init/global_event_handler.ts @@ -129,7 +129,11 @@ class GlobalEventHandler { ); } - fetchConfigAndLicense(serverUrl); + const fetchTimeout = setTimeout(() => { + // Defer the call to avoid collision with other request writting to the db + fetchConfigAndLicense(serverUrl); + clearTimeout(fetchTimeout); + }, 3000); } }; diff --git a/app/init/launch.ts b/app/init/launch.ts index 9ccf97a8e..2b8744874 100644 --- a/app/init/launch.ts +++ b/app/init/launch.ts @@ -5,8 +5,11 @@ import {Linking} from 'react-native'; import {Notifications} from 'react-native-notifications'; import {Screens} from '@constants'; +import DatabaseManager from '@database/manager'; import {getActiveServerUrl, getServerCredentials} from '@init/credentials'; +import {queryThemeForCurrentTeam} from '@queries/servers/preference'; import {goToScreen, resetToChannel, resetToSelectServer} from '@screens/navigation'; +import EphemeralStore from '@store/ephemeral_store'; import {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkType, DeepLinkWithData, LaunchProps, LaunchType} from '@typings/launch'; import {parseDeepLink} from '@utils/url'; @@ -62,6 +65,10 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => { if (serverUrl) { const credentials = await getServerCredentials(serverUrl); if (credentials) { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (database) { + EphemeralStore.theme = await queryThemeForCurrentTeam(database); + } launchToChannel({...props, serverUrl}, resetNavigation); return; } diff --git a/app/queries/servers/channel.ts b/app/queries/servers/channel.ts index 420431301..09bc10cd9 100644 --- a/app/queries/servers/channel.ts +++ b/app/queries/servers/channel.ts @@ -8,8 +8,9 @@ import {MM_TABLES} from '@constants/database'; import type ServerDataOperator from '@database/operator/server_data_operator'; import type ChannelModel from '@typings/database/models/servers/channel'; import type ChannelInfoModel from '@typings/database/models/servers/channel_info'; +import type MyChannelModel from '@typings/database/models/servers/my_channel'; -const {SERVER: {CHANNEL}} = MM_TABLES; +const {SERVER: {CHANNEL, MY_CHANNEL}} = MM_TABLES; export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, teamId: string, channels: Channel[], channelMembers: ChannelMembership[]) => { const allChannelsForTeam = await queryAllChannelsForTeam(operator.database, teamId); @@ -43,7 +44,7 @@ export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, tea const channelRecords = operator.handleChannel({channels, prepareRecordsOnly: true}); const channelInfoRecords = operator.handleChannelInfo({channelInfos, prepareRecordsOnly: true}); const membershipRecords = operator.handleChannelMembership({channelMemberships: memberships, prepareRecordsOnly: true}); - const myChannelRecords = operator.handleMyChannel({myChannels: memberships, prepareRecordsOnly: true}); + const myChannelRecords = operator.handleMyChannel({channels, myChannels: memberships, prepareRecordsOnly: true}); const myChannelSettingsRecords = operator.handleMyChannelSettings({settings: memberships, prepareRecordsOnly: true}); return [channelRecords, channelInfoRecords, membershipRecords, myChannelRecords, myChannelSettingsRecords]; @@ -55,3 +56,25 @@ export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, tea export const queryAllChannelsForTeam = (database: Database, teamId: string) => { return database.get(CHANNEL).query(Q.where('team_id', teamId)).fetch() as Promise; }; + +export const queryMyChannel = async (database: Database, channelId: string) => { + try { + const member = await database.get(MY_CHANNEL).find(channelId) as MyChannelModel; + return member; + } catch { + return undefined; + } +}; + +export const queryChannelByName = async (database: Database, channelName: string) => { + try { + const channels = await database.get(CHANNEL).query(Q.where('name', channelName)).fetch() as ChannelModel[]; + if (channels.length) { + return channels[0]; + } + + return undefined; + } catch { + return undefined; + } +}; diff --git a/app/queries/servers/post.ts b/app/queries/servers/post.ts index 7c4993b73..e67ccdf91 100644 --- a/app/queries/servers/post.ts +++ b/app/queries/servers/post.ts @@ -13,7 +13,7 @@ const {SERVER: {POST, POSTS_IN_CHANNEL}} = MM_TABLES; export const queryPostsInChannel = (database: Database, channelId: string): Promise => { try { return database.get(POSTS_IN_CHANNEL).query( - Q.where('channel_id', channelId), + Q.where('id', channelId), Q.experimentalSortBy('latest', Q.desc), ).fetch() as Promise; } catch { @@ -21,12 +21,12 @@ export const queryPostsInChannel = (database: Database, channelId: string): Prom } }; -export const queryPostsChunk = (database: Database, channelId: string, earlies: number, latest: number): Promise => { +export const queryPostsChunk = (database: Database, channelId: string, earliest: number, latest: number): Promise => { try { return database.get(POST).query( Q.and( Q.where('channel_id', channelId), - Q.where('create_at', Q.between(earlies, latest)), + Q.where('create_at', Q.between(earliest, latest)), Q.where('delete_at', Q.eq(0)), ), Q.experimentalSortBy('create_at', Q.desc), diff --git a/app/queries/servers/preference.ts b/app/queries/servers/preference.ts index 2c41822ba..961a46769 100644 --- a/app/queries/servers/preference.ts +++ b/app/queries/servers/preference.ts @@ -3,8 +3,11 @@ import {Database, Q} from '@nozbe/watermelondb'; +import {Preferences} from '@constants'; import {MM_TABLES} from '@constants/database'; +import {queryCurrentTeamId} from './system'; + import type ServerDataOperator from '@database/operator/server_data_operator'; import type PreferenceModel from '@typings/database/models/servers/preference'; @@ -25,3 +28,17 @@ export const queryPreferencesByCategoryAndName = (database: Database, category: Q.where('name', name), ).fetch() as Promise; }; + +export const queryThemeForCurrentTeam = async (database: Database) => { + const currentTeamId = await queryCurrentTeamId(database); + const teamTheme = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_THEME, currentTeamId); + if (teamTheme.length) { + try { + return JSON.parse(teamTheme[0].value) as Theme; + } catch { + return undefined; + } + } + + return undefined; +}; diff --git a/app/queries/servers/system.ts b/app/queries/servers/system.ts index 398dddfae..904cc4b8a 100644 --- a/app/queries/servers/system.ts +++ b/app/queries/servers/system.ts @@ -27,6 +27,15 @@ export const queryCurrentChannelId = async (serverDatabase: Database) => { } }; +export const queryCurrentTeamId = async (serverDatabase: Database) => { + try { + const currentTeamId = await serverDatabase.get(SYSTEM).find(SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID) as SystemModel; + return (currentTeamId?.value || '') as string; + } catch { + return ''; + } +}; + export const queryCurrentUserId = async (serverDatabase: Database) => { try { const currentUserId = await serverDatabase.get(SYSTEM).find(SYSTEM_IDENTIFIERS.CURRENT_USER_ID) as SystemModel; @@ -129,3 +138,32 @@ export const prepareCommonSystemValues = ( return undefined; } }; + +export const setCurrentChannelId = async (operator: ServerDataOperator, channelId: string) => { + try { + const models = await prepareCommonSystemValues(operator, {currentChannelId: channelId}); + if (models) { + await operator.batchRecords(models); + } + + return {currentChannelId: channelId}; + } catch (error) { + return {error}; + } +}; + +export const setCurrentTeamAndChannelId = async (operator: ServerDataOperator, teamId?: string, channelId?: string) => { + try { + const models = await prepareCommonSystemValues(operator, { + currentChannelId: channelId, + currentTeamId: teamId, + }); + if (models) { + await operator.batchRecords(models); + } + + return {currentChannelId: channelId}; + } catch (error) { + return {error}; + } +}; diff --git a/app/queries/servers/team.ts b/app/queries/servers/team.ts index 4b54f4fac..d40c1b3e0 100644 --- a/app/queries/servers/team.ts +++ b/app/queries/servers/team.ts @@ -1,22 +1,28 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Database} from '@nozbe/watermelondb'; +import {Database, Q} from '@nozbe/watermelondb'; import {Database as DatabaseConstants} from '@constants'; import type ServerDataOperator from '@database/operator/server_data_operator'; +import type MyTeamModel from '@typings/database/models/servers/my_team'; import type TeamChannelHistoryModel from '@typings/database/models/servers/team_channel_history'; import type TeamModel from '@typings/database/models/servers/team'; -const {TEAM, TEAM_CHANNEL_HISTORY} = DatabaseConstants.MM_TABLES.SERVER; +const {MY_TEAM, TEAM, TEAM_CHANNEL_HISTORY} = DatabaseConstants.MM_TABLES.SERVER; export const addChannelToTeamHistory = async (operator: ServerDataOperator, teamId: string, channelId: string, prepareRecordsOnly = false) => { let tch: TeamChannelHistory|undefined; try { const teamChannelHistory = (await operator.database.get(TEAM_CHANNEL_HISTORY).find(teamId)) as TeamChannelHistoryModel; - const channelIds = teamChannelHistory.channelIds; + const channelIdSet = new Set(teamChannelHistory.channelIds); + if (channelIdSet.has(channelId)) { + channelIdSet.delete(channelId); + } + + const channelIds = Array.from(channelIdSet); channelIds.unshift(channelId); tch = { id: teamId, @@ -52,6 +58,15 @@ export const prepareMyTeams = (operator: ServerDataOperator, teams: Team[], memb } }; +export const queryMyTeamById = async (database: Database, teamId: string): Promise => { + try { + const myTeam = (await database.get(MY_TEAM).find(teamId)) as MyTeamModel; + return myTeam; + } catch (err) { + return undefined; + } +}; + export const queryTeamById = async (database: Database, teamId: string): Promise => { try { const team = (await database.get(TEAM).find(teamId)) as TeamModel; @@ -60,3 +75,16 @@ export const queryTeamById = async (database: Database, teamId: string): Promise return undefined; } }; + +export const queryTeamByName = async (database: Database, teamName: string): Promise => { + try { + const team = (await database.get(TEAM).query(Q.where('name', teamName)).fetch()) as TeamModel[]; + if (team.length) { + return team[0]; + } + + return undefined; + } catch { + return undefined; + } +}; diff --git a/app/screens/bottom_sheet/index.tsx b/app/screens/bottom_sheet/index.tsx new file mode 100644 index 000000000..27d80c598 --- /dev/null +++ b/app/screens/bottom_sheet/index.tsx @@ -0,0 +1,100 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {ReactNode, useEffect, useRef} from 'react'; +import {BackHandler, DeviceEventEmitter, StyleSheet, useWindowDimensions, View} from 'react-native'; +import {State, TapGestureHandler} from 'react-native-gesture-handler'; +import Animated from 'react-native-reanimated'; +import RNBottomSheet from 'reanimated-bottom-sheet'; + +import {Navigation} from '@constants'; +import {useTheme} from '@context/theme'; +import {dismissModal} from '@screens/navigation'; +import {hapticFeedback} from '@utils/general'; + +import Indicator from './indicator'; + +type SlideUpPanelProps = { + initialSnapIndex?: number; + renderContent: () => ReactNode; + snapPoints?: Array; +} + +const BottomSheet = ({initialSnapIndex = 0, renderContent, snapPoints = ['90%', '50%', 50]}: SlideUpPanelProps) => { + const sheetRef = useRef(null); + const dimensions = useWindowDimensions(); + const theme = useTheme(); + const lastSnap = snapPoints.length - 1; + + useEffect(() => { + const listener = DeviceEventEmitter.addListener(Navigation.NAVIGATION_CLOSE_MODAL, () => sheetRef.current?.snapTo(lastSnap)); + + return () => listener.remove(); + }, []); + + useEffect(() => { + const listener = BackHandler.addEventListener('hardwareBackPress', () => { + sheetRef.current?.snapTo(1); + return true; + }); + + return () => listener.remove(); + }, []); + + useEffect(() => { + hapticFeedback(); + sheetRef.current?.snapTo(initialSnapIndex); + }, []); + + const renderBackdrop = () => { + return ( + { + if (event.nativeEvent.state === State.END && event.nativeEvent.oldState === State.ACTIVE) { + sheetRef.current?.snapTo(lastSnap); + } + }} + > + + + ); + }; + + const renderContainer = () => ( + + {renderContent()} + + ); + + return ( + <> + dismissModal()} + enabledBottomInitialAnimation={true} + renderHeader={Indicator} + enabledContentTapInteraction={false} + /> + {renderBackdrop()} + + ); +}; + +export default BottomSheet; diff --git a/app/screens/bottom_sheet/indicator.tsx b/app/screens/bottom_sheet/indicator.tsx new file mode 100644 index 000000000..0534a1289 --- /dev/null +++ b/app/screens/bottom_sheet/indicator.tsx @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Animated, StyleSheet, View} from 'react-native'; + +const styles = StyleSheet.create({ + dragIndicatorContainer: { + marginVertical: 10, + alignItems: 'center', + justifyContent: 'center', + }, + dragIndicator: { + backgroundColor: 'white', + height: 5, + width: 62.5, + opacity: 0.9, + borderRadius: 25, + }, +}); + +const Indicator = () => { + return ( + + + + ); +}; + +export default Indicator; diff --git a/app/screens/channel/index.tsx b/app/screens/channel/index.tsx index a0d5219d9..284d555ff 100644 --- a/app/screens/channel/index.tsx +++ b/app/screens/channel/index.tsx @@ -3,8 +3,8 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; -import React, {useMemo} from 'react'; -import {Text, View} from 'react-native'; +import React, {useMemo, useState} from 'react'; +import {Text, View, ScrollView} from 'react-native'; import {SafeAreaView} from 'react-native-safe-area-context'; import {logout} from '@actions/remote/session'; @@ -24,6 +24,12 @@ import type {WithDatabaseArgs} from '@typings/database/database'; import FailedChannels from './failed_channels'; import FailedTeams from './failed_teams'; +import Markdown from '@components/markdown'; +import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; +import md from './md.json'; +import ProgressiveImage from '@components/progressive_image'; +import JumboEmoji from '@components/jumbo_emoji'; + type ChannelProps = WithDatabaseArgs & LaunchProps & { currentChannelId: SystemModel; currentTeamId: SystemModel; @@ -88,6 +94,14 @@ const Channel = ({currentChannelId, currentTeamId, time}: ChannelProps) => { ); }, [currentTeamId.value, currentChannelId.value]); + const textStyle = getMarkdownTextStyles(theme); + const blockStyle = getMarkdownBlockStyles(theme); + const [inViewport, setInViewport] = useState(false); + + setTimeout(async () => { + setInViewport(true); + }, 3000); + return ( { {renderComponent} - - - {`Loaded in: ${time || 0}ms. Logout from ${serverUrl}`} - - + + true} + style={{width: 322.72727272727275, height: 132.96363636363637}} + inViewPort={inViewport} + /> + + + + + {`Loaded in: ${time || 0}ms. Logout from ${serverUrl}`} + + + ); diff --git a/app/screens/channel/md.json b/app/screens/channel/md.json new file mode 100644 index 000000000..efc5b24cf --- /dev/null +++ b/app/screens/channel/md.json @@ -0,0 +1,19 @@ +{ + "test": "This is a file for testing purposes, should be deleted once is not needed", + "value": ":smile: :thanks: :mattermost: :nonexistent:\n\n# Can you please have a look at those 3 files ?\n![welcome image](http://www.microlife-dns.com/welcome-paper-poster-with-colorful-brush-strokes-vector-21849225.jpeg)\n\nlet's add some text here\nand a second line\n\n```js\n1. @components/markdown/markdown.tsx\n2. @components/markdown/transform.ts\n3. @components/markdown/markdown_table_image/index.tsx\n4. another one\n```\n\n> this should be a quote\nof two lines\n\n1. ~~first~~\n2. *second* with a #hashtag\n3. **third**\n\n`code span of one line`\n\n| Syntax | Description |\n| ----------- | ----------- |\n| Header | Title |\n| Paragraph | Text |\n\n[google link](https://google.com)\n\nhttps://community.mattermost.com\n\nsome html

My Great Heading

\n\nthis is an at mention for @elias, @nonexistent and @nahumhbl. and @channel @user15.6.\n\nthis is a @groupMention.\n\n### Mentioning ~town-square and ~forth or a [direct link to the channel](http://192.168.1.6:8065/apps-team/channels/forth). Tap on them to switch channels.", + "channelMentions": { + "forth": { + "display_name": "forth", + "team_name":"apps-team" + } + }, + "imagesMetadata": { + "http://www.microlife-dns.com/welcome-paper-poster-with-colorful-brush-strokes-vector-21849225.jpeg": { + "width": 1000, + "height": 412, + "format": "jpeg", + "frame_count": 0 + } + }, + "jumbo": ":fire: :mattermost: :thanks:\n:the_horns: :not:" +} \ No newline at end of file diff --git a/app/screens/index.tsx b/app/screens/index.tsx index 5644bd8ce..73b858807 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -60,6 +60,9 @@ Navigation.setLazyComponentRegistrator((screenName) => { // case 'AdvancedSettings': // screen = require('@screens/settings/advanced_settings').default; // break; + case 'BottomSheet': + screen = require('@screens/bottom_sheet').default; + break; // case 'ChannelAddMembers': // screen = require('@screens/channel_add_members').default; // break; @@ -157,9 +160,6 @@ Navigation.setLazyComponentRegistrator((screenName) => { // case 'NotificationSettingsMobile': // screen = require('@screens/settings/notification_settings_mobile').default; // break; - // case 'OptionsModal': - // screen = require('@screens/options_modal').default; - // break; // case 'Permalink': // screen = require('@screens/permalink').default; // break; @@ -220,7 +220,7 @@ Navigation.setLazyComponentRegistrator((screenName) => { } if (screen) { - Navigation.registerComponent(screenName, () => withGestures(withIntl(withManagedConfig(screen)), extraStyles)); + Navigation.registerComponent(screenName, () => withSafeAreaInsets(withGestures(withIntl(withManagedConfig(screen)), extraStyles))); } }); @@ -228,6 +228,6 @@ export function registerScreens() { const channelScreen = require('@screens/channel').default; const serverScreen = require('@screens/server').default; - Navigation.registerComponent(Screens.CHANNEL, () => withSafeAreaInsets(withIntl(withServerDatabase(withManagedConfig(channelScreen))))); + Navigation.registerComponent(Screens.CHANNEL, () => withSafeAreaInsets(withGestures(withIntl(withServerDatabase(withManagedConfig(channelScreen))), undefined))); Navigation.registerComponent(Screens.SERVER, () => withIntl(withManagedConfig(serverScreen))); } diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index 0e34b27fe..47ddc3c9d 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -12,6 +12,9 @@ import EphemeralStore from '@store/ephemeral_store'; import type {LaunchProps} from '@typings/launch'; function getThemeFromState() { + if (EphemeralStore.theme) { + return EphemeralStore.theme; + } if (Appearance.getColorScheme() === 'dark') { return Preferences.THEMES.windows10; } @@ -52,28 +55,29 @@ export function resetToChannel(passProps = {}) { }], }; - let platformStack: Layout = {stack}; - if (Platform.OS === 'android') { - platformStack = { - sideMenu: { - left: { - component: { - id: Screens.MAIN_SIDEBAR, - name: Screens.MAIN_SIDEBAR, - }, - }, - center: { - stack, - }, - right: { - component: { - id: Screens.SETTINGS_SIDEBAR, - name: Screens.SETTINGS_SIDEBAR, - }, - }, - }, - }; - } + const platformStack: Layout = {stack}; + + // if (Platform.OS === 'android') { + // platformStack = { + // sideMenu: { + // left: { + // component: { + // id: Screens.MAIN_SIDEBAR, + // name: Screens.MAIN_SIDEBAR, + // }, + // }, + // center: { + // stack, + // }, + // right: { + // component: { + // id: Screens.SETTINGS_SIDEBAR, + // name: Screens.SETTINGS_SIDEBAR, + // }, + // }, + // }, + // }; + // } Navigation.setRoot({ root: { diff --git a/app/store/ephemeral_store.ts b/app/store/ephemeral_store.ts index a8102b3fd..faa179b6b 100644 --- a/app/store/ephemeral_store.ts +++ b/app/store/ephemeral_store.ts @@ -5,6 +5,7 @@ class EphemeralStore { allNavigationComponentIds: string[] = []; navigationComponentIdStack: string[] = []; navigationModalStack: string[] = []; + theme: Theme | undefined; getNavigationTopComponentId = () => this.navigationComponentIdStack[0]; diff --git a/app/utils/draft/index.ts b/app/utils/draft/index.ts new file mode 100644 index 000000000..9c1c64836 --- /dev/null +++ b/app/utils/draft/index.ts @@ -0,0 +1,35 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {MessageDescriptor} from '@formatjs/intl/src/types'; +import {Alert, AlertButton} from 'react-native'; + +import type {IntlShape} from 'react-intl'; + +import {t} from '@utils/i18n'; + +export function errorBadChannel(intl: IntlShape) { + const message = { + id: t('mobile.server_link.unreachable_channel.error'), + defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.', + }; + + return alertErrorWithFallback(intl, {}, message); +} + +export function permalinkBadTeam(intl: IntlShape) { + const message = { + id: t('mobile.server_link.unreachable_team.error'), + defaultMessage: 'This link belongs to a deleted team or to a team to which you do not have access.', + }; + + alertErrorWithFallback(intl, {}, message); +} + +export function alertErrorWithFallback(intl: IntlShape, error: any, fallback: MessageDescriptor, values?: Record, buttons?: AlertButton[]) { + let msg = error?.message; + if (!msg || msg === 'Network request failed') { + msg = intl.formatMessage(fallback, values); + } + Alert.alert('', msg, buttons); +} diff --git a/app/utils/emoji/emoji.json b/app/utils/emoji/emoji.json new file mode 100644 index 000000000..39daf4a3c --- /dev/null +++ b/app/utils/emoji/emoji.json @@ -0,0 +1 @@ +[{"name":"GRINNING FACE","unified":"1F600","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f600","sheet_x":30,"sheet_y":32,"short_name":"grinning","short_names":["grinning"],"text":":D","texts":null,"category":"smileys-emotion","subcategory":"face-smiling","sort_order":1,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f600.png"},{"name":"SMILING FACE WITH OPEN MOUTH","unified":"1F603","non_qualified":null,"docomo":"E6F0","au":"E471","softbank":"E057","google":"FE330","image":"1f603","sheet_x":30,"sheet_y":35,"short_name":"smiley","short_names":["smiley"],"text":":)","texts":["=)","=-)"],"category":"smileys-emotion","subcategory":"face-smiling","sort_order":2,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f603.png"},{"name":"SMILING FACE WITH OPEN MOUTH AND SMILING EYES","unified":"1F604","non_qualified":null,"docomo":"E6F0","au":"E471","softbank":"E415","google":"FE338","image":"1f604","sheet_x":30,"sheet_y":36,"short_name":"smile","short_names":["smile"],"text":":)","texts":["C:","c:",":D",":-D"],"category":"smileys-emotion","subcategory":"face-smiling","sort_order":3,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f604.png"},{"name":"GRINNING FACE WITH SMILING EYES","unified":"1F601","non_qualified":null,"docomo":"E753","au":"EB80","softbank":"E404","google":"FE333","image":"1f601","sheet_x":30,"sheet_y":33,"short_name":"grin","short_names":["grin"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-smiling","sort_order":4,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f601.png"},{"name":"SMILING FACE WITH OPEN MOUTH AND TIGHTLY-CLOSED EYES","unified":"1F606","non_qualified":null,"docomo":"E72A","au":"EAC5","softbank":null,"google":"FE332","image":"1f606","sheet_x":30,"sheet_y":38,"short_name":"laughing","short_names":["laughing","satisfied"],"text":null,"texts":[":>",":->"],"category":"smileys-emotion","subcategory":"face-smiling","sort_order":5,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f606.png"},{"name":"SMILING FACE WITH OPEN MOUTH AND COLD SWEAT","unified":"1F605","non_qualified":null,"docomo":"E722","au":"E471-E5B1","softbank":null,"google":"FE331","image":"1f605","sheet_x":30,"sheet_y":37,"short_name":"sweat_smile","short_names":["sweat_smile"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-smiling","sort_order":6,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f605.png"},{"name":"ROLLING ON THE FLOOR LAUGHING","unified":"1F923","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f923","sheet_x":38,"sheet_y":19,"short_name":"rolling_on_the_floor_laughing","short_names":["rolling_on_the_floor_laughing","rofl"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-smiling","sort_order":7,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f923.png"},{"name":"FACE WITH TEARS OF JOY","unified":"1F602","non_qualified":null,"docomo":"E72A","au":"EB64","softbank":"E412","google":"FE334","image":"1f602","sheet_x":30,"sheet_y":34,"short_name":"joy","short_names":["joy"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-smiling","sort_order":8,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f602.png"},{"name":"SLIGHTLY SMILING FACE","unified":"1F642","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f642","sheet_x":31,"sheet_y":40,"short_name":"slightly_smiling_face","short_names":["slightly_smiling_face"],"text":null,"texts":[":)","(:",":-)"],"category":"smileys-emotion","subcategory":"face-smiling","sort_order":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f642.png"},{"name":"UPSIDE-DOWN FACE","unified":"1F643","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f643","sheet_x":31,"sheet_y":41,"short_name":"upside_down_face","short_names":["upside_down_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-smiling","sort_order":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f643.png"},{"name":"WINKING FACE","unified":"1F609","non_qualified":null,"docomo":"E729","au":"E5C3","softbank":"E405","google":"FE347","image":"1f609","sheet_x":30,"sheet_y":41,"short_name":"wink","short_names":["wink"],"text":";)","texts":[";)",";-)"],"category":"smileys-emotion","subcategory":"face-smiling","sort_order":11,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f609.png"},{"name":"SMILING FACE WITH SMILING EYES","unified":"1F60A","non_qualified":null,"docomo":"E6F0","au":"EACD","softbank":"E056","google":"FE335","image":"1f60a","sheet_x":30,"sheet_y":42,"short_name":"blush","short_names":["blush"],"text":":)","texts":null,"category":"smileys-emotion","subcategory":"face-smiling","sort_order":12,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f60a.png"},{"name":"SMILING FACE WITH HALO","unified":"1F607","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f607","sheet_x":30,"sheet_y":39,"short_name":"innocent","short_names":["innocent"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-smiling","sort_order":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f607.png"},{"name":"SMILING FACE WITH SMILING EYES AND THREE HEARTS","unified":"1F970","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f970","sheet_x":42,"sheet_y":13,"short_name":"smiling_face_with_3_hearts","short_names":["smiling_face_with_3_hearts"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-affection","sort_order":14,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f970.png"},{"name":"SMILING FACE WITH HEART-SHAPED EYES","unified":"1F60D","non_qualified":null,"docomo":"E726","au":"E5C4","softbank":"E106","google":"FE327","image":"1f60d","sheet_x":30,"sheet_y":45,"short_name":"heart_eyes","short_names":["heart_eyes"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-affection","sort_order":15,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f60d.png"},{"name":"GRINNING FACE WITH STAR EYES","unified":"1F929","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f929","sheet_x":38,"sheet_y":42,"short_name":"star-struck","short_names":["star-struck","grinning_face_with_star_eyes"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-affection","sort_order":16,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f929.png"},{"name":"FACE THROWING A KISS","unified":"1F618","non_qualified":null,"docomo":"E726","au":"EACF","softbank":"E418","google":"FE32C","image":"1f618","sheet_x":30,"sheet_y":56,"short_name":"kissing_heart","short_names":["kissing_heart"],"text":null,"texts":[":*",":-*"],"category":"smileys-emotion","subcategory":"face-affection","sort_order":17,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f618.png"},{"name":"KISSING FACE","unified":"1F617","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f617","sheet_x":30,"sheet_y":55,"short_name":"kissing","short_names":["kissing"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-affection","sort_order":18,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f617.png"},{"name":"WHITE SMILING FACE","unified":"263A-FE0F","non_qualified":"263A","docomo":"E6F0","au":"E4FB","softbank":"E414","google":"FE336","image":"263a-fe0f","sheet_x":54,"sheet_y":22,"short_name":"relaxed","short_names":["relaxed"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-affection","sort_order":19,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"263a-fe0f.png"},{"name":"KISSING FACE WITH CLOSED EYES","unified":"1F61A","non_qualified":null,"docomo":"E726","au":"EACE","softbank":"E417","google":"FE32D","image":"1f61a","sheet_x":31,"sheet_y":0,"short_name":"kissing_closed_eyes","short_names":["kissing_closed_eyes"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-affection","sort_order":20,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f61a.png"},{"name":"KISSING FACE WITH SMILING EYES","unified":"1F619","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f619","sheet_x":30,"sheet_y":57,"short_name":"kissing_smiling_eyes","short_names":["kissing_smiling_eyes"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-affection","sort_order":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f619.png"},{"name":"SMILING FACE WITH TEAR","unified":"1F972","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f972","sheet_x":42,"sheet_y":15,"short_name":"smiling_face_with_tear","short_names":["smiling_face_with_tear"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-affection","sort_order":22,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f972.png"},{"name":"FACE SAVOURING DELICIOUS FOOD","unified":"1F60B","non_qualified":null,"docomo":"E752","au":"EACD","softbank":null,"google":"FE32B","image":"1f60b","sheet_x":30,"sheet_y":43,"short_name":"yum","short_names":["yum"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-tongue","sort_order":23,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f60b.png"},{"name":"FACE WITH STUCK-OUT TONGUE","unified":"1F61B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f61b","sheet_x":31,"sheet_y":1,"short_name":"stuck_out_tongue","short_names":["stuck_out_tongue"],"text":":p","texts":[":p",":-p",":P",":-P",":b",":-b"],"category":"smileys-emotion","subcategory":"face-tongue","sort_order":24,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f61b.png"},{"name":"FACE WITH STUCK-OUT TONGUE AND WINKING EYE","unified":"1F61C","non_qualified":null,"docomo":"E728","au":"E4E7","softbank":"E105","google":"FE329","image":"1f61c","sheet_x":31,"sheet_y":2,"short_name":"stuck_out_tongue_winking_eye","short_names":["stuck_out_tongue_winking_eye"],"text":";p","texts":[";p",";-p",";b",";-b",";P",";-P"],"category":"smileys-emotion","subcategory":"face-tongue","sort_order":25,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f61c.png"},{"name":"GRINNING FACE WITH ONE LARGE AND ONE SMALL EYE","unified":"1F92A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92a","sheet_x":38,"sheet_y":43,"short_name":"zany_face","short_names":["zany_face","grinning_face_with_one_large_and_one_small_eye"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-tongue","sort_order":26,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f92a.png"},{"name":"FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES","unified":"1F61D","non_qualified":null,"docomo":"E728","au":"E4E7","softbank":"E409","google":"FE32A","image":"1f61d","sheet_x":31,"sheet_y":3,"short_name":"stuck_out_tongue_closed_eyes","short_names":["stuck_out_tongue_closed_eyes"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-tongue","sort_order":27,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f61d.png"},{"name":"MONEY-MOUTH FACE","unified":"1F911","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f911","sheet_x":37,"sheet_y":24,"short_name":"money_mouth_face","short_names":["money_mouth_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-tongue","sort_order":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f911.png"},{"name":"HUGGING FACE","unified":"1F917","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f917","sheet_x":37,"sheet_y":30,"short_name":"hugging_face","short_names":["hugging_face","hugs"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-hand","sort_order":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f917.png"},{"name":"SMILING FACE WITH SMILING EYES AND HAND COVERING MOUTH","unified":"1F92D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92d","sheet_x":38,"sheet_y":46,"short_name":"face_with_hand_over_mouth","short_names":["face_with_hand_over_mouth","smiling_face_with_smiling_eyes_and_hand_covering_mouth"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-hand","sort_order":30,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f92d.png"},{"name":"FACE WITH FINGER COVERING CLOSED LIPS","unified":"1F92B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92b","sheet_x":38,"sheet_y":44,"short_name":"shushing_face","short_names":["shushing_face","face_with_finger_covering_closed_lips"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-hand","sort_order":31,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f92b.png"},{"name":"THINKING FACE","unified":"1F914","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f914","sheet_x":37,"sheet_y":27,"short_name":"thinking_face","short_names":["thinking_face","thinking"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-hand","sort_order":32,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f914.png"},{"name":"ZIPPER-MOUTH FACE","unified":"1F910","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f910","sheet_x":37,"sheet_y":23,"short_name":"zipper_mouth_face","short_names":["zipper_mouth_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-neutral-skeptical","sort_order":33,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f910.png"},{"name":"FACE WITH ONE EYEBROW RAISED","unified":"1F928","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f928","sheet_x":38,"sheet_y":41,"short_name":"face_with_raised_eyebrow","short_names":["face_with_raised_eyebrow","face_with_one_eyebrow_raised"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-neutral-skeptical","sort_order":34,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f928.png"},{"name":"NEUTRAL FACE","unified":"1F610","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f610","sheet_x":30,"sheet_y":48,"short_name":"neutral_face","short_names":["neutral_face"],"text":null,"texts":[":|",":-|"],"category":"smileys-emotion","subcategory":"face-neutral-skeptical","sort_order":35,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f610.png"},{"name":"EXPRESSIONLESS FACE","unified":"1F611","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f611","sheet_x":30,"sheet_y":49,"short_name":"expressionless","short_names":["expressionless"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-neutral-skeptical","sort_order":36,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f611.png"},{"name":"FACE WITHOUT MOUTH","unified":"1F636","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f636","sheet_x":31,"sheet_y":28,"short_name":"no_mouth","short_names":["no_mouth"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-neutral-skeptical","sort_order":37,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f636.png"},{"name":"SMIRKING FACE","unified":"1F60F","non_qualified":null,"docomo":"E72C","au":"EABF","softbank":"E402","google":"FE343","image":"1f60f","sheet_x":30,"sheet_y":47,"short_name":"smirk","short_names":["smirk"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-neutral-skeptical","sort_order":38,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f60f.png"},{"name":"UNAMUSED FACE","unified":"1F612","non_qualified":null,"docomo":"E725","au":"EAC9","softbank":"E40E","google":"FE326","image":"1f612","sheet_x":30,"sheet_y":50,"short_name":"unamused","short_names":["unamused"],"text":":(","texts":null,"category":"smileys-emotion","subcategory":"face-neutral-skeptical","sort_order":39,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f612.png"},{"name":"FACE WITH ROLLING EYES","unified":"1F644","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f644","sheet_x":31,"sheet_y":42,"short_name":"face_with_rolling_eyes","short_names":["face_with_rolling_eyes","roll_eyes"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-neutral-skeptical","sort_order":40,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f644.png"},{"name":"GRIMACING FACE","unified":"1F62C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f62c","sheet_x":31,"sheet_y":18,"short_name":"grimacing","short_names":["grimacing"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-neutral-skeptical","sort_order":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f62c.png"},{"name":"LYING FACE","unified":"1F925","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f925","sheet_x":38,"sheet_y":21,"short_name":"lying_face","short_names":["lying_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-neutral-skeptical","sort_order":42,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f925.png"},{"name":"RELIEVED FACE","unified":"1F60C","non_qualified":null,"docomo":"E721","au":"EAC5","softbank":"E40A","google":"FE33E","image":"1f60c","sheet_x":30,"sheet_y":44,"short_name":"relieved","short_names":["relieved"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-sleepy","sort_order":43,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f60c.png"},{"name":"PENSIVE FACE","unified":"1F614","non_qualified":null,"docomo":"E720","au":"EAC0","softbank":"E403","google":"FE340","image":"1f614","sheet_x":30,"sheet_y":52,"short_name":"pensive","short_names":["pensive"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-sleepy","sort_order":44,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f614.png"},{"name":"SLEEPY FACE","unified":"1F62A","non_qualified":null,"docomo":"E701","au":"EAC4","softbank":"E408","google":"FE342","image":"1f62a","sheet_x":31,"sheet_y":16,"short_name":"sleepy","short_names":["sleepy"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-sleepy","sort_order":45,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f62a.png"},{"name":"DROOLING FACE","unified":"1F924","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f924","sheet_x":38,"sheet_y":20,"short_name":"drooling_face","short_names":["drooling_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-sleepy","sort_order":46,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f924.png"},{"name":"SLEEPING FACE","unified":"1F634","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f634","sheet_x":31,"sheet_y":26,"short_name":"sleeping","short_names":["sleeping"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-sleepy","sort_order":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f634.png"},{"name":"FACE WITH MEDICAL MASK","unified":"1F637","non_qualified":null,"docomo":null,"au":"EAC7","softbank":"E40C","google":"FE32E","image":"1f637","sheet_x":31,"sheet_y":29,"short_name":"mask","short_names":["mask"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-unwell","sort_order":48,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f637.png"},{"name":"FACE WITH THERMOMETER","unified":"1F912","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f912","sheet_x":37,"sheet_y":25,"short_name":"face_with_thermometer","short_names":["face_with_thermometer"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-unwell","sort_order":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f912.png"},{"name":"FACE WITH HEAD-BANDAGE","unified":"1F915","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f915","sheet_x":37,"sheet_y":28,"short_name":"face_with_head_bandage","short_names":["face_with_head_bandage"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-unwell","sort_order":50,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f915.png"},{"name":"NAUSEATED FACE","unified":"1F922","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f922","sheet_x":38,"sheet_y":18,"short_name":"nauseated_face","short_names":["nauseated_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-unwell","sort_order":51,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f922.png"},{"name":"FACE WITH OPEN MOUTH VOMITING","unified":"1F92E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92e","sheet_x":38,"sheet_y":47,"short_name":"face_vomiting","short_names":["face_vomiting","face_with_open_mouth_vomiting"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-unwell","sort_order":52,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f92e.png"},{"name":"SNEEZING FACE","unified":"1F927","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f927","sheet_x":38,"sheet_y":40,"short_name":"sneezing_face","short_names":["sneezing_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-unwell","sort_order":53,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f927.png"},{"name":"OVERHEATED FACE","unified":"1F975","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f975","sheet_x":42,"sheet_y":18,"short_name":"hot_face","short_names":["hot_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-unwell","sort_order":54,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f975.png"},{"name":"FREEZING FACE","unified":"1F976","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f976","sheet_x":42,"sheet_y":19,"short_name":"cold_face","short_names":["cold_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-unwell","sort_order":55,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f976.png"},{"name":"FACE WITH UNEVEN EYES AND WAVY MOUTH","unified":"1F974","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f974","sheet_x":42,"sheet_y":17,"short_name":"woozy_face","short_names":["woozy_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-unwell","sort_order":56,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f974.png"},{"name":"DIZZY FACE","unified":"1F635","non_qualified":null,"docomo":"E6F4","au":"E5AE","softbank":null,"google":"FE324","image":"1f635","sheet_x":31,"sheet_y":27,"short_name":"dizzy_face","short_names":["dizzy_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-unwell","sort_order":57,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f635.png"},{"name":"SHOCKED FACE WITH EXPLODING HEAD","unified":"1F92F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92f","sheet_x":38,"sheet_y":48,"short_name":"exploding_head","short_names":["exploding_head","shocked_face_with_exploding_head"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-unwell","sort_order":58,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f92f.png"},{"name":"FACE WITH COWBOY HAT","unified":"1F920","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f920","sheet_x":38,"sheet_y":16,"short_name":"face_with_cowboy_hat","short_names":["face_with_cowboy_hat","cowboy_hat_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-hat","sort_order":59,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f920.png"},{"name":"FACE WITH PARTY HORN AND PARTY HAT","unified":"1F973","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f973","sheet_x":42,"sheet_y":16,"short_name":"partying_face","short_names":["partying_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-hat","sort_order":60,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f973.png"},{"name":"DISGUISED FACE","unified":"1F978","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f978","sheet_x":42,"sheet_y":26,"short_name":"disguised_face","short_names":["disguised_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-hat","sort_order":61,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f978.png"},{"name":"SMILING FACE WITH SUNGLASSES","unified":"1F60E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f60e","sheet_x":30,"sheet_y":46,"short_name":"sunglasses","short_names":["sunglasses"],"text":null,"texts":["8)"],"category":"smileys-emotion","subcategory":"face-glasses","sort_order":62,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f60e.png"},{"name":"NERD FACE","unified":"1F913","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f913","sheet_x":37,"sheet_y":26,"short_name":"nerd_face","short_names":["nerd_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-glasses","sort_order":63,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f913.png"},{"name":"FACE WITH MONOCLE","unified":"1F9D0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d0","sheet_x":45,"sheet_y":34,"short_name":"face_with_monocle","short_names":["face_with_monocle"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-glasses","sort_order":64,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f9d0.png"},{"name":"CONFUSED FACE","unified":"1F615","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f615","sheet_x":30,"sheet_y":53,"short_name":"confused","short_names":["confused"],"text":null,"texts":[":\\",":-\\",":/",":-/"],"category":"smileys-emotion","subcategory":"face-concerned","sort_order":65,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f615.png"},{"name":"WORRIED FACE","unified":"1F61F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f61f","sheet_x":31,"sheet_y":5,"short_name":"worried","short_names":["worried"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":66,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f61f.png"},{"name":"SLIGHTLY FROWNING FACE","unified":"1F641","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f641","sheet_x":31,"sheet_y":39,"short_name":"slightly_frowning_face","short_names":["slightly_frowning_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":67,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f641.png"},{"name":"FROWNING FACE","unified":"2639-FE0F","non_qualified":"2639","docomo":null,"au":null,"softbank":null,"google":null,"image":"2639-fe0f","sheet_x":54,"sheet_y":21,"short_name":"white_frowning_face","short_names":["white_frowning_face","frowning_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":68,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"2639-fe0f.png"},{"name":"FACE WITH OPEN MOUTH","unified":"1F62E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f62e","sheet_x":31,"sheet_y":20,"short_name":"open_mouth","short_names":["open_mouth"],"text":null,"texts":[":o",":-o",":O",":-O"],"category":"smileys-emotion","subcategory":"face-concerned","sort_order":69,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f62e.png"},{"name":"HUSHED FACE","unified":"1F62F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f62f","sheet_x":31,"sheet_y":21,"short_name":"hushed","short_names":["hushed"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":70,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f62f.png"},{"name":"ASTONISHED FACE","unified":"1F632","non_qualified":null,"docomo":"E6F4","au":"EACA","softbank":"E410","google":"FE322","image":"1f632","sheet_x":31,"sheet_y":24,"short_name":"astonished","short_names":["astonished"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":71,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f632.png"},{"name":"FLUSHED FACE","unified":"1F633","non_qualified":null,"docomo":"E72A","au":"EAC8","softbank":"E40D","google":"FE32F","image":"1f633","sheet_x":31,"sheet_y":25,"short_name":"flushed","short_names":["flushed"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":72,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f633.png"},{"name":"FACE WITH PLEADING EYES","unified":"1F97A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97a","sheet_x":42,"sheet_y":27,"short_name":"pleading_face","short_names":["pleading_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":73,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f97a.png"},{"name":"FROWNING FACE WITH OPEN MOUTH","unified":"1F626","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f626","sheet_x":31,"sheet_y":12,"short_name":"frowning","short_names":["frowning"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":74,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f626.png"},{"name":"ANGUISHED FACE","unified":"1F627","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f627","sheet_x":31,"sheet_y":13,"short_name":"anguished","short_names":["anguished"],"text":null,"texts":["D:"],"category":"smileys-emotion","subcategory":"face-concerned","sort_order":75,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f627.png"},{"name":"FEARFUL FACE","unified":"1F628","non_qualified":null,"docomo":"E757","au":"EAC6","softbank":"E40B","google":"FE33B","image":"1f628","sheet_x":31,"sheet_y":14,"short_name":"fearful","short_names":["fearful"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":76,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f628.png"},{"name":"FACE WITH OPEN MOUTH AND COLD SWEAT","unified":"1F630","non_qualified":null,"docomo":"E723","au":"EACB","softbank":"E40F","google":"FE325","image":"1f630","sheet_x":31,"sheet_y":22,"short_name":"cold_sweat","short_names":["cold_sweat"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":77,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f630.png"},{"name":"DISAPPOINTED BUT RELIEVED FACE","unified":"1F625","non_qualified":null,"docomo":"E723","au":"E5C6","softbank":"E401","google":"FE345","image":"1f625","sheet_x":31,"sheet_y":11,"short_name":"disappointed_relieved","short_names":["disappointed_relieved"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":78,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f625.png"},{"name":"CRYING FACE","unified":"1F622","non_qualified":null,"docomo":"E72E","au":"EB69","softbank":"E413","google":"FE339","image":"1f622","sheet_x":31,"sheet_y":8,"short_name":"cry","short_names":["cry"],"text":":'(","texts":[":'("],"category":"smileys-emotion","subcategory":"face-concerned","sort_order":79,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f622.png"},{"name":"LOUDLY CRYING FACE","unified":"1F62D","non_qualified":null,"docomo":"E72D","au":"E473","softbank":"E411","google":"FE33A","image":"1f62d","sheet_x":31,"sheet_y":19,"short_name":"sob","short_names":["sob"],"text":":'(","texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":80,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f62d.png"},{"name":"FACE SCREAMING IN FEAR","unified":"1F631","non_qualified":null,"docomo":"E757","au":"E5C5","softbank":"E107","google":"FE341","image":"1f631","sheet_x":31,"sheet_y":23,"short_name":"scream","short_names":["scream"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":81,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f631.png"},{"name":"CONFOUNDED FACE","unified":"1F616","non_qualified":null,"docomo":"E6F3","au":"EAC3","softbank":"E407","google":"FE33F","image":"1f616","sheet_x":30,"sheet_y":54,"short_name":"confounded","short_names":["confounded"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":82,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f616.png"},{"name":"PERSEVERING FACE","unified":"1F623","non_qualified":null,"docomo":"E72B","au":"EAC2","softbank":"E406","google":"FE33C","image":"1f623","sheet_x":31,"sheet_y":9,"short_name":"persevere","short_names":["persevere"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":83,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f623.png"},{"name":"DISAPPOINTED FACE","unified":"1F61E","non_qualified":null,"docomo":"E6F2","au":"EAC0","softbank":"E058","google":"FE323","image":"1f61e","sheet_x":31,"sheet_y":4,"short_name":"disappointed","short_names":["disappointed"],"text":":(","texts":["):",":(",":-("],"category":"smileys-emotion","subcategory":"face-concerned","sort_order":84,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f61e.png"},{"name":"FACE WITH COLD SWEAT","unified":"1F613","non_qualified":null,"docomo":"E723","au":"E5C6","softbank":"E108","google":"FE344","image":"1f613","sheet_x":30,"sheet_y":51,"short_name":"sweat","short_names":["sweat"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":85,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f613.png"},{"name":"WEARY FACE","unified":"1F629","non_qualified":null,"docomo":"E6F3","au":"EB67","softbank":null,"google":"FE321","image":"1f629","sheet_x":31,"sheet_y":15,"short_name":"weary","short_names":["weary"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":86,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f629.png"},{"name":"TIRED FACE","unified":"1F62B","non_qualified":null,"docomo":"E72B","au":"E474","softbank":null,"google":"FE346","image":"1f62b","sheet_x":31,"sheet_y":17,"short_name":"tired_face","short_names":["tired_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":87,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f62b.png"},{"name":"YAWNING FACE","unified":"1F971","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f971","sheet_x":42,"sheet_y":14,"short_name":"yawning_face","short_names":["yawning_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-concerned","sort_order":88,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f971.png"},{"name":"FACE WITH LOOK OF TRIUMPH","unified":"1F624","non_qualified":null,"docomo":"E753","au":"EAC1","softbank":null,"google":"FE328","image":"1f624","sheet_x":31,"sheet_y":10,"short_name":"triumph","short_names":["triumph"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-negative","sort_order":89,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f624.png"},{"name":"POUTING FACE","unified":"1F621","non_qualified":null,"docomo":"E724","au":"EB5D","softbank":"E416","google":"FE33D","image":"1f621","sheet_x":31,"sheet_y":7,"short_name":"rage","short_names":["rage","pout"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-negative","sort_order":90,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f621.png"},{"name":"ANGRY FACE","unified":"1F620","non_qualified":null,"docomo":"E6F1","au":"E472","softbank":"E059","google":"FE320","image":"1f620","sheet_x":31,"sheet_y":6,"short_name":"angry","short_names":["angry"],"text":null,"texts":[">:(",">:-("],"category":"smileys-emotion","subcategory":"face-negative","sort_order":91,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f620.png"},{"name":"SERIOUS FACE WITH SYMBOLS COVERING MOUTH","unified":"1F92C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92c","sheet_x":38,"sheet_y":45,"short_name":"face_with_symbols_on_mouth","short_names":["face_with_symbols_on_mouth","serious_face_with_symbols_covering_mouth"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-negative","sort_order":92,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f92c.png"},{"name":"SMILING FACE WITH HORNS","unified":"1F608","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f608","sheet_x":30,"sheet_y":40,"short_name":"smiling_imp","short_names":["smiling_imp"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-negative","sort_order":93,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f608.png"},{"name":"IMP","unified":"1F47F","non_qualified":null,"docomo":null,"au":"E4EF","softbank":"E11A","google":"FE1B2","image":"1f47f","sheet_x":23,"sheet_y":49,"short_name":"imp","short_names":["imp"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-negative","sort_order":94,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f47f.png"},{"name":"SKULL","unified":"1F480","non_qualified":null,"docomo":null,"au":"E4F8","softbank":"E11C","google":"FE1B3","image":"1f480","sheet_x":23,"sheet_y":50,"short_name":"skull","short_names":["skull"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-negative","sort_order":95,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f480.png"},{"name":"SKULL AND CROSSBONES","unified":"2620-FE0F","non_qualified":"2620","docomo":null,"au":null,"softbank":null,"google":null,"image":"2620-fe0f","sheet_x":54,"sheet_y":13,"short_name":"skull_and_crossbones","short_names":["skull_and_crossbones"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-negative","sort_order":96,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"2620-fe0f.png"},{"name":"PILE OF POO","unified":"1F4A9","non_qualified":null,"docomo":null,"au":"E4F5","softbank":"E05A","google":"FE4F4","image":"1f4a9","sheet_x":25,"sheet_y":53,"short_name":"hankey","short_names":["hankey","poop","shit"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-costume","sort_order":97,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f4a9.png"},{"name":"CLOWN FACE","unified":"1F921","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f921","sheet_x":38,"sheet_y":17,"short_name":"clown_face","short_names":["clown_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-costume","sort_order":98,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f921.png"},{"name":"JAPANESE OGRE","unified":"1F479","non_qualified":null,"docomo":null,"au":"EB44","softbank":null,"google":"FE1AC","image":"1f479","sheet_x":23,"sheet_y":38,"short_name":"japanese_ogre","short_names":["japanese_ogre"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-costume","sort_order":99,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f479.png"},{"name":"JAPANESE GOBLIN","unified":"1F47A","non_qualified":null,"docomo":null,"au":"EB45","softbank":null,"google":"FE1AD","image":"1f47a","sheet_x":23,"sheet_y":39,"short_name":"japanese_goblin","short_names":["japanese_goblin"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-costume","sort_order":100,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f47a.png"},{"name":"GHOST","unified":"1F47B","non_qualified":null,"docomo":null,"au":"E4CB","softbank":"E11B","google":"FE1AE","image":"1f47b","sheet_x":23,"sheet_y":40,"short_name":"ghost","short_names":["ghost"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-costume","sort_order":101,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f47b.png"},{"name":"EXTRATERRESTRIAL ALIEN","unified":"1F47D","non_qualified":null,"docomo":null,"au":"E50E","softbank":"E10C","google":"FE1B0","image":"1f47d","sheet_x":23,"sheet_y":47,"short_name":"alien","short_names":["alien"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-costume","sort_order":102,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f47d.png"},{"name":"ALIEN MONSTER","unified":"1F47E","non_qualified":null,"docomo":null,"au":"E4EC","softbank":"E12B","google":"FE1B1","image":"1f47e","sheet_x":23,"sheet_y":48,"short_name":"space_invader","short_names":["space_invader"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-costume","sort_order":103,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f47e.png"},{"name":"ROBOT FACE","unified":"1F916","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f916","sheet_x":37,"sheet_y":29,"short_name":"robot_face","short_names":["robot_face","robot"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"face-costume","sort_order":104,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f916.png"},{"name":"SMILING CAT FACE WITH OPEN MOUTH","unified":"1F63A","non_qualified":null,"docomo":"E6F0","au":"EB61","softbank":null,"google":"FE348","image":"1f63a","sheet_x":31,"sheet_y":32,"short_name":"smiley_cat","short_names":["smiley_cat"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"cat-face","sort_order":105,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f63a.png"},{"name":"GRINNING CAT FACE WITH SMILING EYES","unified":"1F638","non_qualified":null,"docomo":"E753","au":"EB7F","softbank":null,"google":"FE349","image":"1f638","sheet_x":31,"sheet_y":30,"short_name":"smile_cat","short_names":["smile_cat"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"cat-face","sort_order":106,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f638.png"},{"name":"CAT FACE WITH TEARS OF JOY","unified":"1F639","non_qualified":null,"docomo":"E72A","au":"EB63","softbank":null,"google":"FE34A","image":"1f639","sheet_x":31,"sheet_y":31,"short_name":"joy_cat","short_names":["joy_cat"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"cat-face","sort_order":107,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f639.png"},{"name":"SMILING CAT FACE WITH HEART-SHAPED EYES","unified":"1F63B","non_qualified":null,"docomo":"E726","au":"EB65","softbank":null,"google":"FE34C","image":"1f63b","sheet_x":31,"sheet_y":33,"short_name":"heart_eyes_cat","short_names":["heart_eyes_cat"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"cat-face","sort_order":108,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f63b.png"},{"name":"CAT FACE WITH WRY SMILE","unified":"1F63C","non_qualified":null,"docomo":"E753","au":"EB6A","softbank":null,"google":"FE34F","image":"1f63c","sheet_x":31,"sheet_y":34,"short_name":"smirk_cat","short_names":["smirk_cat"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"cat-face","sort_order":109,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f63c.png"},{"name":"KISSING CAT FACE WITH CLOSED EYES","unified":"1F63D","non_qualified":null,"docomo":"E726","au":"EB60","softbank":null,"google":"FE34B","image":"1f63d","sheet_x":31,"sheet_y":35,"short_name":"kissing_cat","short_names":["kissing_cat"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"cat-face","sort_order":110,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f63d.png"},{"name":"WEARY CAT FACE","unified":"1F640","non_qualified":null,"docomo":"E6F3","au":"EB66","softbank":null,"google":"FE350","image":"1f640","sheet_x":31,"sheet_y":38,"short_name":"scream_cat","short_names":["scream_cat"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"cat-face","sort_order":111,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f640.png"},{"name":"CRYING CAT FACE","unified":"1F63F","non_qualified":null,"docomo":"E72E","au":"EB68","softbank":null,"google":"FE34D","image":"1f63f","sheet_x":31,"sheet_y":37,"short_name":"crying_cat_face","short_names":["crying_cat_face"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"cat-face","sort_order":112,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f63f.png"},{"name":"POUTING CAT FACE","unified":"1F63E","non_qualified":null,"docomo":"E724","au":"EB5E","softbank":null,"google":"FE34E","image":"1f63e","sheet_x":31,"sheet_y":36,"short_name":"pouting_cat","short_names":["pouting_cat"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"cat-face","sort_order":113,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f63e.png"},{"name":"SEE-NO-EVIL MONKEY","unified":"1F648","non_qualified":null,"docomo":null,"au":"EB50","softbank":null,"google":"FE354","image":"1f648","sheet_x":32,"sheet_y":39,"short_name":"see_no_evil","short_names":["see_no_evil"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"monkey-face","sort_order":114,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f648.png"},{"name":"HEAR-NO-EVIL MONKEY","unified":"1F649","non_qualified":null,"docomo":null,"au":"EB52","softbank":null,"google":"FE356","image":"1f649","sheet_x":32,"sheet_y":40,"short_name":"hear_no_evil","short_names":["hear_no_evil"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"monkey-face","sort_order":115,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f649.png"},{"name":"SPEAK-NO-EVIL MONKEY","unified":"1F64A","non_qualified":null,"docomo":null,"au":"EB51","softbank":null,"google":"FE355","image":"1f64a","sheet_x":32,"sheet_y":41,"short_name":"speak_no_evil","short_names":["speak_no_evil"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"monkey-face","sort_order":116,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f64a.png"},{"name":"KISS MARK","unified":"1F48B","non_qualified":null,"docomo":"E6F9","au":"E4EB","softbank":"E003","google":"FE823","image":"1f48b","sheet_x":25,"sheet_y":23,"short_name":"kiss","short_names":["kiss"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"emotion","sort_order":117,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f48b.png"},{"name":"LOVE LETTER","unified":"1F48C","non_qualified":null,"docomo":"E717","au":"EB78","softbank":null,"google":"FE824","image":"1f48c","sheet_x":25,"sheet_y":24,"short_name":"love_letter","short_names":["love_letter"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"emotion","sort_order":118,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f48c.png"},{"name":"HEART WITH ARROW","unified":"1F498","non_qualified":null,"docomo":"E6EC","au":"E4EA","softbank":"E329","google":"FEB12","image":"1f498","sheet_x":25,"sheet_y":36,"short_name":"cupid","short_names":["cupid"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"emotion","sort_order":119,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f498.png"},{"name":"HEART WITH RIBBON","unified":"1F49D","non_qualified":null,"docomo":"E6EC","au":"EB54","softbank":"E437","google":"FEB17","image":"1f49d","sheet_x":25,"sheet_y":41,"short_name":"gift_heart","short_names":["gift_heart"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"emotion","sort_order":120,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f49d.png"},{"name":"SPARKLING HEART","unified":"1F496","non_qualified":null,"docomo":"E6EC","au":"EAA6","softbank":null,"google":"FEB10","image":"1f496","sheet_x":25,"sheet_y":34,"short_name":"sparkling_heart","short_names":["sparkling_heart"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"emotion","sort_order":121,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f496.png"},{"name":"GROWING HEART","unified":"1F497","non_qualified":null,"docomo":"E6ED","au":"EB75","softbank":"E328","google":"FEB11","image":"1f497","sheet_x":25,"sheet_y":35,"short_name":"heartpulse","short_names":["heartpulse"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"emotion","sort_order":122,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f497.png"},{"name":"BEATING HEART","unified":"1F493","non_qualified":null,"docomo":"E6ED","au":"EB75","softbank":"E327","google":"FEB0D","image":"1f493","sheet_x":25,"sheet_y":31,"short_name":"heartbeat","short_names":["heartbeat"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"emotion","sort_order":123,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f493.png"},{"name":"REVOLVING HEARTS","unified":"1F49E","non_qualified":null,"docomo":"E6ED","au":"E5AF","softbank":null,"google":"FEB18","image":"1f49e","sheet_x":25,"sheet_y":42,"short_name":"revolving_hearts","short_names":["revolving_hearts"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"emotion","sort_order":124,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f49e.png"},{"name":"TWO HEARTS","unified":"1F495","non_qualified":null,"docomo":"E6EF","au":"E478","softbank":null,"google":"FEB0F","image":"1f495","sheet_x":25,"sheet_y":33,"short_name":"two_hearts","short_names":["two_hearts"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"emotion","sort_order":125,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f495.png"},{"name":"HEART DECORATION","unified":"1F49F","non_qualified":null,"docomo":"E6F8","au":"E595","softbank":"E204","google":"FEB19","image":"1f49f","sheet_x":25,"sheet_y":43,"short_name":"heart_decoration","short_names":["heart_decoration"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"emotion","sort_order":126,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"1f49f.png"},{"name":"HEART EXCLAMATION","unified":"2763-FE0F","non_qualified":"2763","docomo":null,"au":null,"softbank":null,"google":null,"image":"2763-fe0f","sheet_x":56,"sheet_y":31,"short_name":"heavy_heart_exclamation_mark_ornament","short_names":["heavy_heart_exclamation_mark_ornament","heavy_heart_exclamation"],"text":null,"texts":null,"category":"smileys-emotion","subcategory":"emotion","sort_order":127,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"fileName":"2763-fe0f.png"},{"name":"BROKEN HEART","unified":"1F494","non_qualified":null,"docomo":"E6EE","au":"E477","softbank":"E023","google":"FEB0E","image":"1f494","sheet_x":25,"sheet_y":32,"short_name":"broken_heart","short_names":["broken_heart"],"text":" = { + slightly_smiling_face: /(^|\s)(:-?\))(?=$|\s)/g, // :) + wink: /(^|\s)(;-?\))(?=$|\s)/g, // ;) + open_mouth: /(^|\s)(:o)(?=$|\s)/gi, // :o + scream: /(^|\s)(:-o)(?=$|\s)/gi, // :-o + smirk: /(^|\s)(:-?])(?=$|\s)/g, // :] + smile: /(^|\s)(:-?d)(?=$|\s)/gi, // :D + stuck_out_tongue_closed_eyes: /(^|\s)(x-d)(?=$|\s)/gi, // x-d + stuck_out_tongue: /(^|\s)(:-?p)(?=$|\s)/gi, // :p + rage: /(^|\s)(:-?[[@])(?=$|\s)/g, // :@ + slightly_frowning_face: /(^|\s)(:-?\()(?=$|\s)/g, // :( + cry: /(^|\s)(:[`'’]-?\(|:'\(|:'\()(?=$|\s)/g, // :`( + confused: /(^|\s)(:-?\/)(?=$|\s)/g, // :/ + confounded: /(^|\s)(:-?s)(?=$|\s)/gi, // :s + neutral_face: /(^|\s)(:-?\|)(?=$|\s)/g, // :| + flushed: /(^|\s)(:-?\$)(?=$|\s)/g, // :$ + mask: /(^|\s)(:-x)(?=$|\s)/gi, // :-x + heart: /(^|\s)(<3|<3)(?=$|\s)/g, // <3 + broken_heart: /(^|\s)(<\/3|</3)(?=$|\s)/g, // value.match(RE_EMOTICON[key]) !== null); +} + +export function hasEmojisOnly(message: string, customEmojis: Map) { + if (!message || message.length === 0 || (/^\s{4}/).test(message)) { + return {isEmojiOnly: false, isJumboEmoji: false}; + } + + const chunks = message.trim().replace(/\n/g, ' ').split(' ').filter((m) => m && m.length > 0); + + if (chunks.length === 0) { + return {isEmojiOnly: false, isJumboEmoji: false}; + } + + let emojiCount = 0; + for (const chunk of chunks) { + if (doesMatchNamedEmoji(chunk)) { + const emojiName = chunk.substring(1, chunk.length - 1); + if (EmojiIndicesByAlias.has(emojiName)) { + emojiCount++; + continue; + } + + if (customEmojis && customEmojis.has(emojiName)) { + emojiCount++; + continue; + } + } + + const matchUnicodeEmoji = chunk.match(RE_UNICODE_EMOJI); + if (matchUnicodeEmoji && matchUnicodeEmoji.join('') === chunk) { + emojiCount += matchUnicodeEmoji.length; + continue; + } + + if (isEmoticon(chunk)) { + emojiCount++; + continue; + } + + return {isEmojiOnly: false, isJumboEmoji: false}; + } + + return { + isEmojiOnly: true, + isJumboEmoji: emojiCount > 0 && emojiCount <= MAX_JUMBO_EMOJIS, + }; +} + +export function doesMatchNamedEmoji(emojiName: string) { + const match = emojiName.match(RE_NAMED_EMOJI); + + if (match && match[0] === emojiName) { + return true; + } + + return false; +} + +export function getEmojiByName(emojiName: string) { + if (EmojiIndicesByAlias.has(emojiName)) { + return Emojis[EmojiIndicesByAlias.get(emojiName)!]; + } + + return null; +} + +// Since there is no shared logic between the web and mobile app +// this is copied from the webapp as custom sorting logic for emojis + +const defaultComparisonRule = (aName: string, bName: string) => { + return aName.localeCompare(bName); +}; + +const thumbsDownComparisonRule = (other: string) => (other === 'thumbsup' || other === '+1' ? 1 : 0); + +const thumbsUpComparisonRule = (other: string) => (other === 'thumbsdown' || other === '-1' ? -1 : 0); + +type Comparators = Record number)>; + +const customComparisonRules: Comparators = { + thumbsdown: thumbsDownComparisonRule, + '-1': thumbsDownComparisonRule, + thumbsup: thumbsUpComparisonRule, + '+1': thumbsUpComparisonRule, +}; + +function doDefaultComparison(aName: string, bName: string) { + if (customComparisonRules[aName]) { + return customComparisonRules[aName](bName) || defaultComparisonRule(aName, bName); + } + + return defaultComparisonRule(aName, bName); +} + +type EmojiType = { + short_name: string; + name: string; +} + +export function compareEmojis(emojiA: string | Partial, emojiB: string | Partial, searchedName: string) { + if (!emojiA) { + return 1; + } + + if (!emojiB) { + return -1; + } + let aName; + if (typeof emojiA === 'string') { + aName = emojiA; + } else { + aName = 'short_name' in emojiA ? emojiA.short_name : emojiA.name; + } + let bName; + if (typeof emojiB === 'string') { + bName = emojiB; + } else { + bName = 'short_name' in emojiB ? emojiB.short_name : emojiB.name; + } + + if (!searchedName) { + return doDefaultComparison(aName!, bName!); + } + + // Have the emojis that start with the search appear first + const aPrefix = aName!.startsWith(searchedName); + const bPrefix = bName!.startsWith(searchedName); + + if (aPrefix && bPrefix) { + return doDefaultComparison(aName!, bName!); + } else if (aPrefix) { + return -1; + } else if (bPrefix) { + return 1; + } + + // Have the emojis that contain the search appear next + const aIncludes = aName!.includes(searchedName); + const bIncludes = bName!.includes(searchedName); + + if (aIncludes && bIncludes) { + return doDefaultComparison(aName!, bName!); + } else if (aIncludes) { + return -1; + } else if (bIncludes) { + return 1; + } + + return doDefaultComparison(aName!, bName!); +} diff --git a/app/utils/emoji/index.js b/app/utils/emoji/index.js new file mode 100644 index 000000000..caf847d70 --- /dev/null +++ b/app/utils/emoji/index.js @@ -0,0 +1,47 @@ +// This file is automatically generated via `make emojis`. Do not modify it manually. + +/* eslint-disable */ + +import {t} from '@i18n'; + +import emojis from './emoji.json'; + +export const Emojis = emojis; + +export const EmojiIndicesByAlias = new Map([["grinning",0],["smiley",1],["smile",2],["grin",3],["laughing",4],["satisfied",4],["sweat_smile",5],["rolling_on_the_floor_laughing",6],["rofl",6],["joy",7],["slightly_smiling_face",8],["upside_down_face",9],["wink",10],["blush",11],["innocent",12],["smiling_face_with_3_hearts",13],["heart_eyes",14],["star-struck",15],["grinning_face_with_star_eyes",15],["kissing_heart",16],["kissing",17],["relaxed",18],["kissing_closed_eyes",19],["kissing_smiling_eyes",20],["smiling_face_with_tear",21],["yum",22],["stuck_out_tongue",23],["stuck_out_tongue_winking_eye",24],["zany_face",25],["grinning_face_with_one_large_and_one_small_eye",25],["stuck_out_tongue_closed_eyes",26],["money_mouth_face",27],["hugging_face",28],["hugs",28],["face_with_hand_over_mouth",29],["smiling_face_with_smiling_eyes_and_hand_covering_mouth",29],["shushing_face",30],["face_with_finger_covering_closed_lips",30],["thinking_face",31],["thinking",31],["zipper_mouth_face",32],["face_with_raised_eyebrow",33],["face_with_one_eyebrow_raised",33],["neutral_face",34],["expressionless",35],["no_mouth",36],["smirk",37],["unamused",38],["face_with_rolling_eyes",39],["roll_eyes",39],["grimacing",40],["lying_face",41],["relieved",42],["pensive",43],["sleepy",44],["drooling_face",45],["sleeping",46],["mask",47],["face_with_thermometer",48],["face_with_head_bandage",49],["nauseated_face",50],["face_vomiting",51],["face_with_open_mouth_vomiting",51],["sneezing_face",52],["hot_face",53],["cold_face",54],["woozy_face",55],["dizzy_face",56],["exploding_head",57],["shocked_face_with_exploding_head",57],["face_with_cowboy_hat",58],["cowboy_hat_face",58],["partying_face",59],["disguised_face",60],["sunglasses",61],["nerd_face",62],["face_with_monocle",63],["confused",64],["worried",65],["slightly_frowning_face",66],["white_frowning_face",67],["frowning_face",67],["open_mouth",68],["hushed",69],["astonished",70],["flushed",71],["pleading_face",72],["frowning",73],["anguished",74],["fearful",75],["cold_sweat",76],["disappointed_relieved",77],["cry",78],["sob",79],["scream",80],["confounded",81],["persevere",82],["disappointed",83],["sweat",84],["weary",85],["tired_face",86],["yawning_face",87],["triumph",88],["rage",89],["pout",89],["angry",90],["face_with_symbols_on_mouth",91],["serious_face_with_symbols_covering_mouth",91],["smiling_imp",92],["imp",93],["skull",94],["skull_and_crossbones",95],["hankey",96],["poop",96],["shit",96],["clown_face",97],["japanese_ogre",98],["japanese_goblin",99],["ghost",100],["alien",101],["space_invader",102],["robot_face",103],["robot",103],["smiley_cat",104],["smile_cat",105],["joy_cat",106],["heart_eyes_cat",107],["smirk_cat",108],["kissing_cat",109],["scream_cat",110],["crying_cat_face",111],["pouting_cat",112],["see_no_evil",113],["hear_no_evil",114],["speak_no_evil",115],["kiss",116],["love_letter",117],["cupid",118],["gift_heart",119],["sparkling_heart",120],["heartpulse",121],["heartbeat",122],["revolving_hearts",123],["two_hearts",124],["heart_decoration",125],["heavy_heart_exclamation_mark_ornament",126],["heavy_heart_exclamation",126],["broken_heart",127],["heart",128],["orange_heart",129],["yellow_heart",130],["green_heart",131],["blue_heart",132],["purple_heart",133],["brown_heart",134],["black_heart",135],["white_heart",136],["100",137],["anger",138],["boom",139],["collision",139],["dizzy",140],["sweat_drops",141],["dash",142],["hole",143],["bomb",144],["speech_balloon",145],["eye-in-speech-bubble",146],["left_speech_bubble",147],["right_anger_bubble",148],["thought_balloon",149],["zzz",150],["wave",151],["raised_back_of_hand",152],["raised_hand_with_fingers_splayed",153],["hand",154],["raised_hand",154],["spock-hand",155],["vulcan_salute",155],["ok_hand",156],["pinched_fingers",157],["pinching_hand",158],["v",159],["crossed_fingers",160],["hand_with_index_and_middle_fingers_crossed",160],["i_love_you_hand_sign",161],["the_horns",162],["sign_of_the_horns",162],["metal",162],["call_me_hand",163],["point_left",164],["point_right",165],["point_up_2",166],["middle_finger",167],["reversed_hand_with_middle_finger_extended",167],["fu",167],["point_down",168],["point_up",169],["+1",170],["thumbsup",170],["-1",171],["thumbsdown",171],["fist",172],["fist_raised",172],["facepunch",173],["punch",173],["fist_oncoming",173],["left-facing_fist",174],["fist_left",174],["right-facing_fist",175],["fist_right",175],["clap",176],["raised_hands",177],["open_hands",178],["palms_up_together",179],["handshake",180],["pray",181],["writing_hand",182],["nail_care",183],["selfie",184],["muscle",185],["mechanical_arm",186],["mechanical_leg",187],["leg",188],["foot",189],["ear",190],["ear_with_hearing_aid",191],["nose",192],["brain",193],["anatomical_heart",194],["lungs",195],["tooth",196],["bone",197],["eyes",198],["eye",199],["tongue",200],["lips",201],["baby",202],["child",203],["boy",204],["girl",205],["adult",206],["person_with_blond_hair",207],["man",208],["bearded_person",209],["red_haired_man",210],["curly_haired_man",211],["white_haired_man",212],["bald_man",213],["woman",214],["red_haired_woman",215],["red_haired_person",216],["curly_haired_woman",217],["curly_haired_person",218],["white_haired_woman",219],["white_haired_person",220],["bald_woman",221],["bald_person",222],["blond-haired-woman",223],["blonde_woman",223],["blond-haired-man",224],["blonde_man",224],["older_adult",225],["older_man",226],["older_woman",227],["person_frowning",228],["man-frowning",229],["frowning_man",229],["woman-frowning",230],["frowning_woman",230],["person_with_pouting_face",231],["man-pouting",232],["pouting_man",232],["woman-pouting",233],["pouting_woman",233],["no_good",234],["man-gesturing-no",235],["ng_man",235],["no_good_man",235],["woman-gesturing-no",236],["no_good_woman",236],["ng_woman",236],["ok_woman",237],["man-gesturing-ok",238],["ok_man",238],["woman-gesturing-ok",239],["information_desk_person",240],["man-tipping-hand",241],["tipping_hand_man",241],["woman-tipping-hand",242],["tipping_hand_woman",242],["raising_hand",243],["man-raising-hand",244],["raising_hand_man",244],["woman-raising-hand",245],["raising_hand_woman",245],["deaf_person",246],["deaf_man",247],["deaf_woman",248],["bow",249],["man-bowing",250],["bowing_man",250],["woman-bowing",251],["bowing_woman",251],["face_palm",252],["man-facepalming",253],["man_facepalming",253],["woman-facepalming",254],["woman_facepalming",254],["shrug",255],["man-shrugging",256],["man_shrugging",256],["woman-shrugging",257],["woman_shrugging",257],["health_worker",258],["male-doctor",259],["man_health_worker",259],["female-doctor",260],["woman_health_worker",260],["student",261],["male-student",262],["man_student",262],["female-student",263],["woman_student",263],["teacher",264],["male-teacher",265],["man_teacher",265],["female-teacher",266],["woman_teacher",266],["judge",267],["male-judge",268],["man_judge",268],["female-judge",269],["woman_judge",269],["farmer",270],["male-farmer",271],["man_farmer",271],["female-farmer",272],["woman_farmer",272],["cook",273],["male-cook",274],["man_cook",274],["female-cook",275],["woman_cook",275],["mechanic",276],["male-mechanic",277],["man_mechanic",277],["female-mechanic",278],["woman_mechanic",278],["factory_worker",279],["male-factory-worker",280],["man_factory_worker",280],["female-factory-worker",281],["woman_factory_worker",281],["office_worker",282],["male-office-worker",283],["man_office_worker",283],["female-office-worker",284],["woman_office_worker",284],["scientist",285],["male-scientist",286],["man_scientist",286],["female-scientist",287],["woman_scientist",287],["technologist",288],["male-technologist",289],["man_technologist",289],["female-technologist",290],["woman_technologist",290],["singer",291],["male-singer",292],["man_singer",292],["female-singer",293],["woman_singer",293],["artist",294],["male-artist",295],["man_artist",295],["female-artist",296],["woman_artist",296],["pilot",297],["male-pilot",298],["man_pilot",298],["female-pilot",299],["woman_pilot",299],["astronaut",300],["male-astronaut",301],["man_astronaut",301],["female-astronaut",302],["woman_astronaut",302],["firefighter",303],["male-firefighter",304],["man_firefighter",304],["female-firefighter",305],["woman_firefighter",305],["cop",306],["male-police-officer",307],["policeman",307],["female-police-officer",308],["policewoman",308],["sleuth_or_spy",309],["detective",309],["male-detective",310],["male_detective",310],["female-detective",311],["female_detective",311],["guardsman",312],["male-guard",313],["female-guard",314],["guardswoman",314],["ninja",315],["construction_worker",316],["male-construction-worker",317],["construction_worker_man",317],["female-construction-worker",318],["construction_worker_woman",318],["prince",319],["princess",320],["man_with_turban",321],["man-wearing-turban",322],["woman-wearing-turban",323],["woman_with_turban",323],["man_with_gua_pi_mao",324],["person_with_headscarf",325],["person_in_tuxedo",326],["man_in_tuxedo",327],["woman_in_tuxedo",328],["bride_with_veil",329],["man_with_veil",330],["woman_with_veil",331],["pregnant_woman",332],["breast-feeding",333],["woman_feeding_baby",334],["man_feeding_baby",335],["person_feeding_baby",336],["angel",337],["santa",338],["mrs_claus",339],["mother_christmas",339],["mx_claus",340],["superhero",341],["male_superhero",342],["female_superhero",343],["supervillain",344],["male_supervillain",345],["female_supervillain",346],["mage",347],["male_mage",348],["female_mage",349],["fairy",350],["male_fairy",351],["female_fairy",352],["vampire",353],["male_vampire",354],["female_vampire",355],["merperson",356],["merman",357],["mermaid",358],["elf",359],["male_elf",360],["female_elf",361],["genie",362],["male_genie",363],["female_genie",364],["zombie",365],["male_zombie",366],["female_zombie",367],["massage",368],["man-getting-massage",369],["massage_man",369],["woman-getting-massage",370],["massage_woman",370],["haircut",371],["man-getting-haircut",372],["haircut_man",372],["woman-getting-haircut",373],["haircut_woman",373],["walking",374],["man-walking",375],["walking_man",375],["woman-walking",376],["walking_woman",376],["standing_person",377],["man_standing",378],["woman_standing",379],["kneeling_person",380],["man_kneeling",381],["woman_kneeling",382],["person_with_probing_cane",383],["man_with_probing_cane",384],["woman_with_probing_cane",385],["person_in_motorized_wheelchair",386],["man_in_motorized_wheelchair",387],["woman_in_motorized_wheelchair",388],["person_in_manual_wheelchair",389],["man_in_manual_wheelchair",390],["woman_in_manual_wheelchair",391],["runner",392],["running",392],["man-running",393],["running_man",393],["woman-running",394],["running_woman",394],["dancer",395],["man_dancing",396],["man_in_business_suit_levitating",397],["business_suit_levitating",397],["dancers",398],["man-with-bunny-ears-partying",399],["dancing_men",399],["woman-with-bunny-ears-partying",400],["dancing_women",400],["person_in_steamy_room",401],["man_in_steamy_room",402],["woman_in_steamy_room",403],["person_climbing",404],["man_climbing",405],["woman_climbing",406],["fencer",407],["person_fencing",407],["horse_racing",408],["skier",409],["snowboarder",410],["golfer",411],["man-golfing",412],["golfing_man",412],["woman-golfing",413],["golfing_woman",413],["surfer",414],["man-surfing",415],["surfing_man",415],["woman-surfing",416],["surfing_woman",416],["rowboat",417],["man-rowing-boat",418],["rowing_man",418],["woman-rowing-boat",419],["rowing_woman",419],["swimmer",420],["man-swimming",421],["swimming_man",421],["woman-swimming",422],["swimming_woman",422],["person_with_ball",423],["man-bouncing-ball",424],["basketball_man",424],["woman-bouncing-ball",425],["basketball_woman",425],["weight_lifter",426],["man-lifting-weights",427],["weight_lifting_man",427],["woman-lifting-weights",428],["weight_lifting_woman",428],["bicyclist",429],["man-biking",430],["biking_man",430],["woman-biking",431],["biking_woman",431],["mountain_bicyclist",432],["man-mountain-biking",433],["mountain_biking_man",433],["woman-mountain-biking",434],["mountain_biking_woman",434],["person_doing_cartwheel",435],["man-cartwheeling",436],["man_cartwheeling",436],["woman-cartwheeling",437],["woman_cartwheeling",437],["wrestlers",438],["man-wrestling",439],["men_wrestling",439],["woman-wrestling",440],["women_wrestling",440],["water_polo",441],["man-playing-water-polo",442],["man_playing_water_polo",442],["woman-playing-water-polo",443],["woman_playing_water_polo",443],["handball",444],["man-playing-handball",445],["man_playing_handball",445],["woman-playing-handball",446],["woman_playing_handball",446],["juggling",447],["man-juggling",448],["man_juggling",448],["woman-juggling",449],["woman_juggling",449],["person_in_lotus_position",450],["man_in_lotus_position",451],["woman_in_lotus_position",452],["bath",453],["sleeping_accommodation",454],["sleeping_bed",454],["people_holding_hands",455],["two_women_holding_hands",456],["women_holding_hands",456],["man_and_woman_holding_hands",457],["woman_and_man_holding_hands",457],["couple",457],["two_men_holding_hands",458],["men_holding_hands",458],["couplekiss",459],["woman-kiss-man",460],["couplekiss_man_woman",460],["man-kiss-man",461],["couplekiss_man_man",461],["woman-kiss-woman",462],["couplekiss_woman_woman",462],["couple_with_heart",463],["woman-heart-man",464],["couple_with_heart_woman_man",464],["man-heart-man",465],["couple_with_heart_man_man",465],["woman-heart-woman",466],["couple_with_heart_woman_woman",466],["family",467],["man-woman-boy",468],["family_man_woman_boy",468],["man-woman-girl",469],["family_man_woman_girl",469],["man-woman-girl-boy",470],["family_man_woman_girl_boy",470],["man-woman-boy-boy",471],["family_man_woman_boy_boy",471],["man-woman-girl-girl",472],["family_man_woman_girl_girl",472],["man-man-boy",473],["family_man_man_boy",473],["man-man-girl",474],["family_man_man_girl",474],["man-man-girl-boy",475],["family_man_man_girl_boy",475],["man-man-boy-boy",476],["family_man_man_boy_boy",476],["man-man-girl-girl",477],["family_man_man_girl_girl",477],["woman-woman-boy",478],["family_woman_woman_boy",478],["woman-woman-girl",479],["family_woman_woman_girl",479],["woman-woman-girl-boy",480],["family_woman_woman_girl_boy",480],["woman-woman-boy-boy",481],["family_woman_woman_boy_boy",481],["woman-woman-girl-girl",482],["family_woman_woman_girl_girl",482],["man-boy",483],["family_man_boy",483],["man-boy-boy",484],["family_man_boy_boy",484],["man-girl",485],["family_man_girl",485],["man-girl-boy",486],["family_man_girl_boy",486],["man-girl-girl",487],["family_man_girl_girl",487],["woman-boy",488],["family_woman_boy",488],["woman-boy-boy",489],["family_woman_boy_boy",489],["woman-girl",490],["family_woman_girl",490],["woman-girl-boy",491],["family_woman_girl_boy",491],["woman-girl-girl",492],["family_woman_girl_girl",492],["speaking_head_in_silhouette",493],["speaking_head",493],["bust_in_silhouette",494],["busts_in_silhouette",495],["people_hugging",496],["footprints",497],["skin-tone-2",498],["skin-tone-3",499],["skin-tone-4",500],["skin-tone-5",501],["skin-tone-6",502],["monkey_face",503],["monkey",504],["gorilla",505],["orangutan",506],["dog",507],["dog2",508],["guide_dog",509],["service_dog",510],["poodle",511],["wolf",512],["fox_face",513],["raccoon",514],["cat",515],["cat2",516],["black_cat",517],["lion_face",518],["lion",518],["tiger",519],["tiger2",520],["leopard",521],["horse",522],["racehorse",523],["unicorn_face",524],["unicorn",524],["zebra_face",525],["deer",526],["bison",527],["cow",528],["ox",529],["water_buffalo",530],["cow2",531],["pig",532],["pig2",533],["boar",534],["pig_nose",535],["ram",536],["sheep",537],["goat",538],["dromedary_camel",539],["camel",540],["llama",541],["giraffe_face",542],["elephant",543],["mammoth",544],["rhinoceros",545],["hippopotamus",546],["mouse",547],["mouse2",548],["rat",549],["hamster",550],["rabbit",551],["rabbit2",552],["chipmunk",553],["beaver",554],["hedgehog",555],["bat",556],["bear",557],["polar_bear",558],["koala",559],["panda_face",560],["sloth",561],["otter",562],["skunk",563],["kangaroo",564],["badger",565],["feet",566],["paw_prints",566],["turkey",567],["chicken",568],["rooster",569],["hatching_chick",570],["baby_chick",571],["hatched_chick",572],["bird",573],["penguin",574],["dove_of_peace",575],["dove",575],["eagle",576],["duck",577],["swan",578],["owl",579],["dodo",580],["feather",581],["flamingo",582],["peacock",583],["parrot",584],["frog",585],["crocodile",586],["turtle",587],["lizard",588],["snake",589],["dragon_face",590],["dragon",591],["sauropod",592],["t-rex",593],["whale",594],["whale2",595],["dolphin",596],["flipper",596],["seal",597],["fish",598],["tropical_fish",599],["blowfish",600],["shark",601],["octopus",602],["shell",603],["snail",604],["butterfly",605],["bug",606],["ant",607],["bee",608],["honeybee",608],["beetle",609],["ladybug",610],["lady_beetle",610],["cricket",611],["cockroach",612],["spider",613],["spider_web",614],["scorpion",615],["mosquito",616],["fly",617],["worm",618],["microbe",619],["bouquet",620],["cherry_blossom",621],["white_flower",622],["rosette",623],["rose",624],["wilted_flower",625],["hibiscus",626],["sunflower",627],["blossom",628],["tulip",629],["seedling",630],["potted_plant",631],["evergreen_tree",632],["deciduous_tree",633],["palm_tree",634],["cactus",635],["ear_of_rice",636],["herb",637],["shamrock",638],["four_leaf_clover",639],["maple_leaf",640],["fallen_leaf",641],["leaves",642],["grapes",643],["melon",644],["watermelon",645],["tangerine",646],["mandarin",646],["orange",646],["lemon",647],["banana",648],["pineapple",649],["mango",650],["apple",651],["green_apple",652],["pear",653],["peach",654],["cherries",655],["strawberry",656],["blueberries",657],["kiwifruit",658],["kiwi_fruit",658],["tomato",659],["olive",660],["coconut",661],["avocado",662],["eggplant",663],["potato",664],["carrot",665],["corn",666],["hot_pepper",667],["bell_pepper",668],["cucumber",669],["leafy_green",670],["broccoli",671],["garlic",672],["onion",673],["mushroom",674],["peanuts",675],["chestnut",676],["bread",677],["croissant",678],["baguette_bread",679],["flatbread",680],["pretzel",681],["bagel",682],["pancakes",683],["waffle",684],["cheese_wedge",685],["cheese",685],["meat_on_bone",686],["poultry_leg",687],["cut_of_meat",688],["bacon",689],["hamburger",690],["fries",691],["pizza",692],["hotdog",693],["sandwich",694],["taco",695],["burrito",696],["tamale",697],["stuffed_flatbread",698],["falafel",699],["egg",700],["fried_egg",701],["cooking",701],["shallow_pan_of_food",702],["stew",703],["fondue",704],["bowl_with_spoon",705],["green_salad",706],["popcorn",707],["butter",708],["salt",709],["canned_food",710],["bento",711],["rice_cracker",712],["rice_ball",713],["rice",714],["curry",715],["ramen",716],["spaghetti",717],["sweet_potato",718],["oden",719],["sushi",720],["fried_shrimp",721],["fish_cake",722],["moon_cake",723],["dango",724],["dumpling",725],["fortune_cookie",726],["takeout_box",727],["crab",728],["lobster",729],["shrimp",730],["squid",731],["oyster",732],["icecream",733],["shaved_ice",734],["ice_cream",735],["doughnut",736],["cookie",737],["birthday",738],["cake",739],["cupcake",740],["pie",741],["chocolate_bar",742],["candy",743],["lollipop",744],["custard",745],["honey_pot",746],["baby_bottle",747],["glass_of_milk",748],["milk_glass",748],["coffee",749],["teapot",750],["tea",751],["sake",752],["champagne",753],["wine_glass",754],["cocktail",755],["tropical_drink",756],["beer",757],["beers",758],["clinking_glasses",759],["tumbler_glass",760],["cup_with_straw",761],["bubble_tea",762],["beverage_box",763],["mate_drink",764],["ice_cube",765],["chopsticks",766],["knife_fork_plate",767],["plate_with_cutlery",767],["fork_and_knife",768],["spoon",769],["hocho",770],["knife",770],["amphora",771],["earth_africa",772],["earth_americas",773],["earth_asia",774],["globe_with_meridians",775],["world_map",776],["japan",777],["compass",778],["snow_capped_mountain",779],["mountain_snow",779],["mountain",780],["volcano",781],["mount_fuji",782],["camping",783],["beach_with_umbrella",784],["beach_umbrella",784],["desert",785],["desert_island",786],["national_park",787],["stadium",788],["classical_building",789],["building_construction",790],["bricks",791],["rock",792],["wood",793],["hut",794],["house_buildings",795],["houses",795],["derelict_house_building",796],["derelict_house",796],["house",797],["house_with_garden",798],["office",799],["post_office",800],["european_post_office",801],["hospital",802],["bank",803],["hotel",804],["love_hotel",805],["convenience_store",806],["school",807],["department_store",808],["factory",809],["japanese_castle",810],["european_castle",811],["wedding",812],["tokyo_tower",813],["statue_of_liberty",814],["church",815],["mosque",816],["hindu_temple",817],["synagogue",818],["shinto_shrine",819],["kaaba",820],["fountain",821],["tent",822],["foggy",823],["night_with_stars",824],["cityscape",825],["sunrise_over_mountains",826],["sunrise",827],["city_sunset",828],["city_sunrise",829],["bridge_at_night",830],["hotsprings",831],["carousel_horse",832],["ferris_wheel",833],["roller_coaster",834],["barber",835],["circus_tent",836],["steam_locomotive",837],["railway_car",838],["bullettrain_side",839],["bullettrain_front",840],["train2",841],["metro",842],["light_rail",843],["station",844],["tram",845],["monorail",846],["mountain_railway",847],["train",848],["bus",849],["oncoming_bus",850],["trolleybus",851],["minibus",852],["ambulance",853],["fire_engine",854],["police_car",855],["oncoming_police_car",856],["taxi",857],["oncoming_taxi",858],["car",859],["red_car",859],["oncoming_automobile",860],["blue_car",861],["pickup_truck",862],["truck",863],["articulated_lorry",864],["tractor",865],["racing_car",866],["racing_motorcycle",867],["motorcycle",867],["motor_scooter",868],["manual_wheelchair",869],["motorized_wheelchair",870],["auto_rickshaw",871],["bike",872],["scooter",873],["kick_scooter",873],["skateboard",874],["roller_skate",875],["busstop",876],["motorway",877],["railway_track",878],["oil_drum",879],["fuelpump",880],["rotating_light",881],["traffic_light",882],["vertical_traffic_light",883],["octagonal_sign",884],["stop_sign",884],["construction",885],["anchor",886],["boat",887],["sailboat",887],["canoe",888],["speedboat",889],["passenger_ship",890],["ferry",891],["motor_boat",892],["ship",893],["airplane",894],["small_airplane",895],["airplane_departure",896],["flight_departure",896],["airplane_arriving",897],["flight_arrival",897],["parachute",898],["seat",899],["helicopter",900],["suspension_railway",901],["mountain_cableway",902],["aerial_tramway",903],["satellite",904],["artificial_satellite",904],["rocket",905],["flying_saucer",906],["bellhop_bell",907],["luggage",908],["hourglass",909],["hourglass_flowing_sand",910],["watch",911],["alarm_clock",912],["stopwatch",913],["timer_clock",914],["mantelpiece_clock",915],["clock12",916],["clock1230",917],["clock1",918],["clock130",919],["clock2",920],["clock230",921],["clock3",922],["clock330",923],["clock4",924],["clock430",925],["clock5",926],["clock530",927],["clock6",928],["clock630",929],["clock7",930],["clock730",931],["clock8",932],["clock830",933],["clock9",934],["clock930",935],["clock10",936],["clock1030",937],["clock11",938],["clock1130",939],["new_moon",940],["waxing_crescent_moon",941],["first_quarter_moon",942],["moon",943],["waxing_gibbous_moon",943],["full_moon",944],["waning_gibbous_moon",945],["last_quarter_moon",946],["waning_crescent_moon",947],["crescent_moon",948],["new_moon_with_face",949],["first_quarter_moon_with_face",950],["last_quarter_moon_with_face",951],["thermometer",952],["sunny",953],["full_moon_with_face",954],["sun_with_face",955],["ringed_planet",956],["star",957],["star2",958],["stars",959],["milky_way",960],["cloud",961],["partly_sunny",962],["thunder_cloud_and_rain",963],["cloud_with_lightning_and_rain",963],["mostly_sunny",964],["sun_small_cloud",964],["sun_behind_small_cloud",964],["barely_sunny",965],["sun_behind_cloud",965],["sun_behind_large_cloud",965],["partly_sunny_rain",966],["sun_behind_rain_cloud",966],["rain_cloud",967],["cloud_with_rain",967],["snow_cloud",968],["cloud_with_snow",968],["lightning",969],["lightning_cloud",969],["cloud_with_lightning",969],["tornado",970],["tornado_cloud",970],["fog",971],["wind_blowing_face",972],["wind_face",972],["cyclone",973],["rainbow",974],["closed_umbrella",975],["umbrella",976],["open_umbrella",976],["umbrella_with_rain_drops",977],["umbrella_on_ground",978],["parasol_on_ground",978],["zap",979],["snowflake",980],["snowman",981],["snowman_with_snow",981],["snowman_without_snow",982],["comet",983],["fire",984],["droplet",985],["ocean",986],["jack_o_lantern",987],["christmas_tree",988],["fireworks",989],["sparkler",990],["firecracker",991],["sparkles",992],["balloon",993],["tada",994],["confetti_ball",995],["tanabata_tree",996],["bamboo",997],["dolls",998],["flags",999],["wind_chime",1000],["rice_scene",1001],["red_envelope",1002],["ribbon",1003],["gift",1004],["reminder_ribbon",1005],["admission_tickets",1006],["tickets",1006],["ticket",1007],["medal",1008],["medal_military",1008],["trophy",1009],["sports_medal",1010],["medal_sports",1010],["first_place_medal",1011],["1st_place_medal",1011],["second_place_medal",1012],["2nd_place_medal",1012],["third_place_medal",1013],["3rd_place_medal",1013],["soccer",1014],["baseball",1015],["softball",1016],["basketball",1017],["volleyball",1018],["football",1019],["rugby_football",1020],["tennis",1021],["flying_disc",1022],["bowling",1023],["cricket_bat_and_ball",1024],["field_hockey_stick_and_ball",1025],["field_hockey",1025],["ice_hockey_stick_and_puck",1026],["ice_hockey",1026],["lacrosse",1027],["table_tennis_paddle_and_ball",1028],["ping_pong",1028],["badminton_racquet_and_shuttlecock",1029],["badminton",1029],["boxing_glove",1030],["martial_arts_uniform",1031],["goal_net",1032],["golf",1033],["ice_skate",1034],["fishing_pole_and_fish",1035],["diving_mask",1036],["running_shirt_with_sash",1037],["ski",1038],["sled",1039],["curling_stone",1040],["dart",1041],["yo-yo",1042],["kite",1043],["8ball",1044],["crystal_ball",1045],["magic_wand",1046],["nazar_amulet",1047],["video_game",1048],["joystick",1049],["slot_machine",1050],["game_die",1051],["jigsaw",1052],["teddy_bear",1053],["pinata",1054],["nesting_dolls",1055],["spades",1056],["hearts",1057],["diamonds",1058],["clubs",1059],["chess_pawn",1060],["black_joker",1061],["mahjong",1062],["flower_playing_cards",1063],["performing_arts",1064],["frame_with_picture",1065],["framed_picture",1065],["art",1066],["thread",1067],["sewing_needle",1068],["yarn",1069],["knot",1070],["eyeglasses",1071],["dark_sunglasses",1072],["goggles",1073],["lab_coat",1074],["safety_vest",1075],["necktie",1076],["shirt",1077],["tshirt",1077],["jeans",1078],["scarf",1079],["gloves",1080],["coat",1081],["socks",1082],["dress",1083],["kimono",1084],["sari",1085],["one-piece_swimsuit",1086],["briefs",1087],["shorts",1088],["bikini",1089],["womans_clothes",1090],["purse",1091],["handbag",1092],["pouch",1093],["shopping_bags",1094],["shopping",1094],["school_satchel",1095],["thong_sandal",1096],["mans_shoe",1097],["shoe",1097],["athletic_shoe",1098],["hiking_boot",1099],["womans_flat_shoe",1100],["high_heel",1101],["sandal",1102],["ballet_shoes",1103],["boot",1104],["crown",1105],["womans_hat",1106],["tophat",1107],["mortar_board",1108],["billed_cap",1109],["military_helmet",1110],["helmet_with_white_cross",1111],["rescue_worker_helmet",1111],["prayer_beads",1112],["lipstick",1113],["ring",1114],["gem",1115],["mute",1116],["speaker",1117],["sound",1118],["loud_sound",1119],["loudspeaker",1120],["mega",1121],["postal_horn",1122],["bell",1123],["no_bell",1124],["musical_score",1125],["musical_note",1126],["notes",1127],["studio_microphone",1128],["level_slider",1129],["control_knobs",1130],["microphone",1131],["headphones",1132],["radio",1133],["saxophone",1134],["accordion",1135],["guitar",1136],["musical_keyboard",1137],["trumpet",1138],["violin",1139],["banjo",1140],["drum_with_drumsticks",1141],["drum",1141],["long_drum",1142],["iphone",1143],["calling",1144],["phone",1145],["telephone",1145],["telephone_receiver",1146],["pager",1147],["fax",1148],["battery",1149],["electric_plug",1150],["computer",1151],["desktop_computer",1152],["printer",1153],["keyboard",1154],["three_button_mouse",1155],["computer_mouse",1155],["trackball",1156],["minidisc",1157],["floppy_disk",1158],["cd",1159],["dvd",1160],["abacus",1161],["movie_camera",1162],["film_frames",1163],["film_strip",1163],["film_projector",1164],["clapper",1165],["tv",1166],["camera",1167],["camera_with_flash",1168],["camera_flash",1168],["video_camera",1169],["vhs",1170],["mag",1171],["mag_right",1172],["candle",1173],["bulb",1174],["flashlight",1175],["izakaya_lantern",1176],["lantern",1176],["diya_lamp",1177],["notebook_with_decorative_cover",1178],["closed_book",1179],["book",1180],["open_book",1180],["green_book",1181],["blue_book",1182],["orange_book",1183],["books",1184],["notebook",1185],["ledger",1186],["page_with_curl",1187],["scroll",1188],["page_facing_up",1189],["newspaper",1190],["rolled_up_newspaper",1191],["newspaper_roll",1191],["bookmark_tabs",1192],["bookmark",1193],["label",1194],["moneybag",1195],["coin",1196],["yen",1197],["dollar",1198],["euro",1199],["pound",1200],["money_with_wings",1201],["credit_card",1202],["receipt",1203],["chart",1204],["email",1205],["envelope",1205],["e-mail",1206],["incoming_envelope",1207],["envelope_with_arrow",1208],["outbox_tray",1209],["inbox_tray",1210],["package",1211],["mailbox",1212],["mailbox_closed",1213],["mailbox_with_mail",1214],["mailbox_with_no_mail",1215],["postbox",1216],["ballot_box_with_ballot",1217],["ballot_box",1217],["pencil2",1218],["black_nib",1219],["lower_left_fountain_pen",1220],["fountain_pen",1220],["lower_left_ballpoint_pen",1221],["pen",1221],["lower_left_paintbrush",1222],["paintbrush",1222],["lower_left_crayon",1223],["crayon",1223],["memo",1224],["pencil",1224],["briefcase",1225],["file_folder",1226],["open_file_folder",1227],["card_index_dividers",1228],["date",1229],["calendar",1230],["spiral_note_pad",1231],["spiral_notepad",1231],["spiral_calendar_pad",1232],["spiral_calendar",1232],["card_index",1233],["chart_with_upwards_trend",1234],["chart_with_downwards_trend",1235],["bar_chart",1236],["clipboard",1237],["pushpin",1238],["round_pushpin",1239],["paperclip",1240],["linked_paperclips",1241],["paperclips",1241],["straight_ruler",1242],["triangular_ruler",1243],["scissors",1244],["card_file_box",1245],["file_cabinet",1246],["wastebasket",1247],["lock",1248],["unlock",1249],["lock_with_ink_pen",1250],["closed_lock_with_key",1251],["key",1252],["old_key",1253],["hammer",1254],["axe",1255],["pick",1256],["hammer_and_pick",1257],["hammer_and_wrench",1258],["dagger_knife",1259],["dagger",1259],["crossed_swords",1260],["gun",1261],["boomerang",1262],["bow_and_arrow",1263],["shield",1264],["carpentry_saw",1265],["wrench",1266],["screwdriver",1267],["nut_and_bolt",1268],["gear",1269],["compression",1270],["clamp",1270],["scales",1271],["balance_scale",1271],["probing_cane",1272],["link",1273],["chains",1274],["hook",1275],["toolbox",1276],["magnet",1277],["ladder",1278],["alembic",1279],["test_tube",1280],["petri_dish",1281],["dna",1282],["microscope",1283],["telescope",1284],["satellite_antenna",1285],["syringe",1286],["drop_of_blood",1287],["pill",1288],["adhesive_bandage",1289],["stethoscope",1290],["door",1291],["elevator",1292],["mirror",1293],["window",1294],["bed",1295],["couch_and_lamp",1296],["chair",1297],["toilet",1298],["plunger",1299],["shower",1300],["bathtub",1301],["mouse_trap",1302],["razor",1303],["lotion_bottle",1304],["safety_pin",1305],["broom",1306],["basket",1307],["roll_of_paper",1308],["bucket",1309],["soap",1310],["toothbrush",1311],["sponge",1312],["fire_extinguisher",1313],["shopping_trolley",1314],["shopping_cart",1314],["smoking",1315],["coffin",1316],["headstone",1317],["funeral_urn",1318],["moyai",1319],["placard",1320],["atm",1321],["put_litter_in_its_place",1322],["potable_water",1323],["wheelchair",1324],["mens",1325],["womens",1326],["restroom",1327],["baby_symbol",1328],["wc",1329],["passport_control",1330],["customs",1331],["baggage_claim",1332],["left_luggage",1333],["warning",1334],["children_crossing",1335],["no_entry",1336],["no_entry_sign",1337],["no_bicycles",1338],["no_smoking",1339],["do_not_litter",1340],["non-potable_water",1341],["no_pedestrians",1342],["no_mobile_phones",1343],["underage",1344],["radioactive_sign",1345],["radioactive",1345],["biohazard_sign",1346],["biohazard",1346],["arrow_up",1347],["arrow_upper_right",1348],["arrow_right",1349],["arrow_lower_right",1350],["arrow_down",1351],["arrow_lower_left",1352],["arrow_left",1353],["arrow_upper_left",1354],["arrow_up_down",1355],["left_right_arrow",1356],["leftwards_arrow_with_hook",1357],["arrow_right_hook",1358],["arrow_heading_up",1359],["arrow_heading_down",1360],["arrows_clockwise",1361],["arrows_counterclockwise",1362],["back",1363],["end",1364],["on",1365],["soon",1366],["top",1367],["place_of_worship",1368],["atom_symbol",1369],["om_symbol",1370],["om",1370],["star_of_david",1371],["wheel_of_dharma",1372],["yin_yang",1373],["latin_cross",1374],["orthodox_cross",1375],["star_and_crescent",1376],["peace_symbol",1377],["menorah_with_nine_branches",1378],["menorah",1378],["six_pointed_star",1379],["aries",1380],["taurus",1381],["gemini",1382],["cancer",1383],["leo",1384],["virgo",1385],["libra",1386],["scorpius",1387],["sagittarius",1388],["capricorn",1389],["aquarius",1390],["pisces",1391],["ophiuchus",1392],["twisted_rightwards_arrows",1393],["repeat",1394],["repeat_one",1395],["arrow_forward",1396],["fast_forward",1397],["black_right_pointing_double_triangle_with_vertical_bar",1398],["next_track_button",1398],["black_right_pointing_triangle_with_double_vertical_bar",1399],["play_or_pause_button",1399],["arrow_backward",1400],["rewind",1401],["black_left_pointing_double_triangle_with_vertical_bar",1402],["previous_track_button",1402],["arrow_up_small",1403],["arrow_double_up",1404],["arrow_down_small",1405],["arrow_double_down",1406],["double_vertical_bar",1407],["pause_button",1407],["black_square_for_stop",1408],["stop_button",1408],["black_circle_for_record",1409],["record_button",1409],["eject",1410],["cinema",1411],["low_brightness",1412],["high_brightness",1413],["signal_strength",1414],["vibration_mode",1415],["mobile_phone_off",1416],["female_sign",1417],["male_sign",1418],["transgender_symbol",1419],["heavy_multiplication_x",1420],["heavy_plus_sign",1421],["heavy_minus_sign",1422],["heavy_division_sign",1423],["infinity",1424],["bangbang",1425],["interrobang",1426],["question",1427],["grey_question",1428],["grey_exclamation",1429],["exclamation",1430],["heavy_exclamation_mark",1430],["wavy_dash",1431],["currency_exchange",1432],["heavy_dollar_sign",1433],["medical_symbol",1434],["staff_of_aesculapius",1434],["recycle",1435],["fleur_de_lis",1436],["trident",1437],["name_badge",1438],["beginner",1439],["o",1440],["white_check_mark",1441],["ballot_box_with_check",1442],["heavy_check_mark",1443],["x",1444],["negative_squared_cross_mark",1445],["curly_loop",1446],["loop",1447],["part_alternation_mark",1448],["eight_spoked_asterisk",1449],["eight_pointed_black_star",1450],["sparkle",1451],["copyright",1452],["registered",1453],["tm",1454],["hash",1455],["keycap_star",1456],["asterisk",1456],["zero",1457],["one",1458],["two",1459],["three",1460],["four",1461],["five",1462],["six",1463],["seven",1464],["eight",1465],["nine",1466],["keycap_ten",1467],["capital_abcd",1468],["abcd",1469],["1234",1470],["symbols",1471],["abc",1472],["a",1473],["ab",1474],["b",1475],["cl",1476],["cool",1477],["free",1478],["information_source",1479],["id",1480],["m",1481],["new",1482],["ng",1483],["o2",1484],["ok",1485],["parking",1486],["sos",1487],["up",1488],["vs",1489],["koko",1490],["sa",1491],["u6708",1492],["u6709",1493],["u6307",1494],["ideograph_advantage",1495],["u5272",1496],["u7121",1497],["u7981",1498],["accept",1499],["u7533",1500],["u5408",1501],["u7a7a",1502],["congratulations",1503],["secret",1504],["u55b6",1505],["u6e80",1506],["red_circle",1507],["large_orange_circle",1508],["large_yellow_circle",1509],["large_green_circle",1510],["large_blue_circle",1511],["large_purple_circle",1512],["large_brown_circle",1513],["black_circle",1514],["white_circle",1515],["large_red_square",1516],["large_orange_square",1517],["large_yellow_square",1518],["large_green_square",1519],["large_blue_square",1520],["large_purple_square",1521],["large_brown_square",1522],["black_large_square",1523],["white_large_square",1524],["black_medium_square",1525],["white_medium_square",1526],["black_medium_small_square",1527],["white_medium_small_square",1528],["black_small_square",1529],["white_small_square",1530],["large_orange_diamond",1531],["large_blue_diamond",1532],["small_orange_diamond",1533],["small_blue_diamond",1534],["small_red_triangle",1535],["small_red_triangle_down",1536],["diamond_shape_with_a_dot_inside",1537],["radio_button",1538],["white_square_button",1539],["black_square_button",1540],["checkered_flag",1541],["triangular_flag_on_post",1542],["crossed_flags",1543],["waving_black_flag",1544],["black_flag",1544],["waving_white_flag",1545],["white_flag",1545],["rainbow-flag",1546],["rainbow_flag",1546],["transgender_flag",1547],["pirate_flag",1548],["flag-ac",1549],["flag-ad",1550],["andorra",1550],["flag-ae",1551],["united_arab_emirates",1551],["flag-af",1552],["afghanistan",1552],["flag-ag",1553],["antigua_barbuda",1553],["flag-ai",1554],["anguilla",1554],["flag-al",1555],["albania",1555],["flag-am",1556],["armenia",1556],["flag-ao",1557],["angola",1557],["flag-aq",1558],["antarctica",1558],["flag-ar",1559],["argentina",1559],["flag-as",1560],["american_samoa",1560],["flag-at",1561],["austria",1561],["flag-au",1562],["australia",1562],["flag-aw",1563],["aruba",1563],["flag-ax",1564],["aland_islands",1564],["flag-az",1565],["azerbaijan",1565],["flag-ba",1566],["bosnia_herzegovina",1566],["flag-bb",1567],["barbados",1567],["flag-bd",1568],["bangladesh",1568],["flag-be",1569],["belgium",1569],["flag-bf",1570],["burkina_faso",1570],["flag-bg",1571],["bulgaria",1571],["flag-bh",1572],["bahrain",1572],["flag-bi",1573],["burundi",1573],["flag-bj",1574],["benin",1574],["flag-bl",1575],["st_barthelemy",1575],["flag-bm",1576],["bermuda",1576],["flag-bn",1577],["brunei",1577],["flag-bo",1578],["bolivia",1578],["flag-bq",1579],["caribbean_netherlands",1579],["flag-br",1580],["brazil",1580],["flag-bs",1581],["bahamas",1581],["flag-bt",1582],["bhutan",1582],["flag-bv",1583],["flag-bw",1584],["botswana",1584],["flag-by",1585],["belarus",1585],["flag-bz",1586],["belize",1586],["flag-ca",1587],["ca",1587],["canada",1587],["flag-cc",1588],["cocos_islands",1588],["flag-cd",1589],["congo_kinshasa",1589],["flag-cf",1590],["central_african_republic",1590],["flag-cg",1591],["congo_brazzaville",1591],["flag-ch",1592],["switzerland",1592],["flag-ci",1593],["cote_divoire",1593],["flag-ck",1594],["cook_islands",1594],["flag-cl",1595],["chile",1595],["flag-cm",1596],["cameroon",1596],["cn",1597],["flag-cn",1597],["flag-co",1598],["colombia",1598],["flag-cp",1599],["flag-cr",1600],["costa_rica",1600],["flag-cu",1601],["cuba",1601],["flag-cv",1602],["cape_verde",1602],["flag-cw",1603],["curacao",1603],["flag-cx",1604],["christmas_island",1604],["flag-cy",1605],["cyprus",1605],["flag-cz",1606],["czech_republic",1606],["de",1607],["flag-de",1607],["flag-dg",1608],["flag-dj",1609],["djibouti",1609],["flag-dk",1610],["denmark",1610],["flag-dm",1611],["dominica",1611],["flag-do",1612],["dominican_republic",1612],["flag-dz",1613],["algeria",1613],["flag-ea",1614],["flag-ec",1615],["ecuador",1615],["flag-ee",1616],["estonia",1616],["flag-eg",1617],["egypt",1617],["flag-eh",1618],["western_sahara",1618],["flag-er",1619],["eritrea",1619],["es",1620],["flag-es",1620],["flag-et",1621],["ethiopia",1621],["flag-eu",1622],["eu",1622],["european_union",1622],["flag-fi",1623],["finland",1623],["flag-fj",1624],["fiji",1624],["flag-fk",1625],["falkland_islands",1625],["flag-fm",1626],["micronesia",1626],["flag-fo",1627],["faroe_islands",1627],["fr",1628],["flag-fr",1628],["flag-ga",1629],["gabon",1629],["gb",1630],["uk",1630],["flag-gb",1630],["flag-gd",1631],["grenada",1631],["flag-ge",1632],["georgia",1632],["flag-gf",1633],["french_guiana",1633],["flag-gg",1634],["guernsey",1634],["flag-gh",1635],["ghana",1635],["flag-gi",1636],["gibraltar",1636],["flag-gl",1637],["greenland",1637],["flag-gm",1638],["gambia",1638],["flag-gn",1639],["guinea",1639],["flag-gp",1640],["guadeloupe",1640],["flag-gq",1641],["equatorial_guinea",1641],["flag-gr",1642],["greece",1642],["flag-gs",1643],["south_georgia_south_sandwich_islands",1643],["flag-gt",1644],["guatemala",1644],["flag-gu",1645],["guam",1645],["flag-gw",1646],["guinea_bissau",1646],["flag-gy",1647],["guyana",1647],["flag-hk",1648],["hong_kong",1648],["flag-hm",1649],["flag-hn",1650],["honduras",1650],["flag-hr",1651],["croatia",1651],["flag-ht",1652],["haiti",1652],["flag-hu",1653],["hungary",1653],["flag-ic",1654],["canary_islands",1654],["flag-id",1655],["indonesia",1655],["flag-ie",1656],["ireland",1656],["flag-il",1657],["israel",1657],["flag-im",1658],["isle_of_man",1658],["flag-in",1659],["india",1659],["flag-io",1660],["british_indian_ocean_territory",1660],["flag-iq",1661],["iraq",1661],["flag-ir",1662],["iran",1662],["flag-is",1663],["iceland",1663],["it",1664],["flag-it",1664],["flag-je",1665],["jersey",1665],["flag-jm",1666],["jamaica",1666],["flag-jo",1667],["jordan",1667],["jp",1668],["flag-jp",1668],["flag-ke",1669],["kenya",1669],["flag-kg",1670],["kyrgyzstan",1670],["flag-kh",1671],["cambodia",1671],["flag-ki",1672],["kiribati",1672],["flag-km",1673],["comoros",1673],["flag-kn",1674],["st_kitts_nevis",1674],["flag-kp",1675],["north_korea",1675],["kr",1676],["flag-kr",1676],["flag-kw",1677],["kuwait",1677],["flag-ky",1678],["cayman_islands",1678],["flag-kz",1679],["kazakhstan",1679],["flag-la",1680],["laos",1680],["flag-lb",1681],["lebanon",1681],["flag-lc",1682],["st_lucia",1682],["flag-li",1683],["liechtenstein",1683],["flag-lk",1684],["sri_lanka",1684],["flag-lr",1685],["liberia",1685],["flag-ls",1686],["lesotho",1686],["flag-lt",1687],["lithuania",1687],["flag-lu",1688],["luxembourg",1688],["flag-lv",1689],["latvia",1689],["flag-ly",1690],["libya",1690],["flag-ma",1691],["morocco",1691],["flag-mc",1692],["monaco",1692],["flag-md",1693],["moldova",1693],["flag-me",1694],["montenegro",1694],["flag-mf",1695],["flag-mg",1696],["madagascar",1696],["flag-mh",1697],["marshall_islands",1697],["flag-mk",1698],["macedonia",1698],["flag-ml",1699],["mali",1699],["flag-mm",1700],["myanmar",1700],["flag-mn",1701],["mongolia",1701],["flag-mo",1702],["macau",1702],["flag-mp",1703],["northern_mariana_islands",1703],["flag-mq",1704],["martinique",1704],["flag-mr",1705],["mauritania",1705],["flag-ms",1706],["montserrat",1706],["flag-mt",1707],["malta",1707],["flag-mu",1708],["mauritius",1708],["flag-mv",1709],["maldives",1709],["flag-mw",1710],["malawi",1710],["flag-mx",1711],["mexico",1711],["flag-my",1712],["malaysia",1712],["flag-mz",1713],["mozambique",1713],["flag-na",1714],["namibia",1714],["flag-nc",1715],["new_caledonia",1715],["flag-ne",1716],["niger",1716],["flag-nf",1717],["norfolk_island",1717],["flag-ng",1718],["nigeria",1718],["flag-ni",1719],["nicaragua",1719],["flag-nl",1720],["netherlands",1720],["flag-no",1721],["norway",1721],["flag-np",1722],["nepal",1722],["flag-nr",1723],["nauru",1723],["flag-nu",1724],["niue",1724],["flag-nz",1725],["new_zealand",1725],["flag-om",1726],["oman",1726],["flag-pa",1727],["panama",1727],["flag-pe",1728],["peru",1728],["flag-pf",1729],["french_polynesia",1729],["flag-pg",1730],["papua_new_guinea",1730],["flag-ph",1731],["philippines",1731],["flag-pk",1732],["pakistan",1732],["pk",1732],["flag-pl",1733],["poland",1733],["flag-pm",1734],["st_pierre_miquelon",1734],["flag-pn",1735],["pitcairn_islands",1735],["flag-pr",1736],["puerto_rico",1736],["flag-ps",1737],["palestinian_territories",1737],["flag-pt",1738],["portugal",1738],["flag-pw",1739],["palau",1739],["flag-py",1740],["paraguay",1740],["flag-qa",1741],["qatar",1741],["flag-re",1742],["reunion",1742],["flag-ro",1743],["romania",1743],["flag-rs",1744],["serbia",1744],["ru",1745],["flag-ru",1745],["flag-rw",1746],["rwanda",1746],["flag-sa",1747],["saudi_arabia",1747],["flag-sb",1748],["solomon_islands",1748],["flag-sc",1749],["seychelles",1749],["flag-sd",1750],["sudan",1750],["flag-se",1751],["sweden",1751],["flag-sg",1752],["singapore",1752],["flag-sh",1753],["st_helena",1753],["flag-si",1754],["slovenia",1754],["flag-sj",1755],["flag-sk",1756],["slovakia",1756],["flag-sl",1757],["sierra_leone",1757],["flag-sm",1758],["san_marino",1758],["flag-sn",1759],["senegal",1759],["flag-so",1760],["somalia",1760],["flag-sr",1761],["suriname",1761],["flag-ss",1762],["south_sudan",1762],["flag-st",1763],["sao_tome_principe",1763],["flag-sv",1764],["el_salvador",1764],["flag-sx",1765],["sint_maarten",1765],["flag-sy",1766],["syria",1766],["flag-sz",1767],["swaziland",1767],["flag-ta",1768],["flag-tc",1769],["turks_caicos_islands",1769],["flag-td",1770],["chad",1770],["flag-tf",1771],["french_southern_territories",1771],["flag-tg",1772],["togo",1772],["flag-th",1773],["thailand",1773],["flag-tj",1774],["tajikistan",1774],["flag-tk",1775],["tokelau",1775],["flag-tl",1776],["timor_leste",1776],["flag-tm",1777],["turkmenistan",1777],["flag-tn",1778],["tunisia",1778],["flag-to",1779],["tonga",1779],["flag-tr",1780],["tr",1780],["flag-tt",1781],["trinidad_tobago",1781],["flag-tv",1782],["tuvalu",1782],["flag-tw",1783],["taiwan",1783],["flag-tz",1784],["tanzania",1784],["flag-ua",1785],["ukraine",1785],["flag-ug",1786],["uganda",1786],["flag-um",1787],["flag-un",1788],["us",1789],["flag-us",1789],["flag-uy",1790],["uruguay",1790],["flag-uz",1791],["uzbekistan",1791],["flag-va",1792],["vatican_city",1792],["flag-vc",1793],["st_vincent_grenadines",1793],["flag-ve",1794],["venezuela",1794],["flag-vg",1795],["british_virgin_islands",1795],["flag-vi",1796],["us_virgin_islands",1796],["flag-vn",1797],["vietnam",1797],["flag-vu",1798],["vanuatu",1798],["flag-wf",1799],["wallis_futuna",1799],["flag-ws",1800],["samoa",1800],["flag-xk",1801],["kosovo",1801],["flag-ye",1802],["yemen",1802],["flag-yt",1803],["mayotte",1803],["flag-za",1804],["south_africa",1804],["za",1804],["flag-zm",1805],["zambia",1805],["flag-zw",1806],["zimbabwe",1806],["flag-england",1807],["flag-scotland",1808],["flag-wales",1809],["santa_light_skin_tone",1810],["santa_medium_light_skin_tone",1811],["santa_medium_skin_tone",1812],["santa_medium_dark_skin_tone",1813],["santa_dark_skin_tone",1814],["snowboarder_light_skin_tone",1815],["snowboarder_medium_light_skin_tone",1816],["snowboarder_medium_skin_tone",1817],["snowboarder_medium_dark_skin_tone",1818],["snowboarder_dark_skin_tone",1819],["woman-running_light_skin_tone",1820],["running_woman_light_skin_tone",1820],["woman-running_medium_light_skin_tone",1821],["running_woman_medium_light_skin_tone",1821],["woman-running_medium_skin_tone",1822],["running_woman_medium_skin_tone",1822],["woman-running_medium_dark_skin_tone",1823],["running_woman_medium_dark_skin_tone",1823],["woman-running_dark_skin_tone",1824],["running_woman_dark_skin_tone",1824],["man-running_light_skin_tone",1825],["running_man_light_skin_tone",1825],["man-running_medium_light_skin_tone",1826],["running_man_medium_light_skin_tone",1826],["man-running_medium_skin_tone",1827],["running_man_medium_skin_tone",1827],["man-running_medium_dark_skin_tone",1828],["running_man_medium_dark_skin_tone",1828],["man-running_dark_skin_tone",1829],["running_man_dark_skin_tone",1829],["runner_light_skin_tone",1830],["running_light_skin_tone",1830],["runner_medium_light_skin_tone",1831],["running_medium_light_skin_tone",1831],["runner_medium_skin_tone",1832],["running_medium_skin_tone",1832],["runner_medium_dark_skin_tone",1833],["running_medium_dark_skin_tone",1833],["runner_dark_skin_tone",1834],["running_dark_skin_tone",1834],["woman-surfing_light_skin_tone",1835],["surfing_woman_light_skin_tone",1835],["woman-surfing_medium_light_skin_tone",1836],["surfing_woman_medium_light_skin_tone",1836],["woman-surfing_medium_skin_tone",1837],["surfing_woman_medium_skin_tone",1837],["woman-surfing_medium_dark_skin_tone",1838],["surfing_woman_medium_dark_skin_tone",1838],["woman-surfing_dark_skin_tone",1839],["surfing_woman_dark_skin_tone",1839],["man-surfing_light_skin_tone",1840],["surfing_man_light_skin_tone",1840],["man-surfing_medium_light_skin_tone",1841],["surfing_man_medium_light_skin_tone",1841],["man-surfing_medium_skin_tone",1842],["surfing_man_medium_skin_tone",1842],["man-surfing_medium_dark_skin_tone",1843],["surfing_man_medium_dark_skin_tone",1843],["man-surfing_dark_skin_tone",1844],["surfing_man_dark_skin_tone",1844],["surfer_light_skin_tone",1845],["surfer_medium_light_skin_tone",1846],["surfer_medium_skin_tone",1847],["surfer_medium_dark_skin_tone",1848],["surfer_dark_skin_tone",1849],["horse_racing_light_skin_tone",1850],["horse_racing_medium_light_skin_tone",1851],["horse_racing_medium_skin_tone",1852],["horse_racing_medium_dark_skin_tone",1853],["horse_racing_dark_skin_tone",1854],["woman-swimming_light_skin_tone",1855],["swimming_woman_light_skin_tone",1855],["woman-swimming_medium_light_skin_tone",1856],["swimming_woman_medium_light_skin_tone",1856],["woman-swimming_medium_skin_tone",1857],["swimming_woman_medium_skin_tone",1857],["woman-swimming_medium_dark_skin_tone",1858],["swimming_woman_medium_dark_skin_tone",1858],["woman-swimming_dark_skin_tone",1859],["swimming_woman_dark_skin_tone",1859],["man-swimming_light_skin_tone",1860],["swimming_man_light_skin_tone",1860],["man-swimming_medium_light_skin_tone",1861],["swimming_man_medium_light_skin_tone",1861],["man-swimming_medium_skin_tone",1862],["swimming_man_medium_skin_tone",1862],["man-swimming_medium_dark_skin_tone",1863],["swimming_man_medium_dark_skin_tone",1863],["man-swimming_dark_skin_tone",1864],["swimming_man_dark_skin_tone",1864],["swimmer_light_skin_tone",1865],["swimmer_medium_light_skin_tone",1866],["swimmer_medium_skin_tone",1867],["swimmer_medium_dark_skin_tone",1868],["swimmer_dark_skin_tone",1869],["woman-lifting-weights_light_skin_tone",1870],["weight_lifting_woman_light_skin_tone",1870],["woman-lifting-weights_medium_light_skin_tone",1871],["weight_lifting_woman_medium_light_skin_tone",1871],["woman-lifting-weights_medium_skin_tone",1872],["weight_lifting_woman_medium_skin_tone",1872],["woman-lifting-weights_medium_dark_skin_tone",1873],["weight_lifting_woman_medium_dark_skin_tone",1873],["woman-lifting-weights_dark_skin_tone",1874],["weight_lifting_woman_dark_skin_tone",1874],["man-lifting-weights_light_skin_tone",1875],["weight_lifting_man_light_skin_tone",1875],["man-lifting-weights_medium_light_skin_tone",1876],["weight_lifting_man_medium_light_skin_tone",1876],["man-lifting-weights_medium_skin_tone",1877],["weight_lifting_man_medium_skin_tone",1877],["man-lifting-weights_medium_dark_skin_tone",1878],["weight_lifting_man_medium_dark_skin_tone",1878],["man-lifting-weights_dark_skin_tone",1879],["weight_lifting_man_dark_skin_tone",1879],["weight_lifter_light_skin_tone",1880],["weight_lifter_medium_light_skin_tone",1881],["weight_lifter_medium_skin_tone",1882],["weight_lifter_medium_dark_skin_tone",1883],["weight_lifter_dark_skin_tone",1884],["woman-golfing_light_skin_tone",1885],["golfing_woman_light_skin_tone",1885],["woman-golfing_medium_light_skin_tone",1886],["golfing_woman_medium_light_skin_tone",1886],["woman-golfing_medium_skin_tone",1887],["golfing_woman_medium_skin_tone",1887],["woman-golfing_medium_dark_skin_tone",1888],["golfing_woman_medium_dark_skin_tone",1888],["woman-golfing_dark_skin_tone",1889],["golfing_woman_dark_skin_tone",1889],["man-golfing_light_skin_tone",1890],["golfing_man_light_skin_tone",1890],["man-golfing_medium_light_skin_tone",1891],["golfing_man_medium_light_skin_tone",1891],["man-golfing_medium_skin_tone",1892],["golfing_man_medium_skin_tone",1892],["man-golfing_medium_dark_skin_tone",1893],["golfing_man_medium_dark_skin_tone",1893],["man-golfing_dark_skin_tone",1894],["golfing_man_dark_skin_tone",1894],["golfer_light_skin_tone",1895],["golfer_medium_light_skin_tone",1896],["golfer_medium_skin_tone",1897],["golfer_medium_dark_skin_tone",1898],["golfer_dark_skin_tone",1899],["ear_light_skin_tone",1900],["ear_medium_light_skin_tone",1901],["ear_medium_skin_tone",1902],["ear_medium_dark_skin_tone",1903],["ear_dark_skin_tone",1904],["nose_light_skin_tone",1905],["nose_medium_light_skin_tone",1906],["nose_medium_skin_tone",1907],["nose_medium_dark_skin_tone",1908],["nose_dark_skin_tone",1909],["point_up_2_light_skin_tone",1910],["point_up_2_medium_light_skin_tone",1911],["point_up_2_medium_skin_tone",1912],["point_up_2_medium_dark_skin_tone",1913],["point_up_2_dark_skin_tone",1914],["point_down_light_skin_tone",1915],["point_down_medium_light_skin_tone",1916],["point_down_medium_skin_tone",1917],["point_down_medium_dark_skin_tone",1918],["point_down_dark_skin_tone",1919],["point_left_light_skin_tone",1920],["point_left_medium_light_skin_tone",1921],["point_left_medium_skin_tone",1922],["point_left_medium_dark_skin_tone",1923],["point_left_dark_skin_tone",1924],["point_right_light_skin_tone",1925],["point_right_medium_light_skin_tone",1926],["point_right_medium_skin_tone",1927],["point_right_medium_dark_skin_tone",1928],["point_right_dark_skin_tone",1929],["facepunch_light_skin_tone",1930],["punch_light_skin_tone",1930],["fist_oncoming_light_skin_tone",1930],["facepunch_medium_light_skin_tone",1931],["punch_medium_light_skin_tone",1931],["fist_oncoming_medium_light_skin_tone",1931],["facepunch_medium_skin_tone",1932],["punch_medium_skin_tone",1932],["fist_oncoming_medium_skin_tone",1932],["facepunch_medium_dark_skin_tone",1933],["punch_medium_dark_skin_tone",1933],["fist_oncoming_medium_dark_skin_tone",1933],["facepunch_dark_skin_tone",1934],["punch_dark_skin_tone",1934],["fist_oncoming_dark_skin_tone",1934],["wave_light_skin_tone",1935],["wave_medium_light_skin_tone",1936],["wave_medium_skin_tone",1937],["wave_medium_dark_skin_tone",1938],["wave_dark_skin_tone",1939],["ok_hand_light_skin_tone",1940],["ok_hand_medium_light_skin_tone",1941],["ok_hand_medium_skin_tone",1942],["ok_hand_medium_dark_skin_tone",1943],["ok_hand_dark_skin_tone",1944],["+1_light_skin_tone",1945],["thumbsup_light_skin_tone",1945],["+1_medium_light_skin_tone",1946],["thumbsup_medium_light_skin_tone",1946],["+1_medium_skin_tone",1947],["thumbsup_medium_skin_tone",1947],["+1_medium_dark_skin_tone",1948],["thumbsup_medium_dark_skin_tone",1948],["+1_dark_skin_tone",1949],["thumbsup_dark_skin_tone",1949],["-1_light_skin_tone",1950],["thumbsdown_light_skin_tone",1950],["-1_medium_light_skin_tone",1951],["thumbsdown_medium_light_skin_tone",1951],["-1_medium_skin_tone",1952],["thumbsdown_medium_skin_tone",1952],["-1_medium_dark_skin_tone",1953],["thumbsdown_medium_dark_skin_tone",1953],["-1_dark_skin_tone",1954],["thumbsdown_dark_skin_tone",1954],["clap_light_skin_tone",1955],["clap_medium_light_skin_tone",1956],["clap_medium_skin_tone",1957],["clap_medium_dark_skin_tone",1958],["clap_dark_skin_tone",1959],["open_hands_light_skin_tone",1960],["open_hands_medium_light_skin_tone",1961],["open_hands_medium_skin_tone",1962],["open_hands_medium_dark_skin_tone",1963],["open_hands_dark_skin_tone",1964],["boy_light_skin_tone",1965],["boy_medium_light_skin_tone",1966],["boy_medium_skin_tone",1967],["boy_medium_dark_skin_tone",1968],["boy_dark_skin_tone",1969],["girl_light_skin_tone",1970],["girl_medium_light_skin_tone",1971],["girl_medium_skin_tone",1972],["girl_medium_dark_skin_tone",1973],["girl_dark_skin_tone",1974],["male-farmer_light_skin_tone",1975],["man_farmer_light_skin_tone",1975],["male-farmer_medium_light_skin_tone",1976],["man_farmer_medium_light_skin_tone",1976],["male-farmer_medium_skin_tone",1977],["man_farmer_medium_skin_tone",1977],["male-farmer_medium_dark_skin_tone",1978],["man_farmer_medium_dark_skin_tone",1978],["male-farmer_dark_skin_tone",1979],["man_farmer_dark_skin_tone",1979],["male-cook_light_skin_tone",1980],["man_cook_light_skin_tone",1980],["male-cook_medium_light_skin_tone",1981],["man_cook_medium_light_skin_tone",1981],["male-cook_medium_skin_tone",1982],["man_cook_medium_skin_tone",1982],["male-cook_medium_dark_skin_tone",1983],["man_cook_medium_dark_skin_tone",1983],["male-cook_dark_skin_tone",1984],["man_cook_dark_skin_tone",1984],["man_feeding_baby_light_skin_tone",1985],["man_feeding_baby_medium_light_skin_tone",1986],["man_feeding_baby_medium_skin_tone",1987],["man_feeding_baby_medium_dark_skin_tone",1988],["man_feeding_baby_dark_skin_tone",1989],["male-student_light_skin_tone",1990],["man_student_light_skin_tone",1990],["male-student_medium_light_skin_tone",1991],["man_student_medium_light_skin_tone",1991],["male-student_medium_skin_tone",1992],["man_student_medium_skin_tone",1992],["male-student_medium_dark_skin_tone",1993],["man_student_medium_dark_skin_tone",1993],["male-student_dark_skin_tone",1994],["man_student_dark_skin_tone",1994],["male-singer_light_skin_tone",1995],["man_singer_light_skin_tone",1995],["male-singer_medium_light_skin_tone",1996],["man_singer_medium_light_skin_tone",1996],["male-singer_medium_skin_tone",1997],["man_singer_medium_skin_tone",1997],["male-singer_medium_dark_skin_tone",1998],["man_singer_medium_dark_skin_tone",1998],["male-singer_dark_skin_tone",1999],["man_singer_dark_skin_tone",1999],["male-artist_light_skin_tone",2000],["man_artist_light_skin_tone",2000],["male-artist_medium_light_skin_tone",2001],["man_artist_medium_light_skin_tone",2001],["male-artist_medium_skin_tone",2002],["man_artist_medium_skin_tone",2002],["male-artist_medium_dark_skin_tone",2003],["man_artist_medium_dark_skin_tone",2003],["male-artist_dark_skin_tone",2004],["man_artist_dark_skin_tone",2004],["male-teacher_light_skin_tone",2005],["man_teacher_light_skin_tone",2005],["male-teacher_medium_light_skin_tone",2006],["man_teacher_medium_light_skin_tone",2006],["male-teacher_medium_skin_tone",2007],["man_teacher_medium_skin_tone",2007],["male-teacher_medium_dark_skin_tone",2008],["man_teacher_medium_dark_skin_tone",2008],["male-teacher_dark_skin_tone",2009],["man_teacher_dark_skin_tone",2009],["male-factory-worker_light_skin_tone",2010],["man_factory_worker_light_skin_tone",2010],["male-factory-worker_medium_light_skin_tone",2011],["man_factory_worker_medium_light_skin_tone",2011],["male-factory-worker_medium_skin_tone",2012],["man_factory_worker_medium_skin_tone",2012],["male-factory-worker_medium_dark_skin_tone",2013],["man_factory_worker_medium_dark_skin_tone",2013],["male-factory-worker_dark_skin_tone",2014],["man_factory_worker_dark_skin_tone",2014],["male-technologist_light_skin_tone",2015],["man_technologist_light_skin_tone",2015],["male-technologist_medium_light_skin_tone",2016],["man_technologist_medium_light_skin_tone",2016],["male-technologist_medium_skin_tone",2017],["man_technologist_medium_skin_tone",2017],["male-technologist_medium_dark_skin_tone",2018],["man_technologist_medium_dark_skin_tone",2018],["male-technologist_dark_skin_tone",2019],["man_technologist_dark_skin_tone",2019],["male-office-worker_light_skin_tone",2020],["man_office_worker_light_skin_tone",2020],["male-office-worker_medium_light_skin_tone",2021],["man_office_worker_medium_light_skin_tone",2021],["male-office-worker_medium_skin_tone",2022],["man_office_worker_medium_skin_tone",2022],["male-office-worker_medium_dark_skin_tone",2023],["man_office_worker_medium_dark_skin_tone",2023],["male-office-worker_dark_skin_tone",2024],["man_office_worker_dark_skin_tone",2024],["male-mechanic_light_skin_tone",2025],["man_mechanic_light_skin_tone",2025],["male-mechanic_medium_light_skin_tone",2026],["man_mechanic_medium_light_skin_tone",2026],["male-mechanic_medium_skin_tone",2027],["man_mechanic_medium_skin_tone",2027],["male-mechanic_medium_dark_skin_tone",2028],["man_mechanic_medium_dark_skin_tone",2028],["male-mechanic_dark_skin_tone",2029],["man_mechanic_dark_skin_tone",2029],["male-scientist_light_skin_tone",2030],["man_scientist_light_skin_tone",2030],["male-scientist_medium_light_skin_tone",2031],["man_scientist_medium_light_skin_tone",2031],["male-scientist_medium_skin_tone",2032],["man_scientist_medium_skin_tone",2032],["male-scientist_medium_dark_skin_tone",2033],["man_scientist_medium_dark_skin_tone",2033],["male-scientist_dark_skin_tone",2034],["man_scientist_dark_skin_tone",2034],["male-astronaut_light_skin_tone",2035],["man_astronaut_light_skin_tone",2035],["male-astronaut_medium_light_skin_tone",2036],["man_astronaut_medium_light_skin_tone",2036],["male-astronaut_medium_skin_tone",2037],["man_astronaut_medium_skin_tone",2037],["male-astronaut_medium_dark_skin_tone",2038],["man_astronaut_medium_dark_skin_tone",2038],["male-astronaut_dark_skin_tone",2039],["man_astronaut_dark_skin_tone",2039],["male-firefighter_light_skin_tone",2040],["man_firefighter_light_skin_tone",2040],["male-firefighter_medium_light_skin_tone",2041],["man_firefighter_medium_light_skin_tone",2041],["male-firefighter_medium_skin_tone",2042],["man_firefighter_medium_skin_tone",2042],["male-firefighter_medium_dark_skin_tone",2043],["man_firefighter_medium_dark_skin_tone",2043],["male-firefighter_dark_skin_tone",2044],["man_firefighter_dark_skin_tone",2044],["man_with_probing_cane_light_skin_tone",2045],["man_with_probing_cane_medium_light_skin_tone",2046],["man_with_probing_cane_medium_skin_tone",2047],["man_with_probing_cane_medium_dark_skin_tone",2048],["man_with_probing_cane_dark_skin_tone",2049],["red_haired_man_light_skin_tone",2050],["red_haired_man_medium_light_skin_tone",2051],["red_haired_man_medium_skin_tone",2052],["red_haired_man_medium_dark_skin_tone",2053],["red_haired_man_dark_skin_tone",2054],["curly_haired_man_light_skin_tone",2055],["curly_haired_man_medium_light_skin_tone",2056],["curly_haired_man_medium_skin_tone",2057],["curly_haired_man_medium_dark_skin_tone",2058],["curly_haired_man_dark_skin_tone",2059],["bald_man_light_skin_tone",2060],["bald_man_medium_light_skin_tone",2061],["bald_man_medium_skin_tone",2062],["bald_man_medium_dark_skin_tone",2063],["bald_man_dark_skin_tone",2064],["white_haired_man_light_skin_tone",2065],["white_haired_man_medium_light_skin_tone",2066],["white_haired_man_medium_skin_tone",2067],["white_haired_man_medium_dark_skin_tone",2068],["white_haired_man_dark_skin_tone",2069],["man_in_motorized_wheelchair_light_skin_tone",2070],["man_in_motorized_wheelchair_medium_light_skin_tone",2071],["man_in_motorized_wheelchair_medium_skin_tone",2072],["man_in_motorized_wheelchair_medium_dark_skin_tone",2073],["man_in_motorized_wheelchair_dark_skin_tone",2074],["man_in_manual_wheelchair_light_skin_tone",2075],["man_in_manual_wheelchair_medium_light_skin_tone",2076],["man_in_manual_wheelchair_medium_skin_tone",2077],["man_in_manual_wheelchair_medium_dark_skin_tone",2078],["man_in_manual_wheelchair_dark_skin_tone",2079],["male-doctor_light_skin_tone",2080],["man_health_worker_light_skin_tone",2080],["male-doctor_medium_light_skin_tone",2081],["man_health_worker_medium_light_skin_tone",2081],["male-doctor_medium_skin_tone",2082],["man_health_worker_medium_skin_tone",2082],["male-doctor_medium_dark_skin_tone",2083],["man_health_worker_medium_dark_skin_tone",2083],["male-doctor_dark_skin_tone",2084],["man_health_worker_dark_skin_tone",2084],["male-judge_light_skin_tone",2085],["man_judge_light_skin_tone",2085],["male-judge_medium_light_skin_tone",2086],["man_judge_medium_light_skin_tone",2086],["male-judge_medium_skin_tone",2087],["man_judge_medium_skin_tone",2087],["male-judge_medium_dark_skin_tone",2088],["man_judge_medium_dark_skin_tone",2088],["male-judge_dark_skin_tone",2089],["man_judge_dark_skin_tone",2089],["male-pilot_light_skin_tone",2090],["man_pilot_light_skin_tone",2090],["male-pilot_medium_light_skin_tone",2091],["man_pilot_medium_light_skin_tone",2091],["male-pilot_medium_skin_tone",2092],["man_pilot_medium_skin_tone",2092],["male-pilot_medium_dark_skin_tone",2093],["man_pilot_medium_dark_skin_tone",2093],["male-pilot_dark_skin_tone",2094],["man_pilot_dark_skin_tone",2094],["man_light_skin_tone",2095],["man_medium_light_skin_tone",2096],["man_medium_skin_tone",2097],["man_medium_dark_skin_tone",2098],["man_dark_skin_tone",2099],["female-farmer_light_skin_tone",2100],["woman_farmer_light_skin_tone",2100],["female-farmer_medium_light_skin_tone",2101],["woman_farmer_medium_light_skin_tone",2101],["female-farmer_medium_skin_tone",2102],["woman_farmer_medium_skin_tone",2102],["female-farmer_medium_dark_skin_tone",2103],["woman_farmer_medium_dark_skin_tone",2103],["female-farmer_dark_skin_tone",2104],["woman_farmer_dark_skin_tone",2104],["female-cook_light_skin_tone",2105],["woman_cook_light_skin_tone",2105],["female-cook_medium_light_skin_tone",2106],["woman_cook_medium_light_skin_tone",2106],["female-cook_medium_skin_tone",2107],["woman_cook_medium_skin_tone",2107],["female-cook_medium_dark_skin_tone",2108],["woman_cook_medium_dark_skin_tone",2108],["female-cook_dark_skin_tone",2109],["woman_cook_dark_skin_tone",2109],["woman_feeding_baby_light_skin_tone",2110],["woman_feeding_baby_medium_light_skin_tone",2111],["woman_feeding_baby_medium_skin_tone",2112],["woman_feeding_baby_medium_dark_skin_tone",2113],["woman_feeding_baby_dark_skin_tone",2114],["female-student_light_skin_tone",2115],["woman_student_light_skin_tone",2115],["female-student_medium_light_skin_tone",2116],["woman_student_medium_light_skin_tone",2116],["female-student_medium_skin_tone",2117],["woman_student_medium_skin_tone",2117],["female-student_medium_dark_skin_tone",2118],["woman_student_medium_dark_skin_tone",2118],["female-student_dark_skin_tone",2119],["woman_student_dark_skin_tone",2119],["female-singer_light_skin_tone",2120],["woman_singer_light_skin_tone",2120],["female-singer_medium_light_skin_tone",2121],["woman_singer_medium_light_skin_tone",2121],["female-singer_medium_skin_tone",2122],["woman_singer_medium_skin_tone",2122],["female-singer_medium_dark_skin_tone",2123],["woman_singer_medium_dark_skin_tone",2123],["female-singer_dark_skin_tone",2124],["woman_singer_dark_skin_tone",2124],["female-artist_light_skin_tone",2125],["woman_artist_light_skin_tone",2125],["female-artist_medium_light_skin_tone",2126],["woman_artist_medium_light_skin_tone",2126],["female-artist_medium_skin_tone",2127],["woman_artist_medium_skin_tone",2127],["female-artist_medium_dark_skin_tone",2128],["woman_artist_medium_dark_skin_tone",2128],["female-artist_dark_skin_tone",2129],["woman_artist_dark_skin_tone",2129],["female-teacher_light_skin_tone",2130],["woman_teacher_light_skin_tone",2130],["female-teacher_medium_light_skin_tone",2131],["woman_teacher_medium_light_skin_tone",2131],["female-teacher_medium_skin_tone",2132],["woman_teacher_medium_skin_tone",2132],["female-teacher_medium_dark_skin_tone",2133],["woman_teacher_medium_dark_skin_tone",2133],["female-teacher_dark_skin_tone",2134],["woman_teacher_dark_skin_tone",2134],["female-factory-worker_light_skin_tone",2135],["woman_factory_worker_light_skin_tone",2135],["female-factory-worker_medium_light_skin_tone",2136],["woman_factory_worker_medium_light_skin_tone",2136],["female-factory-worker_medium_skin_tone",2137],["woman_factory_worker_medium_skin_tone",2137],["female-factory-worker_medium_dark_skin_tone",2138],["woman_factory_worker_medium_dark_skin_tone",2138],["female-factory-worker_dark_skin_tone",2139],["woman_factory_worker_dark_skin_tone",2139],["female-technologist_light_skin_tone",2140],["woman_technologist_light_skin_tone",2140],["female-technologist_medium_light_skin_tone",2141],["woman_technologist_medium_light_skin_tone",2141],["female-technologist_medium_skin_tone",2142],["woman_technologist_medium_skin_tone",2142],["female-technologist_medium_dark_skin_tone",2143],["woman_technologist_medium_dark_skin_tone",2143],["female-technologist_dark_skin_tone",2144],["woman_technologist_dark_skin_tone",2144],["female-office-worker_light_skin_tone",2145],["woman_office_worker_light_skin_tone",2145],["female-office-worker_medium_light_skin_tone",2146],["woman_office_worker_medium_light_skin_tone",2146],["female-office-worker_medium_skin_tone",2147],["woman_office_worker_medium_skin_tone",2147],["female-office-worker_medium_dark_skin_tone",2148],["woman_office_worker_medium_dark_skin_tone",2148],["female-office-worker_dark_skin_tone",2149],["woman_office_worker_dark_skin_tone",2149],["female-mechanic_light_skin_tone",2150],["woman_mechanic_light_skin_tone",2150],["female-mechanic_medium_light_skin_tone",2151],["woman_mechanic_medium_light_skin_tone",2151],["female-mechanic_medium_skin_tone",2152],["woman_mechanic_medium_skin_tone",2152],["female-mechanic_medium_dark_skin_tone",2153],["woman_mechanic_medium_dark_skin_tone",2153],["female-mechanic_dark_skin_tone",2154],["woman_mechanic_dark_skin_tone",2154],["female-scientist_light_skin_tone",2155],["woman_scientist_light_skin_tone",2155],["female-scientist_medium_light_skin_tone",2156],["woman_scientist_medium_light_skin_tone",2156],["female-scientist_medium_skin_tone",2157],["woman_scientist_medium_skin_tone",2157],["female-scientist_medium_dark_skin_tone",2158],["woman_scientist_medium_dark_skin_tone",2158],["female-scientist_dark_skin_tone",2159],["woman_scientist_dark_skin_tone",2159],["female-astronaut_light_skin_tone",2160],["woman_astronaut_light_skin_tone",2160],["female-astronaut_medium_light_skin_tone",2161],["woman_astronaut_medium_light_skin_tone",2161],["female-astronaut_medium_skin_tone",2162],["woman_astronaut_medium_skin_tone",2162],["female-astronaut_medium_dark_skin_tone",2163],["woman_astronaut_medium_dark_skin_tone",2163],["female-astronaut_dark_skin_tone",2164],["woman_astronaut_dark_skin_tone",2164],["female-firefighter_light_skin_tone",2165],["woman_firefighter_light_skin_tone",2165],["female-firefighter_medium_light_skin_tone",2166],["woman_firefighter_medium_light_skin_tone",2166],["female-firefighter_medium_skin_tone",2167],["woman_firefighter_medium_skin_tone",2167],["female-firefighter_medium_dark_skin_tone",2168],["woman_firefighter_medium_dark_skin_tone",2168],["female-firefighter_dark_skin_tone",2169],["woman_firefighter_dark_skin_tone",2169],["woman_with_probing_cane_light_skin_tone",2170],["woman_with_probing_cane_medium_light_skin_tone",2171],["woman_with_probing_cane_medium_skin_tone",2172],["woman_with_probing_cane_medium_dark_skin_tone",2173],["woman_with_probing_cane_dark_skin_tone",2174],["red_haired_woman_light_skin_tone",2175],["red_haired_woman_medium_light_skin_tone",2176],["red_haired_woman_medium_skin_tone",2177],["red_haired_woman_medium_dark_skin_tone",2178],["red_haired_woman_dark_skin_tone",2179],["curly_haired_woman_light_skin_tone",2180],["curly_haired_woman_medium_light_skin_tone",2181],["curly_haired_woman_medium_skin_tone",2182],["curly_haired_woman_medium_dark_skin_tone",2183],["curly_haired_woman_dark_skin_tone",2184],["bald_woman_light_skin_tone",2185],["bald_woman_medium_light_skin_tone",2186],["bald_woman_medium_skin_tone",2187],["bald_woman_medium_dark_skin_tone",2188],["bald_woman_dark_skin_tone",2189],["white_haired_woman_light_skin_tone",2190],["white_haired_woman_medium_light_skin_tone",2191],["white_haired_woman_medium_skin_tone",2192],["white_haired_woman_medium_dark_skin_tone",2193],["white_haired_woman_dark_skin_tone",2194],["woman_in_motorized_wheelchair_light_skin_tone",2195],["woman_in_motorized_wheelchair_medium_light_skin_tone",2196],["woman_in_motorized_wheelchair_medium_skin_tone",2197],["woman_in_motorized_wheelchair_medium_dark_skin_tone",2198],["woman_in_motorized_wheelchair_dark_skin_tone",2199],["woman_in_manual_wheelchair_light_skin_tone",2200],["woman_in_manual_wheelchair_medium_light_skin_tone",2201],["woman_in_manual_wheelchair_medium_skin_tone",2202],["woman_in_manual_wheelchair_medium_dark_skin_tone",2203],["woman_in_manual_wheelchair_dark_skin_tone",2204],["female-doctor_light_skin_tone",2205],["woman_health_worker_light_skin_tone",2205],["female-doctor_medium_light_skin_tone",2206],["woman_health_worker_medium_light_skin_tone",2206],["female-doctor_medium_skin_tone",2207],["woman_health_worker_medium_skin_tone",2207],["female-doctor_medium_dark_skin_tone",2208],["woman_health_worker_medium_dark_skin_tone",2208],["female-doctor_dark_skin_tone",2209],["woman_health_worker_dark_skin_tone",2209],["female-judge_light_skin_tone",2210],["woman_judge_light_skin_tone",2210],["female-judge_medium_light_skin_tone",2211],["woman_judge_medium_light_skin_tone",2211],["female-judge_medium_skin_tone",2212],["woman_judge_medium_skin_tone",2212],["female-judge_medium_dark_skin_tone",2213],["woman_judge_medium_dark_skin_tone",2213],["female-judge_dark_skin_tone",2214],["woman_judge_dark_skin_tone",2214],["female-pilot_light_skin_tone",2215],["woman_pilot_light_skin_tone",2215],["female-pilot_medium_light_skin_tone",2216],["woman_pilot_medium_light_skin_tone",2216],["female-pilot_medium_skin_tone",2217],["woman_pilot_medium_skin_tone",2217],["female-pilot_medium_dark_skin_tone",2218],["woman_pilot_medium_dark_skin_tone",2218],["female-pilot_dark_skin_tone",2219],["woman_pilot_dark_skin_tone",2219],["woman_light_skin_tone",2220],["woman_medium_light_skin_tone",2221],["woman_medium_skin_tone",2222],["woman_medium_dark_skin_tone",2223],["woman_dark_skin_tone",2224],["man_and_woman_holding_hands_light_skin_tone",2225],["woman_and_man_holding_hands_light_skin_tone",2225],["couple_light_skin_tone",2225],["man_and_woman_holding_hands_medium_light_skin_tone",2226],["woman_and_man_holding_hands_medium_light_skin_tone",2226],["couple_medium_light_skin_tone",2226],["man_and_woman_holding_hands_medium_skin_tone",2227],["woman_and_man_holding_hands_medium_skin_tone",2227],["couple_medium_skin_tone",2227],["man_and_woman_holding_hands_medium_dark_skin_tone",2228],["woman_and_man_holding_hands_medium_dark_skin_tone",2228],["couple_medium_dark_skin_tone",2228],["man_and_woman_holding_hands_dark_skin_tone",2229],["woman_and_man_holding_hands_dark_skin_tone",2229],["couple_dark_skin_tone",2229],["man_and_woman_holding_hands_light_skin_tone_medium_light_skin_tone",2230],["woman_and_man_holding_hands_light_skin_tone_medium_light_skin_tone",2230],["couple_light_skin_tone_medium_light_skin_tone",2230],["man_and_woman_holding_hands_light_skin_tone_medium_skin_tone",2231],["woman_and_man_holding_hands_light_skin_tone_medium_skin_tone",2231],["couple_light_skin_tone_medium_skin_tone",2231],["man_and_woman_holding_hands_light_skin_tone_medium_dark_skin_tone",2232],["woman_and_man_holding_hands_light_skin_tone_medium_dark_skin_tone",2232],["couple_light_skin_tone_medium_dark_skin_tone",2232],["man_and_woman_holding_hands_light_skin_tone_dark_skin_tone",2233],["woman_and_man_holding_hands_light_skin_tone_dark_skin_tone",2233],["couple_light_skin_tone_dark_skin_tone",2233],["man_and_woman_holding_hands_medium_light_skin_tone_light_skin_tone",2234],["woman_and_man_holding_hands_medium_light_skin_tone_light_skin_tone",2234],["couple_medium_light_skin_tone_light_skin_tone",2234],["man_and_woman_holding_hands_medium_light_skin_tone_medium_skin_tone",2235],["woman_and_man_holding_hands_medium_light_skin_tone_medium_skin_tone",2235],["couple_medium_light_skin_tone_medium_skin_tone",2235],["man_and_woman_holding_hands_medium_light_skin_tone_medium_dark_skin_tone",2236],["woman_and_man_holding_hands_medium_light_skin_tone_medium_dark_skin_tone",2236],["couple_medium_light_skin_tone_medium_dark_skin_tone",2236],["man_and_woman_holding_hands_medium_light_skin_tone_dark_skin_tone",2237],["woman_and_man_holding_hands_medium_light_skin_tone_dark_skin_tone",2237],["couple_medium_light_skin_tone_dark_skin_tone",2237],["man_and_woman_holding_hands_medium_skin_tone_light_skin_tone",2238],["woman_and_man_holding_hands_medium_skin_tone_light_skin_tone",2238],["couple_medium_skin_tone_light_skin_tone",2238],["man_and_woman_holding_hands_medium_skin_tone_medium_light_skin_tone",2239],["woman_and_man_holding_hands_medium_skin_tone_medium_light_skin_tone",2239],["couple_medium_skin_tone_medium_light_skin_tone",2239],["man_and_woman_holding_hands_medium_skin_tone_medium_dark_skin_tone",2240],["woman_and_man_holding_hands_medium_skin_tone_medium_dark_skin_tone",2240],["couple_medium_skin_tone_medium_dark_skin_tone",2240],["man_and_woman_holding_hands_medium_skin_tone_dark_skin_tone",2241],["woman_and_man_holding_hands_medium_skin_tone_dark_skin_tone",2241],["couple_medium_skin_tone_dark_skin_tone",2241],["man_and_woman_holding_hands_medium_dark_skin_tone_light_skin_tone",2242],["woman_and_man_holding_hands_medium_dark_skin_tone_light_skin_tone",2242],["couple_medium_dark_skin_tone_light_skin_tone",2242],["man_and_woman_holding_hands_medium_dark_skin_tone_medium_light_skin_tone",2243],["woman_and_man_holding_hands_medium_dark_skin_tone_medium_light_skin_tone",2243],["couple_medium_dark_skin_tone_medium_light_skin_tone",2243],["man_and_woman_holding_hands_medium_dark_skin_tone_medium_skin_tone",2244],["woman_and_man_holding_hands_medium_dark_skin_tone_medium_skin_tone",2244],["couple_medium_dark_skin_tone_medium_skin_tone",2244],["man_and_woman_holding_hands_medium_dark_skin_tone_dark_skin_tone",2245],["woman_and_man_holding_hands_medium_dark_skin_tone_dark_skin_tone",2245],["couple_medium_dark_skin_tone_dark_skin_tone",2245],["man_and_woman_holding_hands_dark_skin_tone_light_skin_tone",2246],["woman_and_man_holding_hands_dark_skin_tone_light_skin_tone",2246],["couple_dark_skin_tone_light_skin_tone",2246],["man_and_woman_holding_hands_dark_skin_tone_medium_light_skin_tone",2247],["woman_and_man_holding_hands_dark_skin_tone_medium_light_skin_tone",2247],["couple_dark_skin_tone_medium_light_skin_tone",2247],["man_and_woman_holding_hands_dark_skin_tone_medium_skin_tone",2248],["woman_and_man_holding_hands_dark_skin_tone_medium_skin_tone",2248],["couple_dark_skin_tone_medium_skin_tone",2248],["man_and_woman_holding_hands_dark_skin_tone_medium_dark_skin_tone",2249],["woman_and_man_holding_hands_dark_skin_tone_medium_dark_skin_tone",2249],["couple_dark_skin_tone_medium_dark_skin_tone",2249],["two_men_holding_hands_light_skin_tone",2250],["men_holding_hands_light_skin_tone",2250],["two_men_holding_hands_medium_light_skin_tone",2251],["men_holding_hands_medium_light_skin_tone",2251],["two_men_holding_hands_medium_skin_tone",2252],["men_holding_hands_medium_skin_tone",2252],["two_men_holding_hands_medium_dark_skin_tone",2253],["men_holding_hands_medium_dark_skin_tone",2253],["two_men_holding_hands_dark_skin_tone",2254],["men_holding_hands_dark_skin_tone",2254],["two_men_holding_hands_light_skin_tone_medium_light_skin_tone",2255],["men_holding_hands_light_skin_tone_medium_light_skin_tone",2255],["two_men_holding_hands_light_skin_tone_medium_skin_tone",2256],["men_holding_hands_light_skin_tone_medium_skin_tone",2256],["two_men_holding_hands_light_skin_tone_medium_dark_skin_tone",2257],["men_holding_hands_light_skin_tone_medium_dark_skin_tone",2257],["two_men_holding_hands_light_skin_tone_dark_skin_tone",2258],["men_holding_hands_light_skin_tone_dark_skin_tone",2258],["two_men_holding_hands_medium_light_skin_tone_light_skin_tone",2259],["men_holding_hands_medium_light_skin_tone_light_skin_tone",2259],["two_men_holding_hands_medium_light_skin_tone_medium_skin_tone",2260],["men_holding_hands_medium_light_skin_tone_medium_skin_tone",2260],["two_men_holding_hands_medium_light_skin_tone_medium_dark_skin_tone",2261],["men_holding_hands_medium_light_skin_tone_medium_dark_skin_tone",2261],["two_men_holding_hands_medium_light_skin_tone_dark_skin_tone",2262],["men_holding_hands_medium_light_skin_tone_dark_skin_tone",2262],["two_men_holding_hands_medium_skin_tone_light_skin_tone",2263],["men_holding_hands_medium_skin_tone_light_skin_tone",2263],["two_men_holding_hands_medium_skin_tone_medium_light_skin_tone",2264],["men_holding_hands_medium_skin_tone_medium_light_skin_tone",2264],["two_men_holding_hands_medium_skin_tone_medium_dark_skin_tone",2265],["men_holding_hands_medium_skin_tone_medium_dark_skin_tone",2265],["two_men_holding_hands_medium_skin_tone_dark_skin_tone",2266],["men_holding_hands_medium_skin_tone_dark_skin_tone",2266],["two_men_holding_hands_medium_dark_skin_tone_light_skin_tone",2267],["men_holding_hands_medium_dark_skin_tone_light_skin_tone",2267],["two_men_holding_hands_medium_dark_skin_tone_medium_light_skin_tone",2268],["men_holding_hands_medium_dark_skin_tone_medium_light_skin_tone",2268],["two_men_holding_hands_medium_dark_skin_tone_medium_skin_tone",2269],["men_holding_hands_medium_dark_skin_tone_medium_skin_tone",2269],["two_men_holding_hands_medium_dark_skin_tone_dark_skin_tone",2270],["men_holding_hands_medium_dark_skin_tone_dark_skin_tone",2270],["two_men_holding_hands_dark_skin_tone_light_skin_tone",2271],["men_holding_hands_dark_skin_tone_light_skin_tone",2271],["two_men_holding_hands_dark_skin_tone_medium_light_skin_tone",2272],["men_holding_hands_dark_skin_tone_medium_light_skin_tone",2272],["two_men_holding_hands_dark_skin_tone_medium_skin_tone",2273],["men_holding_hands_dark_skin_tone_medium_skin_tone",2273],["two_men_holding_hands_dark_skin_tone_medium_dark_skin_tone",2274],["men_holding_hands_dark_skin_tone_medium_dark_skin_tone",2274],["two_women_holding_hands_light_skin_tone",2275],["women_holding_hands_light_skin_tone",2275],["two_women_holding_hands_medium_light_skin_tone",2276],["women_holding_hands_medium_light_skin_tone",2276],["two_women_holding_hands_medium_skin_tone",2277],["women_holding_hands_medium_skin_tone",2277],["two_women_holding_hands_medium_dark_skin_tone",2278],["women_holding_hands_medium_dark_skin_tone",2278],["two_women_holding_hands_dark_skin_tone",2279],["women_holding_hands_dark_skin_tone",2279],["two_women_holding_hands_light_skin_tone_medium_light_skin_tone",2280],["women_holding_hands_light_skin_tone_medium_light_skin_tone",2280],["two_women_holding_hands_light_skin_tone_medium_skin_tone",2281],["women_holding_hands_light_skin_tone_medium_skin_tone",2281],["two_women_holding_hands_light_skin_tone_medium_dark_skin_tone",2282],["women_holding_hands_light_skin_tone_medium_dark_skin_tone",2282],["two_women_holding_hands_light_skin_tone_dark_skin_tone",2283],["women_holding_hands_light_skin_tone_dark_skin_tone",2283],["two_women_holding_hands_medium_light_skin_tone_light_skin_tone",2284],["women_holding_hands_medium_light_skin_tone_light_skin_tone",2284],["two_women_holding_hands_medium_light_skin_tone_medium_skin_tone",2285],["women_holding_hands_medium_light_skin_tone_medium_skin_tone",2285],["two_women_holding_hands_medium_light_skin_tone_medium_dark_skin_tone",2286],["women_holding_hands_medium_light_skin_tone_medium_dark_skin_tone",2286],["two_women_holding_hands_medium_light_skin_tone_dark_skin_tone",2287],["women_holding_hands_medium_light_skin_tone_dark_skin_tone",2287],["two_women_holding_hands_medium_skin_tone_light_skin_tone",2288],["women_holding_hands_medium_skin_tone_light_skin_tone",2288],["two_women_holding_hands_medium_skin_tone_medium_light_skin_tone",2289],["women_holding_hands_medium_skin_tone_medium_light_skin_tone",2289],["two_women_holding_hands_medium_skin_tone_medium_dark_skin_tone",2290],["women_holding_hands_medium_skin_tone_medium_dark_skin_tone",2290],["two_women_holding_hands_medium_skin_tone_dark_skin_tone",2291],["women_holding_hands_medium_skin_tone_dark_skin_tone",2291],["two_women_holding_hands_medium_dark_skin_tone_light_skin_tone",2292],["women_holding_hands_medium_dark_skin_tone_light_skin_tone",2292],["two_women_holding_hands_medium_dark_skin_tone_medium_light_skin_tone",2293],["women_holding_hands_medium_dark_skin_tone_medium_light_skin_tone",2293],["two_women_holding_hands_medium_dark_skin_tone_medium_skin_tone",2294],["women_holding_hands_medium_dark_skin_tone_medium_skin_tone",2294],["two_women_holding_hands_medium_dark_skin_tone_dark_skin_tone",2295],["women_holding_hands_medium_dark_skin_tone_dark_skin_tone",2295],["two_women_holding_hands_dark_skin_tone_light_skin_tone",2296],["women_holding_hands_dark_skin_tone_light_skin_tone",2296],["two_women_holding_hands_dark_skin_tone_medium_light_skin_tone",2297],["women_holding_hands_dark_skin_tone_medium_light_skin_tone",2297],["two_women_holding_hands_dark_skin_tone_medium_skin_tone",2298],["women_holding_hands_dark_skin_tone_medium_skin_tone",2298],["two_women_holding_hands_dark_skin_tone_medium_dark_skin_tone",2299],["women_holding_hands_dark_skin_tone_medium_dark_skin_tone",2299],["female-police-officer_light_skin_tone",2300],["policewoman_light_skin_tone",2300],["female-police-officer_medium_light_skin_tone",2301],["policewoman_medium_light_skin_tone",2301],["female-police-officer_medium_skin_tone",2302],["policewoman_medium_skin_tone",2302],["female-police-officer_medium_dark_skin_tone",2303],["policewoman_medium_dark_skin_tone",2303],["female-police-officer_dark_skin_tone",2304],["policewoman_dark_skin_tone",2304],["male-police-officer_light_skin_tone",2305],["policeman_light_skin_tone",2305],["male-police-officer_medium_light_skin_tone",2306],["policeman_medium_light_skin_tone",2306],["male-police-officer_medium_skin_tone",2307],["policeman_medium_skin_tone",2307],["male-police-officer_medium_dark_skin_tone",2308],["policeman_medium_dark_skin_tone",2308],["male-police-officer_dark_skin_tone",2309],["policeman_dark_skin_tone",2309],["cop_light_skin_tone",2310],["cop_medium_light_skin_tone",2311],["cop_medium_skin_tone",2312],["cop_medium_dark_skin_tone",2313],["cop_dark_skin_tone",2314],["woman_with_veil_light_skin_tone",2315],["woman_with_veil_medium_light_skin_tone",2316],["woman_with_veil_medium_skin_tone",2317],["woman_with_veil_medium_dark_skin_tone",2318],["woman_with_veil_dark_skin_tone",2319],["man_with_veil_light_skin_tone",2320],["man_with_veil_medium_light_skin_tone",2321],["man_with_veil_medium_skin_tone",2322],["man_with_veil_medium_dark_skin_tone",2323],["man_with_veil_dark_skin_tone",2324],["bride_with_veil_light_skin_tone",2325],["bride_with_veil_medium_light_skin_tone",2326],["bride_with_veil_medium_skin_tone",2327],["bride_with_veil_medium_dark_skin_tone",2328],["bride_with_veil_dark_skin_tone",2329],["blond-haired-woman_light_skin_tone",2330],["blonde_woman_light_skin_tone",2330],["blond-haired-woman_medium_light_skin_tone",2331],["blonde_woman_medium_light_skin_tone",2331],["blond-haired-woman_medium_skin_tone",2332],["blonde_woman_medium_skin_tone",2332],["blond-haired-woman_medium_dark_skin_tone",2333],["blonde_woman_medium_dark_skin_tone",2333],["blond-haired-woman_dark_skin_tone",2334],["blonde_woman_dark_skin_tone",2334],["blond-haired-man_light_skin_tone",2335],["blonde_man_light_skin_tone",2335],["blond-haired-man_medium_light_skin_tone",2336],["blonde_man_medium_light_skin_tone",2336],["blond-haired-man_medium_skin_tone",2337],["blonde_man_medium_skin_tone",2337],["blond-haired-man_medium_dark_skin_tone",2338],["blonde_man_medium_dark_skin_tone",2338],["blond-haired-man_dark_skin_tone",2339],["blonde_man_dark_skin_tone",2339],["person_with_blond_hair_light_skin_tone",2340],["person_with_blond_hair_medium_light_skin_tone",2341],["person_with_blond_hair_medium_skin_tone",2342],["person_with_blond_hair_medium_dark_skin_tone",2343],["person_with_blond_hair_dark_skin_tone",2344],["man_with_gua_pi_mao_light_skin_tone",2345],["man_with_gua_pi_mao_medium_light_skin_tone",2346],["man_with_gua_pi_mao_medium_skin_tone",2347],["man_with_gua_pi_mao_medium_dark_skin_tone",2348],["man_with_gua_pi_mao_dark_skin_tone",2349],["woman-wearing-turban_light_skin_tone",2350],["woman_with_turban_light_skin_tone",2350],["woman-wearing-turban_medium_light_skin_tone",2351],["woman_with_turban_medium_light_skin_tone",2351],["woman-wearing-turban_medium_skin_tone",2352],["woman_with_turban_medium_skin_tone",2352],["woman-wearing-turban_medium_dark_skin_tone",2353],["woman_with_turban_medium_dark_skin_tone",2353],["woman-wearing-turban_dark_skin_tone",2354],["woman_with_turban_dark_skin_tone",2354],["man-wearing-turban_light_skin_tone",2355],["man-wearing-turban_medium_light_skin_tone",2356],["man-wearing-turban_medium_skin_tone",2357],["man-wearing-turban_medium_dark_skin_tone",2358],["man-wearing-turban_dark_skin_tone",2359],["man_with_turban_light_skin_tone",2360],["man_with_turban_medium_light_skin_tone",2361],["man_with_turban_medium_skin_tone",2362],["man_with_turban_medium_dark_skin_tone",2363],["man_with_turban_dark_skin_tone",2364],["older_man_light_skin_tone",2365],["older_man_medium_light_skin_tone",2366],["older_man_medium_skin_tone",2367],["older_man_medium_dark_skin_tone",2368],["older_man_dark_skin_tone",2369],["older_woman_light_skin_tone",2370],["older_woman_medium_light_skin_tone",2371],["older_woman_medium_skin_tone",2372],["older_woman_medium_dark_skin_tone",2373],["older_woman_dark_skin_tone",2374],["baby_light_skin_tone",2375],["baby_medium_light_skin_tone",2376],["baby_medium_skin_tone",2377],["baby_medium_dark_skin_tone",2378],["baby_dark_skin_tone",2379],["female-construction-worker_light_skin_tone",2380],["construction_worker_woman_light_skin_tone",2380],["female-construction-worker_medium_light_skin_tone",2381],["construction_worker_woman_medium_light_skin_tone",2381],["female-construction-worker_medium_skin_tone",2382],["construction_worker_woman_medium_skin_tone",2382],["female-construction-worker_medium_dark_skin_tone",2383],["construction_worker_woman_medium_dark_skin_tone",2383],["female-construction-worker_dark_skin_tone",2384],["construction_worker_woman_dark_skin_tone",2384],["male-construction-worker_light_skin_tone",2385],["construction_worker_man_light_skin_tone",2385],["male-construction-worker_medium_light_skin_tone",2386],["construction_worker_man_medium_light_skin_tone",2386],["male-construction-worker_medium_skin_tone",2387],["construction_worker_man_medium_skin_tone",2387],["male-construction-worker_medium_dark_skin_tone",2388],["construction_worker_man_medium_dark_skin_tone",2388],["male-construction-worker_dark_skin_tone",2389],["construction_worker_man_dark_skin_tone",2389],["construction_worker_light_skin_tone",2390],["construction_worker_medium_light_skin_tone",2391],["construction_worker_medium_skin_tone",2392],["construction_worker_medium_dark_skin_tone",2393],["construction_worker_dark_skin_tone",2394],["princess_light_skin_tone",2395],["princess_medium_light_skin_tone",2396],["princess_medium_skin_tone",2397],["princess_medium_dark_skin_tone",2398],["princess_dark_skin_tone",2399],["angel_light_skin_tone",2400],["angel_medium_light_skin_tone",2401],["angel_medium_skin_tone",2402],["angel_medium_dark_skin_tone",2403],["angel_dark_skin_tone",2404],["woman-tipping-hand_light_skin_tone",2405],["tipping_hand_woman_light_skin_tone",2405],["woman-tipping-hand_medium_light_skin_tone",2406],["tipping_hand_woman_medium_light_skin_tone",2406],["woman-tipping-hand_medium_skin_tone",2407],["tipping_hand_woman_medium_skin_tone",2407],["woman-tipping-hand_medium_dark_skin_tone",2408],["tipping_hand_woman_medium_dark_skin_tone",2408],["woman-tipping-hand_dark_skin_tone",2409],["tipping_hand_woman_dark_skin_tone",2409],["man-tipping-hand_light_skin_tone",2410],["tipping_hand_man_light_skin_tone",2410],["man-tipping-hand_medium_light_skin_tone",2411],["tipping_hand_man_medium_light_skin_tone",2411],["man-tipping-hand_medium_skin_tone",2412],["tipping_hand_man_medium_skin_tone",2412],["man-tipping-hand_medium_dark_skin_tone",2413],["tipping_hand_man_medium_dark_skin_tone",2413],["man-tipping-hand_dark_skin_tone",2414],["tipping_hand_man_dark_skin_tone",2414],["information_desk_person_light_skin_tone",2415],["information_desk_person_medium_light_skin_tone",2416],["information_desk_person_medium_skin_tone",2417],["information_desk_person_medium_dark_skin_tone",2418],["information_desk_person_dark_skin_tone",2419],["female-guard_light_skin_tone",2420],["guardswoman_light_skin_tone",2420],["female-guard_medium_light_skin_tone",2421],["guardswoman_medium_light_skin_tone",2421],["female-guard_medium_skin_tone",2422],["guardswoman_medium_skin_tone",2422],["female-guard_medium_dark_skin_tone",2423],["guardswoman_medium_dark_skin_tone",2423],["female-guard_dark_skin_tone",2424],["guardswoman_dark_skin_tone",2424],["male-guard_light_skin_tone",2425],["male-guard_medium_light_skin_tone",2426],["male-guard_medium_skin_tone",2427],["male-guard_medium_dark_skin_tone",2428],["male-guard_dark_skin_tone",2429],["guardsman_light_skin_tone",2430],["guardsman_medium_light_skin_tone",2431],["guardsman_medium_skin_tone",2432],["guardsman_medium_dark_skin_tone",2433],["guardsman_dark_skin_tone",2434],["dancer_light_skin_tone",2435],["dancer_medium_light_skin_tone",2436],["dancer_medium_skin_tone",2437],["dancer_medium_dark_skin_tone",2438],["dancer_dark_skin_tone",2439],["nail_care_light_skin_tone",2440],["nail_care_medium_light_skin_tone",2441],["nail_care_medium_skin_tone",2442],["nail_care_medium_dark_skin_tone",2443],["nail_care_dark_skin_tone",2444],["woman-getting-massage_light_skin_tone",2445],["massage_woman_light_skin_tone",2445],["woman-getting-massage_medium_light_skin_tone",2446],["massage_woman_medium_light_skin_tone",2446],["woman-getting-massage_medium_skin_tone",2447],["massage_woman_medium_skin_tone",2447],["woman-getting-massage_medium_dark_skin_tone",2448],["massage_woman_medium_dark_skin_tone",2448],["woman-getting-massage_dark_skin_tone",2449],["massage_woman_dark_skin_tone",2449],["man-getting-massage_light_skin_tone",2450],["massage_man_light_skin_tone",2450],["man-getting-massage_medium_light_skin_tone",2451],["massage_man_medium_light_skin_tone",2451],["man-getting-massage_medium_skin_tone",2452],["massage_man_medium_skin_tone",2452],["man-getting-massage_medium_dark_skin_tone",2453],["massage_man_medium_dark_skin_tone",2453],["man-getting-massage_dark_skin_tone",2454],["massage_man_dark_skin_tone",2454],["massage_light_skin_tone",2455],["massage_medium_light_skin_tone",2456],["massage_medium_skin_tone",2457],["massage_medium_dark_skin_tone",2458],["massage_dark_skin_tone",2459],["woman-getting-haircut_light_skin_tone",2460],["haircut_woman_light_skin_tone",2460],["woman-getting-haircut_medium_light_skin_tone",2461],["haircut_woman_medium_light_skin_tone",2461],["woman-getting-haircut_medium_skin_tone",2462],["haircut_woman_medium_skin_tone",2462],["woman-getting-haircut_medium_dark_skin_tone",2463],["haircut_woman_medium_dark_skin_tone",2463],["woman-getting-haircut_dark_skin_tone",2464],["haircut_woman_dark_skin_tone",2464],["man-getting-haircut_light_skin_tone",2465],["haircut_man_light_skin_tone",2465],["man-getting-haircut_medium_light_skin_tone",2466],["haircut_man_medium_light_skin_tone",2466],["man-getting-haircut_medium_skin_tone",2467],["haircut_man_medium_skin_tone",2467],["man-getting-haircut_medium_dark_skin_tone",2468],["haircut_man_medium_dark_skin_tone",2468],["man-getting-haircut_dark_skin_tone",2469],["haircut_man_dark_skin_tone",2469],["haircut_light_skin_tone",2470],["haircut_medium_light_skin_tone",2471],["haircut_medium_skin_tone",2472],["haircut_medium_dark_skin_tone",2473],["haircut_dark_skin_tone",2474],["muscle_light_skin_tone",2475],["muscle_medium_light_skin_tone",2476],["muscle_medium_skin_tone",2477],["muscle_medium_dark_skin_tone",2478],["muscle_dark_skin_tone",2479],["man_in_business_suit_levitating_light_skin_tone",2480],["business_suit_levitating_light_skin_tone",2480],["man_in_business_suit_levitating_medium_light_skin_tone",2481],["business_suit_levitating_medium_light_skin_tone",2481],["man_in_business_suit_levitating_medium_skin_tone",2482],["business_suit_levitating_medium_skin_tone",2482],["man_in_business_suit_levitating_medium_dark_skin_tone",2483],["business_suit_levitating_medium_dark_skin_tone",2483],["man_in_business_suit_levitating_dark_skin_tone",2484],["business_suit_levitating_dark_skin_tone",2484],["female-detective_light_skin_tone",2485],["female_detective_light_skin_tone",2485],["female-detective_medium_light_skin_tone",2486],["female_detective_medium_light_skin_tone",2486],["female-detective_medium_skin_tone",2487],["female_detective_medium_skin_tone",2487],["female-detective_medium_dark_skin_tone",2488],["female_detective_medium_dark_skin_tone",2488],["female-detective_dark_skin_tone",2489],["female_detective_dark_skin_tone",2489],["male-detective_light_skin_tone",2490],["male_detective_light_skin_tone",2490],["male-detective_medium_light_skin_tone",2491],["male_detective_medium_light_skin_tone",2491],["male-detective_medium_skin_tone",2492],["male_detective_medium_skin_tone",2492],["male-detective_medium_dark_skin_tone",2493],["male_detective_medium_dark_skin_tone",2493],["male-detective_dark_skin_tone",2494],["male_detective_dark_skin_tone",2494],["sleuth_or_spy_light_skin_tone",2495],["sleuth_or_spy_medium_light_skin_tone",2496],["sleuth_or_spy_medium_skin_tone",2497],["sleuth_or_spy_medium_dark_skin_tone",2498],["sleuth_or_spy_dark_skin_tone",2499],["man_dancing_light_skin_tone",2500],["man_dancing_medium_light_skin_tone",2501],["man_dancing_medium_skin_tone",2502],["man_dancing_medium_dark_skin_tone",2503],["man_dancing_dark_skin_tone",2504],["raised_hand_with_fingers_splayed_light_skin_tone",2505],["raised_hand_with_fingers_splayed_medium_light_skin_tone",2506],["raised_hand_with_fingers_splayed_medium_skin_tone",2507],["raised_hand_with_fingers_splayed_medium_dark_skin_tone",2508],["raised_hand_with_fingers_splayed_dark_skin_tone",2509],["middle_finger_light_skin_tone",2510],["reversed_hand_with_middle_finger_extended_light_skin_tone",2510],["middle_finger_medium_light_skin_tone",2511],["reversed_hand_with_middle_finger_extended_medium_light_skin_tone",2511],["middle_finger_medium_skin_tone",2512],["reversed_hand_with_middle_finger_extended_medium_skin_tone",2512],["middle_finger_medium_dark_skin_tone",2513],["reversed_hand_with_middle_finger_extended_medium_dark_skin_tone",2513],["middle_finger_dark_skin_tone",2514],["reversed_hand_with_middle_finger_extended_dark_skin_tone",2514],["spock-hand_light_skin_tone",2515],["vulcan_salute_light_skin_tone",2515],["spock-hand_medium_light_skin_tone",2516],["vulcan_salute_medium_light_skin_tone",2516],["spock-hand_medium_skin_tone",2517],["vulcan_salute_medium_skin_tone",2517],["spock-hand_medium_dark_skin_tone",2518],["vulcan_salute_medium_dark_skin_tone",2518],["spock-hand_dark_skin_tone",2519],["vulcan_salute_dark_skin_tone",2519],["woman-gesturing-no_light_skin_tone",2520],["no_good_woman_light_skin_tone",2520],["woman-gesturing-no_medium_light_skin_tone",2521],["no_good_woman_medium_light_skin_tone",2521],["woman-gesturing-no_medium_skin_tone",2522],["no_good_woman_medium_skin_tone",2522],["woman-gesturing-no_medium_dark_skin_tone",2523],["no_good_woman_medium_dark_skin_tone",2523],["woman-gesturing-no_dark_skin_tone",2524],["no_good_woman_dark_skin_tone",2524],["man-gesturing-no_light_skin_tone",2525],["no_good_man_light_skin_tone",2525],["man-gesturing-no_medium_light_skin_tone",2526],["no_good_man_medium_light_skin_tone",2526],["man-gesturing-no_medium_skin_tone",2527],["no_good_man_medium_skin_tone",2527],["man-gesturing-no_medium_dark_skin_tone",2528],["no_good_man_medium_dark_skin_tone",2528],["man-gesturing-no_dark_skin_tone",2529],["no_good_man_dark_skin_tone",2529],["no_good_light_skin_tone",2530],["no_good_medium_light_skin_tone",2531],["no_good_medium_skin_tone",2532],["no_good_medium_dark_skin_tone",2533],["no_good_dark_skin_tone",2534],["woman-gesturing-ok_light_skin_tone",2535],["woman-gesturing-ok_medium_light_skin_tone",2536],["woman-gesturing-ok_medium_skin_tone",2537],["woman-gesturing-ok_medium_dark_skin_tone",2538],["woman-gesturing-ok_dark_skin_tone",2539],["man-gesturing-ok_light_skin_tone",2540],["ok_man_light_skin_tone",2540],["man-gesturing-ok_medium_light_skin_tone",2541],["ok_man_medium_light_skin_tone",2541],["man-gesturing-ok_medium_skin_tone",2542],["ok_man_medium_skin_tone",2542],["man-gesturing-ok_medium_dark_skin_tone",2543],["ok_man_medium_dark_skin_tone",2543],["man-gesturing-ok_dark_skin_tone",2544],["ok_man_dark_skin_tone",2544],["ok_woman_light_skin_tone",2545],["ok_woman_medium_light_skin_tone",2546],["ok_woman_medium_skin_tone",2547],["ok_woman_medium_dark_skin_tone",2548],["ok_woman_dark_skin_tone",2549],["woman-bowing_light_skin_tone",2550],["bowing_woman_light_skin_tone",2550],["woman-bowing_medium_light_skin_tone",2551],["bowing_woman_medium_light_skin_tone",2551],["woman-bowing_medium_skin_tone",2552],["bowing_woman_medium_skin_tone",2552],["woman-bowing_medium_dark_skin_tone",2553],["bowing_woman_medium_dark_skin_tone",2553],["woman-bowing_dark_skin_tone",2554],["bowing_woman_dark_skin_tone",2554],["man-bowing_light_skin_tone",2555],["bowing_man_light_skin_tone",2555],["man-bowing_medium_light_skin_tone",2556],["bowing_man_medium_light_skin_tone",2556],["man-bowing_medium_skin_tone",2557],["bowing_man_medium_skin_tone",2557],["man-bowing_medium_dark_skin_tone",2558],["bowing_man_medium_dark_skin_tone",2558],["man-bowing_dark_skin_tone",2559],["bowing_man_dark_skin_tone",2559],["bow_light_skin_tone",2560],["bow_medium_light_skin_tone",2561],["bow_medium_skin_tone",2562],["bow_medium_dark_skin_tone",2563],["bow_dark_skin_tone",2564],["woman-raising-hand_light_skin_tone",2565],["raising_hand_woman_light_skin_tone",2565],["woman-raising-hand_medium_light_skin_tone",2566],["raising_hand_woman_medium_light_skin_tone",2566],["woman-raising-hand_medium_skin_tone",2567],["raising_hand_woman_medium_skin_tone",2567],["woman-raising-hand_medium_dark_skin_tone",2568],["raising_hand_woman_medium_dark_skin_tone",2568],["woman-raising-hand_dark_skin_tone",2569],["raising_hand_woman_dark_skin_tone",2569],["man-raising-hand_light_skin_tone",2570],["raising_hand_man_light_skin_tone",2570],["man-raising-hand_medium_light_skin_tone",2571],["raising_hand_man_medium_light_skin_tone",2571],["man-raising-hand_medium_skin_tone",2572],["raising_hand_man_medium_skin_tone",2572],["man-raising-hand_medium_dark_skin_tone",2573],["raising_hand_man_medium_dark_skin_tone",2573],["man-raising-hand_dark_skin_tone",2574],["raising_hand_man_dark_skin_tone",2574],["raising_hand_light_skin_tone",2575],["raising_hand_medium_light_skin_tone",2576],["raising_hand_medium_skin_tone",2577],["raising_hand_medium_dark_skin_tone",2578],["raising_hand_dark_skin_tone",2579],["raised_hands_light_skin_tone",2580],["raised_hands_medium_light_skin_tone",2581],["raised_hands_medium_skin_tone",2582],["raised_hands_medium_dark_skin_tone",2583],["raised_hands_dark_skin_tone",2584],["woman-frowning_light_skin_tone",2585],["frowning_woman_light_skin_tone",2585],["woman-frowning_medium_light_skin_tone",2586],["frowning_woman_medium_light_skin_tone",2586],["woman-frowning_medium_skin_tone",2587],["frowning_woman_medium_skin_tone",2587],["woman-frowning_medium_dark_skin_tone",2588],["frowning_woman_medium_dark_skin_tone",2588],["woman-frowning_dark_skin_tone",2589],["frowning_woman_dark_skin_tone",2589],["man-frowning_light_skin_tone",2590],["frowning_man_light_skin_tone",2590],["man-frowning_medium_light_skin_tone",2591],["frowning_man_medium_light_skin_tone",2591],["man-frowning_medium_skin_tone",2592],["frowning_man_medium_skin_tone",2592],["man-frowning_medium_dark_skin_tone",2593],["frowning_man_medium_dark_skin_tone",2593],["man-frowning_dark_skin_tone",2594],["frowning_man_dark_skin_tone",2594],["person_frowning_light_skin_tone",2595],["person_frowning_medium_light_skin_tone",2596],["person_frowning_medium_skin_tone",2597],["person_frowning_medium_dark_skin_tone",2598],["person_frowning_dark_skin_tone",2599],["woman-pouting_light_skin_tone",2600],["pouting_woman_light_skin_tone",2600],["woman-pouting_medium_light_skin_tone",2601],["pouting_woman_medium_light_skin_tone",2601],["woman-pouting_medium_skin_tone",2602],["pouting_woman_medium_skin_tone",2602],["woman-pouting_medium_dark_skin_tone",2603],["pouting_woman_medium_dark_skin_tone",2603],["woman-pouting_dark_skin_tone",2604],["pouting_woman_dark_skin_tone",2604],["man-pouting_light_skin_tone",2605],["pouting_man_light_skin_tone",2605],["man-pouting_medium_light_skin_tone",2606],["pouting_man_medium_light_skin_tone",2606],["man-pouting_medium_skin_tone",2607],["pouting_man_medium_skin_tone",2607],["man-pouting_medium_dark_skin_tone",2608],["pouting_man_medium_dark_skin_tone",2608],["man-pouting_dark_skin_tone",2609],["pouting_man_dark_skin_tone",2609],["person_with_pouting_face_light_skin_tone",2610],["person_with_pouting_face_medium_light_skin_tone",2611],["person_with_pouting_face_medium_skin_tone",2612],["person_with_pouting_face_medium_dark_skin_tone",2613],["person_with_pouting_face_dark_skin_tone",2614],["pray_light_skin_tone",2615],["pray_medium_light_skin_tone",2616],["pray_medium_skin_tone",2617],["pray_medium_dark_skin_tone",2618],["pray_dark_skin_tone",2619],["woman-rowing-boat_light_skin_tone",2620],["rowing_woman_light_skin_tone",2620],["woman-rowing-boat_medium_light_skin_tone",2621],["rowing_woman_medium_light_skin_tone",2621],["woman-rowing-boat_medium_skin_tone",2622],["rowing_woman_medium_skin_tone",2622],["woman-rowing-boat_medium_dark_skin_tone",2623],["rowing_woman_medium_dark_skin_tone",2623],["woman-rowing-boat_dark_skin_tone",2624],["rowing_woman_dark_skin_tone",2624],["man-rowing-boat_light_skin_tone",2625],["rowing_man_light_skin_tone",2625],["man-rowing-boat_medium_light_skin_tone",2626],["rowing_man_medium_light_skin_tone",2626],["man-rowing-boat_medium_skin_tone",2627],["rowing_man_medium_skin_tone",2627],["man-rowing-boat_medium_dark_skin_tone",2628],["rowing_man_medium_dark_skin_tone",2628],["man-rowing-boat_dark_skin_tone",2629],["rowing_man_dark_skin_tone",2629],["rowboat_light_skin_tone",2630],["rowboat_medium_light_skin_tone",2631],["rowboat_medium_skin_tone",2632],["rowboat_medium_dark_skin_tone",2633],["rowboat_dark_skin_tone",2634],["woman-biking_light_skin_tone",2635],["biking_woman_light_skin_tone",2635],["woman-biking_medium_light_skin_tone",2636],["biking_woman_medium_light_skin_tone",2636],["woman-biking_medium_skin_tone",2637],["biking_woman_medium_skin_tone",2637],["woman-biking_medium_dark_skin_tone",2638],["biking_woman_medium_dark_skin_tone",2638],["woman-biking_dark_skin_tone",2639],["biking_woman_dark_skin_tone",2639],["man-biking_light_skin_tone",2640],["biking_man_light_skin_tone",2640],["man-biking_medium_light_skin_tone",2641],["biking_man_medium_light_skin_tone",2641],["man-biking_medium_skin_tone",2642],["biking_man_medium_skin_tone",2642],["man-biking_medium_dark_skin_tone",2643],["biking_man_medium_dark_skin_tone",2643],["man-biking_dark_skin_tone",2644],["biking_man_dark_skin_tone",2644],["bicyclist_light_skin_tone",2645],["bicyclist_medium_light_skin_tone",2646],["bicyclist_medium_skin_tone",2647],["bicyclist_medium_dark_skin_tone",2648],["bicyclist_dark_skin_tone",2649],["woman-mountain-biking_light_skin_tone",2650],["mountain_biking_woman_light_skin_tone",2650],["woman-mountain-biking_medium_light_skin_tone",2651],["mountain_biking_woman_medium_light_skin_tone",2651],["woman-mountain-biking_medium_skin_tone",2652],["mountain_biking_woman_medium_skin_tone",2652],["woman-mountain-biking_medium_dark_skin_tone",2653],["mountain_biking_woman_medium_dark_skin_tone",2653],["woman-mountain-biking_dark_skin_tone",2654],["mountain_biking_woman_dark_skin_tone",2654],["man-mountain-biking_light_skin_tone",2655],["mountain_biking_man_light_skin_tone",2655],["man-mountain-biking_medium_light_skin_tone",2656],["mountain_biking_man_medium_light_skin_tone",2656],["man-mountain-biking_medium_skin_tone",2657],["mountain_biking_man_medium_skin_tone",2657],["man-mountain-biking_medium_dark_skin_tone",2658],["mountain_biking_man_medium_dark_skin_tone",2658],["man-mountain-biking_dark_skin_tone",2659],["mountain_biking_man_dark_skin_tone",2659],["mountain_bicyclist_light_skin_tone",2660],["mountain_bicyclist_medium_light_skin_tone",2661],["mountain_bicyclist_medium_skin_tone",2662],["mountain_bicyclist_medium_dark_skin_tone",2663],["mountain_bicyclist_dark_skin_tone",2664],["woman-walking_light_skin_tone",2665],["walking_woman_light_skin_tone",2665],["woman-walking_medium_light_skin_tone",2666],["walking_woman_medium_light_skin_tone",2666],["woman-walking_medium_skin_tone",2667],["walking_woman_medium_skin_tone",2667],["woman-walking_medium_dark_skin_tone",2668],["walking_woman_medium_dark_skin_tone",2668],["woman-walking_dark_skin_tone",2669],["walking_woman_dark_skin_tone",2669],["man-walking_light_skin_tone",2670],["walking_man_light_skin_tone",2670],["man-walking_medium_light_skin_tone",2671],["walking_man_medium_light_skin_tone",2671],["man-walking_medium_skin_tone",2672],["walking_man_medium_skin_tone",2672],["man-walking_medium_dark_skin_tone",2673],["walking_man_medium_dark_skin_tone",2673],["man-walking_dark_skin_tone",2674],["walking_man_dark_skin_tone",2674],["walking_light_skin_tone",2675],["walking_medium_light_skin_tone",2676],["walking_medium_skin_tone",2677],["walking_medium_dark_skin_tone",2678],["walking_dark_skin_tone",2679],["bath_light_skin_tone",2680],["bath_medium_light_skin_tone",2681],["bath_medium_skin_tone",2682],["bath_medium_dark_skin_tone",2683],["bath_dark_skin_tone",2684],["sleeping_accommodation_light_skin_tone",2685],["sleeping_accommodation_medium_light_skin_tone",2686],["sleeping_accommodation_medium_skin_tone",2687],["sleeping_accommodation_medium_dark_skin_tone",2688],["sleeping_accommodation_dark_skin_tone",2689],["pinched_fingers_light_skin_tone",2690],["pinched_fingers_medium_light_skin_tone",2691],["pinched_fingers_medium_skin_tone",2692],["pinched_fingers_medium_dark_skin_tone",2693],["pinched_fingers_dark_skin_tone",2694],["pinching_hand_light_skin_tone",2695],["pinching_hand_medium_light_skin_tone",2696],["pinching_hand_medium_skin_tone",2697],["pinching_hand_medium_dark_skin_tone",2698],["pinching_hand_dark_skin_tone",2699],["the_horns_light_skin_tone",2700],["sign_of_the_horns_light_skin_tone",2700],["metal_light_skin_tone",2700],["the_horns_medium_light_skin_tone",2701],["sign_of_the_horns_medium_light_skin_tone",2701],["metal_medium_light_skin_tone",2701],["the_horns_medium_skin_tone",2702],["sign_of_the_horns_medium_skin_tone",2702],["metal_medium_skin_tone",2702],["the_horns_medium_dark_skin_tone",2703],["sign_of_the_horns_medium_dark_skin_tone",2703],["metal_medium_dark_skin_tone",2703],["the_horns_dark_skin_tone",2704],["sign_of_the_horns_dark_skin_tone",2704],["metal_dark_skin_tone",2704],["call_me_hand_light_skin_tone",2705],["call_me_hand_medium_light_skin_tone",2706],["call_me_hand_medium_skin_tone",2707],["call_me_hand_medium_dark_skin_tone",2708],["call_me_hand_dark_skin_tone",2709],["raised_back_of_hand_light_skin_tone",2710],["raised_back_of_hand_medium_light_skin_tone",2711],["raised_back_of_hand_medium_skin_tone",2712],["raised_back_of_hand_medium_dark_skin_tone",2713],["raised_back_of_hand_dark_skin_tone",2714],["left-facing_fist_light_skin_tone",2715],["fist_left_light_skin_tone",2715],["left-facing_fist_medium_light_skin_tone",2716],["fist_left_medium_light_skin_tone",2716],["left-facing_fist_medium_skin_tone",2717],["fist_left_medium_skin_tone",2717],["left-facing_fist_medium_dark_skin_tone",2718],["fist_left_medium_dark_skin_tone",2718],["left-facing_fist_dark_skin_tone",2719],["fist_left_dark_skin_tone",2719],["right-facing_fist_light_skin_tone",2720],["fist_right_light_skin_tone",2720],["right-facing_fist_medium_light_skin_tone",2721],["fist_right_medium_light_skin_tone",2721],["right-facing_fist_medium_skin_tone",2722],["fist_right_medium_skin_tone",2722],["right-facing_fist_medium_dark_skin_tone",2723],["fist_right_medium_dark_skin_tone",2723],["right-facing_fist_dark_skin_tone",2724],["fist_right_dark_skin_tone",2724],["crossed_fingers_light_skin_tone",2725],["hand_with_index_and_middle_fingers_crossed_light_skin_tone",2725],["crossed_fingers_medium_light_skin_tone",2726],["hand_with_index_and_middle_fingers_crossed_medium_light_skin_tone",2726],["crossed_fingers_medium_skin_tone",2727],["hand_with_index_and_middle_fingers_crossed_medium_skin_tone",2727],["crossed_fingers_medium_dark_skin_tone",2728],["hand_with_index_and_middle_fingers_crossed_medium_dark_skin_tone",2728],["crossed_fingers_dark_skin_tone",2729],["hand_with_index_and_middle_fingers_crossed_dark_skin_tone",2729],["i_love_you_hand_sign_light_skin_tone",2730],["i_love_you_hand_sign_medium_light_skin_tone",2731],["i_love_you_hand_sign_medium_skin_tone",2732],["i_love_you_hand_sign_medium_dark_skin_tone",2733],["i_love_you_hand_sign_dark_skin_tone",2734],["woman-facepalming_light_skin_tone",2735],["woman_facepalming_light_skin_tone",2735],["woman-facepalming_medium_light_skin_tone",2736],["woman_facepalming_medium_light_skin_tone",2736],["woman-facepalming_medium_skin_tone",2737],["woman_facepalming_medium_skin_tone",2737],["woman-facepalming_medium_dark_skin_tone",2738],["woman_facepalming_medium_dark_skin_tone",2738],["woman-facepalming_dark_skin_tone",2739],["woman_facepalming_dark_skin_tone",2739],["man-facepalming_light_skin_tone",2740],["man_facepalming_light_skin_tone",2740],["man-facepalming_medium_light_skin_tone",2741],["man_facepalming_medium_light_skin_tone",2741],["man-facepalming_medium_skin_tone",2742],["man_facepalming_medium_skin_tone",2742],["man-facepalming_medium_dark_skin_tone",2743],["man_facepalming_medium_dark_skin_tone",2743],["man-facepalming_dark_skin_tone",2744],["man_facepalming_dark_skin_tone",2744],["face_palm_light_skin_tone",2745],["face_palm_medium_light_skin_tone",2746],["face_palm_medium_skin_tone",2747],["face_palm_medium_dark_skin_tone",2748],["face_palm_dark_skin_tone",2749],["pregnant_woman_light_skin_tone",2750],["pregnant_woman_medium_light_skin_tone",2751],["pregnant_woman_medium_skin_tone",2752],["pregnant_woman_medium_dark_skin_tone",2753],["pregnant_woman_dark_skin_tone",2754],["breast-feeding_light_skin_tone",2755],["breast-feeding_medium_light_skin_tone",2756],["breast-feeding_medium_skin_tone",2757],["breast-feeding_medium_dark_skin_tone",2758],["breast-feeding_dark_skin_tone",2759],["palms_up_together_light_skin_tone",2760],["palms_up_together_medium_light_skin_tone",2761],["palms_up_together_medium_skin_tone",2762],["palms_up_together_medium_dark_skin_tone",2763],["palms_up_together_dark_skin_tone",2764],["selfie_light_skin_tone",2765],["selfie_medium_light_skin_tone",2766],["selfie_medium_skin_tone",2767],["selfie_medium_dark_skin_tone",2768],["selfie_dark_skin_tone",2769],["prince_light_skin_tone",2770],["prince_medium_light_skin_tone",2771],["prince_medium_skin_tone",2772],["prince_medium_dark_skin_tone",2773],["prince_dark_skin_tone",2774],["woman_in_tuxedo_light_skin_tone",2775],["woman_in_tuxedo_medium_light_skin_tone",2776],["woman_in_tuxedo_medium_skin_tone",2777],["woman_in_tuxedo_medium_dark_skin_tone",2778],["woman_in_tuxedo_dark_skin_tone",2779],["man_in_tuxedo_light_skin_tone",2780],["man_in_tuxedo_medium_light_skin_tone",2781],["man_in_tuxedo_medium_skin_tone",2782],["man_in_tuxedo_medium_dark_skin_tone",2783],["man_in_tuxedo_dark_skin_tone",2784],["person_in_tuxedo_light_skin_tone",2785],["person_in_tuxedo_medium_light_skin_tone",2786],["person_in_tuxedo_medium_skin_tone",2787],["person_in_tuxedo_medium_dark_skin_tone",2788],["person_in_tuxedo_dark_skin_tone",2789],["mrs_claus_light_skin_tone",2790],["mother_christmas_light_skin_tone",2790],["mrs_claus_medium_light_skin_tone",2791],["mother_christmas_medium_light_skin_tone",2791],["mrs_claus_medium_skin_tone",2792],["mother_christmas_medium_skin_tone",2792],["mrs_claus_medium_dark_skin_tone",2793],["mother_christmas_medium_dark_skin_tone",2793],["mrs_claus_dark_skin_tone",2794],["mother_christmas_dark_skin_tone",2794],["woman-shrugging_light_skin_tone",2795],["woman_shrugging_light_skin_tone",2795],["woman-shrugging_medium_light_skin_tone",2796],["woman_shrugging_medium_light_skin_tone",2796],["woman-shrugging_medium_skin_tone",2797],["woman_shrugging_medium_skin_tone",2797],["woman-shrugging_medium_dark_skin_tone",2798],["woman_shrugging_medium_dark_skin_tone",2798],["woman-shrugging_dark_skin_tone",2799],["woman_shrugging_dark_skin_tone",2799],["man-shrugging_light_skin_tone",2800],["man_shrugging_light_skin_tone",2800],["man-shrugging_medium_light_skin_tone",2801],["man_shrugging_medium_light_skin_tone",2801],["man-shrugging_medium_skin_tone",2802],["man_shrugging_medium_skin_tone",2802],["man-shrugging_medium_dark_skin_tone",2803],["man_shrugging_medium_dark_skin_tone",2803],["man-shrugging_dark_skin_tone",2804],["man_shrugging_dark_skin_tone",2804],["shrug_light_skin_tone",2805],["shrug_medium_light_skin_tone",2806],["shrug_medium_skin_tone",2807],["shrug_medium_dark_skin_tone",2808],["shrug_dark_skin_tone",2809],["woman-cartwheeling_light_skin_tone",2810],["woman_cartwheeling_light_skin_tone",2810],["woman-cartwheeling_medium_light_skin_tone",2811],["woman_cartwheeling_medium_light_skin_tone",2811],["woman-cartwheeling_medium_skin_tone",2812],["woman_cartwheeling_medium_skin_tone",2812],["woman-cartwheeling_medium_dark_skin_tone",2813],["woman_cartwheeling_medium_dark_skin_tone",2813],["woman-cartwheeling_dark_skin_tone",2814],["woman_cartwheeling_dark_skin_tone",2814],["man-cartwheeling_light_skin_tone",2815],["man_cartwheeling_light_skin_tone",2815],["man-cartwheeling_medium_light_skin_tone",2816],["man_cartwheeling_medium_light_skin_tone",2816],["man-cartwheeling_medium_skin_tone",2817],["man_cartwheeling_medium_skin_tone",2817],["man-cartwheeling_medium_dark_skin_tone",2818],["man_cartwheeling_medium_dark_skin_tone",2818],["man-cartwheeling_dark_skin_tone",2819],["man_cartwheeling_dark_skin_tone",2819],["person_doing_cartwheel_light_skin_tone",2820],["person_doing_cartwheel_medium_light_skin_tone",2821],["person_doing_cartwheel_medium_skin_tone",2822],["person_doing_cartwheel_medium_dark_skin_tone",2823],["person_doing_cartwheel_dark_skin_tone",2824],["woman-juggling_light_skin_tone",2825],["woman_juggling_light_skin_tone",2825],["woman-juggling_medium_light_skin_tone",2826],["woman_juggling_medium_light_skin_tone",2826],["woman-juggling_medium_skin_tone",2827],["woman_juggling_medium_skin_tone",2827],["woman-juggling_medium_dark_skin_tone",2828],["woman_juggling_medium_dark_skin_tone",2828],["woman-juggling_dark_skin_tone",2829],["woman_juggling_dark_skin_tone",2829],["man-juggling_light_skin_tone",2830],["man_juggling_light_skin_tone",2830],["man-juggling_medium_light_skin_tone",2831],["man_juggling_medium_light_skin_tone",2831],["man-juggling_medium_skin_tone",2832],["man_juggling_medium_skin_tone",2832],["man-juggling_medium_dark_skin_tone",2833],["man_juggling_medium_dark_skin_tone",2833],["man-juggling_dark_skin_tone",2834],["man_juggling_dark_skin_tone",2834],["juggling_light_skin_tone",2835],["juggling_medium_light_skin_tone",2836],["juggling_medium_skin_tone",2837],["juggling_medium_dark_skin_tone",2838],["juggling_dark_skin_tone",2839],["woman-playing-water-polo_light_skin_tone",2840],["woman_playing_water_polo_light_skin_tone",2840],["woman-playing-water-polo_medium_light_skin_tone",2841],["woman_playing_water_polo_medium_light_skin_tone",2841],["woman-playing-water-polo_medium_skin_tone",2842],["woman_playing_water_polo_medium_skin_tone",2842],["woman-playing-water-polo_medium_dark_skin_tone",2843],["woman_playing_water_polo_medium_dark_skin_tone",2843],["woman-playing-water-polo_dark_skin_tone",2844],["woman_playing_water_polo_dark_skin_tone",2844],["man-playing-water-polo_light_skin_tone",2845],["man_playing_water_polo_light_skin_tone",2845],["man-playing-water-polo_medium_light_skin_tone",2846],["man_playing_water_polo_medium_light_skin_tone",2846],["man-playing-water-polo_medium_skin_tone",2847],["man_playing_water_polo_medium_skin_tone",2847],["man-playing-water-polo_medium_dark_skin_tone",2848],["man_playing_water_polo_medium_dark_skin_tone",2848],["man-playing-water-polo_dark_skin_tone",2849],["man_playing_water_polo_dark_skin_tone",2849],["water_polo_light_skin_tone",2850],["water_polo_medium_light_skin_tone",2851],["water_polo_medium_skin_tone",2852],["water_polo_medium_dark_skin_tone",2853],["water_polo_dark_skin_tone",2854],["woman-playing-handball_light_skin_tone",2855],["woman_playing_handball_light_skin_tone",2855],["woman-playing-handball_medium_light_skin_tone",2856],["woman_playing_handball_medium_light_skin_tone",2856],["woman-playing-handball_medium_skin_tone",2857],["woman_playing_handball_medium_skin_tone",2857],["woman-playing-handball_medium_dark_skin_tone",2858],["woman_playing_handball_medium_dark_skin_tone",2858],["woman-playing-handball_dark_skin_tone",2859],["woman_playing_handball_dark_skin_tone",2859],["man-playing-handball_light_skin_tone",2860],["man_playing_handball_light_skin_tone",2860],["man-playing-handball_medium_light_skin_tone",2861],["man_playing_handball_medium_light_skin_tone",2861],["man-playing-handball_medium_skin_tone",2862],["man_playing_handball_medium_skin_tone",2862],["man-playing-handball_medium_dark_skin_tone",2863],["man_playing_handball_medium_dark_skin_tone",2863],["man-playing-handball_dark_skin_tone",2864],["man_playing_handball_dark_skin_tone",2864],["handball_light_skin_tone",2865],["handball_medium_light_skin_tone",2866],["handball_medium_skin_tone",2867],["handball_medium_dark_skin_tone",2868],["handball_dark_skin_tone",2869],["ninja_light_skin_tone",2870],["ninja_medium_light_skin_tone",2871],["ninja_medium_skin_tone",2872],["ninja_medium_dark_skin_tone",2873],["ninja_dark_skin_tone",2874],["leg_light_skin_tone",2875],["leg_medium_light_skin_tone",2876],["leg_medium_skin_tone",2877],["leg_medium_dark_skin_tone",2878],["leg_dark_skin_tone",2879],["foot_light_skin_tone",2880],["foot_medium_light_skin_tone",2881],["foot_medium_skin_tone",2882],["foot_medium_dark_skin_tone",2883],["foot_dark_skin_tone",2884],["female_superhero_light_skin_tone",2885],["female_superhero_medium_light_skin_tone",2886],["female_superhero_medium_skin_tone",2887],["female_superhero_medium_dark_skin_tone",2888],["female_superhero_dark_skin_tone",2889],["male_superhero_light_skin_tone",2890],["male_superhero_medium_light_skin_tone",2891],["male_superhero_medium_skin_tone",2892],["male_superhero_medium_dark_skin_tone",2893],["male_superhero_dark_skin_tone",2894],["superhero_light_skin_tone",2895],["superhero_medium_light_skin_tone",2896],["superhero_medium_skin_tone",2897],["superhero_medium_dark_skin_tone",2898],["superhero_dark_skin_tone",2899],["female_supervillain_light_skin_tone",2900],["female_supervillain_medium_light_skin_tone",2901],["female_supervillain_medium_skin_tone",2902],["female_supervillain_medium_dark_skin_tone",2903],["female_supervillain_dark_skin_tone",2904],["male_supervillain_light_skin_tone",2905],["male_supervillain_medium_light_skin_tone",2906],["male_supervillain_medium_skin_tone",2907],["male_supervillain_medium_dark_skin_tone",2908],["male_supervillain_dark_skin_tone",2909],["supervillain_light_skin_tone",2910],["supervillain_medium_light_skin_tone",2911],["supervillain_medium_skin_tone",2912],["supervillain_medium_dark_skin_tone",2913],["supervillain_dark_skin_tone",2914],["ear_with_hearing_aid_light_skin_tone",2915],["ear_with_hearing_aid_medium_light_skin_tone",2916],["ear_with_hearing_aid_medium_skin_tone",2917],["ear_with_hearing_aid_medium_dark_skin_tone",2918],["ear_with_hearing_aid_dark_skin_tone",2919],["woman_standing_light_skin_tone",2920],["woman_standing_medium_light_skin_tone",2921],["woman_standing_medium_skin_tone",2922],["woman_standing_medium_dark_skin_tone",2923],["woman_standing_dark_skin_tone",2924],["man_standing_light_skin_tone",2925],["man_standing_medium_light_skin_tone",2926],["man_standing_medium_skin_tone",2927],["man_standing_medium_dark_skin_tone",2928],["man_standing_dark_skin_tone",2929],["standing_person_light_skin_tone",2930],["standing_person_medium_light_skin_tone",2931],["standing_person_medium_skin_tone",2932],["standing_person_medium_dark_skin_tone",2933],["standing_person_dark_skin_tone",2934],["woman_kneeling_light_skin_tone",2935],["woman_kneeling_medium_light_skin_tone",2936],["woman_kneeling_medium_skin_tone",2937],["woman_kneeling_medium_dark_skin_tone",2938],["woman_kneeling_dark_skin_tone",2939],["man_kneeling_light_skin_tone",2940],["man_kneeling_medium_light_skin_tone",2941],["man_kneeling_medium_skin_tone",2942],["man_kneeling_medium_dark_skin_tone",2943],["man_kneeling_dark_skin_tone",2944],["kneeling_person_light_skin_tone",2945],["kneeling_person_medium_light_skin_tone",2946],["kneeling_person_medium_skin_tone",2947],["kneeling_person_medium_dark_skin_tone",2948],["kneeling_person_dark_skin_tone",2949],["deaf_woman_light_skin_tone",2950],["deaf_woman_medium_light_skin_tone",2951],["deaf_woman_medium_skin_tone",2952],["deaf_woman_medium_dark_skin_tone",2953],["deaf_woman_dark_skin_tone",2954],["deaf_man_light_skin_tone",2955],["deaf_man_medium_light_skin_tone",2956],["deaf_man_medium_skin_tone",2957],["deaf_man_medium_dark_skin_tone",2958],["deaf_man_dark_skin_tone",2959],["deaf_person_light_skin_tone",2960],["deaf_person_medium_light_skin_tone",2961],["deaf_person_medium_skin_tone",2962],["deaf_person_medium_dark_skin_tone",2963],["deaf_person_dark_skin_tone",2964],["farmer_light_skin_tone",2965],["farmer_medium_light_skin_tone",2966],["farmer_medium_skin_tone",2967],["farmer_medium_dark_skin_tone",2968],["farmer_dark_skin_tone",2969],["cook_light_skin_tone",2970],["cook_medium_light_skin_tone",2971],["cook_medium_skin_tone",2972],["cook_medium_dark_skin_tone",2973],["cook_dark_skin_tone",2974],["person_feeding_baby_light_skin_tone",2975],["person_feeding_baby_medium_light_skin_tone",2976],["person_feeding_baby_medium_skin_tone",2977],["person_feeding_baby_medium_dark_skin_tone",2978],["person_feeding_baby_dark_skin_tone",2979],["mx_claus_light_skin_tone",2980],["mx_claus_medium_light_skin_tone",2981],["mx_claus_medium_skin_tone",2982],["mx_claus_medium_dark_skin_tone",2983],["mx_claus_dark_skin_tone",2984],["student_light_skin_tone",2985],["student_medium_light_skin_tone",2986],["student_medium_skin_tone",2987],["student_medium_dark_skin_tone",2988],["student_dark_skin_tone",2989],["singer_light_skin_tone",2990],["singer_medium_light_skin_tone",2991],["singer_medium_skin_tone",2992],["singer_medium_dark_skin_tone",2993],["singer_dark_skin_tone",2994],["artist_light_skin_tone",2995],["artist_medium_light_skin_tone",2996],["artist_medium_skin_tone",2997],["artist_medium_dark_skin_tone",2998],["artist_dark_skin_tone",2999],["teacher_light_skin_tone",3000],["teacher_medium_light_skin_tone",3001],["teacher_medium_skin_tone",3002],["teacher_medium_dark_skin_tone",3003],["teacher_dark_skin_tone",3004],["factory_worker_light_skin_tone",3005],["factory_worker_medium_light_skin_tone",3006],["factory_worker_medium_skin_tone",3007],["factory_worker_medium_dark_skin_tone",3008],["factory_worker_dark_skin_tone",3009],["technologist_light_skin_tone",3010],["technologist_medium_light_skin_tone",3011],["technologist_medium_skin_tone",3012],["technologist_medium_dark_skin_tone",3013],["technologist_dark_skin_tone",3014],["office_worker_light_skin_tone",3015],["office_worker_medium_light_skin_tone",3016],["office_worker_medium_skin_tone",3017],["office_worker_medium_dark_skin_tone",3018],["office_worker_dark_skin_tone",3019],["mechanic_light_skin_tone",3020],["mechanic_medium_light_skin_tone",3021],["mechanic_medium_skin_tone",3022],["mechanic_medium_dark_skin_tone",3023],["mechanic_dark_skin_tone",3024],["scientist_light_skin_tone",3025],["scientist_medium_light_skin_tone",3026],["scientist_medium_skin_tone",3027],["scientist_medium_dark_skin_tone",3028],["scientist_dark_skin_tone",3029],["astronaut_light_skin_tone",3030],["astronaut_medium_light_skin_tone",3031],["astronaut_medium_skin_tone",3032],["astronaut_medium_dark_skin_tone",3033],["astronaut_dark_skin_tone",3034],["firefighter_light_skin_tone",3035],["firefighter_medium_light_skin_tone",3036],["firefighter_medium_skin_tone",3037],["firefighter_medium_dark_skin_tone",3038],["firefighter_dark_skin_tone",3039],["people_holding_hands_light_skin_tone_light_skin_tone",3040],["people_holding_hands_light_skin_tone_medium_light_skin_tone",3041],["people_holding_hands_light_skin_tone_medium_skin_tone",3042],["people_holding_hands_light_skin_tone_medium_dark_skin_tone",3043],["people_holding_hands_light_skin_tone_dark_skin_tone",3044],["people_holding_hands_medium_light_skin_tone_light_skin_tone",3045],["people_holding_hands_medium_light_skin_tone_medium_light_skin_tone",3046],["people_holding_hands_medium_light_skin_tone_medium_skin_tone",3047],["people_holding_hands_medium_light_skin_tone_medium_dark_skin_tone",3048],["people_holding_hands_medium_light_skin_tone_dark_skin_tone",3049],["people_holding_hands_medium_skin_tone_light_skin_tone",3050],["people_holding_hands_medium_skin_tone_medium_light_skin_tone",3051],["people_holding_hands_medium_skin_tone_medium_skin_tone",3052],["people_holding_hands_medium_skin_tone_medium_dark_skin_tone",3053],["people_holding_hands_medium_skin_tone_dark_skin_tone",3054],["people_holding_hands_medium_dark_skin_tone_light_skin_tone",3055],["people_holding_hands_medium_dark_skin_tone_medium_light_skin_tone",3056],["people_holding_hands_medium_dark_skin_tone_medium_skin_tone",3057],["people_holding_hands_medium_dark_skin_tone_medium_dark_skin_tone",3058],["people_holding_hands_medium_dark_skin_tone_dark_skin_tone",3059],["people_holding_hands_dark_skin_tone_light_skin_tone",3060],["people_holding_hands_dark_skin_tone_medium_light_skin_tone",3061],["people_holding_hands_dark_skin_tone_medium_skin_tone",3062],["people_holding_hands_dark_skin_tone_medium_dark_skin_tone",3063],["people_holding_hands_dark_skin_tone_dark_skin_tone",3064],["person_with_probing_cane_light_skin_tone",3065],["person_with_probing_cane_medium_light_skin_tone",3066],["person_with_probing_cane_medium_skin_tone",3067],["person_with_probing_cane_medium_dark_skin_tone",3068],["person_with_probing_cane_dark_skin_tone",3069],["red_haired_person_light_skin_tone",3070],["red_haired_person_medium_light_skin_tone",3071],["red_haired_person_medium_skin_tone",3072],["red_haired_person_medium_dark_skin_tone",3073],["red_haired_person_dark_skin_tone",3074],["curly_haired_person_light_skin_tone",3075],["curly_haired_person_medium_light_skin_tone",3076],["curly_haired_person_medium_skin_tone",3077],["curly_haired_person_medium_dark_skin_tone",3078],["curly_haired_person_dark_skin_tone",3079],["bald_person_light_skin_tone",3080],["bald_person_medium_light_skin_tone",3081],["bald_person_medium_skin_tone",3082],["bald_person_medium_dark_skin_tone",3083],["bald_person_dark_skin_tone",3084],["white_haired_person_light_skin_tone",3085],["white_haired_person_medium_light_skin_tone",3086],["white_haired_person_medium_skin_tone",3087],["white_haired_person_medium_dark_skin_tone",3088],["white_haired_person_dark_skin_tone",3089],["person_in_motorized_wheelchair_light_skin_tone",3090],["person_in_motorized_wheelchair_medium_light_skin_tone",3091],["person_in_motorized_wheelchair_medium_skin_tone",3092],["person_in_motorized_wheelchair_medium_dark_skin_tone",3093],["person_in_motorized_wheelchair_dark_skin_tone",3094],["person_in_manual_wheelchair_light_skin_tone",3095],["person_in_manual_wheelchair_medium_light_skin_tone",3096],["person_in_manual_wheelchair_medium_skin_tone",3097],["person_in_manual_wheelchair_medium_dark_skin_tone",3098],["person_in_manual_wheelchair_dark_skin_tone",3099],["health_worker_light_skin_tone",3100],["health_worker_medium_light_skin_tone",3101],["health_worker_medium_skin_tone",3102],["health_worker_medium_dark_skin_tone",3103],["health_worker_dark_skin_tone",3104],["judge_light_skin_tone",3105],["judge_medium_light_skin_tone",3106],["judge_medium_skin_tone",3107],["judge_medium_dark_skin_tone",3108],["judge_dark_skin_tone",3109],["pilot_light_skin_tone",3110],["pilot_medium_light_skin_tone",3111],["pilot_medium_skin_tone",3112],["pilot_medium_dark_skin_tone",3113],["pilot_dark_skin_tone",3114],["adult_light_skin_tone",3115],["adult_medium_light_skin_tone",3116],["adult_medium_skin_tone",3117],["adult_medium_dark_skin_tone",3118],["adult_dark_skin_tone",3119],["child_light_skin_tone",3120],["child_medium_light_skin_tone",3121],["child_medium_skin_tone",3122],["child_medium_dark_skin_tone",3123],["child_dark_skin_tone",3124],["older_adult_light_skin_tone",3125],["older_adult_medium_light_skin_tone",3126],["older_adult_medium_skin_tone",3127],["older_adult_medium_dark_skin_tone",3128],["older_adult_dark_skin_tone",3129],["bearded_person_light_skin_tone",3130],["bearded_person_medium_light_skin_tone",3131],["bearded_person_medium_skin_tone",3132],["bearded_person_medium_dark_skin_tone",3133],["bearded_person_dark_skin_tone",3134],["person_with_headscarf_light_skin_tone",3135],["person_with_headscarf_medium_light_skin_tone",3136],["person_with_headscarf_medium_skin_tone",3137],["person_with_headscarf_medium_dark_skin_tone",3138],["person_with_headscarf_dark_skin_tone",3139],["woman_in_steamy_room_light_skin_tone",3140],["woman_in_steamy_room_medium_light_skin_tone",3141],["woman_in_steamy_room_medium_skin_tone",3142],["woman_in_steamy_room_medium_dark_skin_tone",3143],["woman_in_steamy_room_dark_skin_tone",3144],["man_in_steamy_room_light_skin_tone",3145],["man_in_steamy_room_medium_light_skin_tone",3146],["man_in_steamy_room_medium_skin_tone",3147],["man_in_steamy_room_medium_dark_skin_tone",3148],["man_in_steamy_room_dark_skin_tone",3149],["person_in_steamy_room_light_skin_tone",3150],["person_in_steamy_room_medium_light_skin_tone",3151],["person_in_steamy_room_medium_skin_tone",3152],["person_in_steamy_room_medium_dark_skin_tone",3153],["person_in_steamy_room_dark_skin_tone",3154],["woman_climbing_light_skin_tone",3155],["woman_climbing_medium_light_skin_tone",3156],["woman_climbing_medium_skin_tone",3157],["woman_climbing_medium_dark_skin_tone",3158],["woman_climbing_dark_skin_tone",3159],["man_climbing_light_skin_tone",3160],["man_climbing_medium_light_skin_tone",3161],["man_climbing_medium_skin_tone",3162],["man_climbing_medium_dark_skin_tone",3163],["man_climbing_dark_skin_tone",3164],["person_climbing_light_skin_tone",3165],["person_climbing_medium_light_skin_tone",3166],["person_climbing_medium_skin_tone",3167],["person_climbing_medium_dark_skin_tone",3168],["person_climbing_dark_skin_tone",3169],["woman_in_lotus_position_light_skin_tone",3170],["woman_in_lotus_position_medium_light_skin_tone",3171],["woman_in_lotus_position_medium_skin_tone",3172],["woman_in_lotus_position_medium_dark_skin_tone",3173],["woman_in_lotus_position_dark_skin_tone",3174],["man_in_lotus_position_light_skin_tone",3175],["man_in_lotus_position_medium_light_skin_tone",3176],["man_in_lotus_position_medium_skin_tone",3177],["man_in_lotus_position_medium_dark_skin_tone",3178],["man_in_lotus_position_dark_skin_tone",3179],["person_in_lotus_position_light_skin_tone",3180],["person_in_lotus_position_medium_light_skin_tone",3181],["person_in_lotus_position_medium_skin_tone",3182],["person_in_lotus_position_medium_dark_skin_tone",3183],["person_in_lotus_position_dark_skin_tone",3184],["female_mage_light_skin_tone",3185],["female_mage_medium_light_skin_tone",3186],["female_mage_medium_skin_tone",3187],["female_mage_medium_dark_skin_tone",3188],["female_mage_dark_skin_tone",3189],["male_mage_light_skin_tone",3190],["male_mage_medium_light_skin_tone",3191],["male_mage_medium_skin_tone",3192],["male_mage_medium_dark_skin_tone",3193],["male_mage_dark_skin_tone",3194],["mage_light_skin_tone",3195],["mage_medium_light_skin_tone",3196],["mage_medium_skin_tone",3197],["mage_medium_dark_skin_tone",3198],["mage_dark_skin_tone",3199],["female_fairy_light_skin_tone",3200],["female_fairy_medium_light_skin_tone",3201],["female_fairy_medium_skin_tone",3202],["female_fairy_medium_dark_skin_tone",3203],["female_fairy_dark_skin_tone",3204],["male_fairy_light_skin_tone",3205],["male_fairy_medium_light_skin_tone",3206],["male_fairy_medium_skin_tone",3207],["male_fairy_medium_dark_skin_tone",3208],["male_fairy_dark_skin_tone",3209],["fairy_light_skin_tone",3210],["fairy_medium_light_skin_tone",3211],["fairy_medium_skin_tone",3212],["fairy_medium_dark_skin_tone",3213],["fairy_dark_skin_tone",3214],["female_vampire_light_skin_tone",3215],["female_vampire_medium_light_skin_tone",3216],["female_vampire_medium_skin_tone",3217],["female_vampire_medium_dark_skin_tone",3218],["female_vampire_dark_skin_tone",3219],["male_vampire_light_skin_tone",3220],["male_vampire_medium_light_skin_tone",3221],["male_vampire_medium_skin_tone",3222],["male_vampire_medium_dark_skin_tone",3223],["male_vampire_dark_skin_tone",3224],["vampire_light_skin_tone",3225],["vampire_medium_light_skin_tone",3226],["vampire_medium_skin_tone",3227],["vampire_medium_dark_skin_tone",3228],["vampire_dark_skin_tone",3229],["mermaid_light_skin_tone",3230],["mermaid_medium_light_skin_tone",3231],["mermaid_medium_skin_tone",3232],["mermaid_medium_dark_skin_tone",3233],["mermaid_dark_skin_tone",3234],["merman_light_skin_tone",3235],["merman_medium_light_skin_tone",3236],["merman_medium_skin_tone",3237],["merman_medium_dark_skin_tone",3238],["merman_dark_skin_tone",3239],["merperson_light_skin_tone",3240],["merperson_medium_light_skin_tone",3241],["merperson_medium_skin_tone",3242],["merperson_medium_dark_skin_tone",3243],["merperson_dark_skin_tone",3244],["female_elf_light_skin_tone",3245],["female_elf_medium_light_skin_tone",3246],["female_elf_medium_skin_tone",3247],["female_elf_medium_dark_skin_tone",3248],["female_elf_dark_skin_tone",3249],["male_elf_light_skin_tone",3250],["male_elf_medium_light_skin_tone",3251],["male_elf_medium_skin_tone",3252],["male_elf_medium_dark_skin_tone",3253],["male_elf_dark_skin_tone",3254],["elf_light_skin_tone",3255],["elf_medium_light_skin_tone",3256],["elf_medium_skin_tone",3257],["elf_medium_dark_skin_tone",3258],["elf_dark_skin_tone",3259],["point_up_light_skin_tone",3260],["point_up_medium_light_skin_tone",3261],["point_up_medium_skin_tone",3262],["point_up_medium_dark_skin_tone",3263],["point_up_dark_skin_tone",3264],["woman-bouncing-ball_light_skin_tone",3265],["basketball_woman_light_skin_tone",3265],["woman-bouncing-ball_medium_light_skin_tone",3266],["basketball_woman_medium_light_skin_tone",3266],["woman-bouncing-ball_medium_skin_tone",3267],["basketball_woman_medium_skin_tone",3267],["woman-bouncing-ball_medium_dark_skin_tone",3268],["basketball_woman_medium_dark_skin_tone",3268],["woman-bouncing-ball_dark_skin_tone",3269],["basketball_woman_dark_skin_tone",3269],["man-bouncing-ball_light_skin_tone",3270],["basketball_man_light_skin_tone",3270],["man-bouncing-ball_medium_light_skin_tone",3271],["basketball_man_medium_light_skin_tone",3271],["man-bouncing-ball_medium_skin_tone",3272],["basketball_man_medium_skin_tone",3272],["man-bouncing-ball_medium_dark_skin_tone",3273],["basketball_man_medium_dark_skin_tone",3273],["man-bouncing-ball_dark_skin_tone",3274],["basketball_man_dark_skin_tone",3274],["person_with_ball_light_skin_tone",3275],["person_with_ball_medium_light_skin_tone",3276],["person_with_ball_medium_skin_tone",3277],["person_with_ball_medium_dark_skin_tone",3278],["person_with_ball_dark_skin_tone",3279],["fist_light_skin_tone",3280],["fist_raised_light_skin_tone",3280],["fist_medium_light_skin_tone",3281],["fist_raised_medium_light_skin_tone",3281],["fist_medium_skin_tone",3282],["fist_raised_medium_skin_tone",3282],["fist_medium_dark_skin_tone",3283],["fist_raised_medium_dark_skin_tone",3283],["fist_dark_skin_tone",3284],["fist_raised_dark_skin_tone",3284],["hand_light_skin_tone",3285],["raised_hand_light_skin_tone",3285],["hand_medium_light_skin_tone",3286],["raised_hand_medium_light_skin_tone",3286],["hand_medium_skin_tone",3287],["raised_hand_medium_skin_tone",3287],["hand_medium_dark_skin_tone",3288],["raised_hand_medium_dark_skin_tone",3288],["hand_dark_skin_tone",3289],["raised_hand_dark_skin_tone",3289],["v_light_skin_tone",3290],["v_medium_light_skin_tone",3291],["v_medium_skin_tone",3292],["v_medium_dark_skin_tone",3293],["v_dark_skin_tone",3294],["writing_hand_light_skin_tone",3295],["writing_hand_medium_light_skin_tone",3296],["writing_hand_medium_skin_tone",3297],["writing_hand_medium_dark_skin_tone",3298],["writing_hand_dark_skin_tone",3299],["mattermost",3300]]); + +export const EmojiIndicesByUnicode = new Map([["1f600",0],["1f603",1],["1f604",2],["1f601",3],["1f606",4],["1f605",5],["1f923",6],["1f602",7],["1f642",8],["1f643",9],["1f609",10],["1f60a",11],["1f607",12],["1f970",13],["1f60d",14],["1f929",15],["1f618",16],["1f617",17],["263a-fe0f",18],["1f61a",19],["1f619",20],["1f972",21],["1f60b",22],["1f61b",23],["1f61c",24],["1f92a",25],["1f61d",26],["1f911",27],["1f917",28],["1f92d",29],["1f92b",30],["1f914",31],["1f910",32],["1f928",33],["1f610",34],["1f611",35],["1f636",36],["1f60f",37],["1f612",38],["1f644",39],["1f62c",40],["1f925",41],["1f60c",42],["1f614",43],["1f62a",44],["1f924",45],["1f634",46],["1f637",47],["1f912",48],["1f915",49],["1f922",50],["1f92e",51],["1f927",52],["1f975",53],["1f976",54],["1f974",55],["1f635",56],["1f92f",57],["1f920",58],["1f973",59],["1f978",60],["1f60e",61],["1f913",62],["1f9d0",63],["1f615",64],["1f61f",65],["1f641",66],["2639-fe0f",67],["1f62e",68],["1f62f",69],["1f632",70],["1f633",71],["1f97a",72],["1f626",73],["1f627",74],["1f628",75],["1f630",76],["1f625",77],["1f622",78],["1f62d",79],["1f631",80],["1f616",81],["1f623",82],["1f61e",83],["1f613",84],["1f629",85],["1f62b",86],["1f971",87],["1f624",88],["1f621",89],["1f620",90],["1f92c",91],["1f608",92],["1f47f",93],["1f480",94],["2620-fe0f",95],["1f4a9",96],["1f921",97],["1f479",98],["1f47a",99],["1f47b",100],["1f47d",101],["1f47e",102],["1f916",103],["1f63a",104],["1f638",105],["1f639",106],["1f63b",107],["1f63c",108],["1f63d",109],["1f640",110],["1f63f",111],["1f63e",112],["1f648",113],["1f649",114],["1f64a",115],["1f48b",116],["1f48c",117],["1f498",118],["1f49d",119],["1f496",120],["1f497",121],["1f493",122],["1f49e",123],["1f495",124],["1f49f",125],["2763-fe0f",126],["1f494",127],["2764-fe0f",128],["1f9e1",129],["1f49b",130],["1f49a",131],["1f499",132],["1f49c",133],["1f90e",134],["1f5a4",135],["1f90d",136],["1f4af",137],["1f4a2",138],["1f4a5",139],["1f4ab",140],["1f4a6",141],["1f4a8",142],["1f573-fe0f",143],["1f4a3",144],["1f4ac",145],["1f441-fe0f-200d-1f5e8-fe0f",146],["1f5e8-fe0f",147],["1f5ef-fe0f",148],["1f4ad",149],["1f4a4",150],["1f44b",151],["1f91a",152],["1f590-fe0f",153],["270b",154],["1f596",155],["1f44c",156],["1f90c",157],["1f90f",158],["270c-fe0f",159],["1f91e",160],["1f91f",161],["1f918",162],["1f919",163],["1f448",164],["1f449",165],["1f446",166],["1f595",167],["1f447",168],["261d-fe0f",169],["1f44d",170],["1f44e",171],["270a",172],["1f44a",173],["1f91b",174],["1f91c",175],["1f44f",176],["1f64c",177],["1f450",178],["1f932",179],["1f91d",180],["1f64f",181],["270d-fe0f",182],["1f485",183],["1f933",184],["1f4aa",185],["1f9be",186],["1f9bf",187],["1f9b5",188],["1f9b6",189],["1f442",190],["1f9bb",191],["1f443",192],["1f9e0",193],["1fac0",194],["1fac1",195],["1f9b7",196],["1f9b4",197],["1f440",198],["1f441-fe0f",199],["1f445",200],["1f444",201],["1f476",202],["1f9d2",203],["1f466",204],["1f467",205],["1f9d1",206],["1f471",207],["1f468",208],["1f9d4",209],["1f468-200d-1f9b0",210],["1f468-200d-1f9b1",211],["1f468-200d-1f9b3",212],["1f468-200d-1f9b2",213],["1f469",214],["1f469-200d-1f9b0",215],["1f9d1-200d-1f9b0",216],["1f469-200d-1f9b1",217],["1f9d1-200d-1f9b1",218],["1f469-200d-1f9b3",219],["1f9d1-200d-1f9b3",220],["1f469-200d-1f9b2",221],["1f9d1-200d-1f9b2",222],["1f471-200d-2640-fe0f",223],["1f471-200d-2642-fe0f",224],["1f9d3",225],["1f474",226],["1f475",227],["1f64d",228],["1f64d-200d-2642-fe0f",229],["1f64d-200d-2640-fe0f",230],["1f64e",231],["1f64e-200d-2642-fe0f",232],["1f64e-200d-2640-fe0f",233],["1f645",234],["1f645-200d-2642-fe0f",235],["1f645-200d-2640-fe0f",236],["1f646",237],["1f646-200d-2642-fe0f",238],["1f646-200d-2640-fe0f",239],["1f481",240],["1f481-200d-2642-fe0f",241],["1f481-200d-2640-fe0f",242],["1f64b",243],["1f64b-200d-2642-fe0f",244],["1f64b-200d-2640-fe0f",245],["1f9cf",246],["1f9cf-200d-2642-fe0f",247],["1f9cf-200d-2640-fe0f",248],["1f647",249],["1f647-200d-2642-fe0f",250],["1f647-200d-2640-fe0f",251],["1f926",252],["1f926-200d-2642-fe0f",253],["1f926-200d-2640-fe0f",254],["1f937",255],["1f937-200d-2642-fe0f",256],["1f937-200d-2640-fe0f",257],["1f9d1-200d-2695-fe0f",258],["1f468-200d-2695-fe0f",259],["1f469-200d-2695-fe0f",260],["1f9d1-200d-1f393",261],["1f468-200d-1f393",262],["1f469-200d-1f393",263],["1f9d1-200d-1f3eb",264],["1f468-200d-1f3eb",265],["1f469-200d-1f3eb",266],["1f9d1-200d-2696-fe0f",267],["1f468-200d-2696-fe0f",268],["1f469-200d-2696-fe0f",269],["1f9d1-200d-1f33e",270],["1f468-200d-1f33e",271],["1f469-200d-1f33e",272],["1f9d1-200d-1f373",273],["1f468-200d-1f373",274],["1f469-200d-1f373",275],["1f9d1-200d-1f527",276],["1f468-200d-1f527",277],["1f469-200d-1f527",278],["1f9d1-200d-1f3ed",279],["1f468-200d-1f3ed",280],["1f469-200d-1f3ed",281],["1f9d1-200d-1f4bc",282],["1f468-200d-1f4bc",283],["1f469-200d-1f4bc",284],["1f9d1-200d-1f52c",285],["1f468-200d-1f52c",286],["1f469-200d-1f52c",287],["1f9d1-200d-1f4bb",288],["1f468-200d-1f4bb",289],["1f469-200d-1f4bb",290],["1f9d1-200d-1f3a4",291],["1f468-200d-1f3a4",292],["1f469-200d-1f3a4",293],["1f9d1-200d-1f3a8",294],["1f468-200d-1f3a8",295],["1f469-200d-1f3a8",296],["1f9d1-200d-2708-fe0f",297],["1f468-200d-2708-fe0f",298],["1f469-200d-2708-fe0f",299],["1f9d1-200d-1f680",300],["1f468-200d-1f680",301],["1f469-200d-1f680",302],["1f9d1-200d-1f692",303],["1f468-200d-1f692",304],["1f469-200d-1f692",305],["1f46e",306],["1f46e-200d-2642-fe0f",307],["1f46e-200d-2640-fe0f",308],["1f575-fe0f",309],["1f575-fe0f-200d-2642-fe0f",310],["1f575-fe0f-200d-2640-fe0f",311],["1f482",312],["1f482-200d-2642-fe0f",313],["1f482-200d-2640-fe0f",314],["1f977",315],["1f477",316],["1f477-200d-2642-fe0f",317],["1f477-200d-2640-fe0f",318],["1f934",319],["1f478",320],["1f473",321],["1f473-200d-2642-fe0f",322],["1f473-200d-2640-fe0f",323],["1f472",324],["1f9d5",325],["1f935",326],["1f935-200d-2642-fe0f",327],["1f935-200d-2640-fe0f",328],["1f470",329],["1f470-200d-2642-fe0f",330],["1f470-200d-2640-fe0f",331],["1f930",332],["1f931",333],["1f469-200d-1f37c",334],["1f468-200d-1f37c",335],["1f9d1-200d-1f37c",336],["1f47c",337],["1f385",338],["1f936",339],["1f9d1-200d-1f384",340],["1f9b8",341],["1f9b8-200d-2642-fe0f",342],["1f9b8-200d-2640-fe0f",343],["1f9b9",344],["1f9b9-200d-2642-fe0f",345],["1f9b9-200d-2640-fe0f",346],["1f9d9",347],["1f9d9-200d-2642-fe0f",348],["1f9d9-200d-2640-fe0f",349],["1f9da",350],["1f9da-200d-2642-fe0f",351],["1f9da-200d-2640-fe0f",352],["1f9db",353],["1f9db-200d-2642-fe0f",354],["1f9db-200d-2640-fe0f",355],["1f9dc",356],["1f9dc-200d-2642-fe0f",357],["1f9dc-200d-2640-fe0f",358],["1f9dd",359],["1f9dd-200d-2642-fe0f",360],["1f9dd-200d-2640-fe0f",361],["1f9de",362],["1f9de-200d-2642-fe0f",363],["1f9de-200d-2640-fe0f",364],["1f9df",365],["1f9df-200d-2642-fe0f",366],["1f9df-200d-2640-fe0f",367],["1f486",368],["1f486-200d-2642-fe0f",369],["1f486-200d-2640-fe0f",370],["1f487",371],["1f487-200d-2642-fe0f",372],["1f487-200d-2640-fe0f",373],["1f6b6",374],["1f6b6-200d-2642-fe0f",375],["1f6b6-200d-2640-fe0f",376],["1f9cd",377],["1f9cd-200d-2642-fe0f",378],["1f9cd-200d-2640-fe0f",379],["1f9ce",380],["1f9ce-200d-2642-fe0f",381],["1f9ce-200d-2640-fe0f",382],["1f9d1-200d-1f9af",383],["1f468-200d-1f9af",384],["1f469-200d-1f9af",385],["1f9d1-200d-1f9bc",386],["1f468-200d-1f9bc",387],["1f469-200d-1f9bc",388],["1f9d1-200d-1f9bd",389],["1f468-200d-1f9bd",390],["1f469-200d-1f9bd",391],["1f3c3",392],["1f3c3-200d-2642-fe0f",393],["1f3c3-200d-2640-fe0f",394],["1f483",395],["1f57a",396],["1f574-fe0f",397],["1f46f",398],["1f46f-200d-2642-fe0f",399],["1f46f-200d-2640-fe0f",400],["1f9d6",401],["1f9d6-200d-2642-fe0f",402],["1f9d6-200d-2640-fe0f",403],["1f9d7",404],["1f9d7-200d-2642-fe0f",405],["1f9d7-200d-2640-fe0f",406],["1f93a",407],["1f3c7",408],["26f7-fe0f",409],["1f3c2",410],["1f3cc-fe0f",411],["1f3cc-fe0f-200d-2642-fe0f",412],["1f3cc-fe0f-200d-2640-fe0f",413],["1f3c4",414],["1f3c4-200d-2642-fe0f",415],["1f3c4-200d-2640-fe0f",416],["1f6a3",417],["1f6a3-200d-2642-fe0f",418],["1f6a3-200d-2640-fe0f",419],["1f3ca",420],["1f3ca-200d-2642-fe0f",421],["1f3ca-200d-2640-fe0f",422],["26f9-fe0f",423],["26f9-fe0f-200d-2642-fe0f",424],["26f9-fe0f-200d-2640-fe0f",425],["1f3cb-fe0f",426],["1f3cb-fe0f-200d-2642-fe0f",427],["1f3cb-fe0f-200d-2640-fe0f",428],["1f6b4",429],["1f6b4-200d-2642-fe0f",430],["1f6b4-200d-2640-fe0f",431],["1f6b5",432],["1f6b5-200d-2642-fe0f",433],["1f6b5-200d-2640-fe0f",434],["1f938",435],["1f938-200d-2642-fe0f",436],["1f938-200d-2640-fe0f",437],["1f93c",438],["1f93c-200d-2642-fe0f",439],["1f93c-200d-2640-fe0f",440],["1f93d",441],["1f93d-200d-2642-fe0f",442],["1f93d-200d-2640-fe0f",443],["1f93e",444],["1f93e-200d-2642-fe0f",445],["1f93e-200d-2640-fe0f",446],["1f939",447],["1f939-200d-2642-fe0f",448],["1f939-200d-2640-fe0f",449],["1f9d8",450],["1f9d8-200d-2642-fe0f",451],["1f9d8-200d-2640-fe0f",452],["1f6c0",453],["1f6cc",454],["1f9d1-200d-1f91d-200d-1f9d1",455],["1f46d",456],["1f46b",457],["1f46c",458],["1f48f",459],["1f469-200d-2764-fe0f-200d-1f48b-200d-1f468",460],["1f468-200d-2764-fe0f-200d-1f48b-200d-1f468",461],["1f469-200d-2764-fe0f-200d-1f48b-200d-1f469",462],["1f491",463],["1f469-200d-2764-fe0f-200d-1f468",464],["1f468-200d-2764-fe0f-200d-1f468",465],["1f469-200d-2764-fe0f-200d-1f469",466],["1f46a",467],["1f468-200d-1f469-200d-1f466",468],["1f468-200d-1f469-200d-1f467",469],["1f468-200d-1f469-200d-1f467-200d-1f466",470],["1f468-200d-1f469-200d-1f466-200d-1f466",471],["1f468-200d-1f469-200d-1f467-200d-1f467",472],["1f468-200d-1f468-200d-1f466",473],["1f468-200d-1f468-200d-1f467",474],["1f468-200d-1f468-200d-1f467-200d-1f466",475],["1f468-200d-1f468-200d-1f466-200d-1f466",476],["1f468-200d-1f468-200d-1f467-200d-1f467",477],["1f469-200d-1f469-200d-1f466",478],["1f469-200d-1f469-200d-1f467",479],["1f469-200d-1f469-200d-1f467-200d-1f466",480],["1f469-200d-1f469-200d-1f466-200d-1f466",481],["1f469-200d-1f469-200d-1f467-200d-1f467",482],["1f468-200d-1f466",483],["1f468-200d-1f466-200d-1f466",484],["1f468-200d-1f467",485],["1f468-200d-1f467-200d-1f466",486],["1f468-200d-1f467-200d-1f467",487],["1f469-200d-1f466",488],["1f469-200d-1f466-200d-1f466",489],["1f469-200d-1f467",490],["1f469-200d-1f467-200d-1f466",491],["1f469-200d-1f467-200d-1f467",492],["1f5e3-fe0f",493],["1f464",494],["1f465",495],["1fac2",496],["1f463",497],["1f3fb",498],["1f3fc",499],["1f3fd",500],["1f3fe",501],["1f3ff",502],["1f435",503],["1f412",504],["1f98d",505],["1f9a7",506],["1f436",507],["1f415",508],["1f9ae",509],["1f415-200d-1f9ba",510],["1f429",511],["1f43a",512],["1f98a",513],["1f99d",514],["1f431",515],["1f408",516],["1f408-200d-2b1b",517],["1f981",518],["1f42f",519],["1f405",520],["1f406",521],["1f434",522],["1f40e",523],["1f984",524],["1f993",525],["1f98c",526],["1f9ac",527],["1f42e",528],["1f402",529],["1f403",530],["1f404",531],["1f437",532],["1f416",533],["1f417",534],["1f43d",535],["1f40f",536],["1f411",537],["1f410",538],["1f42a",539],["1f42b",540],["1f999",541],["1f992",542],["1f418",543],["1f9a3",544],["1f98f",545],["1f99b",546],["1f42d",547],["1f401",548],["1f400",549],["1f439",550],["1f430",551],["1f407",552],["1f43f-fe0f",553],["1f9ab",554],["1f994",555],["1f987",556],["1f43b",557],["1f43b-200d-2744-fe0f",558],["1f428",559],["1f43c",560],["1f9a5",561],["1f9a6",562],["1f9a8",563],["1f998",564],["1f9a1",565],["1f43e",566],["1f983",567],["1f414",568],["1f413",569],["1f423",570],["1f424",571],["1f425",572],["1f426",573],["1f427",574],["1f54a-fe0f",575],["1f985",576],["1f986",577],["1f9a2",578],["1f989",579],["1f9a4",580],["1fab6",581],["1f9a9",582],["1f99a",583],["1f99c",584],["1f438",585],["1f40a",586],["1f422",587],["1f98e",588],["1f40d",589],["1f432",590],["1f409",591],["1f995",592],["1f996",593],["1f433",594],["1f40b",595],["1f42c",596],["1f9ad",597],["1f41f",598],["1f420",599],["1f421",600],["1f988",601],["1f419",602],["1f41a",603],["1f40c",604],["1f98b",605],["1f41b",606],["1f41c",607],["1f41d",608],["1fab2",609],["1f41e",610],["1f997",611],["1fab3",612],["1f577-fe0f",613],["1f578-fe0f",614],["1f982",615],["1f99f",616],["1fab0",617],["1fab1",618],["1f9a0",619],["1f490",620],["1f338",621],["1f4ae",622],["1f3f5-fe0f",623],["1f339",624],["1f940",625],["1f33a",626],["1f33b",627],["1f33c",628],["1f337",629],["1f331",630],["1fab4",631],["1f332",632],["1f333",633],["1f334",634],["1f335",635],["1f33e",636],["1f33f",637],["2618-fe0f",638],["1f340",639],["1f341",640],["1f342",641],["1f343",642],["1f347",643],["1f348",644],["1f349",645],["1f34a",646],["1f34b",647],["1f34c",648],["1f34d",649],["1f96d",650],["1f34e",651],["1f34f",652],["1f350",653],["1f351",654],["1f352",655],["1f353",656],["1fad0",657],["1f95d",658],["1f345",659],["1fad2",660],["1f965",661],["1f951",662],["1f346",663],["1f954",664],["1f955",665],["1f33d",666],["1f336-fe0f",667],["1fad1",668],["1f952",669],["1f96c",670],["1f966",671],["1f9c4",672],["1f9c5",673],["1f344",674],["1f95c",675],["1f330",676],["1f35e",677],["1f950",678],["1f956",679],["1fad3",680],["1f968",681],["1f96f",682],["1f95e",683],["1f9c7",684],["1f9c0",685],["1f356",686],["1f357",687],["1f969",688],["1f953",689],["1f354",690],["1f35f",691],["1f355",692],["1f32d",693],["1f96a",694],["1f32e",695],["1f32f",696],["1fad4",697],["1f959",698],["1f9c6",699],["1f95a",700],["1f373",701],["1f958",702],["1f372",703],["1fad5",704],["1f963",705],["1f957",706],["1f37f",707],["1f9c8",708],["1f9c2",709],["1f96b",710],["1f371",711],["1f358",712],["1f359",713],["1f35a",714],["1f35b",715],["1f35c",716],["1f35d",717],["1f360",718],["1f362",719],["1f363",720],["1f364",721],["1f365",722],["1f96e",723],["1f361",724],["1f95f",725],["1f960",726],["1f961",727],["1f980",728],["1f99e",729],["1f990",730],["1f991",731],["1f9aa",732],["1f366",733],["1f367",734],["1f368",735],["1f369",736],["1f36a",737],["1f382",738],["1f370",739],["1f9c1",740],["1f967",741],["1f36b",742],["1f36c",743],["1f36d",744],["1f36e",745],["1f36f",746],["1f37c",747],["1f95b",748],["2615",749],["1fad6",750],["1f375",751],["1f376",752],["1f37e",753],["1f377",754],["1f378",755],["1f379",756],["1f37a",757],["1f37b",758],["1f942",759],["1f943",760],["1f964",761],["1f9cb",762],["1f9c3",763],["1f9c9",764],["1f9ca",765],["1f962",766],["1f37d-fe0f",767],["1f374",768],["1f944",769],["1f52a",770],["1f3fa",771],["1f30d",772],["1f30e",773],["1f30f",774],["1f310",775],["1f5fa-fe0f",776],["1f5fe",777],["1f9ed",778],["1f3d4-fe0f",779],["26f0-fe0f",780],["1f30b",781],["1f5fb",782],["1f3d5-fe0f",783],["1f3d6-fe0f",784],["1f3dc-fe0f",785],["1f3dd-fe0f",786],["1f3de-fe0f",787],["1f3df-fe0f",788],["1f3db-fe0f",789],["1f3d7-fe0f",790],["1f9f1",791],["1faa8",792],["1fab5",793],["1f6d6",794],["1f3d8-fe0f",795],["1f3da-fe0f",796],["1f3e0",797],["1f3e1",798],["1f3e2",799],["1f3e3",800],["1f3e4",801],["1f3e5",802],["1f3e6",803],["1f3e8",804],["1f3e9",805],["1f3ea",806],["1f3eb",807],["1f3ec",808],["1f3ed",809],["1f3ef",810],["1f3f0",811],["1f492",812],["1f5fc",813],["1f5fd",814],["26ea",815],["1f54c",816],["1f6d5",817],["1f54d",818],["26e9-fe0f",819],["1f54b",820],["26f2",821],["26fa",822],["1f301",823],["1f303",824],["1f3d9-fe0f",825],["1f304",826],["1f305",827],["1f306",828],["1f307",829],["1f309",830],["2668-fe0f",831],["1f3a0",832],["1f3a1",833],["1f3a2",834],["1f488",835],["1f3aa",836],["1f682",837],["1f683",838],["1f684",839],["1f685",840],["1f686",841],["1f687",842],["1f688",843],["1f689",844],["1f68a",845],["1f69d",846],["1f69e",847],["1f68b",848],["1f68c",849],["1f68d",850],["1f68e",851],["1f690",852],["1f691",853],["1f692",854],["1f693",855],["1f694",856],["1f695",857],["1f696",858],["1f697",859],["1f698",860],["1f699",861],["1f6fb",862],["1f69a",863],["1f69b",864],["1f69c",865],["1f3ce-fe0f",866],["1f3cd-fe0f",867],["1f6f5",868],["1f9bd",869],["1f9bc",870],["1f6fa",871],["1f6b2",872],["1f6f4",873],["1f6f9",874],["1f6fc",875],["1f68f",876],["1f6e3-fe0f",877],["1f6e4-fe0f",878],["1f6e2-fe0f",879],["26fd",880],["1f6a8",881],["1f6a5",882],["1f6a6",883],["1f6d1",884],["1f6a7",885],["2693",886],["26f5",887],["1f6f6",888],["1f6a4",889],["1f6f3-fe0f",890],["26f4-fe0f",891],["1f6e5-fe0f",892],["1f6a2",893],["2708-fe0f",894],["1f6e9-fe0f",895],["1f6eb",896],["1f6ec",897],["1fa82",898],["1f4ba",899],["1f681",900],["1f69f",901],["1f6a0",902],["1f6a1",903],["1f6f0-fe0f",904],["1f680",905],["1f6f8",906],["1f6ce-fe0f",907],["1f9f3",908],["231b",909],["23f3",910],["231a",911],["23f0",912],["23f1-fe0f",913],["23f2-fe0f",914],["1f570-fe0f",915],["1f55b",916],["1f567",917],["1f550",918],["1f55c",919],["1f551",920],["1f55d",921],["1f552",922],["1f55e",923],["1f553",924],["1f55f",925],["1f554",926],["1f560",927],["1f555",928],["1f561",929],["1f556",930],["1f562",931],["1f557",932],["1f563",933],["1f558",934],["1f564",935],["1f559",936],["1f565",937],["1f55a",938],["1f566",939],["1f311",940],["1f312",941],["1f313",942],["1f314",943],["1f315",944],["1f316",945],["1f317",946],["1f318",947],["1f319",948],["1f31a",949],["1f31b",950],["1f31c",951],["1f321-fe0f",952],["2600-fe0f",953],["1f31d",954],["1f31e",955],["1fa90",956],["2b50",957],["1f31f",958],["1f320",959],["1f30c",960],["2601-fe0f",961],["26c5",962],["26c8-fe0f",963],["1f324-fe0f",964],["1f325-fe0f",965],["1f326-fe0f",966],["1f327-fe0f",967],["1f328-fe0f",968],["1f329-fe0f",969],["1f32a-fe0f",970],["1f32b-fe0f",971],["1f32c-fe0f",972],["1f300",973],["1f308",974],["1f302",975],["2602-fe0f",976],["2614",977],["26f1-fe0f",978],["26a1",979],["2744-fe0f",980],["2603-fe0f",981],["26c4",982],["2604-fe0f",983],["1f525",984],["1f4a7",985],["1f30a",986],["1f383",987],["1f384",988],["1f386",989],["1f387",990],["1f9e8",991],["2728",992],["1f388",993],["1f389",994],["1f38a",995],["1f38b",996],["1f38d",997],["1f38e",998],["1f38f",999],["1f390",1000],["1f391",1001],["1f9e7",1002],["1f380",1003],["1f381",1004],["1f397-fe0f",1005],["1f39f-fe0f",1006],["1f3ab",1007],["1f396-fe0f",1008],["1f3c6",1009],["1f3c5",1010],["1f947",1011],["1f948",1012],["1f949",1013],["26bd",1014],["26be",1015],["1f94e",1016],["1f3c0",1017],["1f3d0",1018],["1f3c8",1019],["1f3c9",1020],["1f3be",1021],["1f94f",1022],["1f3b3",1023],["1f3cf",1024],["1f3d1",1025],["1f3d2",1026],["1f94d",1027],["1f3d3",1028],["1f3f8",1029],["1f94a",1030],["1f94b",1031],["1f945",1032],["26f3",1033],["26f8-fe0f",1034],["1f3a3",1035],["1f93f",1036],["1f3bd",1037],["1f3bf",1038],["1f6f7",1039],["1f94c",1040],["1f3af",1041],["1fa80",1042],["1fa81",1043],["1f3b1",1044],["1f52e",1045],["1fa84",1046],["1f9ff",1047],["1f3ae",1048],["1f579-fe0f",1049],["1f3b0",1050],["1f3b2",1051],["1f9e9",1052],["1f9f8",1053],["1fa85",1054],["1fa86",1055],["2660-fe0f",1056],["2665-fe0f",1057],["2666-fe0f",1058],["2663-fe0f",1059],["265f-fe0f",1060],["1f0cf",1061],["1f004",1062],["1f3b4",1063],["1f3ad",1064],["1f5bc-fe0f",1065],["1f3a8",1066],["1f9f5",1067],["1faa1",1068],["1f9f6",1069],["1faa2",1070],["1f453",1071],["1f576-fe0f",1072],["1f97d",1073],["1f97c",1074],["1f9ba",1075],["1f454",1076],["1f455",1077],["1f456",1078],["1f9e3",1079],["1f9e4",1080],["1f9e5",1081],["1f9e6",1082],["1f457",1083],["1f458",1084],["1f97b",1085],["1fa71",1086],["1fa72",1087],["1fa73",1088],["1f459",1089],["1f45a",1090],["1f45b",1091],["1f45c",1092],["1f45d",1093],["1f6cd-fe0f",1094],["1f392",1095],["1fa74",1096],["1f45e",1097],["1f45f",1098],["1f97e",1099],["1f97f",1100],["1f460",1101],["1f461",1102],["1fa70",1103],["1f462",1104],["1f451",1105],["1f452",1106],["1f3a9",1107],["1f393",1108],["1f9e2",1109],["1fa96",1110],["26d1-fe0f",1111],["1f4ff",1112],["1f484",1113],["1f48d",1114],["1f48e",1115],["1f507",1116],["1f508",1117],["1f509",1118],["1f50a",1119],["1f4e2",1120],["1f4e3",1121],["1f4ef",1122],["1f514",1123],["1f515",1124],["1f3bc",1125],["1f3b5",1126],["1f3b6",1127],["1f399-fe0f",1128],["1f39a-fe0f",1129],["1f39b-fe0f",1130],["1f3a4",1131],["1f3a7",1132],["1f4fb",1133],["1f3b7",1134],["1fa97",1135],["1f3b8",1136],["1f3b9",1137],["1f3ba",1138],["1f3bb",1139],["1fa95",1140],["1f941",1141],["1fa98",1142],["1f4f1",1143],["1f4f2",1144],["260e-fe0f",1145],["1f4de",1146],["1f4df",1147],["1f4e0",1148],["1f50b",1149],["1f50c",1150],["1f4bb",1151],["1f5a5-fe0f",1152],["1f5a8-fe0f",1153],["2328-fe0f",1154],["1f5b1-fe0f",1155],["1f5b2-fe0f",1156],["1f4bd",1157],["1f4be",1158],["1f4bf",1159],["1f4c0",1160],["1f9ee",1161],["1f3a5",1162],["1f39e-fe0f",1163],["1f4fd-fe0f",1164],["1f3ac",1165],["1f4fa",1166],["1f4f7",1167],["1f4f8",1168],["1f4f9",1169],["1f4fc",1170],["1f50d",1171],["1f50e",1172],["1f56f-fe0f",1173],["1f4a1",1174],["1f526",1175],["1f3ee",1176],["1fa94",1177],["1f4d4",1178],["1f4d5",1179],["1f4d6",1180],["1f4d7",1181],["1f4d8",1182],["1f4d9",1183],["1f4da",1184],["1f4d3",1185],["1f4d2",1186],["1f4c3",1187],["1f4dc",1188],["1f4c4",1189],["1f4f0",1190],["1f5de-fe0f",1191],["1f4d1",1192],["1f516",1193],["1f3f7-fe0f",1194],["1f4b0",1195],["1fa99",1196],["1f4b4",1197],["1f4b5",1198],["1f4b6",1199],["1f4b7",1200],["1f4b8",1201],["1f4b3",1202],["1f9fe",1203],["1f4b9",1204],["2709-fe0f",1205],["1f4e7",1206],["1f4e8",1207],["1f4e9",1208],["1f4e4",1209],["1f4e5",1210],["1f4e6",1211],["1f4eb",1212],["1f4ea",1213],["1f4ec",1214],["1f4ed",1215],["1f4ee",1216],["1f5f3-fe0f",1217],["270f-fe0f",1218],["2712-fe0f",1219],["1f58b-fe0f",1220],["1f58a-fe0f",1221],["1f58c-fe0f",1222],["1f58d-fe0f",1223],["1f4dd",1224],["1f4bc",1225],["1f4c1",1226],["1f4c2",1227],["1f5c2-fe0f",1228],["1f4c5",1229],["1f4c6",1230],["1f5d2-fe0f",1231],["1f5d3-fe0f",1232],["1f4c7",1233],["1f4c8",1234],["1f4c9",1235],["1f4ca",1236],["1f4cb",1237],["1f4cc",1238],["1f4cd",1239],["1f4ce",1240],["1f587-fe0f",1241],["1f4cf",1242],["1f4d0",1243],["2702-fe0f",1244],["1f5c3-fe0f",1245],["1f5c4-fe0f",1246],["1f5d1-fe0f",1247],["1f512",1248],["1f513",1249],["1f50f",1250],["1f510",1251],["1f511",1252],["1f5dd-fe0f",1253],["1f528",1254],["1fa93",1255],["26cf-fe0f",1256],["2692-fe0f",1257],["1f6e0-fe0f",1258],["1f5e1-fe0f",1259],["2694-fe0f",1260],["1f52b",1261],["1fa83",1262],["1f3f9",1263],["1f6e1-fe0f",1264],["1fa9a",1265],["1f527",1266],["1fa9b",1267],["1f529",1268],["2699-fe0f",1269],["1f5dc-fe0f",1270],["2696-fe0f",1271],["1f9af",1272],["1f517",1273],["26d3-fe0f",1274],["1fa9d",1275],["1f9f0",1276],["1f9f2",1277],["1fa9c",1278],["2697-fe0f",1279],["1f9ea",1280],["1f9eb",1281],["1f9ec",1282],["1f52c",1283],["1f52d",1284],["1f4e1",1285],["1f489",1286],["1fa78",1287],["1f48a",1288],["1fa79",1289],["1fa7a",1290],["1f6aa",1291],["1f6d7",1292],["1fa9e",1293],["1fa9f",1294],["1f6cf-fe0f",1295],["1f6cb-fe0f",1296],["1fa91",1297],["1f6bd",1298],["1faa0",1299],["1f6bf",1300],["1f6c1",1301],["1faa4",1302],["1fa92",1303],["1f9f4",1304],["1f9f7",1305],["1f9f9",1306],["1f9fa",1307],["1f9fb",1308],["1faa3",1309],["1f9fc",1310],["1faa5",1311],["1f9fd",1312],["1f9ef",1313],["1f6d2",1314],["1f6ac",1315],["26b0-fe0f",1316],["1faa6",1317],["26b1-fe0f",1318],["1f5ff",1319],["1faa7",1320],["1f3e7",1321],["1f6ae",1322],["1f6b0",1323],["267f",1324],["1f6b9",1325],["1f6ba",1326],["1f6bb",1327],["1f6bc",1328],["1f6be",1329],["1f6c2",1330],["1f6c3",1331],["1f6c4",1332],["1f6c5",1333],["26a0-fe0f",1334],["1f6b8",1335],["26d4",1336],["1f6ab",1337],["1f6b3",1338],["1f6ad",1339],["1f6af",1340],["1f6b1",1341],["1f6b7",1342],["1f4f5",1343],["1f51e",1344],["2622-fe0f",1345],["2623-fe0f",1346],["2b06-fe0f",1347],["2197-fe0f",1348],["27a1-fe0f",1349],["2198-fe0f",1350],["2b07-fe0f",1351],["2199-fe0f",1352],["2b05-fe0f",1353],["2196-fe0f",1354],["2195-fe0f",1355],["2194-fe0f",1356],["21a9-fe0f",1357],["21aa-fe0f",1358],["2934-fe0f",1359],["2935-fe0f",1360],["1f503",1361],["1f504",1362],["1f519",1363],["1f51a",1364],["1f51b",1365],["1f51c",1366],["1f51d",1367],["1f6d0",1368],["269b-fe0f",1369],["1f549-fe0f",1370],["2721-fe0f",1371],["2638-fe0f",1372],["262f-fe0f",1373],["271d-fe0f",1374],["2626-fe0f",1375],["262a-fe0f",1376],["262e-fe0f",1377],["1f54e",1378],["1f52f",1379],["2648",1380],["2649",1381],["264a",1382],["264b",1383],["264c",1384],["264d",1385],["264e",1386],["264f",1387],["2650",1388],["2651",1389],["2652",1390],["2653",1391],["26ce",1392],["1f500",1393],["1f501",1394],["1f502",1395],["25b6-fe0f",1396],["23e9",1397],["23ed-fe0f",1398],["23ef-fe0f",1399],["25c0-fe0f",1400],["23ea",1401],["23ee-fe0f",1402],["1f53c",1403],["23eb",1404],["1f53d",1405],["23ec",1406],["23f8-fe0f",1407],["23f9-fe0f",1408],["23fa-fe0f",1409],["23cf-fe0f",1410],["1f3a6",1411],["1f505",1412],["1f506",1413],["1f4f6",1414],["1f4f3",1415],["1f4f4",1416],["2640-fe0f",1417],["2642-fe0f",1418],["26a7-fe0f",1419],["2716-fe0f",1420],["2795",1421],["2796",1422],["2797",1423],["267e-fe0f",1424],["203c-fe0f",1425],["2049-fe0f",1426],["2753",1427],["2754",1428],["2755",1429],["2757",1430],["3030-fe0f",1431],["1f4b1",1432],["1f4b2",1433],["2695-fe0f",1434],["267b-fe0f",1435],["269c-fe0f",1436],["1f531",1437],["1f4db",1438],["1f530",1439],["2b55",1440],["2705",1441],["2611-fe0f",1442],["2714-fe0f",1443],["274c",1444],["274e",1445],["27b0",1446],["27bf",1447],["303d-fe0f",1448],["2733-fe0f",1449],["2734-fe0f",1450],["2747-fe0f",1451],["00a9-fe0f",1452],["00ae-fe0f",1453],["2122-fe0f",1454],["0023-fe0f-20e3",1455],["002a-fe0f-20e3",1456],["0030-fe0f-20e3",1457],["0031-fe0f-20e3",1458],["0032-fe0f-20e3",1459],["0033-fe0f-20e3",1460],["0034-fe0f-20e3",1461],["0035-fe0f-20e3",1462],["0036-fe0f-20e3",1463],["0037-fe0f-20e3",1464],["0038-fe0f-20e3",1465],["0039-fe0f-20e3",1466],["1f51f",1467],["1f520",1468],["1f521",1469],["1f522",1470],["1f523",1471],["1f524",1472],["1f170-fe0f",1473],["1f18e",1474],["1f171-fe0f",1475],["1f191",1476],["1f192",1477],["1f193",1478],["2139-fe0f",1479],["1f194",1480],["24c2-fe0f",1481],["1f195",1482],["1f196",1483],["1f17e-fe0f",1484],["1f197",1485],["1f17f-fe0f",1486],["1f198",1487],["1f199",1488],["1f19a",1489],["1f201",1490],["1f202-fe0f",1491],["1f237-fe0f",1492],["1f236",1493],["1f22f",1494],["1f250",1495],["1f239",1496],["1f21a",1497],["1f232",1498],["1f251",1499],["1f238",1500],["1f234",1501],["1f233",1502],["3297-fe0f",1503],["3299-fe0f",1504],["1f23a",1505],["1f235",1506],["1f534",1507],["1f7e0",1508],["1f7e1",1509],["1f7e2",1510],["1f535",1511],["1f7e3",1512],["1f7e4",1513],["26ab",1514],["26aa",1515],["1f7e5",1516],["1f7e7",1517],["1f7e8",1518],["1f7e9",1519],["1f7e6",1520],["1f7ea",1521],["1f7eb",1522],["2b1b",1523],["2b1c",1524],["25fc-fe0f",1525],["25fb-fe0f",1526],["25fe",1527],["25fd",1528],["25aa-fe0f",1529],["25ab-fe0f",1530],["1f536",1531],["1f537",1532],["1f538",1533],["1f539",1534],["1f53a",1535],["1f53b",1536],["1f4a0",1537],["1f518",1538],["1f533",1539],["1f532",1540],["1f3c1",1541],["1f6a9",1542],["1f38c",1543],["1f3f4",1544],["1f3f3-fe0f",1545],["1f3f3-fe0f-200d-1f308",1546],["1f3f3-fe0f-200d-26a7-fe0f",1547],["1f3f4-200d-2620-fe0f",1548],["1f1e6-1f1e8",1549],["1f1e6-1f1e9",1550],["1f1e6-1f1ea",1551],["1f1e6-1f1eb",1552],["1f1e6-1f1ec",1553],["1f1e6-1f1ee",1554],["1f1e6-1f1f1",1555],["1f1e6-1f1f2",1556],["1f1e6-1f1f4",1557],["1f1e6-1f1f6",1558],["1f1e6-1f1f7",1559],["1f1e6-1f1f8",1560],["1f1e6-1f1f9",1561],["1f1e6-1f1fa",1562],["1f1e6-1f1fc",1563],["1f1e6-1f1fd",1564],["1f1e6-1f1ff",1565],["1f1e7-1f1e6",1566],["1f1e7-1f1e7",1567],["1f1e7-1f1e9",1568],["1f1e7-1f1ea",1569],["1f1e7-1f1eb",1570],["1f1e7-1f1ec",1571],["1f1e7-1f1ed",1572],["1f1e7-1f1ee",1573],["1f1e7-1f1ef",1574],["1f1e7-1f1f1",1575],["1f1e7-1f1f2",1576],["1f1e7-1f1f3",1577],["1f1e7-1f1f4",1578],["1f1e7-1f1f6",1579],["1f1e7-1f1f7",1580],["1f1e7-1f1f8",1581],["1f1e7-1f1f9",1582],["1f1e7-1f1fb",1583],["1f1e7-1f1fc",1584],["1f1e7-1f1fe",1585],["1f1e7-1f1ff",1586],["1f1e8-1f1e6",1587],["1f1e8-1f1e8",1588],["1f1e8-1f1e9",1589],["1f1e8-1f1eb",1590],["1f1e8-1f1ec",1591],["1f1e8-1f1ed",1592],["1f1e8-1f1ee",1593],["1f1e8-1f1f0",1594],["1f1e8-1f1f1",1595],["1f1e8-1f1f2",1596],["1f1e8-1f1f3",1597],["1f1e8-1f1f4",1598],["1f1e8-1f1f5",1599],["1f1e8-1f1f7",1600],["1f1e8-1f1fa",1601],["1f1e8-1f1fb",1602],["1f1e8-1f1fc",1603],["1f1e8-1f1fd",1604],["1f1e8-1f1fe",1605],["1f1e8-1f1ff",1606],["1f1e9-1f1ea",1607],["1f1e9-1f1ec",1608],["1f1e9-1f1ef",1609],["1f1e9-1f1f0",1610],["1f1e9-1f1f2",1611],["1f1e9-1f1f4",1612],["1f1e9-1f1ff",1613],["1f1ea-1f1e6",1614],["1f1ea-1f1e8",1615],["1f1ea-1f1ea",1616],["1f1ea-1f1ec",1617],["1f1ea-1f1ed",1618],["1f1ea-1f1f7",1619],["1f1ea-1f1f8",1620],["1f1ea-1f1f9",1621],["1f1ea-1f1fa",1622],["1f1eb-1f1ee",1623],["1f1eb-1f1ef",1624],["1f1eb-1f1f0",1625],["1f1eb-1f1f2",1626],["1f1eb-1f1f4",1627],["1f1eb-1f1f7",1628],["1f1ec-1f1e6",1629],["1f1ec-1f1e7",1630],["1f1ec-1f1e9",1631],["1f1ec-1f1ea",1632],["1f1ec-1f1eb",1633],["1f1ec-1f1ec",1634],["1f1ec-1f1ed",1635],["1f1ec-1f1ee",1636],["1f1ec-1f1f1",1637],["1f1ec-1f1f2",1638],["1f1ec-1f1f3",1639],["1f1ec-1f1f5",1640],["1f1ec-1f1f6",1641],["1f1ec-1f1f7",1642],["1f1ec-1f1f8",1643],["1f1ec-1f1f9",1644],["1f1ec-1f1fa",1645],["1f1ec-1f1fc",1646],["1f1ec-1f1fe",1647],["1f1ed-1f1f0",1648],["1f1ed-1f1f2",1649],["1f1ed-1f1f3",1650],["1f1ed-1f1f7",1651],["1f1ed-1f1f9",1652],["1f1ed-1f1fa",1653],["1f1ee-1f1e8",1654],["1f1ee-1f1e9",1655],["1f1ee-1f1ea",1656],["1f1ee-1f1f1",1657],["1f1ee-1f1f2",1658],["1f1ee-1f1f3",1659],["1f1ee-1f1f4",1660],["1f1ee-1f1f6",1661],["1f1ee-1f1f7",1662],["1f1ee-1f1f8",1663],["1f1ee-1f1f9",1664],["1f1ef-1f1ea",1665],["1f1ef-1f1f2",1666],["1f1ef-1f1f4",1667],["1f1ef-1f1f5",1668],["1f1f0-1f1ea",1669],["1f1f0-1f1ec",1670],["1f1f0-1f1ed",1671],["1f1f0-1f1ee",1672],["1f1f0-1f1f2",1673],["1f1f0-1f1f3",1674],["1f1f0-1f1f5",1675],["1f1f0-1f1f7",1676],["1f1f0-1f1fc",1677],["1f1f0-1f1fe",1678],["1f1f0-1f1ff",1679],["1f1f1-1f1e6",1680],["1f1f1-1f1e7",1681],["1f1f1-1f1e8",1682],["1f1f1-1f1ee",1683],["1f1f1-1f1f0",1684],["1f1f1-1f1f7",1685],["1f1f1-1f1f8",1686],["1f1f1-1f1f9",1687],["1f1f1-1f1fa",1688],["1f1f1-1f1fb",1689],["1f1f1-1f1fe",1690],["1f1f2-1f1e6",1691],["1f1f2-1f1e8",1692],["1f1f2-1f1e9",1693],["1f1f2-1f1ea",1694],["1f1f2-1f1eb",1695],["1f1f2-1f1ec",1696],["1f1f2-1f1ed",1697],["1f1f2-1f1f0",1698],["1f1f2-1f1f1",1699],["1f1f2-1f1f2",1700],["1f1f2-1f1f3",1701],["1f1f2-1f1f4",1702],["1f1f2-1f1f5",1703],["1f1f2-1f1f6",1704],["1f1f2-1f1f7",1705],["1f1f2-1f1f8",1706],["1f1f2-1f1f9",1707],["1f1f2-1f1fa",1708],["1f1f2-1f1fb",1709],["1f1f2-1f1fc",1710],["1f1f2-1f1fd",1711],["1f1f2-1f1fe",1712],["1f1f2-1f1ff",1713],["1f1f3-1f1e6",1714],["1f1f3-1f1e8",1715],["1f1f3-1f1ea",1716],["1f1f3-1f1eb",1717],["1f1f3-1f1ec",1718],["1f1f3-1f1ee",1719],["1f1f3-1f1f1",1720],["1f1f3-1f1f4",1721],["1f1f3-1f1f5",1722],["1f1f3-1f1f7",1723],["1f1f3-1f1fa",1724],["1f1f3-1f1ff",1725],["1f1f4-1f1f2",1726],["1f1f5-1f1e6",1727],["1f1f5-1f1ea",1728],["1f1f5-1f1eb",1729],["1f1f5-1f1ec",1730],["1f1f5-1f1ed",1731],["1f1f5-1f1f0",1732],["1f1f5-1f1f1",1733],["1f1f5-1f1f2",1734],["1f1f5-1f1f3",1735],["1f1f5-1f1f7",1736],["1f1f5-1f1f8",1737],["1f1f5-1f1f9",1738],["1f1f5-1f1fc",1739],["1f1f5-1f1fe",1740],["1f1f6-1f1e6",1741],["1f1f7-1f1ea",1742],["1f1f7-1f1f4",1743],["1f1f7-1f1f8",1744],["1f1f7-1f1fa",1745],["1f1f7-1f1fc",1746],["1f1f8-1f1e6",1747],["1f1f8-1f1e7",1748],["1f1f8-1f1e8",1749],["1f1f8-1f1e9",1750],["1f1f8-1f1ea",1751],["1f1f8-1f1ec",1752],["1f1f8-1f1ed",1753],["1f1f8-1f1ee",1754],["1f1f8-1f1ef",1755],["1f1f8-1f1f0",1756],["1f1f8-1f1f1",1757],["1f1f8-1f1f2",1758],["1f1f8-1f1f3",1759],["1f1f8-1f1f4",1760],["1f1f8-1f1f7",1761],["1f1f8-1f1f8",1762],["1f1f8-1f1f9",1763],["1f1f8-1f1fb",1764],["1f1f8-1f1fd",1765],["1f1f8-1f1fe",1766],["1f1f8-1f1ff",1767],["1f1f9-1f1e6",1768],["1f1f9-1f1e8",1769],["1f1f9-1f1e9",1770],["1f1f9-1f1eb",1771],["1f1f9-1f1ec",1772],["1f1f9-1f1ed",1773],["1f1f9-1f1ef",1774],["1f1f9-1f1f0",1775],["1f1f9-1f1f1",1776],["1f1f9-1f1f2",1777],["1f1f9-1f1f3",1778],["1f1f9-1f1f4",1779],["1f1f9-1f1f7",1780],["1f1f9-1f1f9",1781],["1f1f9-1f1fb",1782],["1f1f9-1f1fc",1783],["1f1f9-1f1ff",1784],["1f1fa-1f1e6",1785],["1f1fa-1f1ec",1786],["1f1fa-1f1f2",1787],["1f1fa-1f1f3",1788],["1f1fa-1f1f8",1789],["1f1fa-1f1fe",1790],["1f1fa-1f1ff",1791],["1f1fb-1f1e6",1792],["1f1fb-1f1e8",1793],["1f1fb-1f1ea",1794],["1f1fb-1f1ec",1795],["1f1fb-1f1ee",1796],["1f1fb-1f1f3",1797],["1f1fb-1f1fa",1798],["1f1fc-1f1eb",1799],["1f1fc-1f1f8",1800],["1f1fd-1f1f0",1801],["1f1fe-1f1ea",1802],["1f1fe-1f1f9",1803],["1f1ff-1f1e6",1804],["1f1ff-1f1f2",1805],["1f1ff-1f1fc",1806],["1f3f4-e0067-e0062-e0065-e006e-e0067-e007f",1807],["1f3f4-e0067-e0062-e0073-e0063-e0074-e007f",1808],["1f3f4-e0067-e0062-e0077-e006c-e0073-e007f",1809],["1f385-1f3fb",1810],["1f385-1f3fc",1811],["1f385-1f3fd",1812],["1f385-1f3fe",1813],["1f385-1f3ff",1814],["1f3c2-1f3fb",1815],["1f3c2-1f3fc",1816],["1f3c2-1f3fd",1817],["1f3c2-1f3fe",1818],["1f3c2-1f3ff",1819],["1f3c3-1f3fb-200d-2640-fe0f",1820],["1f3c3-1f3fc-200d-2640-fe0f",1821],["1f3c3-1f3fd-200d-2640-fe0f",1822],["1f3c3-1f3fe-200d-2640-fe0f",1823],["1f3c3-1f3ff-200d-2640-fe0f",1824],["1f3c3-1f3fb-200d-2642-fe0f",1825],["1f3c3-1f3fc-200d-2642-fe0f",1826],["1f3c3-1f3fd-200d-2642-fe0f",1827],["1f3c3-1f3fe-200d-2642-fe0f",1828],["1f3c3-1f3ff-200d-2642-fe0f",1829],["1f3c3-1f3fb",1830],["1f3c3-1f3fc",1831],["1f3c3-1f3fd",1832],["1f3c3-1f3fe",1833],["1f3c3-1f3ff",1834],["1f3c4-1f3fb-200d-2640-fe0f",1835],["1f3c4-1f3fc-200d-2640-fe0f",1836],["1f3c4-1f3fd-200d-2640-fe0f",1837],["1f3c4-1f3fe-200d-2640-fe0f",1838],["1f3c4-1f3ff-200d-2640-fe0f",1839],["1f3c4-1f3fb-200d-2642-fe0f",1840],["1f3c4-1f3fc-200d-2642-fe0f",1841],["1f3c4-1f3fd-200d-2642-fe0f",1842],["1f3c4-1f3fe-200d-2642-fe0f",1843],["1f3c4-1f3ff-200d-2642-fe0f",1844],["1f3c4-1f3fb",1845],["1f3c4-1f3fc",1846],["1f3c4-1f3fd",1847],["1f3c4-1f3fe",1848],["1f3c4-1f3ff",1849],["1f3c7-1f3fb",1850],["1f3c7-1f3fc",1851],["1f3c7-1f3fd",1852],["1f3c7-1f3fe",1853],["1f3c7-1f3ff",1854],["1f3ca-1f3fb-200d-2640-fe0f",1855],["1f3ca-1f3fc-200d-2640-fe0f",1856],["1f3ca-1f3fd-200d-2640-fe0f",1857],["1f3ca-1f3fe-200d-2640-fe0f",1858],["1f3ca-1f3ff-200d-2640-fe0f",1859],["1f3ca-1f3fb-200d-2642-fe0f",1860],["1f3ca-1f3fc-200d-2642-fe0f",1861],["1f3ca-1f3fd-200d-2642-fe0f",1862],["1f3ca-1f3fe-200d-2642-fe0f",1863],["1f3ca-1f3ff-200d-2642-fe0f",1864],["1f3ca-1f3fb",1865],["1f3ca-1f3fc",1866],["1f3ca-1f3fd",1867],["1f3ca-1f3fe",1868],["1f3ca-1f3ff",1869],["1f3cb-1f3fb-200d-2640-fe0f",1870],["1f3cb-1f3fc-200d-2640-fe0f",1871],["1f3cb-1f3fd-200d-2640-fe0f",1872],["1f3cb-1f3fe-200d-2640-fe0f",1873],["1f3cb-1f3ff-200d-2640-fe0f",1874],["1f3cb-1f3fb-200d-2642-fe0f",1875],["1f3cb-1f3fc-200d-2642-fe0f",1876],["1f3cb-1f3fd-200d-2642-fe0f",1877],["1f3cb-1f3fe-200d-2642-fe0f",1878],["1f3cb-1f3ff-200d-2642-fe0f",1879],["1f3cb-1f3fb",1880],["1f3cb-1f3fc",1881],["1f3cb-1f3fd",1882],["1f3cb-1f3fe",1883],["1f3cb-1f3ff",1884],["1f3cc-1f3fb-200d-2640-fe0f",1885],["1f3cc-1f3fc-200d-2640-fe0f",1886],["1f3cc-1f3fd-200d-2640-fe0f",1887],["1f3cc-1f3fe-200d-2640-fe0f",1888],["1f3cc-1f3ff-200d-2640-fe0f",1889],["1f3cc-1f3fb-200d-2642-fe0f",1890],["1f3cc-1f3fc-200d-2642-fe0f",1891],["1f3cc-1f3fd-200d-2642-fe0f",1892],["1f3cc-1f3fe-200d-2642-fe0f",1893],["1f3cc-1f3ff-200d-2642-fe0f",1894],["1f3cc-1f3fb",1895],["1f3cc-1f3fc",1896],["1f3cc-1f3fd",1897],["1f3cc-1f3fe",1898],["1f3cc-1f3ff",1899],["1f442-1f3fb",1900],["1f442-1f3fc",1901],["1f442-1f3fd",1902],["1f442-1f3fe",1903],["1f442-1f3ff",1904],["1f443-1f3fb",1905],["1f443-1f3fc",1906],["1f443-1f3fd",1907],["1f443-1f3fe",1908],["1f443-1f3ff",1909],["1f446-1f3fb",1910],["1f446-1f3fc",1911],["1f446-1f3fd",1912],["1f446-1f3fe",1913],["1f446-1f3ff",1914],["1f447-1f3fb",1915],["1f447-1f3fc",1916],["1f447-1f3fd",1917],["1f447-1f3fe",1918],["1f447-1f3ff",1919],["1f448-1f3fb",1920],["1f448-1f3fc",1921],["1f448-1f3fd",1922],["1f448-1f3fe",1923],["1f448-1f3ff",1924],["1f449-1f3fb",1925],["1f449-1f3fc",1926],["1f449-1f3fd",1927],["1f449-1f3fe",1928],["1f449-1f3ff",1929],["1f44a-1f3fb",1930],["1f44a-1f3fc",1931],["1f44a-1f3fd",1932],["1f44a-1f3fe",1933],["1f44a-1f3ff",1934],["1f44b-1f3fb",1935],["1f44b-1f3fc",1936],["1f44b-1f3fd",1937],["1f44b-1f3fe",1938],["1f44b-1f3ff",1939],["1f44c-1f3fb",1940],["1f44c-1f3fc",1941],["1f44c-1f3fd",1942],["1f44c-1f3fe",1943],["1f44c-1f3ff",1944],["1f44d-1f3fb",1945],["1f44d-1f3fc",1946],["1f44d-1f3fd",1947],["1f44d-1f3fe",1948],["1f44d-1f3ff",1949],["1f44e-1f3fb",1950],["1f44e-1f3fc",1951],["1f44e-1f3fd",1952],["1f44e-1f3fe",1953],["1f44e-1f3ff",1954],["1f44f-1f3fb",1955],["1f44f-1f3fc",1956],["1f44f-1f3fd",1957],["1f44f-1f3fe",1958],["1f44f-1f3ff",1959],["1f450-1f3fb",1960],["1f450-1f3fc",1961],["1f450-1f3fd",1962],["1f450-1f3fe",1963],["1f450-1f3ff",1964],["1f466-1f3fb",1965],["1f466-1f3fc",1966],["1f466-1f3fd",1967],["1f466-1f3fe",1968],["1f466-1f3ff",1969],["1f467-1f3fb",1970],["1f467-1f3fc",1971],["1f467-1f3fd",1972],["1f467-1f3fe",1973],["1f467-1f3ff",1974],["1f468-1f3fb-200d-1f33e",1975],["1f468-1f3fc-200d-1f33e",1976],["1f468-1f3fd-200d-1f33e",1977],["1f468-1f3fe-200d-1f33e",1978],["1f468-1f3ff-200d-1f33e",1979],["1f468-1f3fb-200d-1f373",1980],["1f468-1f3fc-200d-1f373",1981],["1f468-1f3fd-200d-1f373",1982],["1f468-1f3fe-200d-1f373",1983],["1f468-1f3ff-200d-1f373",1984],["1f468-1f3fb-200d-1f37c",1985],["1f468-1f3fc-200d-1f37c",1986],["1f468-1f3fd-200d-1f37c",1987],["1f468-1f3fe-200d-1f37c",1988],["1f468-1f3ff-200d-1f37c",1989],["1f468-1f3fb-200d-1f393",1990],["1f468-1f3fc-200d-1f393",1991],["1f468-1f3fd-200d-1f393",1992],["1f468-1f3fe-200d-1f393",1993],["1f468-1f3ff-200d-1f393",1994],["1f468-1f3fb-200d-1f3a4",1995],["1f468-1f3fc-200d-1f3a4",1996],["1f468-1f3fd-200d-1f3a4",1997],["1f468-1f3fe-200d-1f3a4",1998],["1f468-1f3ff-200d-1f3a4",1999],["1f468-1f3fb-200d-1f3a8",2000],["1f468-1f3fc-200d-1f3a8",2001],["1f468-1f3fd-200d-1f3a8",2002],["1f468-1f3fe-200d-1f3a8",2003],["1f468-1f3ff-200d-1f3a8",2004],["1f468-1f3fb-200d-1f3eb",2005],["1f468-1f3fc-200d-1f3eb",2006],["1f468-1f3fd-200d-1f3eb",2007],["1f468-1f3fe-200d-1f3eb",2008],["1f468-1f3ff-200d-1f3eb",2009],["1f468-1f3fb-200d-1f3ed",2010],["1f468-1f3fc-200d-1f3ed",2011],["1f468-1f3fd-200d-1f3ed",2012],["1f468-1f3fe-200d-1f3ed",2013],["1f468-1f3ff-200d-1f3ed",2014],["1f468-1f3fb-200d-1f4bb",2015],["1f468-1f3fc-200d-1f4bb",2016],["1f468-1f3fd-200d-1f4bb",2017],["1f468-1f3fe-200d-1f4bb",2018],["1f468-1f3ff-200d-1f4bb",2019],["1f468-1f3fb-200d-1f4bc",2020],["1f468-1f3fc-200d-1f4bc",2021],["1f468-1f3fd-200d-1f4bc",2022],["1f468-1f3fe-200d-1f4bc",2023],["1f468-1f3ff-200d-1f4bc",2024],["1f468-1f3fb-200d-1f527",2025],["1f468-1f3fc-200d-1f527",2026],["1f468-1f3fd-200d-1f527",2027],["1f468-1f3fe-200d-1f527",2028],["1f468-1f3ff-200d-1f527",2029],["1f468-1f3fb-200d-1f52c",2030],["1f468-1f3fc-200d-1f52c",2031],["1f468-1f3fd-200d-1f52c",2032],["1f468-1f3fe-200d-1f52c",2033],["1f468-1f3ff-200d-1f52c",2034],["1f468-1f3fb-200d-1f680",2035],["1f468-1f3fc-200d-1f680",2036],["1f468-1f3fd-200d-1f680",2037],["1f468-1f3fe-200d-1f680",2038],["1f468-1f3ff-200d-1f680",2039],["1f468-1f3fb-200d-1f692",2040],["1f468-1f3fc-200d-1f692",2041],["1f468-1f3fd-200d-1f692",2042],["1f468-1f3fe-200d-1f692",2043],["1f468-1f3ff-200d-1f692",2044],["1f468-1f3fb-200d-1f9af",2045],["1f468-1f3fc-200d-1f9af",2046],["1f468-1f3fd-200d-1f9af",2047],["1f468-1f3fe-200d-1f9af",2048],["1f468-1f3ff-200d-1f9af",2049],["1f468-1f3fb-200d-1f9b0",2050],["1f468-1f3fc-200d-1f9b0",2051],["1f468-1f3fd-200d-1f9b0",2052],["1f468-1f3fe-200d-1f9b0",2053],["1f468-1f3ff-200d-1f9b0",2054],["1f468-1f3fb-200d-1f9b1",2055],["1f468-1f3fc-200d-1f9b1",2056],["1f468-1f3fd-200d-1f9b1",2057],["1f468-1f3fe-200d-1f9b1",2058],["1f468-1f3ff-200d-1f9b1",2059],["1f468-1f3fb-200d-1f9b2",2060],["1f468-1f3fc-200d-1f9b2",2061],["1f468-1f3fd-200d-1f9b2",2062],["1f468-1f3fe-200d-1f9b2",2063],["1f468-1f3ff-200d-1f9b2",2064],["1f468-1f3fb-200d-1f9b3",2065],["1f468-1f3fc-200d-1f9b3",2066],["1f468-1f3fd-200d-1f9b3",2067],["1f468-1f3fe-200d-1f9b3",2068],["1f468-1f3ff-200d-1f9b3",2069],["1f468-1f3fb-200d-1f9bc",2070],["1f468-1f3fc-200d-1f9bc",2071],["1f468-1f3fd-200d-1f9bc",2072],["1f468-1f3fe-200d-1f9bc",2073],["1f468-1f3ff-200d-1f9bc",2074],["1f468-1f3fb-200d-1f9bd",2075],["1f468-1f3fc-200d-1f9bd",2076],["1f468-1f3fd-200d-1f9bd",2077],["1f468-1f3fe-200d-1f9bd",2078],["1f468-1f3ff-200d-1f9bd",2079],["1f468-1f3fb-200d-2695-fe0f",2080],["1f468-1f3fc-200d-2695-fe0f",2081],["1f468-1f3fd-200d-2695-fe0f",2082],["1f468-1f3fe-200d-2695-fe0f",2083],["1f468-1f3ff-200d-2695-fe0f",2084],["1f468-1f3fb-200d-2696-fe0f",2085],["1f468-1f3fc-200d-2696-fe0f",2086],["1f468-1f3fd-200d-2696-fe0f",2087],["1f468-1f3fe-200d-2696-fe0f",2088],["1f468-1f3ff-200d-2696-fe0f",2089],["1f468-1f3fb-200d-2708-fe0f",2090],["1f468-1f3fc-200d-2708-fe0f",2091],["1f468-1f3fd-200d-2708-fe0f",2092],["1f468-1f3fe-200d-2708-fe0f",2093],["1f468-1f3ff-200d-2708-fe0f",2094],["1f468-1f3fb",2095],["1f468-1f3fc",2096],["1f468-1f3fd",2097],["1f468-1f3fe",2098],["1f468-1f3ff",2099],["1f469-1f3fb-200d-1f33e",2100],["1f469-1f3fc-200d-1f33e",2101],["1f469-1f3fd-200d-1f33e",2102],["1f469-1f3fe-200d-1f33e",2103],["1f469-1f3ff-200d-1f33e",2104],["1f469-1f3fb-200d-1f373",2105],["1f469-1f3fc-200d-1f373",2106],["1f469-1f3fd-200d-1f373",2107],["1f469-1f3fe-200d-1f373",2108],["1f469-1f3ff-200d-1f373",2109],["1f469-1f3fb-200d-1f37c",2110],["1f469-1f3fc-200d-1f37c",2111],["1f469-1f3fd-200d-1f37c",2112],["1f469-1f3fe-200d-1f37c",2113],["1f469-1f3ff-200d-1f37c",2114],["1f469-1f3fb-200d-1f393",2115],["1f469-1f3fc-200d-1f393",2116],["1f469-1f3fd-200d-1f393",2117],["1f469-1f3fe-200d-1f393",2118],["1f469-1f3ff-200d-1f393",2119],["1f469-1f3fb-200d-1f3a4",2120],["1f469-1f3fc-200d-1f3a4",2121],["1f469-1f3fd-200d-1f3a4",2122],["1f469-1f3fe-200d-1f3a4",2123],["1f469-1f3ff-200d-1f3a4",2124],["1f469-1f3fb-200d-1f3a8",2125],["1f469-1f3fc-200d-1f3a8",2126],["1f469-1f3fd-200d-1f3a8",2127],["1f469-1f3fe-200d-1f3a8",2128],["1f469-1f3ff-200d-1f3a8",2129],["1f469-1f3fb-200d-1f3eb",2130],["1f469-1f3fc-200d-1f3eb",2131],["1f469-1f3fd-200d-1f3eb",2132],["1f469-1f3fe-200d-1f3eb",2133],["1f469-1f3ff-200d-1f3eb",2134],["1f469-1f3fb-200d-1f3ed",2135],["1f469-1f3fc-200d-1f3ed",2136],["1f469-1f3fd-200d-1f3ed",2137],["1f469-1f3fe-200d-1f3ed",2138],["1f469-1f3ff-200d-1f3ed",2139],["1f469-1f3fb-200d-1f4bb",2140],["1f469-1f3fc-200d-1f4bb",2141],["1f469-1f3fd-200d-1f4bb",2142],["1f469-1f3fe-200d-1f4bb",2143],["1f469-1f3ff-200d-1f4bb",2144],["1f469-1f3fb-200d-1f4bc",2145],["1f469-1f3fc-200d-1f4bc",2146],["1f469-1f3fd-200d-1f4bc",2147],["1f469-1f3fe-200d-1f4bc",2148],["1f469-1f3ff-200d-1f4bc",2149],["1f469-1f3fb-200d-1f527",2150],["1f469-1f3fc-200d-1f527",2151],["1f469-1f3fd-200d-1f527",2152],["1f469-1f3fe-200d-1f527",2153],["1f469-1f3ff-200d-1f527",2154],["1f469-1f3fb-200d-1f52c",2155],["1f469-1f3fc-200d-1f52c",2156],["1f469-1f3fd-200d-1f52c",2157],["1f469-1f3fe-200d-1f52c",2158],["1f469-1f3ff-200d-1f52c",2159],["1f469-1f3fb-200d-1f680",2160],["1f469-1f3fc-200d-1f680",2161],["1f469-1f3fd-200d-1f680",2162],["1f469-1f3fe-200d-1f680",2163],["1f469-1f3ff-200d-1f680",2164],["1f469-1f3fb-200d-1f692",2165],["1f469-1f3fc-200d-1f692",2166],["1f469-1f3fd-200d-1f692",2167],["1f469-1f3fe-200d-1f692",2168],["1f469-1f3ff-200d-1f692",2169],["1f469-1f3fb-200d-1f9af",2170],["1f469-1f3fc-200d-1f9af",2171],["1f469-1f3fd-200d-1f9af",2172],["1f469-1f3fe-200d-1f9af",2173],["1f469-1f3ff-200d-1f9af",2174],["1f469-1f3fb-200d-1f9b0",2175],["1f469-1f3fc-200d-1f9b0",2176],["1f469-1f3fd-200d-1f9b0",2177],["1f469-1f3fe-200d-1f9b0",2178],["1f469-1f3ff-200d-1f9b0",2179],["1f469-1f3fb-200d-1f9b1",2180],["1f469-1f3fc-200d-1f9b1",2181],["1f469-1f3fd-200d-1f9b1",2182],["1f469-1f3fe-200d-1f9b1",2183],["1f469-1f3ff-200d-1f9b1",2184],["1f469-1f3fb-200d-1f9b2",2185],["1f469-1f3fc-200d-1f9b2",2186],["1f469-1f3fd-200d-1f9b2",2187],["1f469-1f3fe-200d-1f9b2",2188],["1f469-1f3ff-200d-1f9b2",2189],["1f469-1f3fb-200d-1f9b3",2190],["1f469-1f3fc-200d-1f9b3",2191],["1f469-1f3fd-200d-1f9b3",2192],["1f469-1f3fe-200d-1f9b3",2193],["1f469-1f3ff-200d-1f9b3",2194],["1f469-1f3fb-200d-1f9bc",2195],["1f469-1f3fc-200d-1f9bc",2196],["1f469-1f3fd-200d-1f9bc",2197],["1f469-1f3fe-200d-1f9bc",2198],["1f469-1f3ff-200d-1f9bc",2199],["1f469-1f3fb-200d-1f9bd",2200],["1f469-1f3fc-200d-1f9bd",2201],["1f469-1f3fd-200d-1f9bd",2202],["1f469-1f3fe-200d-1f9bd",2203],["1f469-1f3ff-200d-1f9bd",2204],["1f469-1f3fb-200d-2695-fe0f",2205],["1f469-1f3fc-200d-2695-fe0f",2206],["1f469-1f3fd-200d-2695-fe0f",2207],["1f469-1f3fe-200d-2695-fe0f",2208],["1f469-1f3ff-200d-2695-fe0f",2209],["1f469-1f3fb-200d-2696-fe0f",2210],["1f469-1f3fc-200d-2696-fe0f",2211],["1f469-1f3fd-200d-2696-fe0f",2212],["1f469-1f3fe-200d-2696-fe0f",2213],["1f469-1f3ff-200d-2696-fe0f",2214],["1f469-1f3fb-200d-2708-fe0f",2215],["1f469-1f3fc-200d-2708-fe0f",2216],["1f469-1f3fd-200d-2708-fe0f",2217],["1f469-1f3fe-200d-2708-fe0f",2218],["1f469-1f3ff-200d-2708-fe0f",2219],["1f469-1f3fb",2220],["1f469-1f3fc",2221],["1f469-1f3fd",2222],["1f469-1f3fe",2223],["1f469-1f3ff",2224],["1f46b-1f3fb",2225],["1f46b-1f3fc",2226],["1f46b-1f3fd",2227],["1f46b-1f3fe",2228],["1f46b-1f3ff",2229],["1f469-1f3fb-200d-1f91d-200d-1f468-1f3fc",2230],["1f469-1f3fb-200d-1f91d-200d-1f468-1f3fd",2231],["1f469-1f3fb-200d-1f91d-200d-1f468-1f3fe",2232],["1f469-1f3fb-200d-1f91d-200d-1f468-1f3ff",2233],["1f469-1f3fc-200d-1f91d-200d-1f468-1f3fb",2234],["1f469-1f3fc-200d-1f91d-200d-1f468-1f3fd",2235],["1f469-1f3fc-200d-1f91d-200d-1f468-1f3fe",2236],["1f469-1f3fc-200d-1f91d-200d-1f468-1f3ff",2237],["1f469-1f3fd-200d-1f91d-200d-1f468-1f3fb",2238],["1f469-1f3fd-200d-1f91d-200d-1f468-1f3fc",2239],["1f469-1f3fd-200d-1f91d-200d-1f468-1f3fe",2240],["1f469-1f3fd-200d-1f91d-200d-1f468-1f3ff",2241],["1f469-1f3fe-200d-1f91d-200d-1f468-1f3fb",2242],["1f469-1f3fe-200d-1f91d-200d-1f468-1f3fc",2243],["1f469-1f3fe-200d-1f91d-200d-1f468-1f3fd",2244],["1f469-1f3fe-200d-1f91d-200d-1f468-1f3ff",2245],["1f469-1f3ff-200d-1f91d-200d-1f468-1f3fb",2246],["1f469-1f3ff-200d-1f91d-200d-1f468-1f3fc",2247],["1f469-1f3ff-200d-1f91d-200d-1f468-1f3fd",2248],["1f469-1f3ff-200d-1f91d-200d-1f468-1f3fe",2249],["1f46c-1f3fb",2250],["1f46c-1f3fc",2251],["1f46c-1f3fd",2252],["1f46c-1f3fe",2253],["1f46c-1f3ff",2254],["1f468-1f3fb-200d-1f91d-200d-1f468-1f3fc",2255],["1f468-1f3fb-200d-1f91d-200d-1f468-1f3fd",2256],["1f468-1f3fb-200d-1f91d-200d-1f468-1f3fe",2257],["1f468-1f3fb-200d-1f91d-200d-1f468-1f3ff",2258],["1f468-1f3fc-200d-1f91d-200d-1f468-1f3fb",2259],["1f468-1f3fc-200d-1f91d-200d-1f468-1f3fd",2260],["1f468-1f3fc-200d-1f91d-200d-1f468-1f3fe",2261],["1f468-1f3fc-200d-1f91d-200d-1f468-1f3ff",2262],["1f468-1f3fd-200d-1f91d-200d-1f468-1f3fb",2263],["1f468-1f3fd-200d-1f91d-200d-1f468-1f3fc",2264],["1f468-1f3fd-200d-1f91d-200d-1f468-1f3fe",2265],["1f468-1f3fd-200d-1f91d-200d-1f468-1f3ff",2266],["1f468-1f3fe-200d-1f91d-200d-1f468-1f3fb",2267],["1f468-1f3fe-200d-1f91d-200d-1f468-1f3fc",2268],["1f468-1f3fe-200d-1f91d-200d-1f468-1f3fd",2269],["1f468-1f3fe-200d-1f91d-200d-1f468-1f3ff",2270],["1f468-1f3ff-200d-1f91d-200d-1f468-1f3fb",2271],["1f468-1f3ff-200d-1f91d-200d-1f468-1f3fc",2272],["1f468-1f3ff-200d-1f91d-200d-1f468-1f3fd",2273],["1f468-1f3ff-200d-1f91d-200d-1f468-1f3fe",2274],["1f46d-1f3fb",2275],["1f46d-1f3fc",2276],["1f46d-1f3fd",2277],["1f46d-1f3fe",2278],["1f46d-1f3ff",2279],["1f469-1f3fb-200d-1f91d-200d-1f469-1f3fc",2280],["1f469-1f3fb-200d-1f91d-200d-1f469-1f3fd",2281],["1f469-1f3fb-200d-1f91d-200d-1f469-1f3fe",2282],["1f469-1f3fb-200d-1f91d-200d-1f469-1f3ff",2283],["1f469-1f3fc-200d-1f91d-200d-1f469-1f3fb",2284],["1f469-1f3fc-200d-1f91d-200d-1f469-1f3fd",2285],["1f469-1f3fc-200d-1f91d-200d-1f469-1f3fe",2286],["1f469-1f3fc-200d-1f91d-200d-1f469-1f3ff",2287],["1f469-1f3fd-200d-1f91d-200d-1f469-1f3fb",2288],["1f469-1f3fd-200d-1f91d-200d-1f469-1f3fc",2289],["1f469-1f3fd-200d-1f91d-200d-1f469-1f3fe",2290],["1f469-1f3fd-200d-1f91d-200d-1f469-1f3ff",2291],["1f469-1f3fe-200d-1f91d-200d-1f469-1f3fb",2292],["1f469-1f3fe-200d-1f91d-200d-1f469-1f3fc",2293],["1f469-1f3fe-200d-1f91d-200d-1f469-1f3fd",2294],["1f469-1f3fe-200d-1f91d-200d-1f469-1f3ff",2295],["1f469-1f3ff-200d-1f91d-200d-1f469-1f3fb",2296],["1f469-1f3ff-200d-1f91d-200d-1f469-1f3fc",2297],["1f469-1f3ff-200d-1f91d-200d-1f469-1f3fd",2298],["1f469-1f3ff-200d-1f91d-200d-1f469-1f3fe",2299],["1f46e-1f3fb-200d-2640-fe0f",2300],["1f46e-1f3fc-200d-2640-fe0f",2301],["1f46e-1f3fd-200d-2640-fe0f",2302],["1f46e-1f3fe-200d-2640-fe0f",2303],["1f46e-1f3ff-200d-2640-fe0f",2304],["1f46e-1f3fb-200d-2642-fe0f",2305],["1f46e-1f3fc-200d-2642-fe0f",2306],["1f46e-1f3fd-200d-2642-fe0f",2307],["1f46e-1f3fe-200d-2642-fe0f",2308],["1f46e-1f3ff-200d-2642-fe0f",2309],["1f46e-1f3fb",2310],["1f46e-1f3fc",2311],["1f46e-1f3fd",2312],["1f46e-1f3fe",2313],["1f46e-1f3ff",2314],["1f470-1f3fb-200d-2640-fe0f",2315],["1f470-1f3fc-200d-2640-fe0f",2316],["1f470-1f3fd-200d-2640-fe0f",2317],["1f470-1f3fe-200d-2640-fe0f",2318],["1f470-1f3ff-200d-2640-fe0f",2319],["1f470-1f3fb-200d-2642-fe0f",2320],["1f470-1f3fc-200d-2642-fe0f",2321],["1f470-1f3fd-200d-2642-fe0f",2322],["1f470-1f3fe-200d-2642-fe0f",2323],["1f470-1f3ff-200d-2642-fe0f",2324],["1f470-1f3fb",2325],["1f470-1f3fc",2326],["1f470-1f3fd",2327],["1f470-1f3fe",2328],["1f470-1f3ff",2329],["1f471-1f3fb-200d-2640-fe0f",2330],["1f471-1f3fc-200d-2640-fe0f",2331],["1f471-1f3fd-200d-2640-fe0f",2332],["1f471-1f3fe-200d-2640-fe0f",2333],["1f471-1f3ff-200d-2640-fe0f",2334],["1f471-1f3fb-200d-2642-fe0f",2335],["1f471-1f3fc-200d-2642-fe0f",2336],["1f471-1f3fd-200d-2642-fe0f",2337],["1f471-1f3fe-200d-2642-fe0f",2338],["1f471-1f3ff-200d-2642-fe0f",2339],["1f471-1f3fb",2340],["1f471-1f3fc",2341],["1f471-1f3fd",2342],["1f471-1f3fe",2343],["1f471-1f3ff",2344],["1f472-1f3fb",2345],["1f472-1f3fc",2346],["1f472-1f3fd",2347],["1f472-1f3fe",2348],["1f472-1f3ff",2349],["1f473-1f3fb-200d-2640-fe0f",2350],["1f473-1f3fc-200d-2640-fe0f",2351],["1f473-1f3fd-200d-2640-fe0f",2352],["1f473-1f3fe-200d-2640-fe0f",2353],["1f473-1f3ff-200d-2640-fe0f",2354],["1f473-1f3fb-200d-2642-fe0f",2355],["1f473-1f3fc-200d-2642-fe0f",2356],["1f473-1f3fd-200d-2642-fe0f",2357],["1f473-1f3fe-200d-2642-fe0f",2358],["1f473-1f3ff-200d-2642-fe0f",2359],["1f473-1f3fb",2360],["1f473-1f3fc",2361],["1f473-1f3fd",2362],["1f473-1f3fe",2363],["1f473-1f3ff",2364],["1f474-1f3fb",2365],["1f474-1f3fc",2366],["1f474-1f3fd",2367],["1f474-1f3fe",2368],["1f474-1f3ff",2369],["1f475-1f3fb",2370],["1f475-1f3fc",2371],["1f475-1f3fd",2372],["1f475-1f3fe",2373],["1f475-1f3ff",2374],["1f476-1f3fb",2375],["1f476-1f3fc",2376],["1f476-1f3fd",2377],["1f476-1f3fe",2378],["1f476-1f3ff",2379],["1f477-1f3fb-200d-2640-fe0f",2380],["1f477-1f3fc-200d-2640-fe0f",2381],["1f477-1f3fd-200d-2640-fe0f",2382],["1f477-1f3fe-200d-2640-fe0f",2383],["1f477-1f3ff-200d-2640-fe0f",2384],["1f477-1f3fb-200d-2642-fe0f",2385],["1f477-1f3fc-200d-2642-fe0f",2386],["1f477-1f3fd-200d-2642-fe0f",2387],["1f477-1f3fe-200d-2642-fe0f",2388],["1f477-1f3ff-200d-2642-fe0f",2389],["1f477-1f3fb",2390],["1f477-1f3fc",2391],["1f477-1f3fd",2392],["1f477-1f3fe",2393],["1f477-1f3ff",2394],["1f478-1f3fb",2395],["1f478-1f3fc",2396],["1f478-1f3fd",2397],["1f478-1f3fe",2398],["1f478-1f3ff",2399],["1f47c-1f3fb",2400],["1f47c-1f3fc",2401],["1f47c-1f3fd",2402],["1f47c-1f3fe",2403],["1f47c-1f3ff",2404],["1f481-1f3fb-200d-2640-fe0f",2405],["1f481-1f3fc-200d-2640-fe0f",2406],["1f481-1f3fd-200d-2640-fe0f",2407],["1f481-1f3fe-200d-2640-fe0f",2408],["1f481-1f3ff-200d-2640-fe0f",2409],["1f481-1f3fb-200d-2642-fe0f",2410],["1f481-1f3fc-200d-2642-fe0f",2411],["1f481-1f3fd-200d-2642-fe0f",2412],["1f481-1f3fe-200d-2642-fe0f",2413],["1f481-1f3ff-200d-2642-fe0f",2414],["1f481-1f3fb",2415],["1f481-1f3fc",2416],["1f481-1f3fd",2417],["1f481-1f3fe",2418],["1f481-1f3ff",2419],["1f482-1f3fb-200d-2640-fe0f",2420],["1f482-1f3fc-200d-2640-fe0f",2421],["1f482-1f3fd-200d-2640-fe0f",2422],["1f482-1f3fe-200d-2640-fe0f",2423],["1f482-1f3ff-200d-2640-fe0f",2424],["1f482-1f3fb-200d-2642-fe0f",2425],["1f482-1f3fc-200d-2642-fe0f",2426],["1f482-1f3fd-200d-2642-fe0f",2427],["1f482-1f3fe-200d-2642-fe0f",2428],["1f482-1f3ff-200d-2642-fe0f",2429],["1f482-1f3fb",2430],["1f482-1f3fc",2431],["1f482-1f3fd",2432],["1f482-1f3fe",2433],["1f482-1f3ff",2434],["1f483-1f3fb",2435],["1f483-1f3fc",2436],["1f483-1f3fd",2437],["1f483-1f3fe",2438],["1f483-1f3ff",2439],["1f485-1f3fb",2440],["1f485-1f3fc",2441],["1f485-1f3fd",2442],["1f485-1f3fe",2443],["1f485-1f3ff",2444],["1f486-1f3fb-200d-2640-fe0f",2445],["1f486-1f3fc-200d-2640-fe0f",2446],["1f486-1f3fd-200d-2640-fe0f",2447],["1f486-1f3fe-200d-2640-fe0f",2448],["1f486-1f3ff-200d-2640-fe0f",2449],["1f486-1f3fb-200d-2642-fe0f",2450],["1f486-1f3fc-200d-2642-fe0f",2451],["1f486-1f3fd-200d-2642-fe0f",2452],["1f486-1f3fe-200d-2642-fe0f",2453],["1f486-1f3ff-200d-2642-fe0f",2454],["1f486-1f3fb",2455],["1f486-1f3fc",2456],["1f486-1f3fd",2457],["1f486-1f3fe",2458],["1f486-1f3ff",2459],["1f487-1f3fb-200d-2640-fe0f",2460],["1f487-1f3fc-200d-2640-fe0f",2461],["1f487-1f3fd-200d-2640-fe0f",2462],["1f487-1f3fe-200d-2640-fe0f",2463],["1f487-1f3ff-200d-2640-fe0f",2464],["1f487-1f3fb-200d-2642-fe0f",2465],["1f487-1f3fc-200d-2642-fe0f",2466],["1f487-1f3fd-200d-2642-fe0f",2467],["1f487-1f3fe-200d-2642-fe0f",2468],["1f487-1f3ff-200d-2642-fe0f",2469],["1f487-1f3fb",2470],["1f487-1f3fc",2471],["1f487-1f3fd",2472],["1f487-1f3fe",2473],["1f487-1f3ff",2474],["1f4aa-1f3fb",2475],["1f4aa-1f3fc",2476],["1f4aa-1f3fd",2477],["1f4aa-1f3fe",2478],["1f4aa-1f3ff",2479],["1f574-1f3fb",2480],["1f574-1f3fc",2481],["1f574-1f3fd",2482],["1f574-1f3fe",2483],["1f574-1f3ff",2484],["1f575-1f3fb-200d-2640-fe0f",2485],["1f575-1f3fc-200d-2640-fe0f",2486],["1f575-1f3fd-200d-2640-fe0f",2487],["1f575-1f3fe-200d-2640-fe0f",2488],["1f575-1f3ff-200d-2640-fe0f",2489],["1f575-1f3fb-200d-2642-fe0f",2490],["1f575-1f3fc-200d-2642-fe0f",2491],["1f575-1f3fd-200d-2642-fe0f",2492],["1f575-1f3fe-200d-2642-fe0f",2493],["1f575-1f3ff-200d-2642-fe0f",2494],["1f575-1f3fb",2495],["1f575-1f3fc",2496],["1f575-1f3fd",2497],["1f575-1f3fe",2498],["1f575-1f3ff",2499],["1f57a-1f3fb",2500],["1f57a-1f3fc",2501],["1f57a-1f3fd",2502],["1f57a-1f3fe",2503],["1f57a-1f3ff",2504],["1f590-1f3fb",2505],["1f590-1f3fc",2506],["1f590-1f3fd",2507],["1f590-1f3fe",2508],["1f590-1f3ff",2509],["1f595-1f3fb",2510],["1f595-1f3fc",2511],["1f595-1f3fd",2512],["1f595-1f3fe",2513],["1f595-1f3ff",2514],["1f596-1f3fb",2515],["1f596-1f3fc",2516],["1f596-1f3fd",2517],["1f596-1f3fe",2518],["1f596-1f3ff",2519],["1f645-1f3fb-200d-2640-fe0f",2520],["1f645-1f3fc-200d-2640-fe0f",2521],["1f645-1f3fd-200d-2640-fe0f",2522],["1f645-1f3fe-200d-2640-fe0f",2523],["1f645-1f3ff-200d-2640-fe0f",2524],["1f645-1f3fb-200d-2642-fe0f",2525],["1f645-1f3fc-200d-2642-fe0f",2526],["1f645-1f3fd-200d-2642-fe0f",2527],["1f645-1f3fe-200d-2642-fe0f",2528],["1f645-1f3ff-200d-2642-fe0f",2529],["1f645-1f3fb",2530],["1f645-1f3fc",2531],["1f645-1f3fd",2532],["1f645-1f3fe",2533],["1f645-1f3ff",2534],["1f646-1f3fb-200d-2640-fe0f",2535],["1f646-1f3fc-200d-2640-fe0f",2536],["1f646-1f3fd-200d-2640-fe0f",2537],["1f646-1f3fe-200d-2640-fe0f",2538],["1f646-1f3ff-200d-2640-fe0f",2539],["1f646-1f3fb-200d-2642-fe0f",2540],["1f646-1f3fc-200d-2642-fe0f",2541],["1f646-1f3fd-200d-2642-fe0f",2542],["1f646-1f3fe-200d-2642-fe0f",2543],["1f646-1f3ff-200d-2642-fe0f",2544],["1f646-1f3fb",2545],["1f646-1f3fc",2546],["1f646-1f3fd",2547],["1f646-1f3fe",2548],["1f646-1f3ff",2549],["1f647-1f3fb-200d-2640-fe0f",2550],["1f647-1f3fc-200d-2640-fe0f",2551],["1f647-1f3fd-200d-2640-fe0f",2552],["1f647-1f3fe-200d-2640-fe0f",2553],["1f647-1f3ff-200d-2640-fe0f",2554],["1f647-1f3fb-200d-2642-fe0f",2555],["1f647-1f3fc-200d-2642-fe0f",2556],["1f647-1f3fd-200d-2642-fe0f",2557],["1f647-1f3fe-200d-2642-fe0f",2558],["1f647-1f3ff-200d-2642-fe0f",2559],["1f647-1f3fb",2560],["1f647-1f3fc",2561],["1f647-1f3fd",2562],["1f647-1f3fe",2563],["1f647-1f3ff",2564],["1f64b-1f3fb-200d-2640-fe0f",2565],["1f64b-1f3fc-200d-2640-fe0f",2566],["1f64b-1f3fd-200d-2640-fe0f",2567],["1f64b-1f3fe-200d-2640-fe0f",2568],["1f64b-1f3ff-200d-2640-fe0f",2569],["1f64b-1f3fb-200d-2642-fe0f",2570],["1f64b-1f3fc-200d-2642-fe0f",2571],["1f64b-1f3fd-200d-2642-fe0f",2572],["1f64b-1f3fe-200d-2642-fe0f",2573],["1f64b-1f3ff-200d-2642-fe0f",2574],["1f64b-1f3fb",2575],["1f64b-1f3fc",2576],["1f64b-1f3fd",2577],["1f64b-1f3fe",2578],["1f64b-1f3ff",2579],["1f64c-1f3fb",2580],["1f64c-1f3fc",2581],["1f64c-1f3fd",2582],["1f64c-1f3fe",2583],["1f64c-1f3ff",2584],["1f64d-1f3fb-200d-2640-fe0f",2585],["1f64d-1f3fc-200d-2640-fe0f",2586],["1f64d-1f3fd-200d-2640-fe0f",2587],["1f64d-1f3fe-200d-2640-fe0f",2588],["1f64d-1f3ff-200d-2640-fe0f",2589],["1f64d-1f3fb-200d-2642-fe0f",2590],["1f64d-1f3fc-200d-2642-fe0f",2591],["1f64d-1f3fd-200d-2642-fe0f",2592],["1f64d-1f3fe-200d-2642-fe0f",2593],["1f64d-1f3ff-200d-2642-fe0f",2594],["1f64d-1f3fb",2595],["1f64d-1f3fc",2596],["1f64d-1f3fd",2597],["1f64d-1f3fe",2598],["1f64d-1f3ff",2599],["1f64e-1f3fb-200d-2640-fe0f",2600],["1f64e-1f3fc-200d-2640-fe0f",2601],["1f64e-1f3fd-200d-2640-fe0f",2602],["1f64e-1f3fe-200d-2640-fe0f",2603],["1f64e-1f3ff-200d-2640-fe0f",2604],["1f64e-1f3fb-200d-2642-fe0f",2605],["1f64e-1f3fc-200d-2642-fe0f",2606],["1f64e-1f3fd-200d-2642-fe0f",2607],["1f64e-1f3fe-200d-2642-fe0f",2608],["1f64e-1f3ff-200d-2642-fe0f",2609],["1f64e-1f3fb",2610],["1f64e-1f3fc",2611],["1f64e-1f3fd",2612],["1f64e-1f3fe",2613],["1f64e-1f3ff",2614],["1f64f-1f3fb",2615],["1f64f-1f3fc",2616],["1f64f-1f3fd",2617],["1f64f-1f3fe",2618],["1f64f-1f3ff",2619],["1f6a3-1f3fb-200d-2640-fe0f",2620],["1f6a3-1f3fc-200d-2640-fe0f",2621],["1f6a3-1f3fd-200d-2640-fe0f",2622],["1f6a3-1f3fe-200d-2640-fe0f",2623],["1f6a3-1f3ff-200d-2640-fe0f",2624],["1f6a3-1f3fb-200d-2642-fe0f",2625],["1f6a3-1f3fc-200d-2642-fe0f",2626],["1f6a3-1f3fd-200d-2642-fe0f",2627],["1f6a3-1f3fe-200d-2642-fe0f",2628],["1f6a3-1f3ff-200d-2642-fe0f",2629],["1f6a3-1f3fb",2630],["1f6a3-1f3fc",2631],["1f6a3-1f3fd",2632],["1f6a3-1f3fe",2633],["1f6a3-1f3ff",2634],["1f6b4-1f3fb-200d-2640-fe0f",2635],["1f6b4-1f3fc-200d-2640-fe0f",2636],["1f6b4-1f3fd-200d-2640-fe0f",2637],["1f6b4-1f3fe-200d-2640-fe0f",2638],["1f6b4-1f3ff-200d-2640-fe0f",2639],["1f6b4-1f3fb-200d-2642-fe0f",2640],["1f6b4-1f3fc-200d-2642-fe0f",2641],["1f6b4-1f3fd-200d-2642-fe0f",2642],["1f6b4-1f3fe-200d-2642-fe0f",2643],["1f6b4-1f3ff-200d-2642-fe0f",2644],["1f6b4-1f3fb",2645],["1f6b4-1f3fc",2646],["1f6b4-1f3fd",2647],["1f6b4-1f3fe",2648],["1f6b4-1f3ff",2649],["1f6b5-1f3fb-200d-2640-fe0f",2650],["1f6b5-1f3fc-200d-2640-fe0f",2651],["1f6b5-1f3fd-200d-2640-fe0f",2652],["1f6b5-1f3fe-200d-2640-fe0f",2653],["1f6b5-1f3ff-200d-2640-fe0f",2654],["1f6b5-1f3fb-200d-2642-fe0f",2655],["1f6b5-1f3fc-200d-2642-fe0f",2656],["1f6b5-1f3fd-200d-2642-fe0f",2657],["1f6b5-1f3fe-200d-2642-fe0f",2658],["1f6b5-1f3ff-200d-2642-fe0f",2659],["1f6b5-1f3fb",2660],["1f6b5-1f3fc",2661],["1f6b5-1f3fd",2662],["1f6b5-1f3fe",2663],["1f6b5-1f3ff",2664],["1f6b6-1f3fb-200d-2640-fe0f",2665],["1f6b6-1f3fc-200d-2640-fe0f",2666],["1f6b6-1f3fd-200d-2640-fe0f",2667],["1f6b6-1f3fe-200d-2640-fe0f",2668],["1f6b6-1f3ff-200d-2640-fe0f",2669],["1f6b6-1f3fb-200d-2642-fe0f",2670],["1f6b6-1f3fc-200d-2642-fe0f",2671],["1f6b6-1f3fd-200d-2642-fe0f",2672],["1f6b6-1f3fe-200d-2642-fe0f",2673],["1f6b6-1f3ff-200d-2642-fe0f",2674],["1f6b6-1f3fb",2675],["1f6b6-1f3fc",2676],["1f6b6-1f3fd",2677],["1f6b6-1f3fe",2678],["1f6b6-1f3ff",2679],["1f6c0-1f3fb",2680],["1f6c0-1f3fc",2681],["1f6c0-1f3fd",2682],["1f6c0-1f3fe",2683],["1f6c0-1f3ff",2684],["1f6cc-1f3fb",2685],["1f6cc-1f3fc",2686],["1f6cc-1f3fd",2687],["1f6cc-1f3fe",2688],["1f6cc-1f3ff",2689],["1f90c-1f3fb",2690],["1f90c-1f3fc",2691],["1f90c-1f3fd",2692],["1f90c-1f3fe",2693],["1f90c-1f3ff",2694],["1f90f-1f3fb",2695],["1f90f-1f3fc",2696],["1f90f-1f3fd",2697],["1f90f-1f3fe",2698],["1f90f-1f3ff",2699],["1f918-1f3fb",2700],["1f918-1f3fc",2701],["1f918-1f3fd",2702],["1f918-1f3fe",2703],["1f918-1f3ff",2704],["1f919-1f3fb",2705],["1f919-1f3fc",2706],["1f919-1f3fd",2707],["1f919-1f3fe",2708],["1f919-1f3ff",2709],["1f91a-1f3fb",2710],["1f91a-1f3fc",2711],["1f91a-1f3fd",2712],["1f91a-1f3fe",2713],["1f91a-1f3ff",2714],["1f91b-1f3fb",2715],["1f91b-1f3fc",2716],["1f91b-1f3fd",2717],["1f91b-1f3fe",2718],["1f91b-1f3ff",2719],["1f91c-1f3fb",2720],["1f91c-1f3fc",2721],["1f91c-1f3fd",2722],["1f91c-1f3fe",2723],["1f91c-1f3ff",2724],["1f91e-1f3fb",2725],["1f91e-1f3fc",2726],["1f91e-1f3fd",2727],["1f91e-1f3fe",2728],["1f91e-1f3ff",2729],["1f91f-1f3fb",2730],["1f91f-1f3fc",2731],["1f91f-1f3fd",2732],["1f91f-1f3fe",2733],["1f91f-1f3ff",2734],["1f926-1f3fb-200d-2640-fe0f",2735],["1f926-1f3fc-200d-2640-fe0f",2736],["1f926-1f3fd-200d-2640-fe0f",2737],["1f926-1f3fe-200d-2640-fe0f",2738],["1f926-1f3ff-200d-2640-fe0f",2739],["1f926-1f3fb-200d-2642-fe0f",2740],["1f926-1f3fc-200d-2642-fe0f",2741],["1f926-1f3fd-200d-2642-fe0f",2742],["1f926-1f3fe-200d-2642-fe0f",2743],["1f926-1f3ff-200d-2642-fe0f",2744],["1f926-1f3fb",2745],["1f926-1f3fc",2746],["1f926-1f3fd",2747],["1f926-1f3fe",2748],["1f926-1f3ff",2749],["1f930-1f3fb",2750],["1f930-1f3fc",2751],["1f930-1f3fd",2752],["1f930-1f3fe",2753],["1f930-1f3ff",2754],["1f931-1f3fb",2755],["1f931-1f3fc",2756],["1f931-1f3fd",2757],["1f931-1f3fe",2758],["1f931-1f3ff",2759],["1f932-1f3fb",2760],["1f932-1f3fc",2761],["1f932-1f3fd",2762],["1f932-1f3fe",2763],["1f932-1f3ff",2764],["1f933-1f3fb",2765],["1f933-1f3fc",2766],["1f933-1f3fd",2767],["1f933-1f3fe",2768],["1f933-1f3ff",2769],["1f934-1f3fb",2770],["1f934-1f3fc",2771],["1f934-1f3fd",2772],["1f934-1f3fe",2773],["1f934-1f3ff",2774],["1f935-1f3fb-200d-2640-fe0f",2775],["1f935-1f3fc-200d-2640-fe0f",2776],["1f935-1f3fd-200d-2640-fe0f",2777],["1f935-1f3fe-200d-2640-fe0f",2778],["1f935-1f3ff-200d-2640-fe0f",2779],["1f935-1f3fb-200d-2642-fe0f",2780],["1f935-1f3fc-200d-2642-fe0f",2781],["1f935-1f3fd-200d-2642-fe0f",2782],["1f935-1f3fe-200d-2642-fe0f",2783],["1f935-1f3ff-200d-2642-fe0f",2784],["1f935-1f3fb",2785],["1f935-1f3fc",2786],["1f935-1f3fd",2787],["1f935-1f3fe",2788],["1f935-1f3ff",2789],["1f936-1f3fb",2790],["1f936-1f3fc",2791],["1f936-1f3fd",2792],["1f936-1f3fe",2793],["1f936-1f3ff",2794],["1f937-1f3fb-200d-2640-fe0f",2795],["1f937-1f3fc-200d-2640-fe0f",2796],["1f937-1f3fd-200d-2640-fe0f",2797],["1f937-1f3fe-200d-2640-fe0f",2798],["1f937-1f3ff-200d-2640-fe0f",2799],["1f937-1f3fb-200d-2642-fe0f",2800],["1f937-1f3fc-200d-2642-fe0f",2801],["1f937-1f3fd-200d-2642-fe0f",2802],["1f937-1f3fe-200d-2642-fe0f",2803],["1f937-1f3ff-200d-2642-fe0f",2804],["1f937-1f3fb",2805],["1f937-1f3fc",2806],["1f937-1f3fd",2807],["1f937-1f3fe",2808],["1f937-1f3ff",2809],["1f938-1f3fb-200d-2640-fe0f",2810],["1f938-1f3fc-200d-2640-fe0f",2811],["1f938-1f3fd-200d-2640-fe0f",2812],["1f938-1f3fe-200d-2640-fe0f",2813],["1f938-1f3ff-200d-2640-fe0f",2814],["1f938-1f3fb-200d-2642-fe0f",2815],["1f938-1f3fc-200d-2642-fe0f",2816],["1f938-1f3fd-200d-2642-fe0f",2817],["1f938-1f3fe-200d-2642-fe0f",2818],["1f938-1f3ff-200d-2642-fe0f",2819],["1f938-1f3fb",2820],["1f938-1f3fc",2821],["1f938-1f3fd",2822],["1f938-1f3fe",2823],["1f938-1f3ff",2824],["1f939-1f3fb-200d-2640-fe0f",2825],["1f939-1f3fc-200d-2640-fe0f",2826],["1f939-1f3fd-200d-2640-fe0f",2827],["1f939-1f3fe-200d-2640-fe0f",2828],["1f939-1f3ff-200d-2640-fe0f",2829],["1f939-1f3fb-200d-2642-fe0f",2830],["1f939-1f3fc-200d-2642-fe0f",2831],["1f939-1f3fd-200d-2642-fe0f",2832],["1f939-1f3fe-200d-2642-fe0f",2833],["1f939-1f3ff-200d-2642-fe0f",2834],["1f939-1f3fb",2835],["1f939-1f3fc",2836],["1f939-1f3fd",2837],["1f939-1f3fe",2838],["1f939-1f3ff",2839],["1f93d-1f3fb-200d-2640-fe0f",2840],["1f93d-1f3fc-200d-2640-fe0f",2841],["1f93d-1f3fd-200d-2640-fe0f",2842],["1f93d-1f3fe-200d-2640-fe0f",2843],["1f93d-1f3ff-200d-2640-fe0f",2844],["1f93d-1f3fb-200d-2642-fe0f",2845],["1f93d-1f3fc-200d-2642-fe0f",2846],["1f93d-1f3fd-200d-2642-fe0f",2847],["1f93d-1f3fe-200d-2642-fe0f",2848],["1f93d-1f3ff-200d-2642-fe0f",2849],["1f93d-1f3fb",2850],["1f93d-1f3fc",2851],["1f93d-1f3fd",2852],["1f93d-1f3fe",2853],["1f93d-1f3ff",2854],["1f93e-1f3fb-200d-2640-fe0f",2855],["1f93e-1f3fc-200d-2640-fe0f",2856],["1f93e-1f3fd-200d-2640-fe0f",2857],["1f93e-1f3fe-200d-2640-fe0f",2858],["1f93e-1f3ff-200d-2640-fe0f",2859],["1f93e-1f3fb-200d-2642-fe0f",2860],["1f93e-1f3fc-200d-2642-fe0f",2861],["1f93e-1f3fd-200d-2642-fe0f",2862],["1f93e-1f3fe-200d-2642-fe0f",2863],["1f93e-1f3ff-200d-2642-fe0f",2864],["1f93e-1f3fb",2865],["1f93e-1f3fc",2866],["1f93e-1f3fd",2867],["1f93e-1f3fe",2868],["1f93e-1f3ff",2869],["1f977-1f3fb",2870],["1f977-1f3fc",2871],["1f977-1f3fd",2872],["1f977-1f3fe",2873],["1f977-1f3ff",2874],["1f9b5-1f3fb",2875],["1f9b5-1f3fc",2876],["1f9b5-1f3fd",2877],["1f9b5-1f3fe",2878],["1f9b5-1f3ff",2879],["1f9b6-1f3fb",2880],["1f9b6-1f3fc",2881],["1f9b6-1f3fd",2882],["1f9b6-1f3fe",2883],["1f9b6-1f3ff",2884],["1f9b8-1f3fb-200d-2640-fe0f",2885],["1f9b8-1f3fc-200d-2640-fe0f",2886],["1f9b8-1f3fd-200d-2640-fe0f",2887],["1f9b8-1f3fe-200d-2640-fe0f",2888],["1f9b8-1f3ff-200d-2640-fe0f",2889],["1f9b8-1f3fb-200d-2642-fe0f",2890],["1f9b8-1f3fc-200d-2642-fe0f",2891],["1f9b8-1f3fd-200d-2642-fe0f",2892],["1f9b8-1f3fe-200d-2642-fe0f",2893],["1f9b8-1f3ff-200d-2642-fe0f",2894],["1f9b8-1f3fb",2895],["1f9b8-1f3fc",2896],["1f9b8-1f3fd",2897],["1f9b8-1f3fe",2898],["1f9b8-1f3ff",2899],["1f9b9-1f3fb-200d-2640-fe0f",2900],["1f9b9-1f3fc-200d-2640-fe0f",2901],["1f9b9-1f3fd-200d-2640-fe0f",2902],["1f9b9-1f3fe-200d-2640-fe0f",2903],["1f9b9-1f3ff-200d-2640-fe0f",2904],["1f9b9-1f3fb-200d-2642-fe0f",2905],["1f9b9-1f3fc-200d-2642-fe0f",2906],["1f9b9-1f3fd-200d-2642-fe0f",2907],["1f9b9-1f3fe-200d-2642-fe0f",2908],["1f9b9-1f3ff-200d-2642-fe0f",2909],["1f9b9-1f3fb",2910],["1f9b9-1f3fc",2911],["1f9b9-1f3fd",2912],["1f9b9-1f3fe",2913],["1f9b9-1f3ff",2914],["1f9bb-1f3fb",2915],["1f9bb-1f3fc",2916],["1f9bb-1f3fd",2917],["1f9bb-1f3fe",2918],["1f9bb-1f3ff",2919],["1f9cd-1f3fb-200d-2640-fe0f",2920],["1f9cd-1f3fc-200d-2640-fe0f",2921],["1f9cd-1f3fd-200d-2640-fe0f",2922],["1f9cd-1f3fe-200d-2640-fe0f",2923],["1f9cd-1f3ff-200d-2640-fe0f",2924],["1f9cd-1f3fb-200d-2642-fe0f",2925],["1f9cd-1f3fc-200d-2642-fe0f",2926],["1f9cd-1f3fd-200d-2642-fe0f",2927],["1f9cd-1f3fe-200d-2642-fe0f",2928],["1f9cd-1f3ff-200d-2642-fe0f",2929],["1f9cd-1f3fb",2930],["1f9cd-1f3fc",2931],["1f9cd-1f3fd",2932],["1f9cd-1f3fe",2933],["1f9cd-1f3ff",2934],["1f9ce-1f3fb-200d-2640-fe0f",2935],["1f9ce-1f3fc-200d-2640-fe0f",2936],["1f9ce-1f3fd-200d-2640-fe0f",2937],["1f9ce-1f3fe-200d-2640-fe0f",2938],["1f9ce-1f3ff-200d-2640-fe0f",2939],["1f9ce-1f3fb-200d-2642-fe0f",2940],["1f9ce-1f3fc-200d-2642-fe0f",2941],["1f9ce-1f3fd-200d-2642-fe0f",2942],["1f9ce-1f3fe-200d-2642-fe0f",2943],["1f9ce-1f3ff-200d-2642-fe0f",2944],["1f9ce-1f3fb",2945],["1f9ce-1f3fc",2946],["1f9ce-1f3fd",2947],["1f9ce-1f3fe",2948],["1f9ce-1f3ff",2949],["1f9cf-1f3fb-200d-2640-fe0f",2950],["1f9cf-1f3fc-200d-2640-fe0f",2951],["1f9cf-1f3fd-200d-2640-fe0f",2952],["1f9cf-1f3fe-200d-2640-fe0f",2953],["1f9cf-1f3ff-200d-2640-fe0f",2954],["1f9cf-1f3fb-200d-2642-fe0f",2955],["1f9cf-1f3fc-200d-2642-fe0f",2956],["1f9cf-1f3fd-200d-2642-fe0f",2957],["1f9cf-1f3fe-200d-2642-fe0f",2958],["1f9cf-1f3ff-200d-2642-fe0f",2959],["1f9cf-1f3fb",2960],["1f9cf-1f3fc",2961],["1f9cf-1f3fd",2962],["1f9cf-1f3fe",2963],["1f9cf-1f3ff",2964],["1f9d1-1f3fb-200d-1f33e",2965],["1f9d1-1f3fc-200d-1f33e",2966],["1f9d1-1f3fd-200d-1f33e",2967],["1f9d1-1f3fe-200d-1f33e",2968],["1f9d1-1f3ff-200d-1f33e",2969],["1f9d1-1f3fb-200d-1f373",2970],["1f9d1-1f3fc-200d-1f373",2971],["1f9d1-1f3fd-200d-1f373",2972],["1f9d1-1f3fe-200d-1f373",2973],["1f9d1-1f3ff-200d-1f373",2974],["1f9d1-1f3fb-200d-1f37c",2975],["1f9d1-1f3fc-200d-1f37c",2976],["1f9d1-1f3fd-200d-1f37c",2977],["1f9d1-1f3fe-200d-1f37c",2978],["1f9d1-1f3ff-200d-1f37c",2979],["1f9d1-1f3fb-200d-1f384",2980],["1f9d1-1f3fc-200d-1f384",2981],["1f9d1-1f3fd-200d-1f384",2982],["1f9d1-1f3fe-200d-1f384",2983],["1f9d1-1f3ff-200d-1f384",2984],["1f9d1-1f3fb-200d-1f393",2985],["1f9d1-1f3fc-200d-1f393",2986],["1f9d1-1f3fd-200d-1f393",2987],["1f9d1-1f3fe-200d-1f393",2988],["1f9d1-1f3ff-200d-1f393",2989],["1f9d1-1f3fb-200d-1f3a4",2990],["1f9d1-1f3fc-200d-1f3a4",2991],["1f9d1-1f3fd-200d-1f3a4",2992],["1f9d1-1f3fe-200d-1f3a4",2993],["1f9d1-1f3ff-200d-1f3a4",2994],["1f9d1-1f3fb-200d-1f3a8",2995],["1f9d1-1f3fc-200d-1f3a8",2996],["1f9d1-1f3fd-200d-1f3a8",2997],["1f9d1-1f3fe-200d-1f3a8",2998],["1f9d1-1f3ff-200d-1f3a8",2999],["1f9d1-1f3fb-200d-1f3eb",3000],["1f9d1-1f3fc-200d-1f3eb",3001],["1f9d1-1f3fd-200d-1f3eb",3002],["1f9d1-1f3fe-200d-1f3eb",3003],["1f9d1-1f3ff-200d-1f3eb",3004],["1f9d1-1f3fb-200d-1f3ed",3005],["1f9d1-1f3fc-200d-1f3ed",3006],["1f9d1-1f3fd-200d-1f3ed",3007],["1f9d1-1f3fe-200d-1f3ed",3008],["1f9d1-1f3ff-200d-1f3ed",3009],["1f9d1-1f3fb-200d-1f4bb",3010],["1f9d1-1f3fc-200d-1f4bb",3011],["1f9d1-1f3fd-200d-1f4bb",3012],["1f9d1-1f3fe-200d-1f4bb",3013],["1f9d1-1f3ff-200d-1f4bb",3014],["1f9d1-1f3fb-200d-1f4bc",3015],["1f9d1-1f3fc-200d-1f4bc",3016],["1f9d1-1f3fd-200d-1f4bc",3017],["1f9d1-1f3fe-200d-1f4bc",3018],["1f9d1-1f3ff-200d-1f4bc",3019],["1f9d1-1f3fb-200d-1f527",3020],["1f9d1-1f3fc-200d-1f527",3021],["1f9d1-1f3fd-200d-1f527",3022],["1f9d1-1f3fe-200d-1f527",3023],["1f9d1-1f3ff-200d-1f527",3024],["1f9d1-1f3fb-200d-1f52c",3025],["1f9d1-1f3fc-200d-1f52c",3026],["1f9d1-1f3fd-200d-1f52c",3027],["1f9d1-1f3fe-200d-1f52c",3028],["1f9d1-1f3ff-200d-1f52c",3029],["1f9d1-1f3fb-200d-1f680",3030],["1f9d1-1f3fc-200d-1f680",3031],["1f9d1-1f3fd-200d-1f680",3032],["1f9d1-1f3fe-200d-1f680",3033],["1f9d1-1f3ff-200d-1f680",3034],["1f9d1-1f3fb-200d-1f692",3035],["1f9d1-1f3fc-200d-1f692",3036],["1f9d1-1f3fd-200d-1f692",3037],["1f9d1-1f3fe-200d-1f692",3038],["1f9d1-1f3ff-200d-1f692",3039],["1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fb",3040],["1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fc",3041],["1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fd",3042],["1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fe",3043],["1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3ff",3044],["1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fb",3045],["1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fc",3046],["1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fd",3047],["1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fe",3048],["1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3ff",3049],["1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fb",3050],["1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fc",3051],["1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fd",3052],["1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fe",3053],["1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3ff",3054],["1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fb",3055],["1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fc",3056],["1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fd",3057],["1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fe",3058],["1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3ff",3059],["1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fb",3060],["1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fc",3061],["1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fd",3062],["1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fe",3063],["1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3ff",3064],["1f9d1-1f3fb-200d-1f9af",3065],["1f9d1-1f3fc-200d-1f9af",3066],["1f9d1-1f3fd-200d-1f9af",3067],["1f9d1-1f3fe-200d-1f9af",3068],["1f9d1-1f3ff-200d-1f9af",3069],["1f9d1-1f3fb-200d-1f9b0",3070],["1f9d1-1f3fc-200d-1f9b0",3071],["1f9d1-1f3fd-200d-1f9b0",3072],["1f9d1-1f3fe-200d-1f9b0",3073],["1f9d1-1f3ff-200d-1f9b0",3074],["1f9d1-1f3fb-200d-1f9b1",3075],["1f9d1-1f3fc-200d-1f9b1",3076],["1f9d1-1f3fd-200d-1f9b1",3077],["1f9d1-1f3fe-200d-1f9b1",3078],["1f9d1-1f3ff-200d-1f9b1",3079],["1f9d1-1f3fb-200d-1f9b2",3080],["1f9d1-1f3fc-200d-1f9b2",3081],["1f9d1-1f3fd-200d-1f9b2",3082],["1f9d1-1f3fe-200d-1f9b2",3083],["1f9d1-1f3ff-200d-1f9b2",3084],["1f9d1-1f3fb-200d-1f9b3",3085],["1f9d1-1f3fc-200d-1f9b3",3086],["1f9d1-1f3fd-200d-1f9b3",3087],["1f9d1-1f3fe-200d-1f9b3",3088],["1f9d1-1f3ff-200d-1f9b3",3089],["1f9d1-1f3fb-200d-1f9bc",3090],["1f9d1-1f3fc-200d-1f9bc",3091],["1f9d1-1f3fd-200d-1f9bc",3092],["1f9d1-1f3fe-200d-1f9bc",3093],["1f9d1-1f3ff-200d-1f9bc",3094],["1f9d1-1f3fb-200d-1f9bd",3095],["1f9d1-1f3fc-200d-1f9bd",3096],["1f9d1-1f3fd-200d-1f9bd",3097],["1f9d1-1f3fe-200d-1f9bd",3098],["1f9d1-1f3ff-200d-1f9bd",3099],["1f9d1-1f3fb-200d-2695-fe0f",3100],["1f9d1-1f3fc-200d-2695-fe0f",3101],["1f9d1-1f3fd-200d-2695-fe0f",3102],["1f9d1-1f3fe-200d-2695-fe0f",3103],["1f9d1-1f3ff-200d-2695-fe0f",3104],["1f9d1-1f3fb-200d-2696-fe0f",3105],["1f9d1-1f3fc-200d-2696-fe0f",3106],["1f9d1-1f3fd-200d-2696-fe0f",3107],["1f9d1-1f3fe-200d-2696-fe0f",3108],["1f9d1-1f3ff-200d-2696-fe0f",3109],["1f9d1-1f3fb-200d-2708-fe0f",3110],["1f9d1-1f3fc-200d-2708-fe0f",3111],["1f9d1-1f3fd-200d-2708-fe0f",3112],["1f9d1-1f3fe-200d-2708-fe0f",3113],["1f9d1-1f3ff-200d-2708-fe0f",3114],["1f9d1-1f3fb",3115],["1f9d1-1f3fc",3116],["1f9d1-1f3fd",3117],["1f9d1-1f3fe",3118],["1f9d1-1f3ff",3119],["1f9d2-1f3fb",3120],["1f9d2-1f3fc",3121],["1f9d2-1f3fd",3122],["1f9d2-1f3fe",3123],["1f9d2-1f3ff",3124],["1f9d3-1f3fb",3125],["1f9d3-1f3fc",3126],["1f9d3-1f3fd",3127],["1f9d3-1f3fe",3128],["1f9d3-1f3ff",3129],["1f9d4-1f3fb",3130],["1f9d4-1f3fc",3131],["1f9d4-1f3fd",3132],["1f9d4-1f3fe",3133],["1f9d4-1f3ff",3134],["1f9d5-1f3fb",3135],["1f9d5-1f3fc",3136],["1f9d5-1f3fd",3137],["1f9d5-1f3fe",3138],["1f9d5-1f3ff",3139],["1f9d6-1f3fb-200d-2640-fe0f",3140],["1f9d6-1f3fc-200d-2640-fe0f",3141],["1f9d6-1f3fd-200d-2640-fe0f",3142],["1f9d6-1f3fe-200d-2640-fe0f",3143],["1f9d6-1f3ff-200d-2640-fe0f",3144],["1f9d6-1f3fb-200d-2642-fe0f",3145],["1f9d6-1f3fc-200d-2642-fe0f",3146],["1f9d6-1f3fd-200d-2642-fe0f",3147],["1f9d6-1f3fe-200d-2642-fe0f",3148],["1f9d6-1f3ff-200d-2642-fe0f",3149],["1f9d6-1f3fb",3150],["1f9d6-1f3fc",3151],["1f9d6-1f3fd",3152],["1f9d6-1f3fe",3153],["1f9d6-1f3ff",3154],["1f9d7-1f3fb-200d-2640-fe0f",3155],["1f9d7-1f3fc-200d-2640-fe0f",3156],["1f9d7-1f3fd-200d-2640-fe0f",3157],["1f9d7-1f3fe-200d-2640-fe0f",3158],["1f9d7-1f3ff-200d-2640-fe0f",3159],["1f9d7-1f3fb-200d-2642-fe0f",3160],["1f9d7-1f3fc-200d-2642-fe0f",3161],["1f9d7-1f3fd-200d-2642-fe0f",3162],["1f9d7-1f3fe-200d-2642-fe0f",3163],["1f9d7-1f3ff-200d-2642-fe0f",3164],["1f9d7-1f3fb",3165],["1f9d7-1f3fc",3166],["1f9d7-1f3fd",3167],["1f9d7-1f3fe",3168],["1f9d7-1f3ff",3169],["1f9d8-1f3fb-200d-2640-fe0f",3170],["1f9d8-1f3fc-200d-2640-fe0f",3171],["1f9d8-1f3fd-200d-2640-fe0f",3172],["1f9d8-1f3fe-200d-2640-fe0f",3173],["1f9d8-1f3ff-200d-2640-fe0f",3174],["1f9d8-1f3fb-200d-2642-fe0f",3175],["1f9d8-1f3fc-200d-2642-fe0f",3176],["1f9d8-1f3fd-200d-2642-fe0f",3177],["1f9d8-1f3fe-200d-2642-fe0f",3178],["1f9d8-1f3ff-200d-2642-fe0f",3179],["1f9d8-1f3fb",3180],["1f9d8-1f3fc",3181],["1f9d8-1f3fd",3182],["1f9d8-1f3fe",3183],["1f9d8-1f3ff",3184],["1f9d9-1f3fb-200d-2640-fe0f",3185],["1f9d9-1f3fc-200d-2640-fe0f",3186],["1f9d9-1f3fd-200d-2640-fe0f",3187],["1f9d9-1f3fe-200d-2640-fe0f",3188],["1f9d9-1f3ff-200d-2640-fe0f",3189],["1f9d9-1f3fb-200d-2642-fe0f",3190],["1f9d9-1f3fc-200d-2642-fe0f",3191],["1f9d9-1f3fd-200d-2642-fe0f",3192],["1f9d9-1f3fe-200d-2642-fe0f",3193],["1f9d9-1f3ff-200d-2642-fe0f",3194],["1f9d9-1f3fb",3195],["1f9d9-1f3fc",3196],["1f9d9-1f3fd",3197],["1f9d9-1f3fe",3198],["1f9d9-1f3ff",3199],["1f9da-1f3fb-200d-2640-fe0f",3200],["1f9da-1f3fc-200d-2640-fe0f",3201],["1f9da-1f3fd-200d-2640-fe0f",3202],["1f9da-1f3fe-200d-2640-fe0f",3203],["1f9da-1f3ff-200d-2640-fe0f",3204],["1f9da-1f3fb-200d-2642-fe0f",3205],["1f9da-1f3fc-200d-2642-fe0f",3206],["1f9da-1f3fd-200d-2642-fe0f",3207],["1f9da-1f3fe-200d-2642-fe0f",3208],["1f9da-1f3ff-200d-2642-fe0f",3209],["1f9da-1f3fb",3210],["1f9da-1f3fc",3211],["1f9da-1f3fd",3212],["1f9da-1f3fe",3213],["1f9da-1f3ff",3214],["1f9db-1f3fb-200d-2640-fe0f",3215],["1f9db-1f3fc-200d-2640-fe0f",3216],["1f9db-1f3fd-200d-2640-fe0f",3217],["1f9db-1f3fe-200d-2640-fe0f",3218],["1f9db-1f3ff-200d-2640-fe0f",3219],["1f9db-1f3fb-200d-2642-fe0f",3220],["1f9db-1f3fc-200d-2642-fe0f",3221],["1f9db-1f3fd-200d-2642-fe0f",3222],["1f9db-1f3fe-200d-2642-fe0f",3223],["1f9db-1f3ff-200d-2642-fe0f",3224],["1f9db-1f3fb",3225],["1f9db-1f3fc",3226],["1f9db-1f3fd",3227],["1f9db-1f3fe",3228],["1f9db-1f3ff",3229],["1f9dc-1f3fb-200d-2640-fe0f",3230],["1f9dc-1f3fc-200d-2640-fe0f",3231],["1f9dc-1f3fd-200d-2640-fe0f",3232],["1f9dc-1f3fe-200d-2640-fe0f",3233],["1f9dc-1f3ff-200d-2640-fe0f",3234],["1f9dc-1f3fb-200d-2642-fe0f",3235],["1f9dc-1f3fc-200d-2642-fe0f",3236],["1f9dc-1f3fd-200d-2642-fe0f",3237],["1f9dc-1f3fe-200d-2642-fe0f",3238],["1f9dc-1f3ff-200d-2642-fe0f",3239],["1f9dc-1f3fb",3240],["1f9dc-1f3fc",3241],["1f9dc-1f3fd",3242],["1f9dc-1f3fe",3243],["1f9dc-1f3ff",3244],["1f9dd-1f3fb-200d-2640-fe0f",3245],["1f9dd-1f3fc-200d-2640-fe0f",3246],["1f9dd-1f3fd-200d-2640-fe0f",3247],["1f9dd-1f3fe-200d-2640-fe0f",3248],["1f9dd-1f3ff-200d-2640-fe0f",3249],["1f9dd-1f3fb-200d-2642-fe0f",3250],["1f9dd-1f3fc-200d-2642-fe0f",3251],["1f9dd-1f3fd-200d-2642-fe0f",3252],["1f9dd-1f3fe-200d-2642-fe0f",3253],["1f9dd-1f3ff-200d-2642-fe0f",3254],["1f9dd-1f3fb",3255],["1f9dd-1f3fc",3256],["1f9dd-1f3fd",3257],["1f9dd-1f3fe",3258],["1f9dd-1f3ff",3259],["261d-1f3fb",3260],["261d-1f3fc",3261],["261d-1f3fd",3262],["261d-1f3fe",3263],["261d-1f3ff",3264],["26f9-1f3fb-200d-2640-fe0f",3265],["26f9-1f3fc-200d-2640-fe0f",3266],["26f9-1f3fd-200d-2640-fe0f",3267],["26f9-1f3fe-200d-2640-fe0f",3268],["26f9-1f3ff-200d-2640-fe0f",3269],["26f9-1f3fb-200d-2642-fe0f",3270],["26f9-1f3fc-200d-2642-fe0f",3271],["26f9-1f3fd-200d-2642-fe0f",3272],["26f9-1f3fe-200d-2642-fe0f",3273],["26f9-1f3ff-200d-2642-fe0f",3274],["26f9-1f3fb",3275],["26f9-1f3fc",3276],["26f9-1f3fd",3277],["26f9-1f3fe",3278],["26f9-1f3ff",3279],["270a-1f3fb",3280],["270a-1f3fc",3281],["270a-1f3fd",3282],["270a-1f3fe",3283],["270a-1f3ff",3284],["270b-1f3fb",3285],["270b-1f3fc",3286],["270b-1f3fd",3287],["270b-1f3fe",3288],["270b-1f3ff",3289],["270c-1f3fb",3290],["270c-1f3fc",3291],["270c-1f3fd",3292],["270c-1f3fe",3293],["270c-1f3ff",3294],["270d-1f3fb",3295],["270d-1f3fc",3296],["270d-1f3fd",3297],["270d-1f3fe",3298],["270d-1f3ff",3299]]); + +export const CategoryNames = ["recent","smileys-emotion","people-body","animals-nature","food-drink","travel-places","activities","objects","symbols","flags","custom"]; + +export const CategoryMessage = new Map([[["smileys-emotion","Smileys & Emotion"],["people-body","People & Body"],["component","Component"],["animals-nature","Animals & Nature"],["food-drink","Food & Drink"],["travel-places","Travel & Places"],["activities","Activities"],["objects","Objects"],["symbols","Symbols"],["flags","Flags"],["custom","Custom"],["recent","Recently Used"],["searchResults","Search Results"]]]); + +export const CategoryTranslations = new Map([['recent', t('emoji_picker.recent')],['searchResults', t('emoji_picker.searchResults')],['recent', t('emoji_picker.recent')],['smileys-emotion', t('emoji_picker.smileys-emotion')],['people-body', t('emoji_picker.people-body')],['animals-nature', t('emoji_picker.animals-nature')],['food-drink', t('emoji_picker.food-drink')],['travel-places', t('emoji_picker.travel-places')],['activities', t('emoji_picker.activities')],['objects', t('emoji_picker.objects')],['symbols', t('emoji_picker.symbols')],['flags', t('emoji_picker.flags')],['custom', t('emoji_picker.custom')]]); + +export const SkinTranslations = new Map([['default', t('emoji_skin.default')], ['1F3FB', t('emoji_skin.light_skin_tone')], ['1F3FC', t('emoji_skin.medium_light_skin_tone')], ['1F3FD', t('emoji_skin.medium_skin_tone')], ['1F3FE', t('emoji_skin.medium_dark_skin_tone')], ['1F3FF', t('emoji_skin.dark_skin_tone')]]); + +export const ComponentCategory = 'Component'; + +export const AllEmojiIndicesByCategory = new Map([["smileys-emotion",[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150]],["people-body",[151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,2041,2042,2043,2044,2045,2046,2047,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088,2089,2090,2091,2092,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105,2106,2107,2108,2109,2110,2111,2112,2113,2114,2115,2116,2117,2118,2119,2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2137,2138,2139,2140,2141,2142,2143,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159,2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195,2196,2197,2198,2199,2200,2201,2202,2203,2204,2205,2206,2207,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218,2219,2220,2221,2222,2223,2224,2225,2226,2227,2228,2229,2230,2231,2232,2233,2234,2235,2236,2237,2238,2239,2240,2241,2242,2243,2244,2245,2246,2247,2248,2249,2250,2251,2252,2253,2254,2255,2256,2257,2258,2259,2260,2261,2262,2263,2264,2265,2266,2267,2268,2269,2270,2271,2272,2273,2274,2275,2276,2277,2278,2279,2280,2281,2282,2283,2284,2285,2286,2287,2288,2289,2290,2291,2292,2293,2294,2295,2296,2297,2298,2299,2300,2301,2302,2303,2304,2305,2306,2307,2308,2309,2310,2311,2312,2313,2314,2315,2316,2317,2318,2319,2320,2321,2322,2323,2324,2325,2326,2327,2328,2329,2330,2331,2332,2333,2334,2335,2336,2337,2338,2339,2340,2341,2342,2343,2344,2345,2346,2347,2348,2349,2350,2351,2352,2353,2354,2355,2356,2357,2358,2359,2360,2361,2362,2363,2364,2365,2366,2367,2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379,2380,2381,2382,2383,2384,2385,2386,2387,2388,2389,2390,2391,2392,2393,2394,2395,2396,2397,2398,2399,2400,2401,2402,2403,2404,2405,2406,2407,2408,2409,2410,2411,2412,2413,2414,2415,2416,2417,2418,2419,2420,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430,2431,2432,2433,2434,2435,2436,2437,2438,2439,2440,2441,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2453,2454,2455,2456,2457,2458,2459,2460,2461,2462,2463,2464,2465,2466,2467,2468,2469,2470,2471,2472,2473,2474,2475,2476,2477,2478,2479,2480,2481,2482,2483,2484,2485,2486,2487,2488,2489,2490,2491,2492,2493,2494,2495,2496,2497,2498,2499,2500,2501,2502,2503,2504,2505,2506,2507,2508,2509,2510,2511,2512,2513,2514,2515,2516,2517,2518,2519,2520,2521,2522,2523,2524,2525,2526,2527,2528,2529,2530,2531,2532,2533,2534,2535,2536,2537,2538,2539,2540,2541,2542,2543,2544,2545,2546,2547,2548,2549,2550,2551,2552,2553,2554,2555,2556,2557,2558,2559,2560,2561,2562,2563,2564,2565,2566,2567,2568,2569,2570,2571,2572,2573,2574,2575,2576,2577,2578,2579,2580,2581,2582,2583,2584,2585,2586,2587,2588,2589,2590,2591,2592,2593,2594,2595,2596,2597,2598,2599,2600,2601,2602,2603,2604,2605,2606,2607,2608,2609,2610,2611,2612,2613,2614,2615,2616,2617,2618,2619,2620,2621,2622,2623,2624,2625,2626,2627,2628,2629,2630,2631,2632,2633,2634,2635,2636,2637,2638,2639,2640,2641,2642,2643,2644,2645,2646,2647,2648,2649,2650,2651,2652,2653,2654,2655,2656,2657,2658,2659,2660,2661,2662,2663,2664,2665,2666,2667,2668,2669,2670,2671,2672,2673,2674,2675,2676,2677,2678,2679,2680,2681,2682,2683,2684,2685,2686,2687,2688,2689,2690,2691,2692,2693,2694,2695,2696,2697,2698,2699,2700,2701,2702,2703,2704,2705,2706,2707,2708,2709,2710,2711,2712,2713,2714,2715,2716,2717,2718,2719,2720,2721,2722,2723,2724,2725,2726,2727,2728,2729,2730,2731,2732,2733,2734,2735,2736,2737,2738,2739,2740,2741,2742,2743,2744,2745,2746,2747,2748,2749,2750,2751,2752,2753,2754,2755,2756,2757,2758,2759,2760,2761,2762,2763,2764,2765,2766,2767,2768,2769,2770,2771,2772,2773,2774,2775,2776,2777,2778,2779,2780,2781,2782,2783,2784,2785,2786,2787,2788,2789,2790,2791,2792,2793,2794,2795,2796,2797,2798,2799,2800,2801,2802,2803,2804,2805,2806,2807,2808,2809,2810,2811,2812,2813,2814,2815,2816,2817,2818,2819,2820,2821,2822,2823,2824,2825,2826,2827,2828,2829,2830,2831,2832,2833,2834,2835,2836,2837,2838,2839,2840,2841,2842,2843,2844,2845,2846,2847,2848,2849,2850,2851,2852,2853,2854,2855,2856,2857,2858,2859,2860,2861,2862,2863,2864,2865,2866,2867,2868,2869,2870,2871,2872,2873,2874,2875,2876,2877,2878,2879,2880,2881,2882,2883,2884,2885,2886,2887,2888,2889,2890,2891,2892,2893,2894,2895,2896,2897,2898,2899,2900,2901,2902,2903,2904,2905,2906,2907,2908,2909,2910,2911,2912,2913,2914,2915,2916,2917,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,2931,2932,2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3241,3242,3243,3244,3245,3246,3247,3248,3249,3250,3251,3252,3253,3254,3255,3256,3257,3258,3259,3260,3261,3262,3263,3264,3265,3266,3267,3268,3269,3270,3271,3272,3273,3274,3275,3276,3277,3278,3279,3280,3281,3282,3283,3284,3285,3286,3287,3288,3289,3290,3291,3292,3293,3294,3295,3296,3297,3298,3299]],["component",[498,499,500,501,502]],["animals-nature",[503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642]],["food-drink",[643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771]],["travel-places",[772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986]],["activities",[987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070]],["objects",[1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320]],["symbols",[1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540]],["flags",[1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809]],["custom",[3300]]]); + +export const EmojiIndicesByCategoryAndSkin = new Map([['default', new Map([["people-body",[151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,181,182,183,184,185,188,189,190,191,192,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,401,402,403,404,405,406,408,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458]]])], ['1F3FB', new Map([["people-body",[1810,1815,1820,1825,1830,1835,1840,1845,1850,1855,1860,1865,1870,1875,1880,1885,1890,1895,1900,1905,1910,1915,1920,1925,1930,1935,1940,1945,1950,1955,1960,1965,1970,1975,1980,1985,1990,1995,2000,2005,2010,2015,2020,2025,2030,2035,2040,2045,2050,2055,2060,2065,2070,2075,2080,2085,2090,2095,2100,2105,2110,2115,2120,2125,2130,2135,2140,2145,2150,2155,2160,2165,2170,2175,2180,2185,2190,2195,2200,2205,2210,2215,2220,2225,2230,2231,2232,2233,2250,2255,2256,2257,2258,2275,2280,2281,2282,2283,2300,2305,2310,2315,2320,2325,2330,2335,2340,2345,2350,2355,2360,2365,2370,2375,2380,2385,2390,2395,2400,2405,2410,2415,2420,2425,2430,2435,2440,2445,2450,2455,2460,2465,2470,2475,2480,2485,2490,2495,2500,2505,2510,2515,2520,2525,2530,2535,2540,2545,2550,2555,2560,2565,2570,2575,2580,2585,2590,2595,2600,2605,2610,2615,2620,2625,2630,2635,2640,2645,2650,2655,2660,2665,2670,2675,2680,2685,2690,2695,2700,2705,2710,2715,2720,2725,2730,2735,2740,2745,2750,2755,2760,2765,2770,2775,2780,2785,2790,2795,2800,2805,2810,2815,2820,2825,2830,2835,2840,2845,2850,2855,2860,2865,2870,2875,2880,2885,2890,2895,2900,2905,2910,2915,2920,2925,2930,2935,2940,2945,2950,2955,2960,2965,2970,2975,2980,2985,2990,2995,3000,3005,3010,3015,3020,3025,3030,3035,3040,3041,3042,3043,3044,3065,3070,3075,3080,3085,3090,3095,3100,3105,3110,3115,3120,3125,3130,3135,3140,3145,3150,3155,3160,3165,3170,3175,3180,3185,3190,3195,3200,3205,3210,3215,3220,3225,3230,3235,3240,3245,3250,3255,3260,3265,3270,3275,3280,3285,3290,3295]]])], ['1F3FC', new Map([["people-body",[1811,1816,1821,1826,1831,1836,1841,1846,1851,1856,1861,1866,1871,1876,1881,1886,1891,1896,1901,1906,1911,1916,1921,1926,1931,1936,1941,1946,1951,1956,1961,1966,1971,1976,1981,1986,1991,1996,2001,2006,2011,2016,2021,2026,2031,2036,2041,2046,2051,2056,2061,2066,2071,2076,2081,2086,2091,2096,2101,2106,2111,2116,2121,2126,2131,2136,2141,2146,2151,2156,2161,2166,2171,2176,2181,2186,2191,2196,2201,2206,2211,2216,2221,2226,2234,2235,2236,2237,2251,2259,2260,2261,2262,2276,2284,2285,2286,2287,2301,2306,2311,2316,2321,2326,2331,2336,2341,2346,2351,2356,2361,2366,2371,2376,2381,2386,2391,2396,2401,2406,2411,2416,2421,2426,2431,2436,2441,2446,2451,2456,2461,2466,2471,2476,2481,2486,2491,2496,2501,2506,2511,2516,2521,2526,2531,2536,2541,2546,2551,2556,2561,2566,2571,2576,2581,2586,2591,2596,2601,2606,2611,2616,2621,2626,2631,2636,2641,2646,2651,2656,2661,2666,2671,2676,2681,2686,2691,2696,2701,2706,2711,2716,2721,2726,2731,2736,2741,2746,2751,2756,2761,2766,2771,2776,2781,2786,2791,2796,2801,2806,2811,2816,2821,2826,2831,2836,2841,2846,2851,2856,2861,2866,2871,2876,2881,2886,2891,2896,2901,2906,2911,2916,2921,2926,2931,2936,2941,2946,2951,2956,2961,2966,2971,2976,2981,2986,2991,2996,3001,3006,3011,3016,3021,3026,3031,3036,3045,3046,3047,3048,3049,3066,3071,3076,3081,3086,3091,3096,3101,3106,3111,3116,3121,3126,3131,3136,3141,3146,3151,3156,3161,3166,3171,3176,3181,3186,3191,3196,3201,3206,3211,3216,3221,3226,3231,3236,3241,3246,3251,3256,3261,3266,3271,3276,3281,3286,3291,3296]]])], ['1F3FD', new Map([["people-body",[1812,1817,1822,1827,1832,1837,1842,1847,1852,1857,1862,1867,1872,1877,1882,1887,1892,1897,1902,1907,1912,1917,1922,1927,1932,1937,1942,1947,1952,1957,1962,1967,1972,1977,1982,1987,1992,1997,2002,2007,2012,2017,2022,2027,2032,2037,2042,2047,2052,2057,2062,2067,2072,2077,2082,2087,2092,2097,2102,2107,2112,2117,2122,2127,2132,2137,2142,2147,2152,2157,2162,2167,2172,2177,2182,2187,2192,2197,2202,2207,2212,2217,2222,2227,2238,2239,2240,2241,2252,2263,2264,2265,2266,2277,2288,2289,2290,2291,2302,2307,2312,2317,2322,2327,2332,2337,2342,2347,2352,2357,2362,2367,2372,2377,2382,2387,2392,2397,2402,2407,2412,2417,2422,2427,2432,2437,2442,2447,2452,2457,2462,2467,2472,2477,2482,2487,2492,2497,2502,2507,2512,2517,2522,2527,2532,2537,2542,2547,2552,2557,2562,2567,2572,2577,2582,2587,2592,2597,2602,2607,2612,2617,2622,2627,2632,2637,2642,2647,2652,2657,2662,2667,2672,2677,2682,2687,2692,2697,2702,2707,2712,2717,2722,2727,2732,2737,2742,2747,2752,2757,2762,2767,2772,2777,2782,2787,2792,2797,2802,2807,2812,2817,2822,2827,2832,2837,2842,2847,2852,2857,2862,2867,2872,2877,2882,2887,2892,2897,2902,2907,2912,2917,2922,2927,2932,2937,2942,2947,2952,2957,2962,2967,2972,2977,2982,2987,2992,2997,3002,3007,3012,3017,3022,3027,3032,3037,3050,3051,3052,3053,3054,3067,3072,3077,3082,3087,3092,3097,3102,3107,3112,3117,3122,3127,3132,3137,3142,3147,3152,3157,3162,3167,3172,3177,3182,3187,3192,3197,3202,3207,3212,3217,3222,3227,3232,3237,3242,3247,3252,3257,3262,3267,3272,3277,3282,3287,3292,3297]]])], ['1F3FE', new Map([["people-body",[1813,1818,1823,1828,1833,1838,1843,1848,1853,1858,1863,1868,1873,1878,1883,1888,1893,1898,1903,1908,1913,1918,1923,1928,1933,1938,1943,1948,1953,1958,1963,1968,1973,1978,1983,1988,1993,1998,2003,2008,2013,2018,2023,2028,2033,2038,2043,2048,2053,2058,2063,2068,2073,2078,2083,2088,2093,2098,2103,2108,2113,2118,2123,2128,2133,2138,2143,2148,2153,2158,2163,2168,2173,2178,2183,2188,2193,2198,2203,2208,2213,2218,2223,2228,2242,2243,2244,2245,2253,2267,2268,2269,2270,2278,2292,2293,2294,2295,2303,2308,2313,2318,2323,2328,2333,2338,2343,2348,2353,2358,2363,2368,2373,2378,2383,2388,2393,2398,2403,2408,2413,2418,2423,2428,2433,2438,2443,2448,2453,2458,2463,2468,2473,2478,2483,2488,2493,2498,2503,2508,2513,2518,2523,2528,2533,2538,2543,2548,2553,2558,2563,2568,2573,2578,2583,2588,2593,2598,2603,2608,2613,2618,2623,2628,2633,2638,2643,2648,2653,2658,2663,2668,2673,2678,2683,2688,2693,2698,2703,2708,2713,2718,2723,2728,2733,2738,2743,2748,2753,2758,2763,2768,2773,2778,2783,2788,2793,2798,2803,2808,2813,2818,2823,2828,2833,2838,2843,2848,2853,2858,2863,2868,2873,2878,2883,2888,2893,2898,2903,2908,2913,2918,2923,2928,2933,2938,2943,2948,2953,2958,2963,2968,2973,2978,2983,2988,2993,2998,3003,3008,3013,3018,3023,3028,3033,3038,3055,3056,3057,3058,3059,3068,3073,3078,3083,3088,3093,3098,3103,3108,3113,3118,3123,3128,3133,3138,3143,3148,3153,3158,3163,3168,3173,3178,3183,3188,3193,3198,3203,3208,3213,3218,3223,3228,3233,3238,3243,3248,3253,3258,3263,3268,3273,3278,3283,3288,3293,3298]]])], ['1F3FF', new Map([["people-body",[1814,1819,1824,1829,1834,1839,1844,1849,1854,1859,1864,1869,1874,1879,1884,1889,1894,1899,1904,1909,1914,1919,1924,1929,1934,1939,1944,1949,1954,1959,1964,1969,1974,1979,1984,1989,1994,1999,2004,2009,2014,2019,2024,2029,2034,2039,2044,2049,2054,2059,2064,2069,2074,2079,2084,2089,2094,2099,2104,2109,2114,2119,2124,2129,2134,2139,2144,2149,2154,2159,2164,2169,2174,2179,2184,2189,2194,2199,2204,2209,2214,2219,2224,2229,2246,2247,2248,2249,2254,2271,2272,2273,2274,2279,2296,2297,2298,2299,2304,2309,2314,2319,2324,2329,2334,2339,2344,2349,2354,2359,2364,2369,2374,2379,2384,2389,2394,2399,2404,2409,2414,2419,2424,2429,2434,2439,2444,2449,2454,2459,2464,2469,2474,2479,2484,2489,2494,2499,2504,2509,2514,2519,2524,2529,2534,2539,2544,2549,2554,2559,2564,2569,2574,2579,2584,2589,2594,2599,2604,2609,2614,2619,2624,2629,2634,2639,2644,2649,2654,2659,2664,2669,2674,2679,2684,2689,2694,2699,2704,2709,2714,2719,2724,2729,2734,2739,2744,2749,2754,2759,2764,2769,2774,2779,2784,2789,2794,2799,2804,2809,2814,2819,2824,2829,2834,2839,2844,2849,2854,2859,2864,2869,2874,2879,2884,2889,2894,2899,2904,2909,2914,2919,2924,2929,2934,2939,2944,2949,2954,2959,2964,2969,2974,2979,2984,2989,2994,2999,3004,3009,3014,3019,3024,3029,3034,3039,3060,3061,3062,3063,3064,3069,3074,3079,3084,3089,3094,3099,3104,3109,3114,3119,3124,3129,3134,3139,3144,3149,3154,3159,3164,3169,3174,3179,3184,3189,3194,3199,3204,3209,3214,3219,3224,3229,3234,3239,3244,3249,3254,3259,3264,3269,3274,3279,3284,3289,3294,3299]]])]]); +export const EmojiIndicesByCategoryNoSkin = new Map([["smileys-emotion",[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150]],["people-body",[180,186,187,193,194,195,196,197,198,199,200,201,362,363,364,365,366,367,398,399,400,407,409,438,439,440,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497]],["component",[498,499,500,501,502]],["animals-nature",[503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642]],["food-drink",[643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771]],["travel-places",[772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986]],["activities",[987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070]],["objects",[1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320]],["symbols",[1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540]],["flags",[1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809]],["custom",[3300]]]); + +export const skinCodes = {"1F3FB":"light_skin_tone","1F3FC":"medium_light_skin_tone","1F3FD":"medium_skin_tone","1F3FE":"medium_dark_skin_tone","1F3FF":"dark_skin_tone","default":"default"}; +export const EMOJI_DEFAULT_SKIN = 'default'; + +// Generate the list of indices that belong to each category by an specified skin +function genSkinnedCategories(skin) { + const result = new Map(); + for (const cat of CategoryNames) { + const indices = []; + const skinCat = (EmojiIndicesByCategoryAndSkin.get(skin) || new Map()).get(cat) || []; + indices.push(...(EmojiIndicesByCategoryNoSkin.get(cat) || [])); + indices.push(...skinCat); + + result.set(cat, indices); + } + return result; +} + +export const EmojiIndicesByCategory = new Map([['default', genSkinnedCategories('default')], ['1F3FB', genSkinnedCategories('1F3FB')], ['1F3FC', genSkinnedCategories('1F3FC')], ['1F3FD', genSkinnedCategories('1F3FD')], ['1F3FE', genSkinnedCategories('1F3FE')], ['1F3FF', genSkinnedCategories('1F3FF')]]); \ No newline at end of file diff --git a/app/utils/file.ts b/app/utils/file.ts deleted file mode 100644 index 72dd30399..000000000 --- a/app/utils/file.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Platform} from 'react-native'; -import {FileSystem} from 'react-native-unimodules'; - -import {hashCode} from './security'; - -export async function deleteFileCache(serverUrl: string) { - const serverDir = hashCode(serverUrl); - const cacheDir = `${FileSystem.cacheDirectory}${serverDir}`; - if (cacheDir) { - const cacheDirInfo = await FileSystem.getInfoAsync(cacheDir); - if (cacheDirInfo.exists) { - if (Platform.OS === 'ios') { - await FileSystem.deleteAsync(cacheDir, {idempotent: true}); - await FileSystem.makeDirectoryAsync(cacheDir, {intermediates: true}); - } else { - const lstat = await FileSystem.readDirectoryAsync(cacheDir); - lstat.forEach((stat: string) => { - FileSystem.deleteAsync(stat, {idempotent: true}); - }); - } - } - } - - return true; -} diff --git a/app/utils/file/index.ts b/app/utils/file/index.ts new file mode 100644 index 000000000..4f6b4a035 --- /dev/null +++ b/app/utils/file/index.ts @@ -0,0 +1,232 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import mimeDB from 'mime-db'; +import {Platform} from 'react-native'; +import {FileSystem} from 'react-native-unimodules'; + +import {hashCode} from '@utils/security'; + +const EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/; +const CONTENT_DISPOSITION_REGEXP = /inline;filename=".*\.([a-z]+)";/i; +const DEFAULT_SERVER_MAX_FILE_SIZE = 50 * 1024 * 1024;// 50 Mb + +export const GENERAL_SUPPORTED_DOCS_FORMAT = [ + 'application/json', + 'application/msword', + 'application/pdf', + 'application/rtf', + 'application/vnd.ms-excel', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/x-x509-ca-cert', + 'application/xml', + 'text/csv', + 'text/plain', +]; + +const SUPPORTED_DOCS_FORMAT = Platform.select({ + android: GENERAL_SUPPORTED_DOCS_FORMAT, + ios: [ + ...GENERAL_SUPPORTED_DOCS_FORMAT, + 'application/vnd.apple.pages', + 'application/vnd.apple.numbers', + 'application/vnd.apple.keynote', + ], +}); + +const SUPPORTED_VIDEO_FORMAT = Platform.select({ + ios: ['video/mp4', 'video/x-m4v', 'video/quicktime'], + android: ['video/3gpp', 'video/x-matroska', 'video/mp4', 'video/webm'], +}); + +const types: Record = {}; +const extensions: Record = {}; + +/** + * Populate the extensions and types maps. + * @private + */ + +function populateMaps() { + // source preference (least -> most) + const preference = ['nginx', 'apache', undefined, 'iana']; + + Object.keys(mimeDB).forEach((type) => { + const mime = mimeDB[type]; + const exts = mime.extensions; + + if (!exts || !exts.length) { + return; + } + + extensions[type] = exts; + + for (let i = 0; i < exts.length; i++) { + const extension = exts[i]; + + if (types[extension]) { + const from = preference.indexOf(mimeDB[types[extension]].source); + const to = preference.indexOf(mime.source); + + if (types[extension] !== 'application/octet-stream' && + (from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) { + continue; + } + } + + types[extension] = type; + } + }); +} + +const vectorIconsDir = 'vectorIcons'; +const dirsToExclude = ['Cache.db', 'WebKit', 'WebView', vectorIconsDir]; +async function getDirectorySize(fileStats: FileSystem.FileInfo) { + if (fileStats?.exists) { + let total = 0; + if (fileStats.isDirectory) { + const exclude = dirsToExclude.find((f) => fileStats.uri.includes(f)); + if (!exclude) { + const paths = await FileSystem.readDirectoryAsync(fileStats.uri); + for await (const path of paths) { + const info = await FileSystem.getInfoAsync(`${fileStats.uri}/${path}`, {size: true}); + if (info.isDirectory) { + const dirSize = await getDirectorySize(info); + total += dirSize; + } else { + total += (info.size || 0); + } + } + } + } else { + total = fileStats.size; + } + + return total; + } + + return 0; +} + +export async function getFileCacheSize() { + if (FileSystem.cacheDirectory) { + const cacheStats = await FileSystem.getInfoAsync(FileSystem.cacheDirectory); + const size = await getDirectorySize(cacheStats); + + return size; + } + + return 0; +} + +export async function deleteFileCache(serverUrl: string) { + const serverDir = hashCode(serverUrl); + const cacheDir = `${FileSystem.cacheDirectory}/${serverDir}`; + if (cacheDir) { + const cacheDirInfo = await FileSystem.getInfoAsync(cacheDir); + if (cacheDirInfo.exists) { + if (Platform.OS === 'ios') { + await FileSystem.deleteAsync(cacheDir, {idempotent: true}); + await FileSystem.makeDirectoryAsync(cacheDir, {intermediates: true}); + } else { + const lstat = await FileSystem.readDirectoryAsync(cacheDir); + lstat.forEach((stat: string) => { + FileSystem.deleteAsync(stat, {idempotent: true}); + }); + } + } + } + + return true; +} + +export function lookupMimeType(filename: string) { + if (!Object.keys(extensions).length) { + populateMaps(); + } + + const ext = filename.split('.').pop()?.toLowerCase(); + return types[ext!] || 'application/octet-stream'; +} + +export function getExtensionFromMime(type: string) { + if (!Object.keys(extensions).length) { + populateMaps(); + } + + if (!type || typeof type !== 'string') { + return undefined; + } + + const match = EXTRACT_TYPE_REGEXP.exec(type); + + // get extensions + const exts = match && extensions[match[1].toLowerCase()]; + + if (!exts || !exts.length) { + return undefined; + } + + return exts[0]; +} + +export function getExtensionFromContentDisposition(contentDisposition: string) { + const match = CONTENT_DISPOSITION_REGEXP.exec(contentDisposition); + let extension = match && match[1]; + if (extension) { + if (!Object.keys(types).length) { + populateMaps(); + } + + extension = extension.toLowerCase(); + if (types[extension]) { + return extension; + } + + return null; + } + + return null; +} + +export const getAllowedServerMaxFileSize = (config: ClientConfig) => { + return config && config.MaxFileSize ? parseInt(config.MaxFileSize, 10) : DEFAULT_SERVER_MAX_FILE_SIZE; +}; + +export const isGif = (file?: FileInfo) => { + let mime = file?.mime_type || ''; + if (mime && mime.includes(';')) { + mime = mime.split(';')[0]; + } else if (!mime && file?.name) { + mime = lookupMimeType(file.name); + } + + return mime === 'image/gif'; +}; + +export const isImage = (file?: FileInfo) => (file?.has_preview_image || isGif(file) || file?.mime_type?.startsWith('image/')); + +export const isDocument = (file?: FileInfo) => { + let mime = file?.mime_type || ''; + if (mime && mime.includes(';')) { + mime = mime.split(';')[0]; + } else if (!mime && file?.name) { + mime = lookupMimeType(file.name); + } + + return SUPPORTED_DOCS_FORMAT!.includes(mime); +}; + +export const isVideo = (file?: FileInfo) => { + let mime = file?.mime_type || ''; + if (mime && mime.includes(';')) { + mime = mime.split(';')[0]; + } else if (!mime && file?.name) { + mime = lookupMimeType(file.name); + } + + return SUPPORTED_VIDEO_FORMAT!.includes(mime); +}; diff --git a/app/utils/gallery/index.ts b/app/utils/gallery/index.ts new file mode 100644 index 000000000..62fc7b777 --- /dev/null +++ b/app/utils/gallery/index.ts @@ -0,0 +1,137 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Dimensions, Keyboard, Platform} from 'react-native'; +import {Options, SharedElementTransition, StackAnimationOptions, ViewAnimationOptions} from 'react-native-navigation'; +import parseUrl from 'url-parse'; + +import {goToScreen} from '@screens/navigation'; +import {isImage, lookupMimeType} from '@utils/file'; +import {generateId} from '@utils/general'; + +export function openGalleryAtIndex(index: number, files: FileInfo[]) { + Keyboard.dismiss(); + requestAnimationFrame(() => { + const screen = 'Gallery'; + const passProps = { + index, + files, + }; + const windowHeight = Dimensions.get('window').height; + const sharedElementTransitions: SharedElementTransition[] = []; + + const contentPush = {} as ViewAnimationOptions; + const contentPop = {} as ViewAnimationOptions; + const file = files[index]; + + if (isImage(file)) { + sharedElementTransitions.push({ + fromId: `image-${file.id}`, + toId: `gallery-${file.id}`, + duration: 300, + interpolation: {type: 'accelerateDecelerate'}, + }); + } else { + contentPush.y = { + from: windowHeight, + to: 0, + duration: 300, + interpolation: {type: 'decelerate'}, + }; + + if (Platform.OS === 'ios') { + contentPop.translationY = { + from: 0, + to: windowHeight, + duration: 300, + }; + } else { + contentPop.y = { + from: 0, + to: windowHeight, + duration: 300, + }; + contentPop.alpha = { + from: 1, + to: 0, + duration: 100, + }; + } + } + + const options: Options = { + layout: { + backgroundColor: '#000', + componentBackgroundColor: '#000', + orientation: ['portrait', 'landscape'], + }, + topBar: { + background: { + color: '#000', + }, + visible: Platform.OS === 'android', + }, + animations: { + push: { + waitForRender: true, + sharedElementTransitions, + }, + }, + }; + + if (Object.keys(contentPush).length) { + options.animations!.push = { + ...options.animations!.push, + ...Platform.select({ + android: contentPush, + ios: { + content: contentPush, + }, + }), + }; + } + + if (Object.keys(contentPop).length) { + options.animations!.pop = Platform.select({ + android: contentPop, + ios: { + content: contentPop, + }, + }); + } + + goToScreen(screen, '', passProps, options); + }); +} + +export function openGallerWithMockFile(uri: string, postId: string, height: number, width: number, fileId?: string) { + const url = decodeURIComponent(uri); + let filename = parseUrl(url.substr(url.lastIndexOf('/'))).pathname.replace('/', ''); + let extension = filename.split('.').pop(); + + if (!extension || extension === filename) { + const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.')); + filename = `${filename}${ext}`; + extension = ext; + } + + const file: FileInfo = { + id: fileId || generateId(), + clientId: 'mock_client_id', + create_at: Date.now(), + delete_at: 0, + extension, + has_preview_image: true, + height, + mime_type: lookupMimeType(filename), + name: filename, + post_id: postId, + size: 0, + update_at: 0, + uri, + user_id: 'mock_user_id', + width, + }; + + openGalleryAtIndex(0, [file]); +} diff --git a/app/utils/general/index.ts b/app/utils/general/index.ts index 84cb14a13..33a6ef87b 100644 --- a/app/utils/general/index.ts +++ b/app/utils/general/index.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import ReactNativeHapticFeedback, {HapticFeedbackTypes} from 'react-native-haptic-feedback'; + type SortByCreatAt = (Session | Channel | Team | Post) & { create_at: number; } @@ -30,6 +32,13 @@ export const generateId = (): string => { return id; }; +export function hapticFeedback(method: HapticFeedbackTypes = 'impactLight') { + ReactNativeHapticFeedback.trigger(method, { + enableVibrateFallback: false, + ignoreAndroidSystemSettings: false, + }); +} + export const sortByNewest = (a: SortByCreatAt, b: SortByCreatAt) => { if (a.create_at > b.create_at) { return -1; diff --git a/app/utils/images/index.ts b/app/utils/images/index.ts new file mode 100644 index 000000000..940a4a09c --- /dev/null +++ b/app/utils/images/index.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Dimensions} from 'react-native'; + +import {Device} from '@constants'; +import {IMAGE_MAX_HEIGHT, IMAGE_MIN_DIMENSION, MAX_GIF_SIZE, VIEWPORT_IMAGE_OFFSET, VIEWPORT_IMAGE_REPLY_OFFSET} from '@constants/image'; + +export const calculateDimensions = (height: number, width: number, viewPortWidth = 0, viewPortHeight = 0) => { + if (!height || !width) { + return { + height: 0, + width: 0, + }; + } + + const ratio = height / width; + const heightRatio = width / height; + + let imageWidth = width; + let imageHeight = height; + + if (width > viewPortWidth) { + imageWidth = viewPortWidth; + imageHeight = imageWidth * ratio; + } else if (width < IMAGE_MIN_DIMENSION) { + imageWidth = IMAGE_MIN_DIMENSION; + imageHeight = imageWidth * ratio; + } + + if ((imageHeight > IMAGE_MAX_HEIGHT || (viewPortHeight && imageHeight > viewPortHeight)) && viewPortHeight <= IMAGE_MAX_HEIGHT) { + imageHeight = viewPortHeight || IMAGE_MAX_HEIGHT; + imageWidth = imageHeight * heightRatio; + } else if (imageHeight < IMAGE_MIN_DIMENSION && IMAGE_MIN_DIMENSION * heightRatio <= viewPortWidth) { + imageHeight = IMAGE_MIN_DIMENSION; + imageWidth = imageHeight * heightRatio; + } else if (viewPortHeight && imageHeight > viewPortHeight) { + imageHeight = viewPortHeight; + imageWidth = imageHeight * heightRatio; + } + + return { + height: imageHeight, + width: imageWidth, + }; +}; + +export function getViewPortWidth(isReplyPost: boolean, permanentSidebar = false) { + const {width, height} = Dimensions.get('window'); + let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET; + + if (permanentSidebar) { + portraitPostWidth -= Device.TABLET_WIDTH; + } + + if (isReplyPost) { + portraitPostWidth -= VIEWPORT_IMAGE_REPLY_OFFSET; + } + + return portraitPostWidth; +} + +// isGifTooLarge returns true if we think that the GIF may cause the device to run out of memory when rendered +// based on the image's dimensions and frame count. +export function isGifTooLarge(imageMetadata?: PostImage) { + if (imageMetadata?.format !== 'gif') { + // Not a gif or from an older server that doesn't count frames + return false; + } + + const {frame_count: frameCount, height, width} = imageMetadata; + + // Try to estimate the in-memory size of the gif to prevent the device out of memory + return width * height * (frameCount || 1) > MAX_GIF_SIZE; +} diff --git a/app/utils/markdown/index.ts b/app/utils/markdown/index.ts index 1cdf2b63c..72fa13c6b 100644 --- a/app/utils/markdown/index.ts +++ b/app/utils/markdown/index.ts @@ -25,7 +25,7 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme: Theme) => { color: theme.linkColor, }, heading1: { - fontSize: 17, + fontSize: 22, fontWeight: '700', lineHeight: 25, }, @@ -33,7 +33,7 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme: Theme) => { paddingBottom: 8, }, heading2: { - fontSize: 17, + fontSize: 20, fontWeight: '700', lineHeight: 25, }, @@ -41,7 +41,7 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme: Theme) => { paddingBottom: 8, }, heading3: { - fontSize: 17, + fontSize: 19, fontWeight: '700', lineHeight: 25, }, @@ -49,7 +49,7 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme: Theme) => { paddingBottom: 8, }, heading4: { - fontSize: 17, + fontSize: 18, fontWeight: '700', lineHeight: 25, }, @@ -65,7 +65,7 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme: Theme) => { paddingBottom: 8, }, heading6: { - fontSize: 17, + fontSize: 16, fontWeight: '700', lineHeight: 25, }, @@ -140,6 +140,7 @@ const languages: Record = { html: 'HTML', java: 'Java', javascript: 'JavaScript', + js: 'JavaScript', json: 'JSON', julia: 'Julia', kotlin: 'Kotlin', diff --git a/app/utils/tap/index.ts b/app/utils/tap/index.ts index e8c14521e..2db01586d 100644 --- a/app/utils/tap/index.ts +++ b/app/utils/tap/index.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -export function preventDoubleTap(func: (...args: any) => any, doublePressDelay = 300) { +export function preventDoubleTap(func: (...args: any) => any, doublePressDelay = 750) { let canPressWrapped = true; // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/app/utils/tap/test.ts b/app/utils/tap/test.ts index 6f6af68c3..73666e8cb 100644 --- a/app/utils/tap/test.ts +++ b/app/utils/tap/test.ts @@ -44,6 +44,6 @@ describe('Prevent double tap', () => { test(); expect(testFunction).toHaveBeenCalledTimes(2); done(); - }, 300); + }, 750); }); }); diff --git a/app/utils/theme/index.ts b/app/utils/theme/index.ts index de0af17c7..c19debdda 100644 --- a/app/utils/theme/index.ts +++ b/app/utils/theme/index.ts @@ -5,6 +5,7 @@ import {StyleSheet} from 'react-native'; import tinyColor from 'tinycolor2'; import {mergeNavigationOptions} from '@screens/navigation'; +import EphemeralStore from '@store/ephemeral_store'; import type {Options} from 'react-native-navigation'; @@ -86,8 +87,8 @@ export function changeOpacity(oldColor: string, opacity: number): string { return `rgba(${red},${green},${blue},${alpha * opacity})`; } -export function concatStyles(...styles: any) { - return [].concat(styles); +export function concatStyles(...styles: T[]) { + return ([] as T[]).concat(...styles); } export function setNavigatorStyles(componentId: string, theme: Theme) { @@ -116,6 +117,12 @@ export function setNavigatorStyles(componentId: string, theme: Theme) { mergeNavigationOptions(componentId, options); } +export function setNavigationStackStyles(theme: Theme) { + EphemeralStore.allNavigationComponentIds.forEach((componentId) => { + setNavigatorStyles(componentId, theme); + }); +} + export function getKeyboardAppearanceFromTheme(theme: Theme) { return tinyColor(theme.centerChannelBg).isLight() ? 'light' : 'dark'; } @@ -149,3 +156,35 @@ export function hexToHue(hexColor: string) { return hue; } + +function blendComponent(background: number, foreground: number, opacity: number): number { + return ((1 - opacity) * background) + (opacity * foreground); +} + +export function blendColors(background: string, foreground: string, opacity: number): string { + const backgroundComponents = getComponents(background); + const foregroundComponents = getComponents(foreground); + + const red = Math.floor(blendComponent( + backgroundComponents.red, + foregroundComponents.red, + opacity, + )); + const green = Math.floor(blendComponent( + backgroundComponents.green, + foregroundComponents.green, + opacity, + )); + const blue = Math.floor(blendComponent( + backgroundComponents.blue, + foregroundComponents.blue, + opacity, + )); + const alpha = blendComponent( + backgroundComponents.alpha, + foregroundComponents.alpha, + opacity, + ); + + return `rgba(${red},${green},${blue},${alpha})`; +} diff --git a/app/utils/user/index.ts b/app/utils/user/index.ts index 9be37f946..5f72b463b 100644 --- a/app/utils/user/index.ts +++ b/app/utils/user/index.ts @@ -2,9 +2,10 @@ // See LICENSE.txt for license information. import {General, Preferences} from '@constants'; +import {UserModel} from '@database/models/server'; import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n'; -export function displayUsername(user?: UserProfile, locale?: string, teammateDisplayNameSetting?: string, useFallbackUsername = true) { +export function displayUsername(user?: UserProfile | UserModel, locale?: string, teammateDisplayNameSetting?: string, useFallbackUsername = true) { let name = useFallbackUsername ? getLocalizedMessage(locale || DEFAULT_LOCALE, t('channel_loader.someone'), 'Someone') : ''; if (user) { @@ -39,13 +40,24 @@ export function displayGroupMessageName(users: UserProfile[], locale?: string, t return names.sort(sortUsernames).join(', '); } -export function getFullName(user: UserProfile): string { - if (user.first_name && user.last_name) { - return `${user.first_name} ${user.last_name}`; - } else if (user.first_name) { - return user.first_name; - } else if (user.last_name) { - return user.last_name; +export function getFullName(user: UserProfile | UserModel): string { + let firstName: string; + let lastName: string; + + if (user instanceof UserModel) { + firstName = user.firstName; + lastName = user.lastName; + } else { + firstName = user.first_name; + lastName = user.last_name; + } + + if (firstName && lastName) { + return `${firstName} ${lastName}`; + } else if (firstName) { + return firstName; + } else if (lastName) { + return lastName; } return ''; @@ -71,3 +83,45 @@ export function isGuest(roles: string): boolean { export function isSystemAdmin(roles: string): boolean { return isRoleInRoles(roles, General.SYSTEM_ADMIN_ROLE); } + +export const getUsersByUsername = (users: UserModel[]) => { + const usersByUsername: Dictionary = {}; + + for (const user of users) { + usersByUsername[user.username] = user; + } + + return usersByUsername; +}; + +export const getUserMentionKeys = (user: UserModel) => { + const keys: UserMentionKey[] = []; + + if (!user.notifyProps) { + return keys; + } + + if (user.notifyProps.mention_keys) { + const mentions = user.notifyProps.mention_keys.split(',').map((key) => ({key})); + keys.push(...mentions); + } + + if (user.notifyProps.first_name === 'true' && user.firstName) { + keys.push({key: user.firstName, caseSensitive: true}); + } + + if (user.notifyProps.channel === 'true') { + keys.push( + {key: '@channel'}, + {key: '@all'}, + {key: '@here'}, + ); + } + + const usernameKey = `@${user.username}`; + if (keys.findIndex((item) => item.key === usernameKey) === -1) { + keys.push({key: usernameKey}); + } + + return keys; +}; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index ec82f3944..ff0ac1e1b 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -4,6 +4,24 @@ "channel.channelHasGuests": "This channel has guests", "channel.hasGuests": "This group message has guests", "channel.isGuest": "This person is a guest", + "emoji_picker.activities": "Activities", + "emoji_picker.animals-nature": "Animals & Nature", + "emoji_picker.custom": "Custom", + "emoji_picker.flags": "Flags", + "emoji_picker.food-drink": "Food & Drink", + "emoji_picker.objects": "Objects", + "emoji_picker.people-body": "People & Body", + "emoji_picker.recent": "Recently Used", + "emoji_picker.searchResults": "Search Results", + "emoji_picker.smileys-emotion": "Smileys & Emotion", + "emoji_picker.symbols": "Symbols", + "emoji_picker.travel-places": "Travel & Places", + "emoji_skin.dark_skin_tone": "dark skin tone", + "emoji_skin.default": "default skin tone", + "emoji_skin.light_skin_tone": "light skin tone", + "emoji_skin.medium_dark_skin_tone": "medium dark skin tone", + "emoji_skin.medium_light_skin_tone": "medium light skin tone", + "emoji_skin.medium_skin_tone": "medium skin tone", "failed_action.fetch_channels": "Channels could not be loaded for {teamName}.", "failed_action.fetch_teams": "An error ocurred while loading the teams of this server", "failed_action.something_wrong": "Something went wrong", @@ -38,6 +56,7 @@ "mobile.error_handler.button": "Relaunch", "mobile.error_handler.description": "\nTap relaunch to open the app again. After restart, you can report the problem from the settings menu.\n", "mobile.error_handler.title": "Unexpected error occurred", + "mobile.join_channel.error": "We couldn't join the channel {displayName}. Please check your connection and try again.", "mobile.launchError.notification": "Did not find a server for this notification", "mobile.link.error.text": "Unable to open the link.", "mobile.link.error.title": "Error", @@ -50,6 +69,11 @@ "mobile.managed.not_secured.ios.touchId": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Touch ID & Passcode.", "mobile.managed.secured_by": "Secured by {vendor}", "mobile.managed.settings": "Go to settings", + "mobile.markdown.code.copy_code": "Copy Code", + "mobile.markdown.code.plusMoreLines": "+{count, number} more {count, plural, one {line} other {lines}}", + "mobile.markdown.image.too_large": "Image exceeds max dimensions of {maxWidth} by {maxHeight}:", + "mobile.markdown.link.copy_url": "Copy URL", + "mobile.mention.copy_mention": "Copy Mention", "mobile.oauth.failed_to_login": "Your login attempt failed. Please try again.", "mobile.oauth.failed_to_open_link": "The link failed to open. Please try again.", "mobile.oauth.failed_to_open_link_no_browser": "The link failed to open. Please verify if a browser is installed in the current space.", @@ -57,15 +81,22 @@ "mobile.oauth.something_wrong.okButon": "Ok", "mobile.oauth.switch_to_browser": "Please use your browser to complete the login", "mobile.oauth.try_again": "Try again", + "mobile.post.cancel": "Cancel", "mobile.push_notification_reply.button": "Send", "mobile.push_notification_reply.placeholder": "Write a reply...", "mobile.push_notification_reply.title": "Reply", "mobile.request.invalid_request_method": "Invalid request method", "mobile.request.invalid_response": "Received invalid response from the server.", + "mobile.routes.code": "{language} Code", + "mobile.routes.code.noLanguage": "Code", "mobile.routes.login": "Login", "mobile.routes.loginOptions": "Login Chooser", "mobile.routes.mfa": "Multi-factor Authentication", "mobile.routes.sso": "Single Sign-On", + "mobile.routes.table": "Table", + "mobile.routes.user_profile": "Profile", + "mobile.server_link.unreachable_channel.error": "This link belongs to a deleted channel or to a channel to which you do not have access.", + "mobile.server_link.unreachable_team.error": "This link belongs to a deleted team or to a team to which you do not have access.", "mobile.server_upgrade.alert_description": "This server version is unsupported and users will be exposed to compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Upgrading to server version {serverVersion} or later is required.", "mobile.server_upgrade.button": "OK", "mobile.server_upgrade.description": "\nA server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.\n", @@ -85,6 +116,11 @@ "password_send.error": "Please enter a valid email address.", "password_send.link": "If the account exists, a password reset email will be sent to:", "password_send.reset": "Reset my password", + "permalink.show_dialog_warn.cancel": "Cancel", + "permalink.show_dialog_warn.description": "You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?", + "permalink.show_dialog_warn.join": "Join", + "permalink.show_dialog_warn.title": "Join private channel", + "post_message_view.edited": "(edited)", "signup.email": "Email and Password", "signup.google": "Google Apps", "signup.office365": "Office 365", diff --git a/assets/base/images/emojis/mattermost.png b/assets/base/images/emojis/mattermost.png new file mode 100644 index 000000000..98f22eedc Binary files /dev/null and b/assets/base/images/emojis/mattermost.png differ diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 4ddd8376e..8dbc8539b 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 7FABDFC22211A39000D0F595 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABDFC12211A39000D0F595 /* Section.swift */; }; 7FABE00A2212650600D0F595 /* ChannelsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0092212650600D0F595 /* ChannelsViewController.swift */; }; 7FABE0562213884700D0F595 /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; }; + 7FCEFB9326B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FCEFB9226B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m */; }; 7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; }; 84E3264B229834C30055068A /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E325FF229834C30055068A /* Config.swift */; }; 9358B95F95184EE0A4DCE629 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */; }; @@ -213,6 +214,8 @@ 7FABDFC12211A39000D0F595 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; 7FABE0092212650600D0F595 /* ChannelsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsViewController.swift; sourceTree = ""; }; 7FABE04022137F2900D0F595 /* UploadAttachments.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = UploadAttachments.xcodeproj; path = UploadAttachments/UploadAttachments.xcodeproj; sourceTree = ""; }; + 7FCEFB9126B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SDWebImageDownloaderOperation+Swizzle.h"; path = "Mattermost/SDWebImageDownloaderOperation+Swizzle.h"; sourceTree = ""; }; + 7FCEFB9226B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "SDWebImageDownloaderOperation+Swizzle.m"; path = "Mattermost/SDWebImageDownloaderOperation+Swizzle.m"; sourceTree = ""; }; 7FEB10991F61019C0039A015 /* MattermostManaged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MattermostManaged.h; path = Mattermost/MattermostManaged.h; sourceTree = ""; }; 7FEB109A1F61019C0039A015 /* MattermostManaged.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MattermostManaged.m; path = Mattermost/MattermostManaged.m; sourceTree = ""; }; 7FFE32B51FD9CCAA0038C7A0 /* FLAnimatedImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FLAnimatedImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -340,6 +343,8 @@ 7FEB109A1F61019C0039A015 /* MattermostManaged.m */, 7F292AA51E8ABB1100A450A3 /* splash.png */, 7F0F4B0924BA173900E14C60 /* LaunchScreen.storyboard */, + 7FCEFB9126B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.h */, + 7FCEFB9226B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m */, ); name = Mattermost; sourceTree = ""; @@ -809,6 +814,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7FCEFB9326B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m in Sources */, 4953BF602368AE8600593328 /* SwimeProxy.swift in Sources */, 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 7F151D3E221B062700FAD8F3 /* RuntimeUtils.swift in Sources */, diff --git a/ios/Mattermost/MattermostManaged.m b/ios/Mattermost/MattermostManaged.m index 43427ee34..c09ef4854 100644 --- a/ios/Mattermost/MattermostManaged.m +++ b/ios/Mattermost/MattermostManaged.m @@ -78,7 +78,7 @@ RCT_EXPORT_METHOD(deleteDatabaseDirectory: (NSString *)databaseName shouldRemov BOOL successCode = [fileManager removeItemAtPath:databaseDir error:&error]; NSNumber * success= [NSNumber numberWithBool:successCode]; - callback(@[error, success]); + callback(@[(error ?: [NSNull null]), success]); } @catch (NSException *exception) { NSLog(@"%@", exception.reason); diff --git a/ios/Mattermost/SDWebImageDownloaderOperation+Swizzle.h b/ios/Mattermost/SDWebImageDownloaderOperation+Swizzle.h new file mode 100644 index 000000000..b431d7402 --- /dev/null +++ b/ios/Mattermost/SDWebImageDownloaderOperation+Swizzle.h @@ -0,0 +1,10 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +// +// SDWebImageDownloaderOperation+Swizzle.h + +#import "SDWebImageDownloaderOperation.h" + +@interface SDWebImageDownloaderOperation (Swizzle) + +@end diff --git a/ios/Mattermost/SDWebImageDownloaderOperation+Swizzle.m b/ios/Mattermost/SDWebImageDownloaderOperation+Swizzle.m new file mode 100644 index 000000000..fdf9238c3 --- /dev/null +++ b/ios/Mattermost/SDWebImageDownloaderOperation+Swizzle.m @@ -0,0 +1,101 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +// +// SDWebImageDownloaderOperation+Swizzle.m + + +#import "SDWebImageDownloaderOperation+Swizzle.h" +@import react_native_network_client; +#import + +@implementation SDWebImageDownloaderOperation (Swizzle) + ++ (void) load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self swizzleInitMethod]; + [self swizzleURLSessionTaskDelegateMethod]; + }); +} + ++ (void) swizzleInitMethod { + Class class = [self class]; + + SEL originalSelector = @selector(initWithRequest:inSession:options:context:); + SEL swizzledSelector = @selector(swizzled_initWithRequest:inSession:options:context:); + + Method originalMethod = class_getInstanceMethod(class, originalSelector); + Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); + + method_exchangeImplementations(originalMethod, swizzledMethod); + +} + ++ (void) swizzleURLSessionTaskDelegateMethod { + Class class = [self class]; + + SEL originalSelector = @selector(URLSession:task:didReceiveChallenge:completionHandler:); + SEL swizzledSelector = @selector(swizzled_URLSession:task:didReceiveChallenge:completionHandler:); + + Method originalMethod = class_getInstanceMethod(class, originalSelector); + Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); + + method_exchangeImplementations(originalMethod, swizzledMethod); +} + +#pragma mark - Method Swizzling + +- (nonnull instancetype)swizzled_initWithRequest:(NSURLRequest *)request inSession:(NSURLSession *)session options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context { + SessionManager *nativeClientSessionManager = [SessionManager default]; + NSURL *sessionBaseUrl = [nativeClientSessionManager getSessionBaseUrlFor:request]; + if (sessionBaseUrl != nil) { + // If we have a session configured for this request then use its configuration + // to create a new session that SDWebImageDownloaderOperation will use for + // this request. In addition, if we have an authorization header being added + // to our session's requests, then we modify the request here as well using + // our BearerAuthenticationAdapter. + NSURLSessionConfiguration *configuration = [nativeClientSessionManager getSessionConfigurationFor:sessionBaseUrl]; + NSURLSession *newSession = [NSURLSession sessionWithConfiguration:configuration + delegate:self + delegateQueue:session.delegateQueue]; + NSURLRequest *authorizedRequest = [BearerAuthenticationAdapter addAuthorizationBearerTokenTo:request withSessionBaseUrlString:sessionBaseUrl.absoluteString]; + + return [self swizzled_initWithRequest:authorizedRequest inSession:newSession options:options context:context]; + } + + return [self swizzled_initWithRequest:request inSession:session options:options context:context]; +} + + +- (void)swizzled_URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { + SessionManager *nativeClientSessionManager = [SessionManager default]; + NSURL *sessionBaseUrl = [nativeClientSessionManager getSessionBaseUrlFor:task.currentRequest]; + if (sessionBaseUrl != nil) { + // If we have a session configured for this request then we'll fetch and + // apply the necessary credentials for NSURLAuthenticationMethodServerTrust + // and NSURLAuthenticationMethodClientCertificate. + NSURLCredential *credential = nil; + NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling; + + NSString *authenticationMethod = challenge.protectionSpace.authenticationMethod; + if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { + if ([nativeClientSessionManager getTrustSelfSignedServerCertificateFor:sessionBaseUrl]) { + credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; + disposition = NSURLSessionAuthChallengeUseCredential; + } + } else if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) { + credential = [nativeClientSessionManager getCredentialFor:sessionBaseUrl]; + disposition = NSURLSessionAuthChallengeUseCredential; + } + + if (completionHandler) { + completionHandler(disposition, credential); + } + + return; + } + + [self swizzled_URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler]; +} + +@end diff --git a/package-lock.json b/package-lock.json index 00adaad1b..16a6dcd98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@react-native-cookies/cookies": "6.0.8", "@rudderstack/rudder-sdk-react-native": "1.0.12", "@sentry/react-native": "2.6.1", + "@types/mime-db": "1.43.1", "commonmark": "0.30.0", "commonmark-react-renderer": "4.3.5", "deep-equal": "2.0.5", @@ -74,6 +75,7 @@ "react-native-video": "5.1.1", "react-native-webview": "11.6.5", "react-native-youtube": "2.0.2", + "reanimated-bottom-sheet": "1.0.0-alpha.22", "rn-placeholder": "3.0.3", "semver": "7.3.5", "serialize-error": "8.1.0", @@ -93,6 +95,8 @@ "@babel/register": "7.14.5", "@react-native-community/eslint-config": "3.0.0", "@testing-library/react-native": "7.2.0", + "@types/commonmark": "0.27.5", + "@types/commonmark-react-renderer": "4.3.1", "@types/jest": "26.0.24", "@types/react": "17.0.15", "@types/react-intl": "3.0.0", @@ -7450,6 +7454,22 @@ "@types/responselike": "*" } }, + "node_modules/@types/commonmark": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@types/commonmark/-/commonmark-0.27.5.tgz", + "integrity": "sha512-vIqgmHyLsc8Or3EWLz6QkhI8/v61FNeH0yxRupA7VqSbA2eFMoHHJAhZSHudplAV89wqg1CKSmShE016ziRXuw==", + "dev": true + }, + "node_modules/@types/commonmark-react-renderer": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/commonmark-react-renderer/-/commonmark-react-renderer-4.3.1.tgz", + "integrity": "sha512-FAP10ymnRsmJUZatePP/xgZiHDpht6RB3fkDnV36bgikOa7OuhKaWTz4jXCJ4x8/ND6ZJRmb5UZagJrXsc25Gg==", + "dev": true, + "dependencies": { + "@types/commonmark": "*", + "@types/react": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.6.tgz", @@ -7576,6 +7596,11 @@ "@types/lodash": "*" } }, + "node_modules/@types/mime-db": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/@types/mime-db/-/mime-db-1.43.1.tgz", + "integrity": "sha512-kGZJY+R+WnR5Rk+RPHUMERtb2qBRViIHCBdtUrY+NmwuGb8pQdfTqQiCKPrxpdoycl8KWm2DLdkpoSdt479XoQ==" + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -27499,6 +27524,17 @@ "node": ">=0.10" } }, + "node_modules/reanimated-bottom-sheet": { + "version": "1.0.0-alpha.22", + "resolved": "https://registry.npmjs.org/reanimated-bottom-sheet/-/reanimated-bottom-sheet-1.0.0-alpha.22.tgz", + "integrity": "sha512-NxecCn+2iA4YzkFuRK5/b86GHHS2OhZ9VRgiM4q18AC20YE/psRilqxzXCKBEvkOjP5AaAvY0yfE7EkEFBjTvw==", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-gesture-handler": "*", + "react-native-reanimated": "*" + } + }, "node_modules/recast": { "version": "0.20.4", "resolved": "https://registry.npmjs.org/recast/-/recast-0.20.4.tgz", @@ -38739,6 +38775,22 @@ "@types/responselike": "*" } }, + "@types/commonmark": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@types/commonmark/-/commonmark-0.27.5.tgz", + "integrity": "sha512-vIqgmHyLsc8Or3EWLz6QkhI8/v61FNeH0yxRupA7VqSbA2eFMoHHJAhZSHudplAV89wqg1CKSmShE016ziRXuw==", + "dev": true + }, + "@types/commonmark-react-renderer": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/commonmark-react-renderer/-/commonmark-react-renderer-4.3.1.tgz", + "integrity": "sha512-FAP10ymnRsmJUZatePP/xgZiHDpht6RB3fkDnV36bgikOa7OuhKaWTz4jXCJ4x8/ND6ZJRmb5UZagJrXsc25Gg==", + "dev": true, + "requires": { + "@types/commonmark": "*", + "@types/react": "*" + } + }, "@types/debug": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.6.tgz", @@ -38865,6 +38917,11 @@ "@types/lodash": "*" } }, + "@types/mime-db": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/@types/mime-db/-/mime-db-1.43.1.tgz", + "integrity": "sha512-kGZJY+R+WnR5Rk+RPHUMERtb2qBRViIHCBdtUrY+NmwuGb8pQdfTqQiCKPrxpdoycl8KWm2DLdkpoSdt479XoQ==" + }, "@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -54612,6 +54669,12 @@ "readable-stream": "^2.0.2" } }, + "reanimated-bottom-sheet": { + "version": "1.0.0-alpha.22", + "resolved": "https://registry.npmjs.org/reanimated-bottom-sheet/-/reanimated-bottom-sheet-1.0.0-alpha.22.tgz", + "integrity": "sha512-NxecCn+2iA4YzkFuRK5/b86GHHS2OhZ9VRgiM4q18AC20YE/psRilqxzXCKBEvkOjP5AaAvY0yfE7EkEFBjTvw==", + "requires": {} + }, "recast": { "version": "0.20.4", "resolved": "https://registry.npmjs.org/recast/-/recast-0.20.4.tgz", diff --git a/package.json b/package.json index 02e006a48..58c97f375 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@react-native-cookies/cookies": "6.0.8", "@rudderstack/rudder-sdk-react-native": "1.0.12", "@sentry/react-native": "2.6.1", + "@types/mime-db": "1.43.1", "commonmark": "0.30.0", "commonmark-react-renderer": "4.3.5", "deep-equal": "2.0.5", @@ -72,6 +73,7 @@ "react-native-video": "5.1.1", "react-native-webview": "11.6.5", "react-native-youtube": "2.0.2", + "reanimated-bottom-sheet": "1.0.0-alpha.22", "rn-placeholder": "3.0.3", "semver": "7.3.5", "serialize-error": "8.1.0", @@ -91,6 +93,8 @@ "@babel/register": "7.14.5", "@react-native-community/eslint-config": "3.0.0", "@testing-library/react-native": "7.2.0", + "@types/commonmark": "0.27.5", + "@types/commonmark-react-renderer": "4.3.1", "@types/jest": "26.0.24", "@types/react": "17.0.15", "@types/react-intl": "3.0.0", diff --git a/patches/@types+commonmark+0.27.5.patch b/patches/@types+commonmark+0.27.5.patch new file mode 100644 index 000000000..87b658f35 --- /dev/null +++ b/patches/@types+commonmark+0.27.5.patch @@ -0,0 +1,33 @@ +diff --git a/node_modules/@types/commonmark/index.d.ts b/node_modules/@types/commonmark/index.d.ts +index 35e9ed6..382cf98 100755 +--- a/node_modules/@types/commonmark/index.d.ts ++++ b/node_modules/@types/commonmark/index.d.ts +@@ -5,8 +5,8 @@ + // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + + export type NodeType = +- 'text' |'softbreak' | 'linebreak' | 'emph' | 'strong' | 'html_inline' | 'link' | 'image' | 'code' | 'document' | 'paragraph' | +- 'block_quote' | 'item' | 'list' | 'heading' | 'code_block' | 'html_block' | 'thematic_break' | 'custom_inline' | 'custom_block'; ++ 'text' |'softbreak' | 'linebreak' | 'emph' | 'strong' | 'html_inline' | 'link' | 'image' | 'code' | 'document' | 'paragraph' | 'mention_highlight' | 'at_mention' | ++ 'block_quote' | 'item' | 'list' | 'heading' | 'code_block' | 'html_block' | 'thematic_break' | 'custom_inline' | 'custom_block' | 'table' | 'edited_indicator'; + + export class Node { + constructor(nodeType: NodeType, sourcepos?: Position); +@@ -125,6 +125,8 @@ export class Node { + * https://github.com/jgm/commonmark.js/issues/74 + */ + _listData: ListData; ++ ++ mentionName?: string; + } + + /** +@@ -200,6 +202,8 @@ export interface ParserOptions { + */ + smart?: boolean | undefined; + time?: boolean | undefined; ++ urlFilter?: (url: string) => boolean; ++ minimumHashtagLength?: number; + } + + export interface HtmlRenderingOptions extends XmlRenderingOptions { diff --git a/patches/@types+commonmark-react-renderer+4.3.1.patch b/patches/@types+commonmark-react-renderer+4.3.1.patch new file mode 100644 index 000000000..a9c2f8be9 --- /dev/null +++ b/patches/@types+commonmark-react-renderer+4.3.1.patch @@ -0,0 +1,21 @@ +diff --git a/node_modules/@types/commonmark-react-renderer/index.d.ts b/node_modules/@types/commonmark-react-renderer/index.d.ts +index 9ee5664..44d9a20 100755 +--- a/node_modules/@types/commonmark-react-renderer/index.d.ts ++++ b/node_modules/@types/commonmark-react-renderer/index.d.ts +@@ -88,6 +88,8 @@ declare namespace ReactRenderer { + transformLinkUri?: ((uri: string) => string) | null | undefined; + transformImageUri?: ((uri: string) => string) | null | undefined; + linkTarget?: string | undefined; ++ renderParagraphsInLists?: boolean; ++ getExtraPropsForNode?: (node: any) => Record; + } + + interface Renderer { +@@ -113,6 +115,7 @@ interface ReactRenderer { + uriTransformer: (uri: string) => string; + types: string[]; + renderers: ReactRenderer.Renderers; ++ forwardChildren: (props: any) => any; + } + + declare const ReactRenderer: ReactRenderer; diff --git a/patches/react-native-fast-image+8.3.7.patch b/patches/react-native-fast-image+8.3.7.patch index bd7ed84eb..2419fb05d 100644 --- a/patches/react-native-fast-image+8.3.7.patch +++ b/patches/react-native-fast-image+8.3.7.patch @@ -1,66 +1,3 @@ -diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageCookieJar.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageCookieJar.java -new file mode 100644 -index 0000000..a302394 ---- /dev/null -+++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageCookieJar.java -@@ -0,0 +1,45 @@ -+package com.dylanvann.fastimage; -+ -+import android.webkit.CookieManager; -+ -+import java.util.Collections; -+import java.util.LinkedList; -+import java.util.List; -+ -+import okhttp3.Cookie; -+import okhttp3.CookieJar; -+import okhttp3.HttpUrl; -+ -+public class FastImageCookieJar implements CookieJar { -+ private CookieManager cookieManager; -+ -+ private CookieManager getCookieManager() { -+ if (cookieManager == null) { -+ cookieManager = CookieManager.getInstance(); -+ } -+ -+ return cookieManager; -+ } -+ -+ @Override -+ public void saveFromResponse(HttpUrl url, List cookies) { -+ // Do nothing -+ } -+ -+ @Override -+ public List loadForRequest(HttpUrl url) { -+ String cookie = getCookieManager().getCookie(url.toString()); -+ -+ if (cookie == null || cookie.isEmpty()) { -+ return Collections.emptyList(); -+ } -+ -+ String[] pairs = cookie.split(";"); -+ List cookies = new LinkedList(); -+ for (String pair : pairs) { -+ cookies.add(Cookie.parse(url, pair)); -+ } -+ -+ return cookies; -+ } -+} -diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java -index e659a61..1bdf34e 100644 ---- a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java -+++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java -@@ -43,6 +43,7 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule { - OkHttpClient client = OkHttpClientProvider - .getOkHttpClient() - .newBuilder() -+ .cookieJar(new FastImageCookieJar()) - .addInterceptor(createInterceptor(progressListener)) - .build(); - OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(client); diff --git a/node_modules/react-native-fast-image/dist/index.d.ts b/node_modules/react-native-fast-image/dist/index.d.ts index 0b1afd5..2226715 100644 --- a/node_modules/react-native-fast-image/dist/index.d.ts diff --git a/types/api/config.d.ts b/types/api/config.d.ts index 1b17cf93b..177bbba72 100644 --- a/types/api/config.d.ts +++ b/types/api/config.d.ts @@ -75,6 +75,7 @@ interface ClientConfig { EnablePreviewFeatures: string; EnablePreviewModeBanner: string; EnablePublicLink: string; + EnableSVGs: string; EnableSaml: string; EnableSignInWithEmail: string; EnableSignInWithUsername: string; @@ -83,7 +84,6 @@ interface ClientConfig { EnableSignUpWithGoogle: string; EnableSignUpWithOffice365: string; EnableSignUpWithOpenId: string; - EnableSVGs: string; EnableTesting: string; EnableThemeSelection: string; EnableTutorial: string; @@ -104,6 +104,7 @@ interface ClientConfig { ExperimentalEnablePostMetadata: string; ExperimentalGroupUnreadChannels: string; ExperimentalHideTownSquareinLHS: string; + ExperimentalNormalizeMarkdownLinks: string; ExperimentalPrimaryTeam: string; ExperimentalTimezone: string; ExperimentalTownSquareIsReadOnly: string; @@ -144,6 +145,7 @@ interface ClientConfig { RequireEmailVerification: string; RestrictDirectMessage: string; RunJobs: string; + SQLDriverName: string; SamlFirstNameAttributeSet: string; SamlLastNameAttributeSet: string; SamlLoginButtonBorderColor: string; @@ -158,7 +160,6 @@ interface ClientConfig { ShowFullName: string; SiteName: string; SiteURL: string; - SQLDriverName: string; SupportEmail: string; TeammateNameDisplay: string; TermsOfServiceLink: string; diff --git a/types/database/database.d.ts b/types/database/database.d.ts index c367f2fdc..9a5359e75 100644 --- a/types/database/database.d.ts +++ b/types/database/database.d.ts @@ -177,6 +177,7 @@ export type HandleTOSArgs = PrepareOnly & { } export type HandleMyChannelArgs = PrepareOnly & { + channels: Channel[]; myChannels: ChannelMembership[]; }; diff --git a/types/database/models/servers/my_channel.d.ts b/types/database/models/servers/my_channel.d.ts index ffb2d775e..9b6ec95af 100644 --- a/types/database/models/servers/my_channel.d.ts +++ b/types/database/models/servers/my_channel.d.ts @@ -17,6 +17,9 @@ export default class MyChannelModel extends Model { /** last_viewed_at : The timestamp showing the user's last viewed post on this channel */ lastViewedAt: number; + /** manually_unread : Determine if the user marked a post as unread */ + manuallyUnread: boolean; + /** mentions_count : The number of mentions on this channel */ mentionsCount: number; diff --git a/types/database/models/servers/team.d.ts b/types/database/models/servers/team.d.ts index 98a7380b8..6b31c14a1 100644 --- a/types/database/models/servers/team.d.ts +++ b/types/database/models/servers/team.d.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Relation} from '@nozbe/watermelondb'; +import {Query, Relation} from '@nozbe/watermelondb'; import Model, {Associations} from '@nozbe/watermelondb/Model'; /** @@ -42,23 +42,23 @@ export default class TeamModel extends Model { allowedDomains: string; /** channels : All the channels associated with this team */ - channels: ChannelModel[]; + channels: Query; /** groupsInTeam : All the groups associated with this team */ - groupsInTeam: GroupsInTeamModel[]; + groupsInTeam: Query; /** myTeam : Retrieves additional information about the team that this user is possibly part of. This query might yield no result if the user isn't part of a team. */ myTeam: Relation; /** slashCommands : All the slash commands associated with this team */ - slashCommands: SlashCommandModel[]; + slashCommands: Query; /** teamChannelHistory : A history of the channels in this team that has been visited, ordered by the most recent and capped to the last 5 */ teamChannelHistory: Relation; /** members : All the users associated with this team */ - members: TeamMembershipModel[]; + members: Query; /** teamSearchHistories : All the searches performed on this team */ - teamSearchHistories: TeamSearchHistoryModel[]; + teamSearchHistories: Query; } diff --git a/types/global/markdown.d.ts b/types/global/markdown.d.ts new file mode 100644 index 000000000..580637487 --- /dev/null +++ b/types/global/markdown.d.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type UserMentionKey= { + key: string; + caseSensitive?: boolean; +}; diff --git a/types/screens/gallery.d.ts b/types/screens/gallery.d.ts index 83d52ba67..e09708f2c 100644 --- a/types/screens/gallery.d.ts +++ b/types/screens/gallery.d.ts @@ -2,10 +2,11 @@ // See LICENSE.txt for license information. import React from 'react'; -import {intlShape} from 'react-intl'; import {StyleProp, ViewStyle} from 'react-native'; import Animated from 'react-native-reanimated'; +import type {IntlShape} from 'react-intl'; + export interface CallbackFunctionWithoutArguments { (): void; } @@ -45,7 +46,7 @@ export interface PrepareFileRef { } export interface FooterProps { - intl: typeof intlShape; + intl: IntlShape; file: FileInfo; } @@ -71,7 +72,7 @@ export interface GalleryItemProps { file: FileInfo; deviceHeight: number; deviceWidth: number; - intl?: typeof intlShape; + intl?: IntlShape; isActive?: boolean; onDoubleTap?: CallbackFunctionWithoutArguments; style?: StyleProp;