diff --git a/app/client/rest/apps.ts b/app/client/rest/apps.ts index ab11a27744..08eb32d756 100644 --- a/app/client/rest/apps.ts +++ b/app/client/rest/apps.ts @@ -1,8 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {AppBinding, AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps'; -import {buildQueryString} from '@mm-redux/utils/helpers'; +import {buildQueryString} from '@utils/helpers'; export interface ClientAppsMix { executeAppCall: (call: AppCallRequest, type: AppCallType) => Promise; diff --git a/app/client/rest/base.ts b/app/client/rest/base.ts index acf0d9666d..0b57fcec17 100644 --- a/app/client/rest/base.ts +++ b/app/client/rest/base.ts @@ -4,15 +4,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {RNFetchBlobFetchRepsonse} from 'rn-fetch-blob'; import urlParse from 'url-parse'; -import {Options} from '@mm-redux/types/client4'; +import {Analytics, create} from '@init/analytics'; import * as ClientConstants from './constants'; import ClientError from './error'; export default class ClientBase { + analitics: Analytics|undefined; clusterId = ''; csrf = ''; defaultHeaders: {[x: string]: string} = {}; @@ -38,8 +38,8 @@ export default class ClientBase { return this.getUrl() + baseUrl; } - getOptions(options: Options) { - const newOptions: Options = {...options}; + getOptions(options: ClientOptions) { + const newOptions: ClientOptions = {...options}; const headers: {[x: string]: string} = { [ClientConstants.HEADER_REQUESTED_WITH]: 'XMLHttpRequest', @@ -127,6 +127,7 @@ export default class ClientBase { setUrl(url: string) { this.url = url.replace(/\/+$/, ''); + this.analitics = create(this.url); } // Routes @@ -279,12 +280,12 @@ export default class ClientBase { } // Client Helpers - handleRedirectProtocol = (url: string, response: RNFetchBlobFetchRepsonse) => { + handleRedirectProtocol = (url: string, response: Response) => { const serverUrl = this.getUrl(); const parsed = urlParse(url); - const {redirects} = response.rnfbRespInfo; - if (redirects) { - const redirectUrl = urlParse(redirects[redirects.length - 1]); + + if (response.redirected) { + const redirectUrl = urlParse(response.url); if (serverUrl === parsed.origin && parsed.host === redirectUrl.host && parsed.protocol !== redirectUrl.protocol) { this.setUrl(serverUrl.replace(parsed.protocol, redirectUrl.protocol)); @@ -292,13 +293,13 @@ export default class ClientBase { } }; - doFetch = async (url: string, options: Options) => { + doFetch = async (url: string, options: ClientOptions) => { const {data} = await this.doFetchWithResponse(url, options); return data; }; - doFetchWithResponse = async (url: string, options: Options) => { + doFetchWithResponse = async (url: string, options: ClientOptions) => { const response = await fetch(url, this.getOptions(options)); const headers = parseAndMergeNestedHeaders(response.headers); diff --git a/app/client/rest/bots.ts b/app/client/rest/bots.ts index 6644324015..b0636e10e6 100644 --- a/app/client/rest/bots.ts +++ b/app/client/rest/bots.ts @@ -1,9 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {buildQueryString} from '@mm-redux/utils/helpers'; - -import type {Bot} from '@mm-redux/types/bots'; +import {buildQueryString} from '@utils/helpers'; export interface ClientBotsMix { getBot: (botUserId: string) => Promise; diff --git a/app/client/rest/channels.ts b/app/client/rest/channels.ts index c09a52020b..5c99a771d3 100644 --- a/app/client/rest/channels.ts +++ b/app/client/rest/channels.ts @@ -1,9 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {analytics} from '@init/analytics'; -import {Channel, ChannelMemberCountByGroup, ChannelMembership, ChannelNotifyProps, ChannelStats} from '@mm-redux/types/channels'; -import {buildQueryString} from '@mm-redux/utils/helpers'; +import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; @@ -58,7 +56,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; createChannel = async (channel: Channel) => { - analytics.trackAPI('api_channels_create', {team_id: channel.team_id}); + this.analytics.trackAPI('api_channels_create', {team_id: channel.team_id}); return this.doFetch( `${this.getChannelsRoute()}`, @@ -67,7 +65,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; createDirectChannel = async (userIds: string[]) => { - analytics.trackAPI('api_channels_create_direct'); + this.analytics.trackAPI('api_channels_create_direct'); return this.doFetch( `${this.getChannelsRoute()}/direct`, @@ -76,7 +74,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; createGroupChannel = async (userIds: string[]) => { - analytics.trackAPI('api_channels_create_group'); + this.analytics.trackAPI('api_channels_create_group'); return this.doFetch( `${this.getChannelsRoute()}/group`, @@ -85,7 +83,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; deleteChannel = async (channelId: string) => { - analytics.trackAPI('api_channels_delete', {channel_id: channelId}); + this.analytics.trackAPI('api_channels_delete', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}`, @@ -94,7 +92,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; unarchiveChannel = async (channelId: string) => { - analytics.trackAPI('api_channels_unarchive', {channel_id: channelId}); + this.analytics.trackAPI('api_channels_unarchive', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}/restore`, @@ -103,7 +101,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; updateChannel = async (channel: Channel) => { - analytics.trackAPI('api_channels_update', {channel_id: channel.id}); + this.analytics.trackAPI('api_channels_update', {channel_id: channel.id}); return this.doFetch( `${this.getChannelRoute(channel.id)}`, @@ -112,7 +110,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; convertChannelToPrivate = async (channelId: string) => { - analytics.trackAPI('api_channels_convert_to_private', {channel_id: channelId}); + this.analytics.trackAPI('api_channels_convert_to_private', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}/convert`, @@ -121,7 +119,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; updateChannelPrivacy = async (channelId: string, privacy: any) => { - analytics.trackAPI('api_channels_update_privacy', {channel_id: channelId, privacy}); + this.analytics.trackAPI('api_channels_update_privacy', {channel_id: channelId, privacy}); return this.doFetch( `${this.getChannelRoute(channelId)}/privacy`, @@ -130,7 +128,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; patchChannel = async (channelId: string, channelPatch: Partial) => { - analytics.trackAPI('api_channels_patch', {channel_id: channelId}); + this.analytics.trackAPI('api_channels_patch', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}/patch`, @@ -139,7 +137,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; updateChannelNotifyProps = async (props: ChannelNotifyProps & {channel_id: string, user_id: string}) => { - analytics.trackAPI('api_users_update_channel_notifications', {channel_id: props.channel_id}); + this.analytics.trackAPI('api_users_update_channel_notifications', {channel_id: props.channel_id}); return this.doFetch( `${this.getChannelMemberRoute(props.channel_id, props.user_id)}/notify_props`, @@ -148,7 +146,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; getChannel = async (channelId: string) => { - analytics.trackAPI('api_channel_get', {channel_id: channelId}); + this.analytics.trackAPI('api_channel_get', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}`, @@ -164,7 +162,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; getChannelByNameAndTeamName = async (teamName: string, channelName: string, includeDeleted = false) => { - analytics.trackAPI('api_channel_get_by_name_and_teamName', {channel_name: channelName, team_name: teamName, include_deleted: includeDeleted}); + this.analytics.trackAPI('api_channel_get_by_name_and_teamName', {channel_name: channelName, team_name: teamName, include_deleted: includeDeleted}); return this.doFetch( `${this.getTeamNameRoute(teamName)}/channels/name/${channelName}?include_deleted=${includeDeleted}`, @@ -239,7 +237,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; addToChannel = async (userId: string, channelId: string, postRootId = '') => { - analytics.trackAPI('api_channels_add_member', {channel_id: channelId}); + this.analytics.trackAPI('api_channels_add_member', {channel_id: channelId}); const member = {user_id: userId, channel_id: channelId, post_root_id: postRootId}; return this.doFetch( @@ -249,7 +247,7 @@ const ClientChannels = (superclass: any) => class extends superclass { }; removeFromChannel = async (userId: string, channelId: string) => { - analytics.trackAPI('api_channels_remove_member', {channel_id: channelId}); + this.analytics.trackAPI('api_channels_remove_member', {channel_id: channelId}); return this.doFetch( `${this.getChannelMemberRoute(channelId, userId)}`, diff --git a/app/client/rest/emojis.ts b/app/client/rest/emojis.ts index 7d2e1b239c..170ca63e15 100644 --- a/app/client/rest/emojis.ts +++ b/app/client/rest/emojis.ts @@ -1,11 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import FormData from 'form-data'; - -import {analytics} from '@init/analytics'; -import {CustomEmoji} from '@mm-redux/types/emojis'; -import {buildQueryString} from '@mm-redux/utils/helpers'; +import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; @@ -22,27 +18,29 @@ 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) => { - analytics.trackAPI('api_emoji_custom_add'); + this.analytics.trackAPI('api_emoji_custom_add'); - const formData = new FormData(); - formData.append('image', imageData); - formData.append('emoji', JSON.stringify(emoji)); - const request: any = { - method: 'post', - body: formData, - }; + // 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()}`, - }; - } + // if (formData.getBoundary) { + // request.headers = { + // 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, + // }; + // } - return this.doFetch( - `${this.getEmojisRoute()}`, - request, - ); + // return this.doFetch( + // `${this.getEmojisRoute()}`, + // request, + // ); }; getCustomEmoji = async (id: string) => { @@ -67,7 +65,7 @@ const ClientEmojis = (superclass: any) => class extends superclass { }; deleteCustomEmoji = async (emojiId: string) => { - analytics.trackAPI('api_emoji_custom_delete'); + this.analytics.trackAPI('api_emoji_custom_delete'); return this.doFetch( `${this.getEmojiRoute(emojiId)}`, diff --git a/app/client/rest/error.ts b/app/client/rest/error.ts index 0bf146ec6d..d367c09705 100644 --- a/app/client/rest/error.ts +++ b/app/client/rest/error.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {cleanUrlForLogging} from '@mm-redux/utils/sentry'; +import {cleanUrlForLogging} from '@utils/url'; export default class ClientError extends Error { url: string; diff --git a/app/client/rest/general.ts b/app/client/rest/general.ts index 19e058b1f3..3077817117 100644 --- a/app/client/rest/general.ts +++ b/app/client/rest/general.ts @@ -1,10 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Config} from '@mm-redux/types/config'; -import {Role} from '@mm-redux/types/roles'; -import {Dictionary} from '@mm-redux/types/utilities'; -import {buildQueryString} from '@mm-redux/utils/helpers'; +import {buildQueryString} from '@utils/helpers'; import ClientError from './error'; @@ -12,12 +9,12 @@ export interface ClientGeneralMix { getOpenGraphMetadata: (url: string) => Promise; ping: () => Promise; logClientError: (message: string, level?: string) => Promise; - getClientConfigOld: () => Promise; + getClientConfigOld: () => Promise; getClientLicenseOld: () => Promise; getTimezones: () => Promise; getDataRetentionPolicy: () => Promise; getRolesByNames: (rolesNames: string[]) => Promise; - getRedirectLocation: (urlParam: string) => Promise>; + getRedirectLocation: (urlParam: string) => Promise>; } const ClientGeneral = (superclass: any) => class extends superclass { diff --git a/app/client/rest/groups.ts b/app/client/rest/groups.ts index 9c22541c5b..324791602b 100644 --- a/app/client/rest/groups.ts +++ b/app/client/rest/groups.ts @@ -1,8 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Group} from '@mm-redux/types/groups'; -import {buildQueryString} from '@mm-redux/utils/helpers'; +import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; diff --git a/app/client/rest/index.test.js b/app/client/rest/index.test.js index e585f33710..881ada458c 100644 --- a/app/client/rest/index.test.js +++ b/app/client/rest/index.test.js @@ -7,7 +7,7 @@ import nock from 'nock'; import {HEADER_X_VERSION_ID} from '@client/rest/constants'; import ClientError from '@client/rest/error'; import TestHelper from 'test/test_helper'; -import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; +import {isMinimumServerVersion} from '@utils/helpers'; describe('Client4', () => { beforeAll(() => { diff --git a/app/client/rest/integrations.ts b/app/client/rest/integrations.ts index 7d2ad546f9..003c2f5305 100644 --- a/app/client/rest/integrations.ts +++ b/app/client/rest/integrations.ts @@ -1,9 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {analytics} from '@init/analytics'; -import {Command, DialogSubmission} from '@mm-redux/types/integrations'; -import {buildQueryString} from '@mm-redux/utils/helpers'; +import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; @@ -39,7 +37,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass { }; executeCommand = async (command: Command, commandArgs = {}) => { - analytics.trackAPI('api_integrations_used'); + this.analytics.trackAPI('api_integrations_used'); return this.doFetch( `${this.getCommandsRoute()}/execute`, @@ -48,7 +46,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass { }; addCommand = async (command: Command) => { - analytics.trackAPI('api_integrations_created'); + this.analytics.trackAPI('api_integrations_created'); return this.doFetch( `${this.getCommandsRoute()}`, @@ -57,7 +55,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass { }; submitInteractiveDialog = async (data: DialogSubmission) => { - analytics.trackAPI('api_interactive_messages_dialog_submitted'); + this.analytics.trackAPI('api_interactive_messages_dialog_submitted'); return this.doFetch( `${this.getBaseRoute()}/actions/dialogs/submit`, {method: 'post', body: JSON.stringify(data)}, diff --git a/app/client/rest/posts.ts b/app/client/rest/posts.ts index d6c99a4b5a..bc4026e447 100644 --- a/app/client/rest/posts.ts +++ b/app/client/rest/posts.ts @@ -1,10 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {analytics} from '@init/analytics'; -import {FileInfo} from '@mm-redux/types/files'; -import {Post} from '@mm-redux/types/posts'; -import {buildQueryString} from '@mm-redux/utils/helpers'; +import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; @@ -36,10 +33,10 @@ export interface ClientPostsMix { const ClientPosts = (superclass: any) => class extends superclass { createPost = async (post: Post) => { - analytics.trackAPI('api_posts_create', {channel_id: post.channel_id}); + this.analytics.trackAPI('api_posts_create', {channel_id: post.channel_id}); if (post.root_id != null && post.root_id !== '') { - analytics.trackAPI('api_posts_replied', {channel_id: post.channel_id}); + this.analytics.trackAPI('api_posts_replied', {channel_id: post.channel_id}); } return this.doFetch( @@ -49,7 +46,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; updatePost = async (post: Post) => { - analytics.trackAPI('api_posts_update', {channel_id: post.channel_id}); + this.analytics.trackAPI('api_posts_update', {channel_id: post.channel_id}); return this.doFetch( `${this.getPostRoute(post.id)}`, @@ -65,7 +62,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; patchPost = async (postPatch: Partial & {id: string}) => { - analytics.trackAPI('api_posts_patch', {channel_id: postPatch.channel_id}); + this.analytics.trackAPI('api_posts_patch', {channel_id: postPatch.channel_id}); return this.doFetch( `${this.getPostRoute(postPatch.id)}/patch`, @@ -74,7 +71,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; deletePost = async (postId: string) => { - analytics.trackAPI('api_posts_delete'); + this.analytics.trackAPI('api_posts_delete'); return this.doFetch( `${this.getPostRoute(postId)}`, @@ -104,7 +101,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; getPostsBefore = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { - analytics.trackAPI('api_posts_get_before', {channel_id: channelId}); + this.analytics.trackAPI('api_posts_get_before', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}/posts${buildQueryString({before: postId, page, per_page: perPage})}`, @@ -113,7 +110,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; getPostsAfter = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { - analytics.trackAPI('api_posts_get_after', {channel_id: channelId}); + this.analytics.trackAPI('api_posts_get_after', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}/posts${buildQueryString({after: postId, page, per_page: perPage})}`, @@ -129,7 +126,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; getFlaggedPosts = async (userId: string, channelId = '', teamId = '', page = 0, perPage = PER_PAGE_DEFAULT) => { - analytics.trackAPI('api_posts_get_flagged', {team_id: teamId}); + this.analytics.trackAPI('api_posts_get_flagged', {team_id: teamId}); return this.doFetch( `${this.getUserRoute(userId)}/posts/flagged${buildQueryString({channel_id: channelId, team_id: teamId, page, per_page: perPage})}`, @@ -138,7 +135,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; getPinnedPosts = async (channelId: string) => { - analytics.trackAPI('api_posts_get_pinned', {channel_id: channelId}); + this.analytics.trackAPI('api_posts_get_pinned', {channel_id: channelId}); return this.doFetch( `${this.getChannelRoute(channelId)}/pinned`, {method: 'get'}, @@ -146,7 +143,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; markPostAsUnread = async (userId: string, postId: string) => { - analytics.trackAPI('api_post_set_unread_post'); + this.analytics.trackAPI('api_post_set_unread_post'); return this.doFetch( `${this.getUserRoute(userId)}/posts/${postId}/set_unread`, @@ -155,7 +152,7 @@ const ClientPosts = (superclass: any) => class extends superclass { } pinPost = async (postId: string) => { - analytics.trackAPI('api_posts_pin'); + this.analytics.trackAPI('api_posts_pin'); return this.doFetch( `${this.getPostRoute(postId)}/pin`, @@ -164,7 +161,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; unpinPost = async (postId: string) => { - analytics.trackAPI('api_posts_unpin'); + this.analytics.trackAPI('api_posts_unpin'); return this.doFetch( `${this.getPostRoute(postId)}/unpin`, @@ -173,7 +170,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; addReaction = async (userId: string, postId: string, emojiName: string) => { - analytics.trackAPI('api_reactions_save', {post_id: postId}); + this.analytics.trackAPI('api_reactions_save', {post_id: postId}); return this.doFetch( `${this.getReactionsRoute()}`, @@ -182,7 +179,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; removeReaction = async (userId: string, postId: string, emojiName: string) => { - analytics.trackAPI('api_reactions_delete', {post_id: postId}); + this.analytics.trackAPI('api_reactions_delete', {post_id: postId}); return this.doFetch( `${this.getUserRoute(userId)}/posts/${postId}/reactions/${emojiName}`, @@ -198,7 +195,7 @@ const ClientPosts = (superclass: any) => class extends superclass { }; searchPostsWithParams = async (teamId: string, params: any) => { - analytics.trackAPI('api_posts_search', {team_id: teamId}); + this.analytics.trackAPI('api_posts_search', {team_id: teamId}); return this.doFetch( `${this.getTeamRoute(teamId)}/posts/search`, @@ -216,9 +213,9 @@ const ClientPosts = (superclass: any) => class extends superclass { doPostActionWithCookie = async (postId: string, actionId: string, actionCookie: string, selectedOption = '') => { if (selectedOption) { - analytics.trackAPI('api_interactive_messages_menu_selected'); + this.analytics.trackAPI('api_interactive_messages_menu_selected'); } else { - analytics.trackAPI('api_interactive_messages_button_clicked'); + this.analytics.trackAPI('api_interactive_messages_button_clicked'); } const msg: any = { diff --git a/app/client/rest/preferences.ts b/app/client/rest/preferences.ts index 5666f36a0d..450bd94142 100644 --- a/app/client/rest/preferences.ts +++ b/app/client/rest/preferences.ts @@ -1,8 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {PreferenceType} from '@mm-redux/types/preferences'; - export interface ClientPreferencesMix { savePreferences: (userId: string, preferences: PreferenceType[]) => Promise; deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise; diff --git a/app/client/rest/teams.ts b/app/client/rest/teams.ts index 35d7b0e107..4cc4fc79e3 100644 --- a/app/client/rest/teams.ts +++ b/app/client/rest/teams.ts @@ -1,9 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {analytics} from '@init/analytics'; -import {Team, TeamMembership, TeamUnread} from '@mm-redux/types/teams'; -import {buildQueryString} from '@mm-redux/utils/helpers'; +import {buildQueryString} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; @@ -30,7 +28,7 @@ export interface ClientTeamsMix { const ClientTeams = (superclass: any) => class extends superclass { createTeam = async (team: Team) => { - analytics.trackAPI('api_teams_create'); + this.analytics.trackAPI('api_teams_create'); return this.doFetch( `${this.getTeamsRoute()}`, @@ -39,7 +37,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; deleteTeam = async (teamId: string) => { - analytics.trackAPI('api_teams_delete'); + this.analytics.trackAPI('api_teams_delete'); return this.doFetch( `${this.getTeamRoute(teamId)}`, @@ -48,7 +46,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; updateTeam = async (team: Team) => { - analytics.trackAPI('api_teams_update_name', {team_id: team.id}); + this.analytics.trackAPI('api_teams_update_name', {team_id: team.id}); return this.doFetch( `${this.getTeamRoute(team.id)}`, @@ -57,7 +55,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; patchTeam = async (team: Partial & {id: string}) => { - analytics.trackAPI('api_teams_patch_name', {team_id: team.id}); + this.analytics.trackAPI('api_teams_patch_name', {team_id: team.id}); return this.doFetch( `${this.getTeamRoute(team.id)}/patch`, @@ -80,7 +78,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; getTeamByName = async (teamName: string) => { - analytics.trackAPI('api_teams_get_team_by_name'); + this.analytics.trackAPI('api_teams_get_team_by_name'); return this.doFetch( this.getTeamNameRoute(teamName), @@ -131,7 +129,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; addToTeam = async (teamId: string, userId: string) => { - analytics.trackAPI('api_teams_invite_members', {team_id: teamId}); + this.analytics.trackAPI('api_teams_invite_members', {team_id: teamId}); const member = {user_id: userId, team_id: teamId}; return this.doFetch( @@ -149,7 +147,7 @@ const ClientTeams = (superclass: any) => class extends superclass { }; removeFromTeam = async (teamId: string, userId: string) => { - analytics.trackAPI('api_teams_remove_members', {team_id: teamId}); + this.analytics.trackAPI('api_teams_remove_members', {team_id: teamId}); return this.doFetch( `${this.getTeamMemberRoute(teamId, userId)}`, diff --git a/app/client/rest/users.ts b/app/client/rest/users.ts index bcc59ce5db..bef25f1815 100644 --- a/app/client/rest/users.ts +++ b/app/client/rest/users.ts @@ -1,10 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {analytics} from '@init/analytics'; -import {General} from '@mm-redux/constants'; -import {UserProfile, UserStatus} from '@mm-redux/types/users'; -import {buildQueryString, isMinimumServerVersion} from '@mm-redux/utils/helpers'; +import {General} from '@constants'; +import {buildQueryString, isMinimumServerVersion} from '@utils/helpers'; import {PER_PAGE_DEFAULT} from './constants'; @@ -47,7 +45,7 @@ export interface ClientUsersMix { const ClientUsers = (superclass: any) => class extends superclass { createUser = async (user: UserProfile, token: string, inviteId: string) => { - analytics.trackAPI('api_users_create'); + this.analytics.trackAPI('api_users_create'); const queryParams: any = {}; @@ -73,7 +71,7 @@ const ClientUsers = (superclass: any) => class extends superclass { } patchUser = async (userPatch: Partial & {id: string}) => { - analytics.trackAPI('api_users_patch'); + this.analytics.trackAPI('api_users_patch'); return this.doFetch( `${this.getUserRoute(userPatch.id)}/patch`, @@ -82,7 +80,7 @@ const ClientUsers = (superclass: any) => class extends superclass { } updateUser = async (user: UserProfile) => { - analytics.trackAPI('api_users_update'); + this.analytics.trackAPI('api_users_update'); return this.doFetch( `${this.getUserRoute(user.id)}`, @@ -91,7 +89,7 @@ const ClientUsers = (superclass: any) => class extends superclass { } demoteUserToGuest = async (userId: string) => { - analytics.trackAPI('api_users_demote_user_to_guest'); + this.analytics.trackAPI('api_users_demote_user_to_guest'); return this.doFetch( `${this.getUserRoute(userId)}/demote`, @@ -100,7 +98,7 @@ const ClientUsers = (superclass: any) => class extends superclass { } getKnownUsers = async () => { - analytics.trackAPI('api_get_known_users'); + this.analytics.trackAPI('api_get_known_users'); return this.doFetch( `${this.getUsersRoute()}/known`, @@ -109,7 +107,7 @@ const ClientUsers = (superclass: any) => class extends superclass { } sendPasswordResetEmail = async (email: string) => { - analytics.trackAPI('api_users_send_password_reset'); + this.analytics.trackAPI('api_users_send_password_reset'); return this.doFetch( `${this.getUsersRoute()}/password/reset/send`, @@ -118,7 +116,7 @@ const ClientUsers = (superclass: any) => class extends superclass { } setDefaultProfileImage = async (userId: string) => { - analytics.trackAPI('api_users_set_default_profile_picture'); + this.analytics.trackAPI('api_users_set_default_profile_picture'); return this.doFetch( `${this.getUserRoute(userId)}/image`, @@ -127,10 +125,10 @@ const ClientUsers = (superclass: any) => class extends superclass { }; login = async (loginId: string, password: string, token = '', deviceId = '', ldapOnly = false) => { - analytics.trackAPI('api_users_login'); + this.analytics.trackAPI('api_users_login'); if (ldapOnly) { - analytics.trackAPI('api_users_login_ldap'); + this.analytics.trackAPI('api_users_login_ldap'); } const body: any = { @@ -157,7 +155,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; loginById = async (id: string, password: string, token = '', deviceId = '') => { - analytics.trackAPI('api_users_login'); + this.analytics.trackAPI('api_users_login'); const body: any = { device_id: deviceId, id, @@ -174,7 +172,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; logout = async () => { - analytics.trackAPI('api_users_logout'); + this.analytics.trackAPI('api_users_logout'); const {response} = await this.doFetchWithResponse( `${this.getUsersRoute()}/logout`, @@ -191,7 +189,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfiles = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => { - analytics.trackAPI('api_profiles_get'); + this.analytics.trackAPI('api_profiles_get'); return this.doFetch( `${this.getUsersRoute()}${buildQueryString({page, per_page: perPage, ...options})}`, @@ -200,7 +198,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesByIds = async (userIds: string[], options = {}) => { - analytics.trackAPI('api_profiles_get_by_ids'); + this.analytics.trackAPI('api_profiles_get_by_ids'); return this.doFetch( `${this.getUsersRoute()}/ids${buildQueryString(options)}`, @@ -209,7 +207,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesByUsernames = async (usernames: string[]) => { - analytics.trackAPI('api_profiles_get_by_usernames'); + this.analytics.trackAPI('api_profiles_get_by_usernames'); return this.doFetch( `${this.getUsersRoute()}/usernames`, @@ -218,7 +216,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesInTeam = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '', options = {}) => { - analytics.trackAPI('api_profiles_get_in_team', {team_id: teamId, sort}); + this.analytics.trackAPI('api_profiles_get_in_team', {team_id: teamId, sort}); return this.doFetch( `${this.getUsersRoute()}${buildQueryString({...options, in_team: teamId, page, per_page: perPage, sort})}`, @@ -227,7 +225,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesNotInTeam = async (teamId: string, groupConstrained: boolean, page = 0, perPage = PER_PAGE_DEFAULT) => { - analytics.trackAPI('api_profiles_get_not_in_team', {team_id: teamId, group_constrained: groupConstrained}); + this.analytics.trackAPI('api_profiles_get_not_in_team', {team_id: teamId, group_constrained: groupConstrained}); const queryStringObj: any = {not_in_team: teamId, page, per_page: perPage}; if (groupConstrained) { @@ -241,7 +239,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesWithoutTeam = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => { - analytics.trackAPI('api_profiles_get_without_team'); + this.analytics.trackAPI('api_profiles_get_without_team'); return this.doFetch( `${this.getUsersRoute()}${buildQueryString({...options, without_team: 1, page, per_page: perPage})}`, @@ -250,7 +248,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesInChannel = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => { - analytics.trackAPI('api_profiles_get_in_channel', {channel_id: channelId}); + this.analytics.trackAPI('api_profiles_get_in_channel', {channel_id: channelId}); const serverVersion = this.getServerVersion(); let queryStringObj; @@ -266,7 +264,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesInGroupChannels = async (channelsIds: string[]) => { - analytics.trackAPI('api_profiles_get_in_group_channels', {channelsIds}); + this.analytics.trackAPI('api_profiles_get_in_group_channels', {channelsIds}); return this.doFetch( `${this.getUsersRoute()}/group_channels`, @@ -275,7 +273,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; getProfilesNotInChannel = async (teamId: string, channelId: string, groupConstrained: boolean, page = 0, perPage = PER_PAGE_DEFAULT) => { - analytics.trackAPI('api_profiles_get_not_in_channel', {team_id: teamId, channel_id: channelId, group_constrained: groupConstrained}); + this.analytics.trackAPI('api_profiles_get_not_in_channel', {team_id: teamId, channel_id: channelId, group_constrained: groupConstrained}); const queryStringObj: any = {in_team: teamId, not_in_channel: channelId, page, per_page: perPage}; if (groupConstrained) { @@ -365,7 +363,7 @@ const ClientUsers = (superclass: any) => class extends superclass { }; searchUsers = async (term: string, options: any) => { - analytics.trackAPI('api_search_users'); + this.analytics.trackAPI('api_search_users'); return this.doFetch( `${this.getUsersRoute()}/search`, diff --git a/app/components/error_text/error_text.test.tsx b/app/components/error_text/error_text.test.tsx index 9bca857d6e..7c8173ffe4 100644 --- a/app/components/error_text/error_text.test.tsx +++ b/app/components/error_text/error_text.test.tsx @@ -2,10 +2,10 @@ // See LICENSE.txt for license information. import React from 'react'; -import {shallow} from 'enzyme'; -import Preferences from '@mm-redux/constants/preferences'; +import {render} from '@testing-library/react-native'; +import {Preferences} from '@constants'; -import ErrorText from './error_text.tsx'; +import ErrorText from './index'; describe('ErrorText', () => { const baseProps = { @@ -15,16 +15,14 @@ describe('ErrorText', () => { marginHorizontal: 15, }, theme: Preferences.THEMES.default, - error: { - message: 'Username must begin with a letter and contain between 3 and 22 characters including numbers, lowercase letters, and the symbols', - }, + error: 'Username must begin with a letter and contain between 3 and 22 characters including numbers, lowercase letters, and the symbols', }; test('should match snapshot', () => { - const wrapper = shallow( + const wrapper = render( , ); - expect(wrapper.getElement()).toMatchSnapshot(); + expect(wrapper.toJSON()).toMatchSnapshot(); }); }); diff --git a/app/components/error_text/error_text.tsx b/app/components/error_text/error_text.tsx deleted file mode 100644 index 718022219d..0000000000 --- a/app/components/error_text/error_text.tsx +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import {Text} from 'react-native'; - -import FormattedText from 'app/components/formatted_text'; -import {GlobalStyles} from '@app/styles'; -import {makeStyleSheetFromTheme} from 'app/utils/theme'; - -export default class ErrorText extends PureComponent { - static propTypes = { - testID: PropTypes.string, - error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - textStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), - theme: PropTypes.object.isRequired, - }; - - render() { - const {testID, error, textStyle, theme} = this.props; - if (!error) { - return null; - } - - const style = getStyleSheet(theme); - - const {intl} = error; - if (intl) { - return ( - - ); - } - - return ( - - {error.message || error} - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - errorLabel: { - color: (theme.errorTextColor || '#DA4A4A'), - }, - }; -}); diff --git a/app/components/error_text/index.ts b/app/components/error_text/index.ts deleted file mode 100644 index de94120e0b..0000000000 --- a/app/components/error_text/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {connect} from 'react-redux'; -import {getTheme} from '@mm-redux/selectors/entities/preferences'; - -import ErrorText from './error_text.tsx'; - -function mapStateToProps(state) { - return { - theme: getTheme(state), - }; -} - -export default connect(mapStateToProps)(ErrorText); diff --git a/app/components/error_text/index.tsx b/app/components/error_text/index.tsx new file mode 100644 index 0000000000..944987c1d3 --- /dev/null +++ b/app/components/error_text/index.tsx @@ -0,0 +1,59 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native'; + +import FormattedText from '@components/formatted_text'; +import {ClientError} from '@utils/client_error'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +export type ClientErrorWithIntl = ClientError & {intl: {values?: Record}} + +type ErrorProps = { + error: ClientErrorWithIntl | string; + testID?: string; + textStyle?: StyleProp | StyleProp + theme: Theme; +} + +const ErrorText = ({error, testID, textStyle, theme}: ErrorProps) => { + const style = getStyleSheet(theme); + const message = typeof (error) === 'string' ? error : error.message; + + if (typeof (error) !== 'string' && error.intl) { + const {intl} = error; + return ( + + ); + } + + return ( + + {message} + + ); +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + errorLabel: { + color: (theme.errorTextColor || '#DA4A4A'), + marginTop: 15, + marginBottom: 15, + fontSize: 12, + textAlign: 'left', + }, + }; +}); + +export default ErrorText; diff --git a/app/components/formatted_text/index.tsx b/app/components/formatted_text/index.tsx index 5876b85e32..053fc985ba 100644 --- a/app/components/formatted_text/index.tsx +++ b/app/components/formatted_text/index.tsx @@ -1,94 +1,88 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {createElement, isValidElement} from 'react'; -import PropTypes from 'prop-types'; -import {Text} from 'react-native'; -import {IntlShape} from 'react-intl'; +import {createElement, isValidElement} from 'react'; +import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native'; +import {useIntl} from 'react-intl'; -export default class FormattedText extends React.PureComponent { - static propTypes = { - id: PropTypes.string.isRequired, - defaultMessage: PropTypes.string, - values: PropTypes.object, - testID: PropTypes.string, - }; - - static defaultProps = { - defaultMessage: '', - }; - - static contextTypes = { - intl: IntlShape.isRequired, - }; - - render() { - const {id, defaultMessage, values, ...props} = this.props; - const {formatMessage} = this.context.intl; - - let tokenDelimiter; - let tokenizedValues; - let elements; - const hasValues = values && Object.keys(values).length > 0; - if (hasValues) { - // Creates a token with a random UID that should not be guessable or - // conflict with other parts of the `message` string. - const uid = Math.floor(Math.random() * 0x10000000000).toString(16); - - const generateToken = (() => { - let counter = 0; - return () => { - const elementId = `ELEMENT-${uid}-${(counter += 1)}`; - return elementId; - }; - })(); - - // Splitting with a delimiter to support IE8. When using a regex - // with a capture group IE8 does not include the capture group in - // the resulting array. - tokenDelimiter = `@__${uid}__@`; - tokenizedValues = {}; - elements = {}; - - // Iterates over the `props` to keep track of any React Element - // values so they can be represented by the `token` as a placeholder - // when the `message` is formatted. This allows the formatted - // message to then be broken-up into parts with references to the - // React Elements inserted back in. - Object.keys(values).forEach((name) => { - const value = values[name]; - - if (isValidElement(value)) { - const token = generateToken(); - tokenizedValues[name] = tokenDelimiter + token + tokenDelimiter; - elements[token] = value; - } else { - tokenizedValues[name] = value; - } - }); - } - - const descriptor = {id, defaultMessage}; - const formattedMessage = formatMessage( - descriptor, - tokenizedValues || values, - ); - const hasElements = elements && Object.keys(elements).length > 0; - - let nodes; - if (hasElements) { - // Split the message into parts so the React Element values captured - // above can be inserted back into the rendered message. This - // approach allows messages to render with React Elements while - // keeping React's virtual diffing working properly. - nodes = formattedMessage. - split(tokenDelimiter). - filter((part) => Boolean(part)). - map((part) => elements[part] || part); - } else { - nodes = [formattedMessage]; - } - - return createElement(Text, props, ...nodes); - } +type FormattedTextProps = { + id: string; + defaultMessage: string; + values?: Record; + testID?: string; + style?: StyleProp | StyleProp } + +const FormattedText = (props: FormattedTextProps) => { + const intl = useIntl(); + const {formatMessage} = intl; + const {id, defaultMessage, values, ...otherProps} = props; + const tokenizedValues: Record = {}; + const elements: Record = {}; + let tokenDelimiter = ''; + + if (values && Object.keys(values).length > 0) { + // Creates a token with a random UID that should not be guessable or + // conflict with other parts of the `message` string. + const uid = Math.floor(Math.random() * 0x10000000000).toString(16); + + const generateToken = (() => { + let counter = 0; + return () => { + const elementId = `ELEMENT-${uid}-${(counter += 1)}`; + return elementId; + }; + })(); + + // Splitting with a delimiter to support IE8. When using a regex + // with a capture group IE8 does not include the capture group in + // the resulting array. + tokenDelimiter = `@__${uid}__@`; + + // Iterates over the `props` to keep track of any React Element + // values so they can be represented by the `token` as a placeholder + // when the `message` is formatted. This allows the formatted + // message to then be broken-up into parts with references to the + // React Elements inserted back in. + Object.keys(values).forEach((name) => { + const value = values[name]; + + if (isValidElement(value)) { + const token = generateToken(); + tokenizedValues[name] = tokenDelimiter + token + tokenDelimiter; + elements[token] = value; + } else { + tokenizedValues[name] = value; + } + }); + } + + const descriptor = {id, defaultMessage}; + const formattedMessage = formatMessage( + descriptor, + tokenizedValues || values, + ); + const hasElements = elements && Object.keys(elements).length > 0; + + let nodes; + if (hasElements) { + // Split the message into parts so the React Element values captured + // above can be inserted back into the rendered message. This + // approach allows messages to render with React Elements while + // keeping React's virtual diffing working properly. + nodes = formattedMessage. + split(tokenDelimiter). + filter((part) => Boolean(part)). + map((part) => elements[part] || part); + } else { + nodes = [formattedMessage]; + } + + return createElement(Text, otherProps, ...nodes); +}; + +FormattedText.defaultProps = { + defaultMessage: '', +}; + +export default FormattedText; diff --git a/app/constants/deep_linking.ts b/app/constants/deep_linking.ts index f3ac632db1..5d7f68e519 100644 --- a/app/constants/deep_linking.ts +++ b/app/constants/deep_linking.ts @@ -3,6 +3,8 @@ export default { CHANNEL: 'channel', - PERMALINK: 'permalink', + DM: 'dmchannel', + GM: 'groupchannel', OTHER: 'other', + PERMALINK: 'permalink', }; diff --git a/app/constants/files.ts b/app/constants/files.ts new file mode 100644 index 0000000000..dae92df3e1 --- /dev/null +++ b/app/constants/files.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +const Files: Record = { + AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'], + CODE_TYPES: ['as', 'applescript', 'osascript', 'scpt', 'bash', 'sh', 'zsh', 'clj', 'boot', 'cl2', 'cljc', 'cljs', 'cljs.hl', 'cljscm', 'cljx', 'hic', 'coffee', '_coffee', 'cake', 'cjsx', 'cson', 'iced', 'cpp', 'c', 'cc', 'h', 'c++', 'h++', 'hpp', 'cs', 'csharp', 'css', 'd', 'di', 'dart', 'delphi', 'dpr', 'dfm', 'pas', 'pascal', 'freepascal', 'lazarus', 'lpr', 'lfm', 'diff', 'django', 'jinja', 'dockerfile', 'docker', 'erl', 'f90', 'f95', 'fsharp', 'fs', 'gcode', 'nc', 'go', 'groovy', 'handlebars', 'hbs', 'html.hbs', 'html.handlebars', 'hs', 'hx', 'java', 'jsp', 'js', 'jsx', 'json', 'jl', 'kt', 'ktm', 'kts', 'less', 'lisp', 'lua', 'mk', 'mak', 'md', 'mkdown', 'mkd', 'matlab', 'm', 'mm', 'objc', 'obj-c', 'ml', 'perl', 'pl', 'php', 'php3', 'php4', 'php5', 'php6', 'ps', 'ps1', 'pp', 'py', 'gyp', 'r', 'ruby', 'rb', 'gemspec', 'podspec', 'thor', 'irb', 'rs', 'scala', 'scm', 'sld', 'scss', 'st', 'sql', 'swift', 'tex', 'vbnet', 'vb', 'bas', 'vbs', 'v', 'veo', 'xml', 'html', 'xhtml', 'rss', 'atom', 'xsl', 'plist', 'yaml'], + IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg', 'tiff', 'tif'], + PATCH_TYPES: ['patch'], + PDF_TYPES: ['pdf'], + PRESENTATION_TYPES: ['ppt', 'pptx'], + SPREADSHEET_TYPES: ['xlsx', 'csv'], + TEXT_TYPES: ['txt', 'rtf'], + VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'], + WORD_TYPES: ['doc', 'docx'], + ZIP_TYPES: ['zip'], +}; + +export default Files; diff --git a/app/constants/general.ts b/app/constants/general.ts new file mode 100644 index 0000000000..72082b8091 --- /dev/null +++ b/app/constants/general.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export default { + CONFIG_CHANGED: 'config_changed', + SERVER_VERSION_CHANGED: 'server_version_changed', + PAGE_SIZE_DEFAULT: 60, + PAGE_SIZE_MAXIMUM: 200, + LOGS_PAGE_SIZE_DEFAULT: 10000, + PROFILE_CHUNK_SIZE: 100, + CHANNELS_CHUNK_SIZE: 50, + TEAMS_CHUNK_SIZE: 50, + SEARCH_TIMEOUT_MILLISECONDS: 100, + STATUS_INTERVAL: 60000, + AUTOCOMPLETE_LIMIT_DEFAULT: 25, + AUTOCOMPLETE_SPLIT_CHARACTERS: ['.', '-', '_'], + MENTION: 'mention', + OUT_OF_OFFICE: 'ooo', + OFFLINE: 'offline', + AWAY: 'away', + ONLINE: 'online', + DND: 'dnd', + PERMISSIONS_ALL: 'all', + PERMISSIONS_CHANNEL_ADMIN: 'channel_admin', + PERMISSIONS_TEAM_ADMIN: 'team_admin', + PERMISSIONS_SYSTEM_ADMIN: 'system_admin', + TEAM_GUEST_ROLE: 'team_guest', + TEAM_USER_ROLE: 'team_user', + TEAM_ADMIN_ROLE: 'team_admin', + CHANNEL_GUEST_ROLE: 'channel_guest', + CHANNEL_USER_ROLE: 'channel_user', + CHANNEL_ADMIN_ROLE: 'channel_admin', + SYSTEM_GUEST_ROLE: 'system_guest', + SYSTEM_USER_ROLE: 'system_user', + SYSTEM_ADMIN_ROLE: 'system_admin', + SYSTEM_USER_ACCESS_TOKEN_ROLE: 'system_user_access_token', + SYSTEM_POST_ALL_ROLE: 'system_post_all', + SYSTEM_POST_ALL_PUBLIC_ROLE: 'system_post_all_public', + ALLOW_EDIT_POST_ALWAYS: 'always', + ALLOW_EDIT_POST_NEVER: 'never', + ALLOW_EDIT_POST_TIME_LIMIT: 'time_limit', + DEFAULT_POST_EDIT_TIME_LIMIT: 300, + RESTRICT_DIRECT_MESSAGE_ANY: 'any', + RESTRICT_DIRECT_MESSAGE_TEAM: 'team', + SWITCH_TO_DEFAULT_CHANNEL: 'switch_to_default_channel', + REMOVED_FROM_CHANNEL: 'removed_from_channel', + DEFAULT_CHANNEL: 'town-square', + DM_CHANNEL: 'D', + OPEN_CHANNEL: 'O', + PRIVATE_CHANNEL: 'P', + GM_CHANNEL: 'G', + PUSH_NOTIFY_APPLE_REACT_NATIVE: 'apple_rn', + PUSH_NOTIFY_ANDROID_REACT_NATIVE: 'android_rn', + STORE_REHYDRATION_COMPLETE: 'store_hydration_complete', + OFFLINE_STORE_RESET: 'offline_store_reset', + OFFLINE_STORE_PURGE: 'offline_store_purge', + TEAMMATE_NAME_DISPLAY: { + SHOW_USERNAME: 'username', + SHOW_NICKNAME_FULLNAME: 'nickname_full_name', + SHOW_FULLNAME: 'full_name', + }, + SPECIAL_MENTIONS: ['all', 'channel', 'here'], + MAX_USERS_IN_GM: 8, + MIN_USERS_IN_GM: 3, + MAX_GROUP_CHANNELS_FOR_PROFILES: 50, + DEFAULT_LOCALE: 'en', + DEFAULT_AUTOLINKED_URL_SCHEMES: ['http', 'https', 'ftp', 'mailto', 'tel', 'mattermost'], + DISABLED: 'disabled', + DEFAULT_ON: 'default_on', + DEFAULT_OFF: 'default_off', + REHYDRATED: 'app/REHYDRATED', +}; diff --git a/app/constants/index.ts b/app/constants/index.ts index e784b9ef38..cab4c0ba59 100644 --- a/app/constants/index.ts +++ b/app/constants/index.ts @@ -5,6 +5,8 @@ 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'; import List from './list'; import Navigation from './navigation'; import Preferences from './preferences'; @@ -17,6 +19,8 @@ export { Database, DeepLink, Device, + Files, + General, List, Navigation, Preferences, diff --git a/app/constants/screens.ts b/app/constants/screens.ts index b65dddf074..ff0327d8dd 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -1,14 +1,18 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +export const SERVER = 'Server'; +export const LOGIN = 'Login'; +export const LOGIN_OPTIONS = 'LoginOptions'; export const CHANNEL = 'Channel'; export const MAIN_SIDEBAR = 'MainSidebar'; -export const SERVER = 'Server'; export const SETTINGS_SIDEBAR = 'SettingsSidebar'; export const THREAD = 'Thread'; export default { CHANNEL, + LOGIN, + LOGIN_OPTIONS, MAIN_SIDEBAR, SERVER, SETTINGS_SIDEBAR, diff --git a/app/constants/view.ts b/app/constants/view.ts index ed4f2af1a2..a5c0ca465f 100644 --- a/app/constants/view.ts +++ b/app/constants/view.ts @@ -77,7 +77,9 @@ const ViewTypes = keyMirror({ REMOVE_LAST_CHANNEL_FOR_TEAM: null, GITLAB: null, + GOOGLE: null, OFFICE365: null, + OPENID: null, SAML: null, SET_INITIAL_POST_VISIBILITY: null, diff --git a/app/i18n/index.ts b/app/i18n/index.ts index 3a1b952a7d..63421b7321 100644 --- a/app/i18n/index.ts +++ b/app/i18n/index.ts @@ -2,82 +2,173 @@ // See LICENSE.txt for license information. import moment from 'moment'; +import {getLocales} from 'react-native-localize'; import en from '@assets/i18n/en.json'; -export const DEFAULT_LOCALE = 'en'; +declare const global: { HermesInternal: null | {} }; -function loadTranslation(locale: string) { +const deviceLocale = getLocales()[0].languageCode; +export const DEFAULT_LOCALE = deviceLocale; + +function loadTranslation(locale?: string) { try { let translations; let momentData; + switch (locale) { case 'de': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/de'); + require('@formatjs/intl-numberformat/locale-data/de'); + require('@formatjs/intl-datetimeformat/locale-data/de'); + } + translations = require('@assets/i18n/de.json'); momentData = require('moment/locale/de'); break; case 'es': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/es'); + require('@formatjs/intl-numberformat/locale-data/es'); + require('@formatjs/intl-datetimeformat/locale-data/es'); + } + translations = require('@assets/i18n/es.json'); momentData = require('moment/locale/es'); break; case 'fr': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/fr'); + require('@formatjs/intl-numberformat/locale-data/fr'); + require('@formatjs/intl-datetimeformat/locale-data/fr'); + } + translations = require('@assets/i18n/fr.json'); momentData = require('moment/locale/fr'); break; case 'it': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/it'); + require('@formatjs/intl-numberformat/locale-data/it'); + require('@formatjs/intl-datetimeformat/locale-data/it'); + } + translations = require('@assets/i18n/it.json'); momentData = require('moment/locale/it'); break; case 'ja': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/ja'); + require('@formatjs/intl-numberformat/locale-data/ja'); + require('@formatjs/intl-datetimeformat/locale-data/ja'); + } + translations = require('@assets/i18n/ja.json'); momentData = require('moment/locale/ja'); break; case 'ko': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/ko'); + require('@formatjs/intl-numberformat/locale-data/ko'); + require('@formatjs/intl-datetimeformat/locale-data/ko'); + } + translations = require('@assets/i18n/ko.json'); momentData = require('moment/locale/ko'); break; case 'nl': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/nl'); + require('@formatjs/intl-numberformat/locale-data/nl'); + require('@formatjs/intl-datetimeformat/locale-data/nl'); + } + translations = require('@assets/i18n/nl.json'); momentData = require('moment/locale/nl'); break; case 'pl': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/pl'); + require('@formatjs/intl-numberformat/locale-data/pl'); + require('@formatjs/intl-datetimeformat/locale-data/pl'); + } + translations = require('@assets/i18n/pl.json'); momentData = require('moment/locale/pl'); break; case 'pt-BR': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/pt'); + require('@formatjs/intl-numberformat/locale-data/pt'); + require('@formatjs/intl-datetimeformat/locale-data/pt'); + } + translations = require('@assets/i18n/pt-BR.json'); momentData = require('moment/locale/pt-br'); break; case 'ro': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/ro'); + require('@formatjs/intl-numberformat/locale-data/ro'); + require('@formatjs/intl-datetimeformat/locale-data/ro'); + } + translations = require('@assets/i18n/ro.json'); momentData = require('moment/locale/ro'); break; case 'ru': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/ru'); + require('@formatjs/intl-numberformat/locale-data/ru'); + require('@formatjs/intl-datetimeformat/locale-data/ru'); + } + translations = require('@assets/i18n/ru.json'); momentData = require('moment/locale/ru'); break; case 'tr': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/tr'); + require('@formatjs/intl-numberformat/locale-data/tr'); + require('@formatjs/intl-datetimeformat/locale-data/tr'); + } + translations = require('@assets/i18n/tr.json'); momentData = require('moment/locale/tr'); break; case 'uk': + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/uk'); + require('@formatjs/intl-numberformat/locale-data/uk'); + require('@formatjs/intl-datetimeformat/locale-data/uk'); + } + translations = require('@assets/i18n/uk.json'); momentData = require('moment/locale/uk'); break; case 'zh-CN': + loadChinesePolyfills(); translations = require('@assets/i18n/zh-CN.json'); momentData = require('moment/locale/zh-cn'); break; case 'zh-TW': + loadChinesePolyfills(); translations = require('@assets/i18n/zh-TW.json'); momentData = require('moment/locale/zh-tw'); break; default: + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/en'); + require('@formatjs/intl-numberformat/locale-data/en'); + require('@formatjs/intl-datetimeformat/locale-data/en'); + } + translations = en; break; } - if (momentData) { + if (momentData && locale) { moment.updateLocale(locale.toLowerCase(), momentData); } else { resetMomentLocale(); @@ -89,11 +180,19 @@ function loadTranslation(locale: string) { } } +function loadChinesePolyfills() { + if (global.HermesInternal) { + require('@formatjs/intl-pluralrules/locale-data/zh'); + require('@formatjs/intl-numberformat/locale-data/zh'); + require('@formatjs/intl-datetimeformat/locale-data/zh'); + } +} + export function resetMomentLocale() { moment.locale(DEFAULT_LOCALE); } -export function getTranslations(locale: string) { +export function getTranslations(locale?: string) { return loadTranslation(locale); } diff --git a/app/init/analytics.ts b/app/init/analytics.ts index 93419b84b2..2289365989 100644 --- a/app/init/analytics.ts +++ b/app/init/analytics.ts @@ -13,7 +13,7 @@ const isSystemAdmin = (roles: string) => { const clientMap: Record = {}; -class Analytics { +export class Analytics { analytics: RudderClient | null = null; context: any; diagnosticId: string | undefined; diff --git a/app/init/fetch.ts b/app/init/fetch.ts deleted file mode 100644 index 822d8daeb8..0000000000 --- a/app/init/fetch.ts +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Platform} from 'react-native'; -import DeviceInfo from 'react-native-device-info'; -import RNFetchBlob from 'rn-fetch-blob'; -import urlParse from 'url-parse'; - -import LocalConfig from '@assets/config'; -import {Client4} from '@client/rest'; -import {HEADER_TOKEN, HEADER_X_CLUSTER_ID, HEADER_X_VERSION_ID} from '@client/rest/constants'; -import ClientError from '@client/rest/error'; -import EventEmitter from '@mm-redux/utils/event_emitter'; -import {General} from '@mm-redux/constants'; -import {t} from '@utils/i18n'; - -import mattermostBucket from 'app/mattermost_bucket'; -import mattermostManaged from 'app/mattermost_managed'; - -/* eslint-disable no-throw-literal */ - -const DEFAULT_TIMEOUT = 10000; - -let managedConfig; - -mattermostManaged.addEventListener('managedConfigDidChange', (config) => { - if (config?.timeout !== managedConfig?.timeout) { - initFetchConfig(); - return; - } - managedConfig = config; -}); - -const handleRedirectProtocol = (url, response) => { - const serverUrl = Client4.getUrl(); - const parsed = urlParse(url); - const {redirects} = response.rnfbRespInfo; - if (redirects) { - const redirectUrl = urlParse(redirects[redirects.length - 1]); - - if (serverUrl === parsed.origin && parsed.host === redirectUrl.host && parsed.protocol !== redirectUrl.protocol) { - Client4.setUrl(serverUrl.replace(parsed.protocol, redirectUrl.protocol)); - } - } -}; - -Client4.doFetchWithResponse = async (url, options) => { - // eslint-disable-next-line no-console - console.log('Request endpoint', url); - const customHeaders = LocalConfig.CustomRequestHeaders; - let waitsForConnectivity = false; - let timeoutIntervalForResource = 30; - - if (managedConfig?.useVPN === 'true') { - waitsForConnectivity = true; - } - - if (managedConfig?.timeoutVPN) { - timeoutIntervalForResource = parseInt(managedConfig.timeoutVPN, 10); - } - - let requestOptions = { - ...Client4.getOptions(options), - waitsForConnectivity, - timeoutIntervalForResource, - }; - - if (customHeaders && Object.keys(customHeaders).length > 0) { - requestOptions = { - ...requestOptions, - headers: { - ...requestOptions.headers, - ...LocalConfig.CustomRequestHeaders, - }, - }; - } - - let response; - let headers; - - let data; - try { - response = await fetch(url, requestOptions); - headers = response.headers; - if (!url.startsWith('https') && response.rnfbRespInfo && response.rnfbRespInfo.redirects && response.rnfbRespInfo.redirects.length > 1) { - handleRedirectProtocol(url, response); - } - - data = await response.json(); - } catch (err) { - if (response && response.resp && response.resp.data && response.resp.data.includes('SSL certificate')) { - throw new ClientError(Client4.getUrl(), { - message: 'You need to use a valid client certificate in order to connect to this Mattermost server', - status_code: 401, - url, - details: err, - }); - } - - throw new ClientError(Client4.getUrl(), { - message: 'Received invalid response from the server.', - intl: { - id: t('mobile.request.invalid_response'), - defaultMessage: 'Received invalid response from the server.', - }, - url, - details: err, - }); - } - - const clusterId = headers[HEADER_X_CLUSTER_ID] || headers[HEADER_X_CLUSTER_ID.toLowerCase()]; - if (clusterId && Client4.clusterId !== clusterId) { - Client4.clusterId = clusterId; /* eslint-disable-line require-atomic-updates */ - } - - const token = headers[HEADER_TOKEN] || headers[HEADER_TOKEN.toLowerCase()]; - if (token) { - Client4.setToken(token); - } - - const serverVersion = headers[HEADER_X_VERSION_ID] || headers[HEADER_X_VERSION_ID.toLowerCase()]; - if (serverVersion && !headers['Cache-Control'] && Client4.serverVersion !== serverVersion) { - Client4.serverVersion = serverVersion; /* eslint-disable-line require-atomic-updates */ - EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion); - } - - if (response.ok) { - const headersMap = new Map(); - Object.keys(headers).forEach((key) => { - headersMap.set(key, headers[key]); - }); - - return { - response, - headers: headersMap, - data, - }; - } - - const msg = data.message || ''; - - if (Client4.logToConsole) { - console.error(msg); // eslint-disable-line no-console - } - - throw new ClientError(Client4.getUrl(), { - message: msg, - server_error_id: data.id, - status_code: data.status_code, - url, - }); -}; - -const initFetchConfig = async () => { - const fetchConfig = { - auto: true, - timeout: DEFAULT_TIMEOUT, // Set the base timeout for every request to 5s - }; - - try { - managedConfig = await mattermostManaged.getConfig(); - - if (managedConfig?.timeout) { - const timeout = parseInt(managedConfig.timeout, 10); - fetchConfig.timeout = timeout || DEFAULT_TIMEOUT; - } - } catch { - // no managed config - } - - const userAgent = await DeviceInfo.getUserAgent(); - Client4.setUserAgent(userAgent); - - if (Platform.OS === 'ios') { - fetchConfig.certificate = await mattermostBucket.getPreference('cert'); - } - - window.fetch = new RNFetchBlob.polyfill.Fetch(fetchConfig).build(); - - return true; -}; - -initFetchConfig(); - -export default initFetchConfig; diff --git a/app/mattermost_bucket/index.ts b/app/mattermost_bucket/index.ts deleted file mode 100644 index 2617865329..0000000000 --- a/app/mattermost_bucket/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {NativeModules, Platform} from 'react-native'; - -//todo: need to remove this file altogether - -// TODO: Remove platform specific once android is implemented -const MattermostBucket = Platform.OS === 'ios' ? NativeModules.MattermostBucketModule : null; - -export default { - setPreference: (key, value) => { - if (MattermostBucket) { - MattermostBucket.setPreference(key, value); - } - }, - getPreference: async (key) => { - if (MattermostBucket) { - const value = await MattermostBucket.getPreference(key); - if (value) { - try { - return JSON.parse(value); - } catch (e) { - return value; - } - } - } - - return null; - }, - removePreference: (key) => { - if (MattermostBucket) { - MattermostBucket.removePreference(key); - } - }, - writeToFile: (fileName, content) => { - if (MattermostBucket) { - MattermostBucket.writeToFile(fileName, content); - } - }, - readFromFile: async (fileName) => { - if (MattermostBucket) { - const value = await MattermostBucket.readFromFile(fileName); - if (value) { - try { - return JSON.parse(value); - } catch (e) { - return value; - } - } - } - - return null; - }, - removeFile: (fileName) => { - if (MattermostBucket) { - MattermostBucket.removeFile(fileName); - } - }, -}; diff --git a/app/navigation/index.ts b/app/navigation/index.ts deleted file mode 100644 index 1e98648394..0000000000 --- a/app/navigation/index.ts +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import merge from 'deepmerge'; -import {Platform} from 'react-native'; -import {Navigation} from 'react-native-navigation'; -import {Store} from 'react-native-navigation/lib/dist/components/Store'; - -//fixme: to needful here for the whole file - -function getThemeFromState() { - const state = Store.redux?.getState() || {}; - - return getTheme(state); -} - -export function goToScreen(name, title, passProps = {}, options = {}) { - const theme = getThemeFromState(); - const componentId = EphemeralStore.getNavigationTopComponentId(); - const defaultOptions = { - layout: { - componentBackgroundColor: theme.centerChannelBg, - }, - popGesture: true, - sideMenu: { - left: {enabled: false}, - right: {enabled: false}, - }, - topBar: { - animate: true, - visible: true, - backButton: { - color: theme.sidebarHeaderTextColor, - enableMenu: false, - title: '', - testID: 'screen.back.button', - }, - background: { - color: theme.sidebarHeaderBg, - }, - title: { - color: theme.sidebarHeaderTextColor, - text: title, - }, - }, - }; - - Navigation.push(componentId, { - component: { - id: name, - name, - passProps, - options: merge(defaultOptions, options), - }, - }); -} - -export function resetToChannel(passProps = {}) { - const theme = getThemeFromState(); - - EphemeralStore.clearNavigationComponents(); - - const stack = { - children: [{ - component: { - id: NavigationTypes.CHANNEL_SCREEN, - name: NavigationTypes.CHANNEL_SCREEN, - passProps, - options: { - layout: { - componentBackgroundColor: theme.centerChannelBg, - }, - statusBar: { - visible: true, - }, - topBar: { - visible: false, - height: 0, - background: { - color: theme.sidebarHeaderBg, - }, - backButton: { - visible: false, - color: theme.sidebarHeaderTextColor, - enableMenu: false, - }, - }, - }, - }, - }], - }; - - let platformStack = {stack}; - if (Platform.OS === 'android') { - platformStack = { - sideMenu: { - left: { - component: { - id: 'MainSidebar', - name: 'MainSidebar', - }, - }, - center: { - stack, - }, - right: { - component: { - id: 'SettingsSidebar', - name: 'SettingsSidebar', - }, - }, - }, - }; - } - - Navigation.setRoot({ - root: { - ...platformStack, - }, - }); -} diff --git a/app/requests/remote/general.ts b/app/requests/remote/general.ts index c845d2899b..ec6304cb22 100644 --- a/app/requests/remote/general.ts +++ b/app/requests/remote/general.ts @@ -4,27 +4,44 @@ //fixme: substitute with network client import {Client4} from '@client/rest'; -export const getPing = async () => { +export const doPing = async (serverUrl?: string) => { let data; - let pingError = { + const pingError = { id: 'mobile.server_ping_failed', defaultMessage: 'Cannot connect to the server. Please check your server URL and internet connection.', }; try { + if (serverUrl) { + Client4.setUrl(serverUrl); + } + data = await Client4.ping(); if (data.status !== 'OK') { // successful ping but not the right return {data} - return {error: pingError}; + return {error: {intl: pingError}}; } } catch (error) { // Client4Error if (error.status_code === 401) { // When the server requires a client certificate to connect. - pingError = error; + return {error}; } - return {error: pingError}; + return {error: {intl: pingError}}; } return {data}; }; + +export const fetchConfigAndLicense = async () => { + try { + const [config, license] = await Promise.all([ + Client4.getClientConfigOld(), + Client4.getClientLicenseOld(), + ]); + + return {config, license}; + } catch (error) { + return {error}; + } +}; diff --git a/app/screens/index.ts b/app/screens/index.tsx similarity index 86% rename from app/screens/index.ts rename to app/screens/index.tsx index 986fd593f9..eb91749b28 100644 --- a/app/screens/index.ts +++ b/app/screens/index.tsx @@ -1,19 +1,21 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {withManagedConfig} from '@mattermost/react-native-emm'; import React from 'react'; import {Platform, StyleProp, ViewStyle} from 'react-native'; -import {withManagedConfig} from '@mattermost/react-native-emm'; +import {IntlProvider} from 'react-intl'; -import {Navigation} from 'react-native-navigation'; +import {Navigation, NavigationComponent, NavigationFunctionComponent} from 'react-native-navigation'; import {gestureHandlerRootHOC} from 'react-native-gesture-handler'; import {Screens} from '@constants'; +import {DEFAULT_LOCALE, getTranslations} from '@i18n'; // TODO: Remove this and uncomment screens as they get added /* eslint-disable */ -const withGestures = (screen: React.ComponentType, styles: StyleProp) => { +const withGestures = (screen: NavigationFunctionComponent, styles: StyleProp) => { if (Platform.OS === 'android') { return gestureHandlerRootHOC(screen, styles); } @@ -21,10 +23,23 @@ const withGestures = (screen: React.ComponentType, styles: StyleProp { + return function IntlEnabledComponent(props: any) { + return ( + + + + ); + } +} + Navigation.setLazyComponentRegistrator((screenName) => { - // let screen: any; - // let extraStyles: StyleProp; - // switch (screenName) { + let screen: any|undefined; + let extraStyles: StyleProp; + switch (screenName) { // case 'About': // screen = require('@screens/about').default; // break; @@ -91,9 +106,9 @@ Navigation.setLazyComponentRegistrator((screenName) => { // case 'Login': // screen = require('@screens/login').default; // break; - // case 'LoginOptions': - // screen = require('@screens/login_options').default; - // break; + case 'LoginOptions': + screen = require('@screens/login_options').default; + break; // case 'LongPost': // screen = require('@screens/long_post').default; // break; @@ -191,11 +206,11 @@ Navigation.setLazyComponentRegistrator((screenName) => { // case 'UserProfile': // screen = require('@screens/user_profile').default; // break; - // } + } - // if (screen) { - // Navigation.registerComponent(screenName, () => withGestures(withManagedConfig(screen), extraStyles)); - // } + if (screen) { + Navigation.registerComponent(screenName, () => withGestures(withIntl(withManagedConfig(screen)), extraStyles)); + } }); export function registerScreens() { @@ -203,5 +218,5 @@ export function registerScreens() { const serverScreen = require('@screens/server').default; Navigation.registerComponent(Screens.CHANNEL, () => withManagedConfig(channelScreen)); - Navigation.registerComponent(Screens.SERVER, () => withManagedConfig(serverScreen)); + Navigation.registerComponent(Screens.SERVER, () => withIntl(withManagedConfig(serverScreen))); } diff --git a/app/screens/login_options/email.tsx b/app/screens/login_options/email.tsx new file mode 100644 index 0000000000..f3f2fa4385 --- /dev/null +++ b/app/screens/login_options/email.tsx @@ -0,0 +1,63 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StyleProp, ViewStyle} from 'react-native'; +import Button from 'react-native-button'; + +import LocalConfig from '@assets/config.json'; +import FormattedText from '@components/formatted_text'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +const EmailOption = ({config, onPress, theme}: LoginOptionWithConfigProps) => { + const styles = getStyleSheet(theme); + const forceHideFromLocal = LocalConfig.HideEmailLoginExperimental; + + if (!forceHideFromLocal && (config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true')) { + const backgroundColor = config.EmailLoginButtonColor || '#2389d7'; + const additionalStyle: StyleProp = { + backgroundColor, + }; + + if (config.EmailLoginButtonBorderColor) { + additionalStyle.borderColor = config.EmailLoginButtonBorderColor; + } + + const textColor = config.EmailLoginButtonTextColor || 'white'; + + return ( + + ); + } + + return null; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ + button: { + borderRadius: 3, + borderColor: theme.buttonBg, + alignItems: 'center', + borderWidth: 1, + alignSelf: 'stretch', + marginTop: 10, + padding: 15, + }, + buttonText: { + textAlign: 'center', + color: theme.buttonBg, + fontSize: 17, + }, +})); + +export default EmailOption; diff --git a/app/screens/login_options/gitlab.tsx b/app/screens/login_options/gitlab.tsx new file mode 100644 index 0000000000..34f4ed47e0 --- /dev/null +++ b/app/screens/login_options/gitlab.tsx @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Image, Text} from 'react-native'; +import Button from 'react-native-button'; + +import LocalConfig from '@assets/config.json'; +import {View} from '@constants'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +const GitLabOption = ({config, onPress, theme}: LoginOptionWithConfigProps) => { + const styles = getStyleSheet(theme); + const forceHideFromLocal = LocalConfig.HideGitLabLoginExperimental; + + const handlePress = () => { + onPress(View.GITLAB); + }; + + if (!forceHideFromLocal && config.EnableSignUpWithGitLab === 'true') { + const additionalButtonStyle = { + backgroundColor: '#548', + borderColor: 'transparent', + borderWidth: 0, + }; + + const logoStyle = { + height: 18, + marginRight: 5, + width: 18, + }; + + const textColor = 'white'; + return ( + + ); + } + + return null; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ + button: { + borderRadius: 3, + borderColor: theme.buttonBg, + alignItems: 'center', + borderWidth: 1, + alignSelf: 'stretch', + marginTop: 10, + padding: 15, + }, + buttonText: { + textAlign: 'center', + color: theme.buttonBg, + fontSize: 17, + }, +})); + +export default GitLabOption; diff --git a/app/screens/login_options/google.tsx b/app/screens/login_options/google.tsx new file mode 100644 index 0000000000..d965bcee82 --- /dev/null +++ b/app/screens/login_options/google.tsx @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Image} from 'react-native'; +import Button from 'react-native-button'; + +import FormattedText from '@components/formatted_text'; +import {View} from '@constants'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +const GoogleOption = ({config, onPress, theme}: LoginOptionWithConfigProps) => { + const styles = getStyleSheet(theme); + + const handlePress = () => { + onPress(View.GOOGLE); + }; + + if (config.EnableSignUpWithGoogle === 'true') { + const additionalButtonStyle = { + backgroundColor: '#c23321', + borderColor: 'transparent', + borderWidth: 0, + }; + + const logoStyle = { + height: 18, + marginRight: 5, + width: 18, + }; + + const textColor = 'white'; + return ( + + ); + } + + return null; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ + button: { + borderRadius: 3, + borderColor: theme.buttonBg, + alignItems: 'center', + borderWidth: 1, + alignSelf: 'stretch', + marginTop: 10, + padding: 15, + }, + buttonText: { + textAlign: 'center', + color: theme.buttonBg, + fontSize: 17, + }, +})); + +export default GoogleOption; diff --git a/app/screens/login_options/index.tsx b/app/screens/login_options/index.tsx new file mode 100644 index 0000000000..656d47ad6a --- /dev/null +++ b/app/screens/login_options/index.tsx @@ -0,0 +1,142 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {useIntl} from 'react-intl'; +import {Image, ScrollView, StatusBar, StyleSheet, Text} from 'react-native'; +import {NavigationFunctionComponent} from 'react-native-navigation'; +import {SafeAreaView} from 'react-native-safe-area-context'; + +import FormattedText from '@components/formatted_text'; +import {goToScreen} from '@screens/navigation'; +import {preventDoubleTap} from '@utils/tap'; + +import EmailOption from './email'; +import GitLabOption from './gitlab'; +import GoogleOption from './google'; +import LdapOption from './ldap'; +import Office365Option from './office365'; +import OpenIdOption from './open_id'; +import SamlOption from './saml'; + +type LoginOptionsProps = { + componentId: string; + config: ClientConfig; + license: ClientLicense; + theme: Theme; +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + textAlign: 'center', + marginTop: 15, + marginBottom: 15, + fontSize: 32, + fontWeight: '600', + }, + subheader: { + textAlign: 'center', + fontSize: 16, + fontWeight: '300', + color: '#777', + marginBottom: 15, + lineHeight: 22, + }, + innerContainer: { + alignItems: 'center', + flexDirection: 'column', + justifyContent: 'center', + paddingHorizontal: 15, + flex: 1, + }, +}); + +const LoginOptions: NavigationFunctionComponent = ({config, license, theme}: LoginOptionsProps) => { + const intl = useIntl(); + + const displayLogin = preventDoubleTap(() => { + const screen = 'Login'; + const title = intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}); + + goToScreen(screen, title); + }); + + const displaySSO = preventDoubleTap((ssoType: string) => { + const screen = 'SSO'; + const title = intl.formatMessage({id: 'mobile.routes.sso', defaultMessage: 'Single Sign-On'}); + + goToScreen(screen, title, {ssoType}); + }); + + return ( + + + + + + {config.SiteName} + + + + + + + + + + + + + ); +}; + +export default LoginOptions; diff --git a/app/screens/login_options/ldap.tsx b/app/screens/login_options/ldap.tsx new file mode 100644 index 0000000000..0c2f0a8107 --- /dev/null +++ b/app/screens/login_options/ldap.tsx @@ -0,0 +1,78 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Text} from 'react-native'; +import Button from 'react-native-button'; + +import LocalConfig from '@assets/config.json'; +import FormattedText from '@components/formatted_text'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +const LdapOption = ({config, license, onPress, theme}: LoginOptionWithConfigAndLicenseProps) => { + const styles = getStyleSheet(theme); + const forceHideFromLocal = LocalConfig.HideLDAPLoginExperimental; + + if (!forceHideFromLocal && license.IsLicensed === 'true' && config.EnableLdap === 'true') { + const backgroundColor = config.LdapLoginButtonColor || '#2389d7'; + const additionalButtonStyle = { + backgroundColor, + borderColor: 'transparent', + borderWidth: 1, + }; + + if (config.LdapLoginButtonBorderColor) { + additionalButtonStyle.borderColor = config.LdapLoginButtonBorderColor; + } + + const textColor = config.LdapLoginButtonTextColor || 'white'; + + let buttonText; + if (config.LdapLoginFieldName) { + buttonText = ( + + {config.LdapLoginFieldName} + + ); + } else { + buttonText = ( + + ); + } + + return ( + + ); + } + + return null; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ + button: { + borderRadius: 3, + borderColor: theme.buttonBg, + alignItems: 'center', + borderWidth: 1, + alignSelf: 'stretch', + marginTop: 10, + padding: 15, + }, + buttonText: { + textAlign: 'center', + color: theme.buttonBg, + fontSize: 17, + }, +})); + +export default LdapOption; diff --git a/app/screens/login_options/office365.tsx b/app/screens/login_options/office365.tsx new file mode 100644 index 0000000000..4f5a6330fa --- /dev/null +++ b/app/screens/login_options/office365.tsx @@ -0,0 +1,66 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import Button from 'react-native-button'; + +import LocalConfig from '@assets/config.json'; +import FormattedText from '@components/formatted_text'; +import {View} from '@constants'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +const Office365Option = ({config, license, onPress, theme}: LoginOptionWithConfigAndLicenseProps) => { + const styles = getStyleSheet(theme); + const forceHideFromLocal = LocalConfig.HideO365LoginExperimental; + const o365Enabled = config.EnableSignUpWithOffice365 === 'true' && license.IsLicensed === 'true' && + license.Office365OAuth === 'true'; + + const handlePress = () => { + onPress(View.OFFICE365); + }; + + if (!forceHideFromLocal && o365Enabled) { + const additionalButtonStyle = { + backgroundColor: '#2389d7', + borderColor: 'transparent', + borderWidth: 0, + }; + + const textColor = 'white'; + + return ( + + ); + } + + return null; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ + button: { + borderRadius: 3, + borderColor: theme.buttonBg, + alignItems: 'center', + borderWidth: 1, + alignSelf: 'stretch', + marginTop: 10, + padding: 15, + }, + buttonText: { + textAlign: 'center', + color: theme.buttonBg, + fontSize: 17, + }, +})); + +export default Office365Option; diff --git a/app/screens/login_options/open_id.tsx b/app/screens/login_options/open_id.tsx new file mode 100644 index 0000000000..c53cb69701 --- /dev/null +++ b/app/screens/login_options/open_id.tsx @@ -0,0 +1,64 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import Button from 'react-native-button'; + +import FormattedText from '@components/formatted_text'; +import {View} from '@constants'; +import {isMinimumServerVersion} from '@utils/helpers'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +const OpenIdOption = ({config, license, onPress, theme}: LoginOptionWithConfigAndLicenseProps) => { + const styles = getStyleSheet(theme); + const openIdEnabled = config.EnableSignUpWithOpenId === 'true' && license.IsLicensed === 'true' && isMinimumServerVersion(config.Version, 5, 33, 0); + + const handlePress = () => { + onPress(View.OPENID); + }; + + if (openIdEnabled) { + const additionalButtonStyle = { + backgroundColor: config.OpenIdButtonColor || '#145DBF', + borderColor: 'transparent', + borderWidth: 0, + }; + + const textColor = 'white'; + + return ( + + ); + } + + return null; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ + button: { + borderRadius: 3, + borderColor: theme.buttonBg, + alignItems: 'center', + borderWidth: 1, + alignSelf: 'stretch', + marginTop: 10, + padding: 15, + }, + buttonText: { + textAlign: 'center', + color: theme.buttonBg, + fontSize: 17, + }, +})); + +export default OpenIdOption; diff --git a/app/screens/login_options/saml.tsx b/app/screens/login_options/saml.tsx new file mode 100644 index 0000000000..8f711ee999 --- /dev/null +++ b/app/screens/login_options/saml.tsx @@ -0,0 +1,71 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Text} from 'react-native'; +import Button from 'react-native-button'; + +import LocalConfig from '@assets/config.json'; +import {View} from '@constants'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +const SamlOption = ({config, license, onPress, theme}: LoginOptionWithConfigAndLicenseProps) => { + const styles = getStyleSheet(theme); + const forceHideFromLocal = LocalConfig.HideSAMLLoginExperimental; + const enabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true'; + + const handlePress = () => { + onPress(View.SAML); + }; + + if (!forceHideFromLocal && enabled) { + const backgroundColor = config.SamlLoginButtonColor || '#34a28b'; + + const additionalStyle = { + backgroundColor, + borderColor: 'transparent', + borderWidth: 0, + }; + + if (config.SamlLoginButtonBorderColor) { + additionalStyle.borderColor = config.SamlLoginButtonBorderColor; + } + + const textColor = config.SamlLoginButtonTextColor || 'white'; + + return ( + + ); + } + + return null; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ + button: { + borderRadius: 3, + borderColor: theme.buttonBg, + alignItems: 'center', + borderWidth: 1, + alignSelf: 'stretch', + marginTop: 10, + padding: 15, + }, + buttonText: { + textAlign: 'center', + color: theme.buttonBg, + fontSize: 17, + }, +})); + +export default SamlOption; diff --git a/app/screens/login_options/types.d.ts b/app/screens/login_options/types.d.ts new file mode 100644 index 0000000000..0e7fb5e809 --- /dev/null +++ b/app/screens/login_options/types.d.ts @@ -0,0 +1,12 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type LoginOptionWithConfigProps = { + config: ClientConfig; + onPress: (type: string|GestureResponderEvent) => void | (() => void); + theme: Theme; +} + +type LoginOptionWithConfigAndLicenseProps = LoginOptionWithConfigProps & { + license: ClientLicense; +}; diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index 744c524a05..44d9a27539 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -81,7 +81,7 @@ export function resetToChannel(passProps = {}) { } export function resetToSelectServer(allowOtherServers: boolean) { - const theme = Preferences.THEMES.default; + const theme = getThemeFromState(); EphemeralStore.clearNavigationComponents(); @@ -94,6 +94,7 @@ export function resetToSelectServer(allowOtherServers: boolean) { name: Screens.SERVER, passProps: { allowOtherServers, + theme, }, options: { layout: { diff --git a/app/screens/select_server/index.ts b/app/screens/select_server/index.ts deleted file mode 100644 index c54a28d21c..0000000000 --- a/app/screens/select_server/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {setLastUpgradeCheck} from '@actions/views/client_upgrade'; -import {loadConfigAndLicense} from '@actions/views/root'; -import {handleServerUrlChanged} from '@actions/views/select_server'; -import {scheduleExpiredNotification} from '@actions/views/session'; -import {getPing, resetPing, setServerVersion} from '@mm-redux/actions/general'; -import {login} from '@mm-redux/actions/users'; -import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; -import getClientUpgrade from '@selectors/client_upgrade'; - -import SelectServer from './select_server'; - -function mapStateToProps(state) { - const config = getConfig(state); - const license = getLicense(state); - const {currentVersion, latestVersion, minVersion} = getClientUpgrade(state); - - return { - ...state.views.selectServer, - config, - currentVersion, - deepLinkURL: state.views.root.deepLinkURL, - hasConfigAndLicense: Object.keys(config).length > 0 && Object.keys(license).length > 0, - latestVersion, - license, - minVersion, - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - getPing, - scheduleExpiredNotification, - handleServerUrlChanged, - loadConfigAndLicense, - login, - resetPing, - setLastUpgradeCheck, - setServerVersion, - }, dispatch), - }; -} - -//todo: Create HOC to pass extra props ( previously known as actions ) to the screen/component (mapDispatchToProps) -//todo: wrap screen/component with `withObservable` HOC for it to observe changes in the DB ( mapStateToProps ) - -export default connect(mapStateToProps, mapDispatchToProps)(SelectServer); diff --git a/app/screens/select_server/select_server.test.ts b/app/screens/select_server/select_server.test.ts deleted file mode 100644 index 69cdb9a699..0000000000 --- a/app/screens/select_server/select_server.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -import {fireEvent, waitFor} from '@testing-library/react-native'; -// See LICENSE.txt for license information. -import {renderWithReduxIntl} from 'test/testing_library'; - -import SelectServer from './select_server'; - -describe('SelectServer', () => { - const actions = { - getPing: jest.fn(), - handleServerUrlChanged: jest.fn(), - scheduleExpiredNotification: jest.fn(), - loadConfigAndLicense: jest.fn(), - login: jest.fn(), - resetPing: jest.fn(), - setLastUpgradeCheck: jest.fn(), - setServerVersion: jest.fn(), - }; - - const baseProps = { - actions, - hasConfigAndLicense: true, - serverUrl: '', - }; - - test('should match error when URL is empty string', async () => { - const {getByTestId, getByText} = renderWithReduxIntl( - ; - ) - - - const button = getByText('Connect'); - fireEvent.press(button); - - await waitFor(() => - expect(getByTestId('select_server.error.text')).toBeTruthy(), - ); - expect(getByText('Please enter a valid server URL')).toBeTruthy(); - }); - - test('should match error when URL is only spaces', async () => { - const {getByTestId, getByText} = renderWithReduxIntl( - ; - ) - - - const urlInput = getByTestId('select_server.server_url.input'); - fireEvent.changeText(urlInput, ' '); - - const button = getByText('Connect'); - fireEvent.press(button); - - await waitFor(() => - expect(getByTestId('select_server.error.text')).toBeTruthy(), - ); - expect(getByText('Please enter a valid server URL')).toBeTruthy(); - }); - - test('should match error when URL does not start with http:// or https://', async () => { - const {getByTestId, getByText} = renderWithReduxIntl( - ; - ) - - - const urlInput = getByTestId('select_server.server_url.input'); - fireEvent.changeText(urlInput, 'ht://invalid:8065'); - - const button = getByText('Connect'); - fireEvent.press(button); - - await waitFor(() => - expect(getByTestId('select_server.error.text')).toBeTruthy(), - ); - expect(getByText('URL must start with http:// or https://')).toBeTruthy(); - }); - - test('should not show error when valid URL is entered', async () => { - const {getByTestId, getByText, queryByTestId} = renderWithReduxIntl( - ; - ) - - - const urlInput = getByTestId('select_server.server_url.input'); - fireEvent.changeText(urlInput, 'http://localhost:8065'); - - const button = getByText('Connect'); - fireEvent.press(button); - - expect(queryByTestId('select_server.error.text')).toBeNull(); - await waitFor(() => expect(getByText('Connecting...')).toBeTruthy()); - }); -}); diff --git a/app/screens/select_server/select_server.tsx b/app/screens/select_server/select_server.tsx deleted file mode 100644 index 3cae1c8613..0000000000 --- a/app/screens/select_server/select_server.tsx +++ /dev/null @@ -1,668 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import mattermostBucket from '@app/mattermost_bucket'; -import {goToScreen, resetToChannel} from '@app/navigation'; -import {getClientUpgrade} from '@app/queries/helpers'; -import {handleServerUrlChanged, setLastUpgradeCheck} from '@app/requests/local/systems'; -import {loadConfigAndLicense} from '@app/requests/remote/systems'; -import {GlobalStyles} from '@app/styles'; -import telemetry from '@app/telemetry'; -import LocalConfig from '@assets/config.json'; -import {Client4} from '@client/rest'; -import AppVersion from '@components/app_version'; -import ErrorText from '@components/error_text'; -import FormattedText from '@components/formatted_text'; -import fetchConfig from '@init/fetch'; -import globalEventHandler from '@init/global_event_handler'; -import withObservables from '@nozbe/with-observables'; -import {getSystems} from '@queries/system'; -import {getPing} from '@requests/remote/general'; -import {scheduleExpiredNotification} from '@requests/remote/session'; -import System from '@typings/database/system'; -import {Styles} from '@typings/utils'; -import {checkUpgradeType, isUpgradeAvailable} from '@utils/client_upgrade'; -import {isMinimumServerVersion} from '@utils/helpers'; -import {t} from '@utils/i18n'; -import {preventDoubleTap} from '@utils/tap'; -import {changeOpacity} from '@utils/theme'; -import tracker from '@utils/time_tracker'; -import {isValidUrl, stripTrailingSlashes} from '@utils/url'; -import merge from 'deepmerge'; -import PropTypes from 'prop-types'; -import React, {PureComponent} from 'react'; -import {IntlShape, IntlContext} from 'react-intl'; -import { - ActivityIndicator, - Alert, - DeviceEventEmitter, - EmitterSubscription, - Image, - Keyboard, - KeyboardAvoidingView, - Platform, - StatusBar, - StyleSheet, - Text, - TextInput, - TouchableWithoutFeedback, - View, -} from 'react-native'; -import Button from 'react-native-button'; -import {EventSubscription, Navigation} from 'react-native-navigation'; -import {SafeAreaView} from 'react-native-safe-area-context'; -import RNFetchBlob from 'rn-fetch-blob'; -import urlParse from 'url-parse'; - -//todo: Once you get a URL and a NAME for a server, you have to init a database and then set it as the currenly active server database. All subsequent calls/queries will use it. -//todo: review all substituted actions - -type SelectServerProps = { - systems: System[]; - allowOtherServers: boolean -}; - -type SelectServerState = { - connected: boolean; - connecting: boolean; - error: null | {} | undefined; - url: string; -}; - -const NEXT_SCREEN_TIMEOUT = 350; - -class SelectServer extends PureComponent { - static propTypes = { - actions: PropTypes.shape({ - login: PropTypes.func.isRequired, - scheduleExpiredNotification: PropTypes.func.isRequired, - setServerVersion: PropTypes.func.isRequired, - }).isRequired, - }; - - static defaultProps = { - allowOtherServers: true, - }; - - //todo: make into functional component in typescript => last resort a class component - - //fixme: relook at below definition for INTL context - you need to use modern Context API; not legacy one - static contextType = IntlContext; - - private cancelPing: (() => void | null) | undefined | null; - private navigationEventListener!: EventSubscription; - private certificateListener!: EmitterSubscription; - private sslProblemListener!: EmitterSubscription; - private textInput!: TextInput; - private nextScreenTimer!: NodeJS.Timeout; - - state: SelectServerState = { - connected: false, - connecting: false, - error: null, - url: '', - }; - - //fixme: do we need getDerivedStateFromProps ? - // static getDerivedStateFromProps(props: SelectServerProps, state: SelectServerState) { - // const {systems} = props; - // const rootRecord = systems.find((systemRecord: System) => systemRecord.name === 'root') as System; - // const {deepLinkURL} = rootRecord.value; - // - // if (state.url === undefined && props.allowOtherServers && deepLinkURL) { - // const url = urlParse(deepLinkURL).host; - // return {url}; - // } else if (state.url === undefined && props.serverUrl) { - // return {url: props.serverUrl}; - // } - // return null; - // } - - componentDidMount() { - this.navigationEventListener = Navigation.events().bindComponent(this); - const { - selectServer: {serverUrl}, - } = this.getSystemsValues(); - - const {allowOtherServers} = this.props; - if (!allowOtherServers && serverUrl) { - // If the app is managed or AutoSelectServerUrl is true in the Config, the server url is set and the user can't change it - // we automatically trigger the ping to move to the next screen - this.handleConnect(); - } - - if (Platform.OS === 'android') { - Keyboard.addListener('keyboardDidHide', this.blur); - } - - this.certificateListener = DeviceEventEmitter.addListener( - 'RNFetchBlobCertificate', - this.selectCertificate, - ); - this.sslProblemListener = DeviceEventEmitter.addListener( - 'RNFetchBlobSslProblem', - this.handleSslProblem, - ); - - telemetry.end(['start:select_server_screen']); - telemetry.save(); - } - - getSystemsValues = () => { - const {systems} = this.props; - const configRecord = systems.find((systemRecord: System) => systemRecord.name === 'config') as System; - const licenseRecord = systems.find((systemRecord: System) => systemRecord.name === 'license') as System; - const selectServerRecord = systems.find((systemRecord: System) => systemRecord.name === 'selectServer') as System; - const rootRecord = systems.find((systemRecord: System) => systemRecord.name === 'root') as System; - - return { - config: configRecord.value, - license: licenseRecord.value, - selectServer: selectServerRecord.value, - root: rootRecord.value, - records: { - configRecord, - licenseRecord, - selectServerRecord, - rootRecord, - }, - }; - }; - - //fixme: Is this life-cycle method necessary ? - componentDidUpdate(prevProps: SelectServerProps, prevState: SelectServerState) { - const {config, license, records: {configRecord}} = this.getSystemsValues(); - const hasConfigAndLicense = Object.keys(config).length > 0 && Object.keys(license).length > 0; - - //todo: need to recheck this logic here as we are retrieving hasConfigAndLicense from the database now - if (this.state.connected && hasConfigAndLicense && !(prevState.connected && hasConfigAndLicense)) { - if (LocalConfig.EnableMobileClientUpgrade) { - setLastUpgradeCheck(configRecord); - - const {currentVersion, minVersion = '', latestVersion = ''} = getClientUpgrade(config); - const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion); - - if (isUpgradeAvailable(upgradeType)) { - this.handleShowClientUpgrade(upgradeType); - } else { - this.handleLoginOptions(); - } - } else { - this.handleLoginOptions(); - } - } - } - - componentWillUnmount() { - if (Platform.OS === 'android') { - Keyboard.removeListener('keyboardDidHide', this.blur); - } - - this.certificateListener?.remove(); - this.sslProblemListener?.remove(); - this.navigationEventListener?.remove(); - clearTimeout(this.nextScreenTimer); - } - - componentDidDisappear() { - this.setState({ - connected: false, - }); - } - - getUrl = async (serverUrl?: string, useHttp = false) => { - let url = this.sanitizeUrl(serverUrl, useHttp); - - try { - const resp = await fetch(url, {method: 'HEAD'}); - if (resp?.rnfbRespInfo?.redirects?.length) { - url = resp.rnfbRespInfo.redirects[resp.rnfbRespInfo.redirects.length - 1]; - } - } catch { - // do nothing - } - - return this.sanitizeUrl(url, useHttp); - }; - - goToNextScreen = (screen: string, title: string, passProps = {}, navOptions = {}) => { - const {allowOtherServers} = this.props; - let visible = !LocalConfig.AutoSelectServerUrl; - - if (!allowOtherServers) { - visible = false; - } - - const defaultOptions = { - popGesture: visible, - topBar: { - visible, - height: visible ? null : 0, - }, - }; - const options = merge(defaultOptions, navOptions); - - goToScreen(screen, title, passProps, options); - }; - - blur = () => { - if (this.textInput) { - this.textInput.blur(); - } - }; - - handleLoginOptions = async () => { - const {formatMessage} = this.context.intl; - const {config, license, selectServer: {serverUrl = ''}} = this.getSystemsValues(); - - const { - EnableSaml, - EnableSignUpWithGitLab, - EnableSignUpWithGoogle, - EnableSignUpWithOffice365, - EnableSignUpWithOpenId, - Version, - ExperimentalClientSideCertEnable, - ExperimentalClientSideCertCheck, - } = config; - - const {IsLicensed, SAML, Office365OAuth} = license; - - const samlEnabled = EnableSaml === 'true' && IsLicensed === 'true' && SAML === 'true'; - const gitlabEnabled = EnableSignUpWithGitLab === 'true'; - const googleEnabled = EnableSignUpWithGoogle === 'true' && IsLicensed === 'true'; - const o365Enabled = EnableSignUpWithOffice365 === 'true' && IsLicensed === 'true' && Office365OAuth === 'true'; - const openIdEnabled = EnableSignUpWithOpenId === 'true' && IsLicensed === 'true' && isMinimumServerVersion(Version, 5, 33, 0); - - let options = 0; - if (samlEnabled || gitlabEnabled || googleEnabled || o365Enabled || openIdEnabled) { - options += 1; - } - - let screen: string; - let title: string; - if (options) { - screen = 'LoginOptions'; - title = formatMessage({id: 'mobile.routes.loginOptions', defaultMessage: 'Login Chooser'}); - } else { - screen = 'Login'; - title = formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}); - } - - //todo: confirm if we should pass in the serverUrl here - await globalEventHandler.configureAnalytics(serverUrl); - - if (Platform.OS === 'ios') { - if (ExperimentalClientSideCertEnable === 'true' && ExperimentalClientSideCertCheck === 'primary') { - // log in automatically and send directly to the channel screen - this.loginWithCertificate(); - return; - } - - this.nextScreenTimer = setTimeout(() => { - this.goToNextScreen(screen, title); - }, NEXT_SCREEN_TIMEOUT); - } else { - this.goToNextScreen(screen, title); - } - }; - - handleShowClientUpgrade = (upgradeType: string) => { - const {formatMessage} = this.context.intl; - const screen = 'ClientUpgrade'; - const title = formatMessage({ - id: 'mobile.client_upgrade', - defaultMessage: 'Client Upgrade', - }); - const passProps = { - closeAction: this.handleLoginOptions, - upgradeType, - }; - const options = { - statusBar: { - visible: false, - }, - }; - - this.goToNextScreen(screen, title, passProps, options); - }; - - handleTextChanged = (url: string) => { - this.setState({url}); - }; - - inputRef = (ref: TextInput) => { - this.textInput = ref; - }; - - loginWithCertificate = async () => { - tracker.initialLoad = Date.now(); - const {intl} = this.context; - await this.props.actions.login('credential', 'password'); - - scheduleExpiredNotification(intl); - - resetToChannel(); - }; - - pingServer = async (url: string, retryWithHttp = true) => { - const { - setServerVersion, - } = this.props.actions; - - this.setState({ - connected: false, - connecting: true, - error: null, - }); - - let cancel = false; - this.cancelPing = () => { - cancel = true; - - this.setState({ - connected: false, - connecting: false, - }); - - this.cancelPing = null; - }; - - const serverUrl = await this.getUrl(url, !retryWithHttp); - Client4.setUrl(serverUrl); - - const {records: {configRecord, licenseRecord, selectServerRecord}} = this.getSystemsValues(); - handleServerUrlChanged({serverUrl, configRecord, licenseRecord, selectServerRecord}); - - try { - const result = await getPing(); - - if (cancel) { - return; - } - - if (result.error && retryWithHttp) { - const nurl = serverUrl.replace('https:', 'http:'); - this.pingServer(nurl, false); - return; - } - - if (!result.error) { - loadConfigAndLicense(); - setServerVersion(Client4.getServerVersion()); - } - - this.setState({ - connected: !result.error, - connecting: false, - error: result.error, - }); - } catch { - if (cancel) { - return; - } - - this.setState({ - connecting: false, - }); - } - }; - - sanitizeUrl = (url: string, useHttp = false) => { - let preUrl = urlParse(url, true); - let protocol = preUrl.protocol; - - if (!preUrl.host || preUrl.protocol === 'file:') { - preUrl = urlParse('https://' + stripTrailingSlashes(url), true); - } - - if (preUrl.protocol === 'http:' && !useHttp) { - protocol = 'https:'; - } - - return stripTrailingSlashes( - `${protocol}//${preUrl.host}${preUrl.pathname}`, - ); - }; - - handleConnect = preventDoubleTap(async () => { - Keyboard.dismiss(); - const {connecting, connected, url} = this.state; - if (connecting || connected) { - this.cancelPing?.(); - return; - } - - if (!url || url.trim() === '') { - this.setState({ - error: { - intl: { - id: t('mobile.server_url.empty'), - defaultMessage: 'Please enter a valid server URL', - }, - }, - }); - - return; - } - - if (!isValidUrl(this.sanitizeUrl(url))) { - this.setState({ - error: { - intl: { - id: t('mobile.server_url.invalid_format'), - defaultMessage: 'URL must start with http:// or https://', - }, - }, - }); - - return; - } - - await globalEventHandler.resetState(); - - //todo: create a ticket for this part - //fixme: ExperimentalClientSideCertEnable does not exist in LocalConfig...do we add it ? - if (LocalConfig.ExperimentalClientSideCertEnable && Platform.OS === 'ios') { - RNFetchBlob.cba.selectCertificate((certificate) => { - if (certificate) { - mattermostBucket.setPreference('cert', certificate); - window.fetch = new RNFetchBlob.polyfill.Fetch({ - auto: true, - certificate, - }).build(); - this.pingServer(url); - } - }); - } else { - this.pingServer(url); - } - }); - - handleSslProblem = () => { - const {connecting, connected, url} = this.state; - - if (!connecting && !connected) { - return null; - } - - this.cancelPing?.(); - - const host = urlParse(url, true).host || url; - - const {formatMessage} = this.context.intl; - Alert.alert( - formatMessage({ - id: 'mobile.server_ssl.error.title', - defaultMessage: 'Untrusted Certificate', - }), - formatMessage( - { - id: 'mobile.server_ssl.error.text', - defaultMessage: 'The certificate from {host} is not trusted.\n\nPlease contact your System Administrator to resolve the certificate issues and allow connections to this server.', - }, - { - host, - }, - ), - [{text: 'OK'}], - {cancelable: false}, - ); - - return null; - }; - - selectCertificate = async () => { - //fixme: how does this work ? - const url = await this.getUrl(); - RNFetchBlob.cba.selectCertificate((certificate) => { - if (certificate) { - mattermostBucket.setPreference('cert', certificate); - fetchConfig().then(() => { - this.pingServer(url, true); - }); - } - }); - }; - - render() { - const {formatMessage} = this.context.intl; - const {allowOtherServers} = this.props; - const {connected, connecting, error, url} = this.state; - - let buttonIcon; - let buttonText; - if (connected || connecting) { - buttonIcon = ( - - ); - buttonText = ( - - ); - } else { - buttonText = ( - - ); - } - - const barStyle = Platform.OS === 'android' ? 'light-content' : 'dark-content'; - - const inputDisabled = !allowOtherServers || connected || connecting; - - const inputStyle: Styles[] = [GlobalStyles.inputBox]; - if (inputDisabled) { - inputStyle.push(style.disabledInput); - } - - return ( - - - - - - - - - - - - - - - - - - - - - ); - } -} - -const style = StyleSheet.create({ - container: { - flex: 1, - }, - disabledInput: { - backgroundColor: '#e3e3e3', - }, - connectButton: { - alignItems: 'center', - }, - connectingIndicator: { - marginRight: 5, - }, -}); - -const withObserver = withObservables([], async () => { - return { - systems: getSystems(), - }; -}); - -export default withObserver(SelectServer); diff --git a/app/screens/server/index.tsx b/app/screens/server/index.tsx index 9d663ac3d2..e018972a7f 100644 --- a/app/screens/server/index.tsx +++ b/app/screens/server/index.tsx @@ -1,105 +1,334 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Colors, DebugInstructions, LearnMoreLinks, ReloadInstructions} from 'react-native/Libraries/NewAppScreen'; -import {SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, View} from 'react-native'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {useIntl} from 'react-intl'; +import { + ActivityIndicator, EventSubscription, Image, Keyboard, KeyboardAvoidingView, + Platform, StatusBar, StyleSheet, TextInput, TouchableWithoutFeedback, View, +} from 'react-native'; +import Button from 'react-native-button'; +import {useManagedConfig} from '@mattermost/react-native-emm'; +import {NavigationFunctionComponent} from 'react-native-navigation'; +import {SafeAreaView} from 'react-native-safe-area-context'; -import React from 'react'; +import LocalConfig from '@assets/config.json'; +import AppVersion from '@components/app_version'; +import ErrorText, {ClientErrorWithIntl} from '@components/error_text'; +import FormattedText from '@components/formatted_text'; import {Screens} from '@constants'; +import {doPing, fetchConfigAndLicense} from '@requests/remote/general'; import {goToScreen} from '@screens/navigation'; +import {isMinimumServerVersion} from '@utils/helpers'; +import {preventDoubleTap} from '@utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {getServerUrlAfterRedirect, isValidUrl, sanitizeUrl} from '@utils/url'; -declare const global: { HermesInternal: null | {} }; +type ServerProps = { + componentId: string; + theme: Theme; +} + +let cancelPing: undefined | (() => void); + +const Server: NavigationFunctionComponent = ({theme}: ServerProps) => { + const intl = useIntl(); + const managedConfig = useManagedConfig(); + const input = useRef(null); + const [connecting, setConnecting] = useState(false); + const [error, setError] = useState(); + const [url, setUrl] = useState(''); + const styles = getStyleSheet(theme); + const {formatMessage} = intl; + + const displayLogin = (config: ClientConfig, license: ClientLicense) => { + const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true'; + const gitlabEnabled = config.EnableSignUpWithGitLab === 'true'; + const googleEnabled = config.EnableSignUpWithGoogle === 'true' && license.IsLicensed === 'true'; + const o365Enabled = config.EnableSignUpWithOffice365 === 'true' && license.IsLicensed === 'true' && license.Office365OAuth === 'true'; + const openIdEnabled = config.EnableSignUpWithOpenId === 'true' && license.IsLicensed === 'true' && isMinimumServerVersion(config.Version, 5, 33, 0); + + let screen = Screens.LOGIN; + let title = formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}); + if (samlEnabled || gitlabEnabled || googleEnabled || o365Enabled || openIdEnabled) { + screen = Screens.LOGIN_OPTIONS; + title = formatMessage({id: 'mobile.routes.loginOptions', defaultMessage: 'Login Chooser'}); + } + + const {allowOtherServers} = managedConfig; + let visible = !LocalConfig.AutoSelectServerUrl; + + if (!allowOtherServers) { + visible = false; + } + + const defaultOptions = { + popGesture: visible, + topBar: { + visible, + height: visible ? null : 0, + }, + }; + + goToScreen(screen, title, {config, license, theme}, defaultOptions); + setConnecting(false); + }; + + const handleConnect = preventDoubleTap(() => { + if (connecting && cancelPing) { + cancelPing(); + return; + } + + if (!url || url.trim() === '') { + setError(intl.formatMessage({ + id: 'mobile.server_url.empty', + defaultMessage: 'Please enter a valid server URL', + })); + + return; + } + + const serverUrl = sanitizeUrl(url); + if (!isValidUrl(serverUrl)) { + setError(formatMessage({ + id: 'mobile.server_url.invalid_format', + defaultMessage: 'URL must start with http:// or https://', + })); + + return; + } + + pingServer(serverUrl); + }); + + const pingServer = async (pingUrl: string, retryWithHttp = true) => { + let canceled = false; + setConnecting(true); + setError(undefined); + + cancelPing = () => { + // We should not need this once we have the cancelable network-client library + canceled = true; + setConnecting(false); + cancelPing = undefined; + }; + + const serverUrl = await getServerUrlAfterRedirect(pingUrl, !retryWithHttp); + + const result = await doPing(serverUrl); + + if (canceled) { + return; + } + + if (result.error && retryWithHttp) { + const nurl = serverUrl.replace('https:', 'http:'); + pingServer(nurl, false); + return; + } else if (result.error) { + setError(result.error); + setConnecting(false); + return; + } + + const data = await fetchConfigAndLicense(); + if (data.error) { + setError(data.error); + setConnecting(false); + return; + } + + displayLogin(data.config!, data.license!); + }; + + const blur = useCallback(() => { + input.current?.blur(); + }, []); + + const handleTextChanged = useCallback((text) => { + setUrl(text); + }, []); + + useEffect(() => { + let listener: EventSubscription; + if (Platform.OS === 'android') { + listener = Keyboard.addListener('keyboardDidHide', blur); + } + + return () => listener?.remove(); + }, []); + + let buttonIcon; + let buttonText; + + if (connecting) { + buttonIcon = ( + + ); + buttonText = ( + + ); + } else { + buttonText = ( + + ); + } + + const barStyle = Platform.OS === 'android' ? 'light-content' : 'dark-content'; + const inputDisabled = managedConfig.allowOtherServers === 'false' || connecting; + + const inputStyle = [styles.inputBox]; + if (inputDisabled) { + inputStyle.push(styles.disabledInput); + } -const App = () => { return ( - <> - - - + + + - {global.HermesInternal == null ? null : ( - - {'Engine: Hermes'} + + + + + - )} - - - {'Step One'} - - {'Edit '} - {'screens/server/index.tsx'}{' to change this'} - {'XXXXXscreen and then come back to see your edits.'} - + + + {Boolean(error) && + + - - goToScreen(Screens.CHANNEL, 'Channel')} - >{'See Your Changes'} - - - - - - {'Debug'} - - - - - - {'Learn More'} - - {'Read the docs to discover what to do next:'} - - - + } - - - + + + + ); }; -const styles = StyleSheet.create({ - scrollView: { - backgroundColor: Colors.lighter, +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + container: { + flex: 1, + backgroundColor: theme.centerChannelBg, }, - engine: { - right: 0, + flex: { + flex: 1, }, - body: { - backgroundColor: Colors.white, + formContainer: { + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + paddingRight: 15, + paddingLeft: 15, }, - sectionContainer: { - marginTop: 32, - paddingHorizontal: 24, + disabledInput: { + backgroundColor: '#e3e3e3', }, - sectionTitle: { - fontSize: 24, - fontWeight: '600', - color: Colors.black, + connectButton: { + borderRadius: 3, + borderColor: theme.buttonBg, + alignItems: 'center', + borderWidth: 1, + alignSelf: 'stretch', + marginTop: 10, + padding: 15, }, - sectionDescription: { - marginTop: 8, - fontSize: 18, + connectingIndicator: { + marginRight: 5, + }, + inputBox: { + fontSize: 16, + height: 45, + borderColor: theme.centerChannelColor, + borderWidth: 1, + marginTop: 5, + marginBottom: 5, + paddingLeft: 10, + alignSelf: 'stretch', + borderRadius: 3, + color: theme.centerChannelColor, + }, + logo: { + height: 72, + resizeMode: 'contain', + }, + header: { + textAlign: 'center', + marginTop: 15, + marginBottom: 15, + fontSize: 20, fontWeight: '400', - color: Colors.dark, }, - highlight: { - fontWeight: '700', + connectText: { + textAlign: 'center', + color: theme.buttonBg, + fontSize: 17, }, - footer: { - color: Colors.dark, - fontSize: 12, - fontWeight: '600', - padding: 4, - paddingRight: 12, - textAlign: 'right', - }, -}); +})); -export default App; +export default Server; diff --git a/app/utils/general/index.ts b/app/utils/general/index.ts new file mode 100644 index 0000000000..e51f6b08d5 --- /dev/null +++ b/app/utils/general/index.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export function emptyErrorHandlingFunction() { // eslint-disable-line no-empty-function, @typescript-eslint/no-unused-vars +} + +export function emptyFunction() { // eslint-disable-line no-empty-function +} diff --git a/app/utils/helpers.ts b/app/utils/helpers.ts index d8d2885fd7..6f40fc6b81 100644 --- a/app/utils/helpers.ts +++ b/app/utils/helpers.ts @@ -43,3 +43,22 @@ export const isMinimumServerVersion = (currentVersion: string, minMajorVersion = // Dot version is equal return true; }; + +export function buildQueryString(parameters: Dictionary): string { + const keys = Object.keys(parameters); + if (keys.length === 0) { + return ''; + } + + let query = '?'; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + query += key + '=' + encodeURIComponent(parameters[key]); + + if (i < keys.length - 1) { + query += '&'; + } + } + + return query; +} diff --git a/app/utils/markdown/index.ts b/app/utils/markdown/index.ts new file mode 100644 index 0000000000..1cdf2b63ce --- /dev/null +++ b/app/utils/markdown/index.ts @@ -0,0 +1,207 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Platform, StyleSheet} from 'react-native'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +export function getCodeFont() { + return Platform.OS === 'ios' ? 'Menlo' : 'monospace'; +} + +export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme: Theme) => { + const codeFont = getCodeFont(); + + return { + emph: { + fontStyle: 'italic', + }, + strong: { + fontWeight: 'bold', + }, + del: { + textDecorationLine: 'line-through', + }, + link: { + color: theme.linkColor, + }, + heading1: { + fontSize: 17, + fontWeight: '700', + lineHeight: 25, + }, + heading1Text: { + paddingBottom: 8, + }, + heading2: { + fontSize: 17, + fontWeight: '700', + lineHeight: 25, + }, + heading2Text: { + paddingBottom: 8, + }, + heading3: { + fontSize: 17, + fontWeight: '700', + lineHeight: 25, + }, + heading3Text: { + paddingBottom: 8, + }, + heading4: { + fontSize: 17, + fontWeight: '700', + lineHeight: 25, + }, + heading4Text: { + paddingBottom: 8, + }, + heading5: { + fontSize: 17, + fontWeight: '700', + lineHeight: 25, + }, + heading5Text: { + paddingBottom: 8, + }, + heading6: { + fontSize: 17, + fontWeight: '700', + lineHeight: 25, + }, + heading6Text: { + paddingBottom: 8, + }, + code: { + alignSelf: 'center', + backgroundColor: changeOpacity(theme.centerChannelColor, 0.07), + fontFamily: codeFont, + }, + codeBlock: { + fontFamily: codeFont, + }, + mention: { + color: theme.linkColor, + }, + error: { + color: theme.errorTextColor, + }, + table_header_row: { + fontWeight: '700', + }, + mention_highlight: { + backgroundColor: theme.mentionHighlightBg, + color: theme.mentionHighlightLink, + }, + }; +}); + +export const getMarkdownBlockStyles = makeStyleSheetFromTheme((theme: Theme) => { + return { + adjacentParagraph: { + marginTop: 6, + }, + horizontalRule: { + backgroundColor: theme.centerChannelColor, + height: StyleSheet.hairlineWidth, + marginVertical: 10, + }, + quoteBlockIcon: { + color: changeOpacity(theme.centerChannelColor, 0.5), + }, + }; +}); + +const languages: Record = { + actionscript: 'ActionScript', + applescript: 'AppleScript', + bash: 'Bash', + clojure: 'Clojure', + coffeescript: 'CoffeeScript', + cpp: 'C++', + cs: 'C#', + css: 'CSS', + d: 'D', + dart: 'Dart', + delphi: 'Delphi', + diff: 'Diff', + django: 'Django', + dockerfile: 'Dockerfile', + elixir: 'Elixir', + erlang: 'Erlang', + fortran: 'Fortran', + fsharp: 'F#', + gcode: 'G-code', + go: 'Go', + groovy: 'Groovy', + handlebars: 'Handlebars', + haskell: 'Haskell', + haxe: 'Haxe', + html: 'HTML', + java: 'Java', + javascript: 'JavaScript', + json: 'JSON', + julia: 'Julia', + kotlin: 'Kotlin', + latex: 'LaTeX', + less: 'Less', + lisp: 'Lisp', + lua: 'Lua', + makefile: 'Makefile', + markdown: 'Markdown', + matlab: 'Matlab', + objectivec: 'Objective-C', + ocaml: 'OCaml', + perl: 'Perl', + php: 'PHP', + powershell: 'PowerShell', + puppet: 'Puppet', + python: 'Python', + r: 'R', + ruby: 'Ruby', + rust: 'Rust', + scala: 'Scala', + scheme: 'Scheme', + scss: 'SCSS', + smalltalk: 'Smalltalk', + sql: 'SQL', + swift: 'Swift', + tex: 'TeX', + vbnet: 'VB.NET', + vbscript: 'VBScript', + verilog: 'Verilog', + xml: 'XML', + yaml: 'YAML', +}; + +export function getDisplayNameForLanguage(language: string) { + return languages[language.toLowerCase()] || ''; +} + +export function escapeRegex(text: string) { + return text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + +export function switchKeyboardForCodeBlocks(value: string, cursorPosition: number) { + if (Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 12) { + const regexForCodeBlock = /^```$(.*?)^```$|^```$(.*)/gms; + + const matches = []; + let nextMatch; + while ((nextMatch = regexForCodeBlock.exec(value)) !== null) { + matches.push({ + startOfMatch: regexForCodeBlock.lastIndex - nextMatch[0].length, + endOfMatch: regexForCodeBlock.lastIndex + 1, + }); + } + + const cursorIsInsideCodeBlock = matches.some((match) => cursorPosition >= match.startOfMatch && cursorPosition <= match.endOfMatch); + + // 'email-address' keyboardType prevents iOS emdash autocorrect + if (cursorIsInsideCodeBlock) { + return 'email-address'; + } + } + + return 'default'; +} diff --git a/app/utils/theme/index.ts b/app/utils/theme/index.ts index 08e695dda8..de0af17c7d 100644 --- a/app/utils/theme/index.ts +++ b/app/utils/theme/index.ts @@ -4,9 +4,9 @@ import {StyleSheet} from 'react-native'; import tinyColor from 'tinycolor2'; -import * as ThemeUtils from '@mm-redux/utils/theme_utils'; +import {mergeNavigationOptions} from '@screens/navigation'; -import {mergeNavigationOptions} from 'app/actions/navigation'; +import type {Options} from 'react-native-navigation'; const MODAL_SCREENS_WITHOUT_BACK = [ 'AddReaction', @@ -24,22 +24,74 @@ const MODAL_SCREENS_WITHOUT_BACK = [ 'UserProfile', ]; -export function makeStyleSheetFromTheme(getStyleFromTheme) { - return ThemeUtils.makeStyleFromTheme((theme) => { - return StyleSheet.create(getStyleFromTheme(theme)); - }); +const rgbPattern = /^rgba?\((\d+),(\d+),(\d+)(?:,([\d.]+))?\)$/; + +export function getComponents(inColor: string): {red: number; green: number; blue: number; alpha: number} { + let color = inColor; + + // RGB color + const match = rgbPattern.exec(color); + if (match) { + return { + red: parseInt(match[1], 10), + green: parseInt(match[2], 10), + blue: parseInt(match[3], 10), + alpha: match[4] ? parseFloat(match[4]) : 1, + }; + } + + // Hex color + if (color[0] === '#') { + color = color.slice(1); + } + + if (color.length === 3) { + const tempColor = color; + color = ''; + + color += tempColor[0] + tempColor[0]; + color += tempColor[1] + tempColor[1]; + color += tempColor[2] + tempColor[2]; + } + + return { + red: parseInt(color.substring(0, 2), 16), + green: parseInt(color.substring(2, 4), 16), + blue: parseInt(color.substring(4, 6), 16), + alpha: 1, + }; } -export const changeOpacity = ThemeUtils.changeOpacity; +export function makeStyleSheetFromTheme(getStyleFromTheme: (a: any) => any): (a: any) => any { + let lastTheme: any; + let style: any; + return (theme: any) => { + if (!style || theme !== lastTheme) { + style = StyleSheet.create(getStyleFromTheme(theme)); + lastTheme = theme; + } -export const blendColors = ThemeUtils.blendColors; + return style; + }; +} -export function concatStyles(...styles) { +export function changeOpacity(oldColor: string, opacity: number): string { + const { + red, + green, + blue, + alpha, + } = getComponents(oldColor); + + return `rgba(${red},${green},${blue},${alpha * opacity})`; +} + +export function concatStyles(...styles: any) { return [].concat(styles); } -export function setNavigatorStyles(componentId, theme) { - const options = { +export function setNavigatorStyles(componentId: string, theme: Theme) { + const options: Options = { topBar: { title: { color: theme.sidebarHeaderTextColor, @@ -55,7 +107,7 @@ export function setNavigatorStyles(componentId, theme) { }, }; - if (!MODAL_SCREENS_WITHOUT_BACK.includes(componentId)) { + if (!MODAL_SCREENS_WITHOUT_BACK.includes(componentId) && options.topBar) { options.topBar.backButton = { color: theme.sidebarHeaderTextColor, }; @@ -64,17 +116,12 @@ export function setNavigatorStyles(componentId, theme) { mergeNavigationOptions(componentId, options); } -export function isThemeSwitchingEnabled(state) { - const {config} = state.entities.general; - return config.EnableThemeSelection === 'true'; -} - -export function getKeyboardAppearanceFromTheme(theme) { +export function getKeyboardAppearanceFromTheme(theme: Theme) { return tinyColor(theme.centerChannelBg).isLight() ? 'light' : 'dark'; } -export function hexToHue(hexColor) { - let {red, green, blue} = ThemeUtils.getComponents(hexColor); +export function hexToHue(hexColor: string) { + let {red, green, blue} = getComponents(hexColor); red /= 255; green /= 255; blue /= 255; diff --git a/app/utils/url/index.ts b/app/utils/url/index.ts index 918a1d92ad..247bc3d424 100644 --- a/app/utils/url/index.ts +++ b/app/utils/url/index.ts @@ -2,15 +2,13 @@ // See LICENSE.txt for license information. import {Linking} from 'react-native'; +import urlParse from 'url-parse'; -import {latinise} from './latinise.js'; -import {escapeRegex} from './markdown'; - -import {Files} from '@mm-redux/constants'; -import {getCurrentServerUrl} from '@init/credentials'; - -import {DeepLinkTypes} from '@constants'; +import {DeepLink, Files} from '@constants'; import {emptyErrorHandlingFunction, emptyFunction} from '@utils/general'; +import {escapeRegex} from '@utils/markdown'; + +import {latinise} from './latinise'; const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&?]*)/; @@ -19,6 +17,38 @@ export function isValidUrl(url = '') { return regex.test(url); } +export function sanitizeUrl(url: string, useHttp = false) { + let preUrl = urlParse(url, true); + let protocol = preUrl.protocol; + + if (!preUrl.host || preUrl.protocol === 'file:') { + preUrl = urlParse('https://' + stripTrailingSlashes(url), true); + } + + if (!protocol || (preUrl.protocol === 'http:' && !useHttp)) { + protocol = 'https:'; + } + + return stripTrailingSlashes( + `${protocol}//${preUrl.host}${preUrl.pathname}`, + ); +} + +export async function getServerUrlAfterRedirect(serverUrl: string, useHttp = false) { + let url = sanitizeUrl(serverUrl, useHttp); + + try { + const resp = await fetch(url, {method: 'HEAD'}); + if (resp.redirected) { + url = resp.url; + } + } catch { + // do nothing + } + + return sanitizeUrl(url, useHttp); +} + export function stripTrailingSlashes(url = '') { return url.replace(/ /g, '').replace(/^\/+/, '').replace(/\/+$/, ''); } @@ -27,7 +57,7 @@ export function removeProtocol(url = '') { return url.replace(/(^\w+:|^)\/\//, ''); } -export function extractFirstLink(text) { +export function extractFirstLink(text: string) { const pattern = /(^|[\s\n]|)((?:https?|ftp):\/\/[-A-Z0-9+\u0026\u2019@#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026@#/%=~()_|])/i; let inText = text; @@ -45,11 +75,11 @@ export function extractFirstLink(text) { return ''; } -export function isYoutubeLink(link) { +export function isYoutubeLink(link: string) { return link.trim().match(ytRegex); } -export function isImageLink(link) { +export function isImageLink(link: string) { let linkWithoutQuery = link; if (link.indexOf('?') !== -1) { linkWithoutQuery = linkWithoutQuery.split('?')[0]; @@ -68,7 +98,7 @@ export function isImageLink(link) { // Converts the protocol of a link (eg. http, ftp) to be lower case since // Android doesn't handle uppercase links. -export function normalizeProtocol(url) { +export function normalizeProtocol(url: string) { const index = url.indexOf(':'); if (index === -1) { // There's no protocol on the link to be normalized @@ -87,7 +117,7 @@ export function getShortenedURL(url = '', getLength = 27) { return url + '/'; } -export function cleanUpUrlable(input) { +export function cleanUpUrlable(input: string) { let cleaned = latinise(input); cleaned = cleaned.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-'); cleaned = cleaned.replace(/-{2,}/, '-'); @@ -96,7 +126,7 @@ export function cleanUpUrlable(input) { return cleaned; } -export function getScheme(url) { +export function getScheme(url: string) { const match = (/([a-z0-9+.-]+):/i).exec(url); return match && match[1]; @@ -104,7 +134,7 @@ export function getScheme(url) { export const PERMALINK_GENERIC_TEAM_NAME_REDIRECT = '_redirect'; -export function matchDeepLink(url, serverURL, siteURL) { +export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string) { if (!url || (!serverURL && !siteURL)) { return null; } @@ -126,28 +156,28 @@ export function matchDeepLink(url, serverURL, siteURL) { match = new RegExp(linkRoot + '\\/([^\\/]+)\\/channels\\/(\\S+)').exec(urlToMatch); if (match) { - return {type: DeepLinkTypes.CHANNEL, teamName: match[1], channelName: match[2]}; + return {type: DeepLink.CHANNEL, teamName: match[1], channelName: match[2]}; } match = new RegExp(linkRoot + '\\/([^\\/]+)\\/pl\\/(\\w+)').exec(urlToMatch); if (match) { - return {type: DeepLinkTypes.PERMALINK, teamName: match[1], postId: match[2]}; + return {type: DeepLink.PERMALINK, teamName: match[1], postId: match[2]}; } match = new RegExp(linkRoot + '\\/([^\\/]+)\\/messages\\/@(\\S+)').exec(urlToMatch); if (match) { - return {type: DeepLinkTypes.DMCHANNEL, teamName: match[1], userName: match[2]}; + return {type: DeepLink.DM, teamName: match[1], userName: match[2]}; } match = new RegExp(linkRoot + '\\/([^\\/]+)\\/messages\\/(\\S+)').exec(urlToMatch); if (match) { - return {type: DeepLinkTypes.GROUPCHANNEL, teamName: match[1], id: match[2]}; + return {type: DeepLink.GM, teamName: match[1], id: match[2]}; } return null; } -export function getYouTubeVideoId(link) { +export function getYouTubeVideoId(link: string) { // https://youtube.com/watch?v= let match = (/youtube\.com\/watch\?\S*\bv=([a-zA-Z0-9_-]{6,11})/g).exec(link); if (match) { @@ -169,25 +199,58 @@ export function getYouTubeVideoId(link) { return ''; } -export async function getURLAndMatch(href, serverURL, siteURL) { - const url = normalizeProtocol(href); - - if (!url) { - return {}; - } - - let serverUrl = serverURL; - if (!serverUrl) { - serverUrl = await getCurrentServerUrl(); - } - - const match = matchDeepLink(url, serverURL, siteURL); - - return {url, match}; -} - -export function tryOpenURL(url, onError = emptyErrorHandlingFunction, onSuccess = emptyFunction) { +export function tryOpenURL(url: string, onError = emptyErrorHandlingFunction, onSuccess = emptyFunction) { Linking.openURL(url). then(onSuccess). catch(onError); } + +// Given a URL from an API request, return a URL that has any parts removed that are either sensitive or that would +// prevent properly grouping the messages in Sentry. +export function cleanUrlForLogging(baseUrl: string, apiUrl: string): string { + let url = apiUrl; + + // Trim the host name + url = url.substring(baseUrl.length); + + // Filter the query string + const index = url.indexOf('?'); + if (index !== -1) { + url = url.substring(0, index); + } + + // A non-exhaustive whitelist to exclude parts of the URL that are unimportant (eg IDs) or may be sentsitive + // (eg email addresses). We prefer filtering out fields that aren't recognized because there should generally + // be enough left over for debugging. + // + // Note that new API routes don't need to be added here since this shouldn't be happening for newly added routes. + const whitelist = [ + 'api', 'v4', 'users', 'teams', 'scheme', 'name', 'members', 'channels', 'posts', 'reactions', 'commands', + 'files', 'preferences', 'hooks', 'incoming', 'outgoing', 'oauth', 'apps', 'emoji', 'brand', 'image', + 'data_retention', 'jobs', 'plugins', 'roles', 'system', 'timezones', 'schemes', 'redirect_location', 'patch', + 'mfa', 'password', 'reset', 'send', 'active', 'verify', 'terms_of_service', 'login', 'logout', 'ids', + 'usernames', 'me', 'username', 'email', 'default', 'sessions', 'revoke', 'all', 'device', 'status', + 'search', 'switch', 'authorized', 'authorize', 'deauthorize', 'tokens', 'disable', 'enable', 'exists', 'unread', + 'invite', 'batch', 'stats', 'import', 'schemeRoles', 'direct', 'group', 'convert', 'view', 'search_autocomplete', + 'thread', 'info', 'flagged', 'pinned', 'pin', 'unpin', 'opengraph', 'actions', 'thumbnail', 'preview', 'link', + 'delete', 'logs', 'ping', 'config', 'client', 'license', 'websocket', 'webrtc', 'token', 'regen_token', + 'autocomplete', 'execute', 'regen_secret', 'policy', 'type', 'cancel', 'reload', 'environment', 's3_test', 'file', + 'caches', 'invalidate', 'database', 'recycle', 'compliance', 'reports', 'cluster', 'ldap', 'test', 'sync', 'saml', + 'certificate', 'public', 'private', 'idp', 'elasticsearch', 'purge_indexes', 'analytics', 'old', 'webapp', 'fake', + ]; + + url = url.split('/').map((part) => { + if (part !== '' && whitelist.indexOf(part) === -1) { + return ''; + } + + return part; + }).join('/'); + + if (index !== -1) { + // Add this on afterwards since it wouldn't pass the whitelist + url += '?'; + } + + return url; +} diff --git a/app/utils/url/latin_map.ts b/app/utils/url/latin_map.ts new file mode 100644 index 0000000000..4caf1a13fa --- /dev/null +++ b/app/utils/url/latin_map.ts @@ -0,0 +1,995 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable max-lines */ +export const latinMap: Record = { + Á: 'A', // LATIN CAPITAL LETTER A WITH ACUTE + Ă: 'A', // LATIN CAPITAL LETTER A WITH BREVE + Ắ: 'A', // LATIN CAPITAL LETTER A WITH BREVE AND ACUTE + Ặ: 'A', // LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW + Ằ: 'A', // LATIN CAPITAL LETTER A WITH BREVE AND GRAVE + Ẳ: 'A', // LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE + Ẵ: 'A', // LATIN CAPITAL LETTER A WITH BREVE AND TILDE + Ǎ: 'A', // LATIN CAPITAL LETTER A WITH CARON + Â: 'A', // LATIN CAPITAL LETTER A WITH CIRCUMFLEX + Ấ: 'A', // LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE + Ậ: 'A', // LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW + Ầ: 'A', // LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE + Ẩ: 'A', // LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE + Ẫ: 'A', // LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE + Ä: 'A', // LATIN CAPITAL LETTER A WITH DIAERESIS + Ǟ: 'A', // LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON + Ȧ: 'A', // LATIN CAPITAL LETTER A WITH DOT ABOVE + Ǡ: 'A', // LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON + Ạ: 'A', // LATIN CAPITAL LETTER A WITH DOT BELOW + Ȁ: 'A', // LATIN CAPITAL LETTER A WITH DOUBLE GRAVE + À: 'A', // LATIN CAPITAL LETTER A WITH GRAVE + Ả: 'A', // LATIN CAPITAL LETTER A WITH HOOK ABOVE + Ȃ: 'A', // LATIN CAPITAL LETTER A WITH INVERTED BREVE + Ā: 'A', // LATIN CAPITAL LETTER A WITH MACRON + Ą: 'A', // LATIN CAPITAL LETTER A WITH OGONEK + Å: 'A', // LATIN CAPITAL LETTER A WITH RING ABOVE + Ǻ: 'A', // LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE + Ḁ: 'A', // LATIN CAPITAL LETTER A WITH RING BELOW + Ⱥ: 'A', // LATIN CAPITAL LETTER A WITH STROKE + Ã: 'A', // LATIN CAPITAL LETTER A WITH TILDE + Ꜳ: 'AA', // LATIN CAPITAL LETTER AA + Æ: 'AE', // LATIN CAPITAL LETTER AE + Ǽ: 'AE', // LATIN CAPITAL LETTER AE WITH ACUTE + Ǣ: 'AE', // LATIN CAPITAL LETTER AE WITH MACRON + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER AFRICAN D' (Ɖ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER ALPHA' (Ɑ) + Ꜵ: 'AO', // LATIN CAPITAL LETTER AO + Ꜷ: 'AU', // LATIN CAPITAL LETTER AU + Ꜹ: 'AV', // LATIN CAPITAL LETTER AV + Ꜻ: 'AV', // LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR + Ꜽ: 'AY', // LATIN CAPITAL LETTER AY + Ḃ: 'B', // LATIN CAPITAL LETTER B WITH DOT ABOVE + Ḅ: 'B', // LATIN CAPITAL LETTER B WITH DOT BELOW + Ɓ: 'B', // LATIN CAPITAL LETTER B WITH HOOK + Ḇ: 'B', // LATIN CAPITAL LETTER B WITH LINE BELOW + Ƀ: 'B', // LATIN CAPITAL LETTER B WITH STROKE + Ƃ: 'B', // LATIN CAPITAL LETTER B WITH TOPBAR + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER BROKEN L' (Ꝇ) + Ć: 'C', // LATIN CAPITAL LETTER C WITH ACUTE + Č: 'C', // LATIN CAPITAL LETTER C WITH CARON + Ç: 'C', // LATIN CAPITAL LETTER C WITH CEDILLA + Ḉ: 'C', // LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE + Ĉ: 'C', // LATIN CAPITAL LETTER C WITH CIRCUMFLEX + Ċ: 'C', // LATIN CAPITAL LETTER C WITH DOT ABOVE + Ƈ: 'C', // LATIN CAPITAL LETTER C WITH HOOK + Ȼ: 'C', // LATIN CAPITAL LETTER C WITH STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER CON' (Ꝯ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER CUATRILLO' (Ꜭ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER CUATRILLO WITH COMMA' (Ꜯ) + Ď: 'D', // LATIN CAPITAL LETTER D WITH CARON + Ḑ: 'D', // LATIN CAPITAL LETTER D WITH CEDILLA + Ḓ: 'D', // LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW + Ḋ: 'D', // LATIN CAPITAL LETTER D WITH DOT ABOVE + Ḍ: 'D', // LATIN CAPITAL LETTER D WITH DOT BELOW + Ɗ: 'D', // LATIN CAPITAL LETTER D WITH HOOK + Ḏ: 'D', // LATIN CAPITAL LETTER D WITH LINE BELOW + Dz: 'D', // LATIN CAPITAL LETTER D WITH SMALL LETTER Z + Dž: 'D', // LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON + Đ: 'D', // LATIN CAPITAL LETTER D WITH STROKE + Ƌ: 'D', // LATIN CAPITAL LETTER D WITH TOPBAR + DZ: 'DZ', // LATIN CAPITAL LETTER DZ + DŽ: 'DZ', // LATIN CAPITAL LETTER DZ WITH CARON + É: 'E', // LATIN CAPITAL LETTER E WITH ACUTE + Ĕ: 'E', // LATIN CAPITAL LETTER E WITH BREVE + Ě: 'E', // LATIN CAPITAL LETTER E WITH CARON + Ȩ: 'E', // LATIN CAPITAL LETTER E WITH CEDILLA + Ḝ: 'E', // LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE + Ê: 'E', // LATIN CAPITAL LETTER E WITH CIRCUMFLEX + Ế: 'E', // LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE + Ệ: 'E', // LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW + Ề: 'E', // LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE + Ể: 'E', // LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE + Ễ: 'E', // LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE + Ḙ: 'E', // LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW + Ë: 'E', // LATIN CAPITAL LETTER E WITH DIAERESIS + Ė: 'E', // LATIN CAPITAL LETTER E WITH DOT ABOVE + Ẹ: 'E', // LATIN CAPITAL LETTER E WITH DOT BELOW + Ȅ: 'E', // LATIN CAPITAL LETTER E WITH DOUBLE GRAVE + È: 'E', // LATIN CAPITAL LETTER E WITH GRAVE + Ẻ: 'E', // LATIN CAPITAL LETTER E WITH HOOK ABOVE + Ȇ: 'E', // LATIN CAPITAL LETTER E WITH INVERTED BREVE + Ē: 'E', // LATIN CAPITAL LETTER E WITH MACRON + Ḗ: 'E', // LATIN CAPITAL LETTER E WITH MACRON AND ACUTE + Ḕ: 'E', // LATIN CAPITAL LETTER E WITH MACRON AND GRAVE + Ę: 'E', // LATIN CAPITAL LETTER E WITH OGONEK + Ɇ: 'E', // LATIN CAPITAL LETTER E WITH STROKE + Ẽ: 'E', // LATIN CAPITAL LETTER E WITH TILDE + Ḛ: 'E', // LATIN CAPITAL LETTER E WITH TILDE BELOW + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER EGYPTOLOGICAL AIN' (Ꜥ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER EGYPTOLOGICAL ALEF' (Ꜣ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER ENG' (Ŋ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER ESH' (Ʃ) + Ꝫ: 'ET', // LATIN CAPITAL LETTER ET + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER ETH' (Ð) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER EZH' (Ʒ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER EZH REVERSED' (Ƹ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER EZH WITH CARON' (Ǯ) + Ḟ: 'F', // LATIN CAPITAL LETTER F WITH DOT ABOVE + Ƒ: 'F', // LATIN CAPITAL LETTER F WITH HOOK + Ǵ: 'G', // LATIN CAPITAL LETTER G WITH ACUTE + Ğ: 'G', // LATIN CAPITAL LETTER G WITH BREVE + Ǧ: 'G', // LATIN CAPITAL LETTER G WITH CARON + Ģ: 'G', // LATIN CAPITAL LETTER G WITH CEDILLA + Ĝ: 'G', // LATIN CAPITAL LETTER G WITH CIRCUMFLEX + Ġ: 'G', // LATIN CAPITAL LETTER G WITH DOT ABOVE + Ɠ: 'G', // LATIN CAPITAL LETTER G WITH HOOK + Ḡ: 'G', // LATIN CAPITAL LETTER G WITH MACRON + Ǥ: 'G', // LATIN CAPITAL LETTER G WITH STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER GAMMA' (Ɣ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER GLOTTAL STOP' (Ɂ) + Ḫ: 'H', // LATIN CAPITAL LETTER H WITH BREVE BELOW + Ȟ: 'H', // LATIN CAPITAL LETTER H WITH CARON + Ḩ: 'H', // LATIN CAPITAL LETTER H WITH CEDILLA + Ĥ: 'H', // LATIN CAPITAL LETTER H WITH CIRCUMFLEX + Ⱨ: 'H', // LATIN CAPITAL LETTER H WITH DESCENDER + Ḧ: 'H', // LATIN CAPITAL LETTER H WITH DIAERESIS + Ḣ: 'H', // LATIN CAPITAL LETTER H WITH DOT ABOVE + Ḥ: 'H', // LATIN CAPITAL LETTER H WITH DOT BELOW + Ħ: 'H', // LATIN CAPITAL LETTER H WITH STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER HALF H' (Ⱶ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER HENG' (Ꜧ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER HWAIR' (Ƕ) + Í: 'I', // LATIN CAPITAL LETTER I WITH ACUTE + Ĭ: 'I', // LATIN CAPITAL LETTER I WITH BREVE + Ǐ: 'I', // LATIN CAPITAL LETTER I WITH CARON + Î: 'I', // LATIN CAPITAL LETTER I WITH CIRCUMFLEX + Ï: 'I', // LATIN CAPITAL LETTER I WITH DIAERESIS + Ḯ: 'I', // LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE + İ: 'I', // LATIN CAPITAL LETTER I WITH DOT ABOVE + Ị: 'I', // LATIN CAPITAL LETTER I WITH DOT BELOW + Ȉ: 'I', // LATIN CAPITAL LETTER I WITH DOUBLE GRAVE + Ì: 'I', // LATIN CAPITAL LETTER I WITH GRAVE + Ỉ: 'I', // LATIN CAPITAL LETTER I WITH HOOK ABOVE + Ȋ: 'I', // LATIN CAPITAL LETTER I WITH INVERTED BREVE + Ī: 'I', // LATIN CAPITAL LETTER I WITH MACRON + Į: 'I', // LATIN CAPITAL LETTER I WITH OGONEK + Ɨ: 'I', // LATIN CAPITAL LETTER I WITH STROKE + Ĩ: 'I', // LATIN CAPITAL LETTER I WITH TILDE + Ḭ: 'I', // LATIN CAPITAL LETTER I WITH TILDE BELOW + Ꝺ: 'D', // LATIN CAPITAL LETTER INSULAR D + Ꝼ: 'F', // LATIN CAPITAL LETTER INSULAR F + Ᵹ: 'G', // LATIN CAPITAL LETTER INSULAR G + Ꞃ: 'R', // LATIN CAPITAL LETTER INSULAR R + Ꞅ: 'S', // LATIN CAPITAL LETTER INSULAR S + Ꞇ: 'T', // LATIN CAPITAL LETTER INSULAR T + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER IOTA' (Ɩ) + Ꝭ: 'IS', // LATIN CAPITAL LETTER IS + Ĵ: 'J', // LATIN CAPITAL LETTER J WITH CIRCUMFLEX + Ɉ: 'J', // LATIN CAPITAL LETTER J WITH STROKE + Ḱ: 'K', // LATIN CAPITAL LETTER K WITH ACUTE + Ǩ: 'K', // LATIN CAPITAL LETTER K WITH CARON + Ķ: 'K', // LATIN CAPITAL LETTER K WITH CEDILLA + Ⱪ: 'K', // LATIN CAPITAL LETTER K WITH DESCENDER + Ꝃ: 'K', // LATIN CAPITAL LETTER K WITH DIAGONAL STROKE + Ḳ: 'K', // LATIN CAPITAL LETTER K WITH DOT BELOW + Ƙ: 'K', // LATIN CAPITAL LETTER K WITH HOOK + Ḵ: 'K', // LATIN CAPITAL LETTER K WITH LINE BELOW + Ꝁ: 'K', // LATIN CAPITAL LETTER K WITH STROKE + Ꝅ: 'K', // LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE + Ĺ: 'L', // LATIN CAPITAL LETTER L WITH ACUTE + Ƚ: 'L', // LATIN CAPITAL LETTER L WITH BAR + Ľ: 'L', // LATIN CAPITAL LETTER L WITH CARON + Ļ: 'L', // LATIN CAPITAL LETTER L WITH CEDILLA + Ḽ: 'L', // LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW + Ḷ: 'L', // LATIN CAPITAL LETTER L WITH DOT BELOW + Ḹ: 'L', // LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON + Ⱡ: 'L', // LATIN CAPITAL LETTER L WITH DOUBLE BAR + Ꝉ: 'L', // LATIN CAPITAL LETTER L WITH HIGH STROKE + Ḻ: 'L', // LATIN CAPITAL LETTER L WITH LINE BELOW + Ŀ: 'L', // LATIN CAPITAL LETTER L WITH MIDDLE DOT + Ɫ: 'L', // LATIN CAPITAL LETTER L WITH MIDDLE TILDE + Lj: 'L', // LATIN CAPITAL LETTER L WITH SMALL LETTER J + Ł: 'L', // LATIN CAPITAL LETTER L WITH STROKE + LJ: 'LJ', // LATIN CAPITAL LETTER LJ + Ḿ: 'M', // LATIN CAPITAL LETTER M WITH ACUTE + Ṁ: 'M', // LATIN CAPITAL LETTER M WITH DOT ABOVE + Ṃ: 'M', // LATIN CAPITAL LETTER M WITH DOT BELOW + Ɱ: 'M', // LATIN CAPITAL LETTER M WITH HOOK + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER MIDDLE-WELSH LL' (Ỻ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER MIDDLE-WELSH V' (Ỽ) + Ń: 'N', // LATIN CAPITAL LETTER N WITH ACUTE + Ň: 'N', // LATIN CAPITAL LETTER N WITH CARON + Ņ: 'N', // LATIN CAPITAL LETTER N WITH CEDILLA + Ṋ: 'N', // LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW + Ṅ: 'N', // LATIN CAPITAL LETTER N WITH DOT ABOVE + Ṇ: 'N', // LATIN CAPITAL LETTER N WITH DOT BELOW + Ǹ: 'N', // LATIN CAPITAL LETTER N WITH GRAVE + Ɲ: 'N', // LATIN CAPITAL LETTER N WITH LEFT HOOK + Ṉ: 'N', // LATIN CAPITAL LETTER N WITH LINE BELOW + Ƞ: 'N', // LATIN CAPITAL LETTER N WITH LONG RIGHT LEG + Nj: 'N', // LATIN CAPITAL LETTER N WITH SMALL LETTER J + Ñ: 'N', // LATIN CAPITAL LETTER N WITH TILDE + NJ: 'NJ', // LATIN CAPITAL LETTER NJ + Ó: 'O', // LATIN CAPITAL LETTER O WITH ACUTE + Ŏ: 'O', // LATIN CAPITAL LETTER O WITH BREVE + Ǒ: 'O', // LATIN CAPITAL LETTER O WITH CARON + Ô: 'O', // LATIN CAPITAL LETTER O WITH CIRCUMFLEX + Ố: 'O', // LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE + Ộ: 'O', // LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW + Ồ: 'O', // LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE + Ổ: 'O', // LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE + Ỗ: 'O', // LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE + Ö: 'O', // LATIN CAPITAL LETTER O WITH DIAERESIS + Ȫ: 'O', // LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON + Ȯ: 'O', // LATIN CAPITAL LETTER O WITH DOT ABOVE + Ȱ: 'O', // LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON + Ọ: 'O', // LATIN CAPITAL LETTER O WITH DOT BELOW + Ő: 'O', // LATIN CAPITAL LETTER O WITH DOUBLE ACUTE + Ȍ: 'O', // LATIN CAPITAL LETTER O WITH DOUBLE GRAVE + Ò: 'O', // LATIN CAPITAL LETTER O WITH GRAVE + Ỏ: 'O', // LATIN CAPITAL LETTER O WITH HOOK ABOVE + Ơ: 'O', // LATIN CAPITAL LETTER O WITH HORN + Ớ: 'O', // LATIN CAPITAL LETTER O WITH HORN AND ACUTE + Ợ: 'O', // LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW + Ờ: 'O', // LATIN CAPITAL LETTER O WITH HORN AND GRAVE + Ở: 'O', // LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE + Ỡ: 'O', // LATIN CAPITAL LETTER O WITH HORN AND TILDE + Ȏ: 'O', // LATIN CAPITAL LETTER O WITH INVERTED BREVE + Ꝋ: 'O', // LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY + Ꝍ: 'O', // LATIN CAPITAL LETTER O WITH LOOP + Ō: 'O', // LATIN CAPITAL LETTER O WITH MACRON + Ṓ: 'O', // LATIN CAPITAL LETTER O WITH MACRON AND ACUTE + Ṑ: 'O', // LATIN CAPITAL LETTER O WITH MACRON AND GRAVE + Ɵ: 'O', // LATIN CAPITAL LETTER O WITH MIDDLE TILDE + Ǫ: 'O', // LATIN CAPITAL LETTER O WITH OGONEK + Ǭ: 'O', // LATIN CAPITAL LETTER O WITH OGONEK AND MACRON + Ø: 'O', // LATIN CAPITAL LETTER O WITH STROKE + Ǿ: 'O', // LATIN CAPITAL LETTER O WITH STROKE AND ACUTE + Õ: 'O', // LATIN CAPITAL LETTER O WITH TILDE + Ṍ: 'O', // LATIN CAPITAL LETTER O WITH TILDE AND ACUTE + Ṏ: 'O', // LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS + Ȭ: 'O', // LATIN CAPITAL LETTER O WITH TILDE AND MACRON + Ƣ: 'OI', // LATIN CAPITAL LETTER OI + Ꝏ: 'OO', // LATIN CAPITAL LETTER OO + Ɛ: 'E', // LATIN CAPITAL LETTER OPEN E + Ɔ: 'O', // LATIN CAPITAL LETTER OPEN O + Ȣ: 'OU', // LATIN CAPITAL LETTER OU + Ṕ: 'P', // LATIN CAPITAL LETTER P WITH ACUTE + Ṗ: 'P', // LATIN CAPITAL LETTER P WITH DOT ABOVE + Ꝓ: 'P', // LATIN CAPITAL LETTER P WITH FLOURISH + Ƥ: 'P', // LATIN CAPITAL LETTER P WITH HOOK + Ꝕ: 'P', // LATIN CAPITAL LETTER P WITH SQUIRREL TAIL + Ᵽ: 'P', // LATIN CAPITAL LETTER P WITH STROKE + Ꝑ: 'P', // LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER + Ꝙ: 'Q', // LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE + Ꝗ: 'Q', // LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER R ROTUNDA' (Ꝛ) + Ŕ: 'R', // LATIN CAPITAL LETTER R WITH ACUTE + Ř: 'R', // LATIN CAPITAL LETTER R WITH CARON + Ŗ: 'R', // LATIN CAPITAL LETTER R WITH CEDILLA + Ṙ: 'R', // LATIN CAPITAL LETTER R WITH DOT ABOVE + Ṛ: 'R', // LATIN CAPITAL LETTER R WITH DOT BELOW + Ṝ: 'R', // LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON + Ȑ: 'R', // LATIN CAPITAL LETTER R WITH DOUBLE GRAVE + Ȓ: 'R', // LATIN CAPITAL LETTER R WITH INVERTED BREVE + Ṟ: 'R', // LATIN CAPITAL LETTER R WITH LINE BELOW + Ɍ: 'R', // LATIN CAPITAL LETTER R WITH STROKE + Ɽ: 'R', // LATIN CAPITAL LETTER R WITH TAIL + Ꜿ: 'C', // LATIN CAPITAL LETTER REVERSED C WITH DOT + Ǝ: 'E', // LATIN CAPITAL LETTER REVERSED E + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER RUM ROTUNDA' (Ꝝ) + Ś: 'S', // LATIN CAPITAL LETTER S WITH ACUTE + Ṥ: 'S', // LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE + Š: 'S', // LATIN CAPITAL LETTER S WITH CARON + Ṧ: 'S', // LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE + Ş: 'S', // LATIN CAPITAL LETTER S WITH CEDILLA + Ŝ: 'S', // LATIN CAPITAL LETTER S WITH CIRCUMFLEX + Ș: 'S', // LATIN CAPITAL LETTER S WITH COMMA BELOW + Ṡ: 'S', // LATIN CAPITAL LETTER S WITH DOT ABOVE + Ṣ: 'S', // LATIN CAPITAL LETTER S WITH DOT BELOW + Ṩ: 'S', // LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER SALTILLO' (Ꞌ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER SCHWA' (Ə) + ẞ: 'SS', // LATIN CAPITAL LETTER SHARP S + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL' (Ɋ) + Ť: 'T', // LATIN CAPITAL LETTER T WITH CARON + Ţ: 'T', // LATIN CAPITAL LETTER T WITH CEDILLA + Ṱ: 'T', // LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW + Ț: 'T', // LATIN CAPITAL LETTER T WITH COMMA BELOW + Ⱦ: 'T', // LATIN CAPITAL LETTER T WITH DIAGONAL STROKE + Ṫ: 'T', // LATIN CAPITAL LETTER T WITH DOT ABOVE + Ṭ: 'T', // LATIN CAPITAL LETTER T WITH DOT BELOW + Ƭ: 'T', // LATIN CAPITAL LETTER T WITH HOOK + Ṯ: 'T', // LATIN CAPITAL LETTER T WITH LINE BELOW + Ʈ: 'T', // LATIN CAPITAL LETTER T WITH RETROFLEX HOOK + Ŧ: 'T', // LATIN CAPITAL LETTER T WITH STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER THORN' (Þ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER THORN WITH STROKE' (Ꝥ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER' (Ꝧ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER TONE FIVE' (Ƽ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER TONE SIX' (Ƅ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER TONE TWO' (Ƨ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER TRESILLO' (Ꜫ) + Ɐ: 'A', // LATIN CAPITAL LETTER TURNED A + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER TURNED INSULAR G' (Ꝿ) + Ꞁ: 'L', // LATIN CAPITAL LETTER TURNED L + Ɯ: 'M', // LATIN CAPITAL LETTER TURNED M + Ʌ: 'V', // LATIN CAPITAL LETTER TURNED V + Ꜩ: 'TZ', // LATIN CAPITAL LETTER TZ + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER U BAR' (Ʉ) + Ú: 'U', // LATIN CAPITAL LETTER U WITH ACUTE + Ŭ: 'U', // LATIN CAPITAL LETTER U WITH BREVE + Ǔ: 'U', // LATIN CAPITAL LETTER U WITH CARON + Û: 'U', // LATIN CAPITAL LETTER U WITH CIRCUMFLEX + Ṷ: 'U', // LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW + Ü: 'U', // LATIN CAPITAL LETTER U WITH DIAERESIS + Ǘ: 'U', // LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE + Ǚ: 'U', // LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON + Ǜ: 'U', // LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE + Ǖ: 'U', // LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON + Ṳ: 'U', // LATIN CAPITAL LETTER U WITH DIAERESIS BELOW + Ụ: 'U', // LATIN CAPITAL LETTER U WITH DOT BELOW + Ű: 'U', // LATIN CAPITAL LETTER U WITH DOUBLE ACUTE + Ȕ: 'U', // LATIN CAPITAL LETTER U WITH DOUBLE GRAVE + Ù: 'U', // LATIN CAPITAL LETTER U WITH GRAVE + Ủ: 'U', // LATIN CAPITAL LETTER U WITH HOOK ABOVE + Ư: 'U', // LATIN CAPITAL LETTER U WITH HORN + Ứ: 'U', // LATIN CAPITAL LETTER U WITH HORN AND ACUTE + Ự: 'U', // LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW + Ừ: 'U', // LATIN CAPITAL LETTER U WITH HORN AND GRAVE + Ử: 'U', // LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE + Ữ: 'U', // LATIN CAPITAL LETTER U WITH HORN AND TILDE + Ȗ: 'U', // LATIN CAPITAL LETTER U WITH INVERTED BREVE + Ū: 'U', // LATIN CAPITAL LETTER U WITH MACRON + Ṻ: 'U', // LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS + Ų: 'U', // LATIN CAPITAL LETTER U WITH OGONEK + Ů: 'U', // LATIN CAPITAL LETTER U WITH RING ABOVE + Ũ: 'U', // LATIN CAPITAL LETTER U WITH TILDE + Ṹ: 'U', // LATIN CAPITAL LETTER U WITH TILDE AND ACUTE + Ṵ: 'U', // LATIN CAPITAL LETTER U WITH TILDE BELOW + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER UPSILON' (Ʊ) + Ꝟ: 'V', // LATIN CAPITAL LETTER V WITH DIAGONAL STROKE + Ṿ: 'V', // LATIN CAPITAL LETTER V WITH DOT BELOW + Ʋ: 'V', // LATIN CAPITAL LETTER V WITH HOOK + Ṽ: 'V', // LATIN CAPITAL LETTER V WITH TILDE + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER VEND' (Ꝩ) + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER VISIGOTHIC Z' (Ꝣ) + Ꝡ: 'VY', // LATIN CAPITAL LETTER VY + Ẃ: 'W', // LATIN CAPITAL LETTER W WITH ACUTE + Ŵ: 'W', // LATIN CAPITAL LETTER W WITH CIRCUMFLEX + Ẅ: 'W', // LATIN CAPITAL LETTER W WITH DIAERESIS + Ẇ: 'W', // LATIN CAPITAL LETTER W WITH DOT ABOVE + Ẉ: 'W', // LATIN CAPITAL LETTER W WITH DOT BELOW + Ẁ: 'W', // LATIN CAPITAL LETTER W WITH GRAVE + Ⱳ: 'W', // LATIN CAPITAL LETTER W WITH HOOK + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER WYNN' (Ƿ) + Ẍ: 'X', // LATIN CAPITAL LETTER X WITH DIAERESIS + Ẋ: 'X', // LATIN CAPITAL LETTER X WITH DOT ABOVE + Ý: 'Y', // LATIN CAPITAL LETTER Y WITH ACUTE + Ŷ: 'Y', // LATIN CAPITAL LETTER Y WITH CIRCUMFLEX + Ÿ: 'Y', // LATIN CAPITAL LETTER Y WITH DIAERESIS + Ẏ: 'Y', // LATIN CAPITAL LETTER Y WITH DOT ABOVE + Ỵ: 'Y', // LATIN CAPITAL LETTER Y WITH DOT BELOW + Ỳ: 'Y', // LATIN CAPITAL LETTER Y WITH GRAVE + Ƴ: 'Y', // LATIN CAPITAL LETTER Y WITH HOOK + Ỷ: 'Y', // LATIN CAPITAL LETTER Y WITH HOOK ABOVE + Ỿ: 'Y', // LATIN CAPITAL LETTER Y WITH LOOP + Ȳ: 'Y', // LATIN CAPITAL LETTER Y WITH MACRON + Ɏ: 'Y', // LATIN CAPITAL LETTER Y WITH STROKE + Ỹ: 'Y', // LATIN CAPITAL LETTER Y WITH TILDE + // CANNOT FIND APPROXIMATION FOR 'LATIN CAPITAL LETTER YOGH' (Ȝ) + Ź: 'Z', // LATIN CAPITAL LETTER Z WITH ACUTE + Ž: 'Z', // LATIN CAPITAL LETTER Z WITH CARON + Ẑ: 'Z', // LATIN CAPITAL LETTER Z WITH CIRCUMFLEX + Ⱬ: 'Z', // LATIN CAPITAL LETTER Z WITH DESCENDER + Ż: 'Z', // LATIN CAPITAL LETTER Z WITH DOT ABOVE + Ẓ: 'Z', // LATIN CAPITAL LETTER Z WITH DOT BELOW + Ȥ: 'Z', // LATIN CAPITAL LETTER Z WITH HOOK + Ẕ: 'Z', // LATIN CAPITAL LETTER Z WITH LINE BELOW + Ƶ: 'Z', // LATIN CAPITAL LETTER Z WITH STROKE + IJ: 'IJ', // LATIN CAPITAL LIGATURE IJ + Œ: 'OE', // LATIN CAPITAL LIGATURE OE + // CANNOT FIND APPROXIMATION FOR 'LATIN CROSS' (✝) + // CANNOT FIND APPROXIMATION FOR 'LATIN EPIGRAPHIC LETTER ARCHAIC M' (ꟿ) + // CANNOT FIND APPROXIMATION FOR 'LATIN EPIGRAPHIC LETTER I LONGA' (ꟾ) + // CANNOT FIND APPROXIMATION FOR 'LATIN EPIGRAPHIC LETTER INVERTED M' (ꟽ) + // CANNOT FIND APPROXIMATION FOR 'LATIN EPIGRAPHIC LETTER REVERSED F' (ꟻ) + // CANNOT FIND APPROXIMATION FOR 'LATIN EPIGRAPHIC LETTER REVERSED P' (ꟼ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER AIN' (ᴥ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER ALVEOLAR CLICK' (ǂ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER BIDENTAL PERCUSSIVE' (ʭ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER BILABIAL CLICK' (ʘ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER BILABIAL PERCUSSIVE' (ʬ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER DENTAL CLICK' (ǀ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER GLOTTAL STOP' (ʔ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER GLOTTAL STOP WITH STROKE' (ʡ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER INVERTED GLOTTAL STOP' (ʖ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER INVERTED GLOTTAL STOP WITH STROKE' (ƾ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER LATERAL CLICK' (ǁ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER PHARYNGEAL VOICED FRICATIVE' (ʕ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER RETROFLEX CLICK' (ǃ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER REVERSED ESH LOOP' (ƪ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER REVERSED GLOTTAL STOP WITH STROKE' (ʢ) + ᴀ: 'A', // LATIN LETTER SMALL CAPITAL A + ᴁ: 'AE', // LATIN LETTER SMALL CAPITAL AE + ʙ: 'B', // LATIN LETTER SMALL CAPITAL B + ᴃ: 'B', // LATIN LETTER SMALL CAPITAL BARRED B + ᴄ: 'C', // LATIN LETTER SMALL CAPITAL C + ᴅ: 'D', // LATIN LETTER SMALL CAPITAL D + ᴇ: 'E', // LATIN LETTER SMALL CAPITAL E + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER SMALL CAPITAL ETH' (ᴆ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER SMALL CAPITAL EZH' (ᴣ) + ꜰ: 'F', // LATIN LETTER SMALL CAPITAL F + ɢ: 'G', // LATIN LETTER SMALL CAPITAL G + ʛ: 'G', // LATIN LETTER SMALL CAPITAL G WITH HOOK + ʜ: 'H', // LATIN LETTER SMALL CAPITAL H + ɪ: 'I', // LATIN LETTER SMALL CAPITAL I + ʁ: 'R', // LATIN LETTER SMALL CAPITAL INVERTED R + ᴊ: 'J', // LATIN LETTER SMALL CAPITAL J + ᴋ: 'K', // LATIN LETTER SMALL CAPITAL K + ʟ: 'L', // LATIN LETTER SMALL CAPITAL L + ᴌ: 'L', // LATIN LETTER SMALL CAPITAL L WITH STROKE + ᴍ: 'M', // LATIN LETTER SMALL CAPITAL M + ɴ: 'N', // LATIN LETTER SMALL CAPITAL N + ᴏ: 'O', // LATIN LETTER SMALL CAPITAL O + ɶ: 'OE', // LATIN LETTER SMALL CAPITAL OE + ᴐ: 'O', // LATIN LETTER SMALL CAPITAL OPEN O + ᴕ: 'OU', // LATIN LETTER SMALL CAPITAL OU + ᴘ: 'P', // LATIN LETTER SMALL CAPITAL P + ʀ: 'R', // LATIN LETTER SMALL CAPITAL R + ᴎ: 'N', // LATIN LETTER SMALL CAPITAL REVERSED N + ᴙ: 'R', // LATIN LETTER SMALL CAPITAL REVERSED R + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER SMALL CAPITAL RUM' (ꝶ) + ꜱ: 'S', // LATIN LETTER SMALL CAPITAL S + ᴛ: 'T', // LATIN LETTER SMALL CAPITAL T + ⱻ: 'E', // LATIN LETTER SMALL CAPITAL TURNED E + ᴚ: 'R', // LATIN LETTER SMALL CAPITAL TURNED R + ᴜ: 'U', // LATIN LETTER SMALL CAPITAL U + ᴠ: 'V', // LATIN LETTER SMALL CAPITAL V + ᴡ: 'W', // LATIN LETTER SMALL CAPITAL W + ʏ: 'Y', // LATIN LETTER SMALL CAPITAL Y + ᴢ: 'Z', // LATIN LETTER SMALL CAPITAL Z + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER STRETCHED C' (ʗ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER TWO WITH STROKE' (ƻ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER VOICED LARYNGEAL SPIRANT' (ᴤ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER WYNN' (ƿ) + // CANNOT FIND APPROXIMATION FOR 'LATIN LETTER YR' (Ʀ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL CAPITAL LETTER I WITH STROKE' (ᵻ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL CAPITAL LETTER U WITH STROKE' (ᵾ) + á: 'a', // LATIN SMALL LETTER A WITH ACUTE + ă: 'a', // LATIN SMALL LETTER A WITH BREVE + ắ: 'a', // LATIN SMALL LETTER A WITH BREVE AND ACUTE + ặ: 'a', // LATIN SMALL LETTER A WITH BREVE AND DOT BELOW + ằ: 'a', // LATIN SMALL LETTER A WITH BREVE AND GRAVE + ẳ: 'a', // LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE + ẵ: 'a', // LATIN SMALL LETTER A WITH BREVE AND TILDE + ǎ: 'a', // LATIN SMALL LETTER A WITH CARON + â: 'a', // LATIN SMALL LETTER A WITH CIRCUMFLEX + ấ: 'a', // LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE + ậ: 'a', // LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW + ầ: 'a', // LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE + ẩ: 'a', // LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE + ẫ: 'a', // LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE + ä: 'a', // LATIN SMALL LETTER A WITH DIAERESIS + ǟ: 'a', // LATIN SMALL LETTER A WITH DIAERESIS AND MACRON + ȧ: 'a', // LATIN SMALL LETTER A WITH DOT ABOVE + ǡ: 'a', // LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON + ạ: 'a', // LATIN SMALL LETTER A WITH DOT BELOW + ȁ: 'a', // LATIN SMALL LETTER A WITH DOUBLE GRAVE + à: 'a', // LATIN SMALL LETTER A WITH GRAVE + ả: 'a', // LATIN SMALL LETTER A WITH HOOK ABOVE + ȃ: 'a', // LATIN SMALL LETTER A WITH INVERTED BREVE + ā: 'a', // LATIN SMALL LETTER A WITH MACRON + ą: 'a', // LATIN SMALL LETTER A WITH OGONEK + ᶏ: 'a', // LATIN SMALL LETTER A WITH RETROFLEX HOOK + ẚ: 'a', // LATIN SMALL LETTER A WITH RIGHT HALF RING + å: 'a', // LATIN SMALL LETTER A WITH RING ABOVE + ǻ: 'a', // LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE + ḁ: 'a', // LATIN SMALL LETTER A WITH RING BELOW + ⱥ: 'a', // LATIN SMALL LETTER A WITH STROKE + ã: 'a', // LATIN SMALL LETTER A WITH TILDE + ꜳ: 'aa', // LATIN SMALL LETTER AA + æ: 'ae', // LATIN SMALL LETTER AE + ǽ: 'ae', // LATIN SMALL LETTER AE WITH ACUTE + ǣ: 'ae', // LATIN SMALL LETTER AE WITH MACRON + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER ALPHA' (ɑ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER ALPHA WITH RETROFLEX HOOK' (ᶐ) + ꜵ: 'ao', // LATIN SMALL LETTER AO + ꜷ: 'au', // LATIN SMALL LETTER AU + ꜹ: 'av', // LATIN SMALL LETTER AV + ꜻ: 'av', // LATIN SMALL LETTER AV WITH HORIZONTAL BAR + ꜽ: 'ay', // LATIN SMALL LETTER AY + ḃ: 'b', // LATIN SMALL LETTER B WITH DOT ABOVE + ḅ: 'b', // LATIN SMALL LETTER B WITH DOT BELOW + ɓ: 'b', // LATIN SMALL LETTER B WITH HOOK + ḇ: 'b', // LATIN SMALL LETTER B WITH LINE BELOW + ᵬ: 'b', // LATIN SMALL LETTER B WITH MIDDLE TILDE + ᶀ: 'b', // LATIN SMALL LETTER B WITH PALATAL HOOK + ƀ: 'b', // LATIN SMALL LETTER B WITH STROKE + ƃ: 'b', // LATIN SMALL LETTER B WITH TOPBAR + ɵ: 'o', // LATIN SMALL LETTER BARRED O + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER BOTTOM HALF O' (ᴗ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER BROKEN L' (ꝇ) + ć: 'c', // LATIN SMALL LETTER C WITH ACUTE + č: 'c', // LATIN SMALL LETTER C WITH CARON + ç: 'c', // LATIN SMALL LETTER C WITH CEDILLA + ḉ: 'c', // LATIN SMALL LETTER C WITH CEDILLA AND ACUTE + ĉ: 'c', // LATIN SMALL LETTER C WITH CIRCUMFLEX + ɕ: 'c', // LATIN SMALL LETTER C WITH CURL + ċ: 'c', // LATIN SMALL LETTER C WITH DOT ABOVE + ƈ: 'c', // LATIN SMALL LETTER C WITH HOOK + ȼ: 'c', // LATIN SMALL LETTER C WITH STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER CLOSED OMEGA' (ɷ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER CLOSED OPEN E' (ʚ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER CLOSED REVERSED OPEN E' (ɞ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER CON' (ꝯ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER CUATRILLO' (ꜭ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER CUATRILLO WITH COMMA' (ꜯ) + ď: 'd', // LATIN SMALL LETTER D WITH CARON + ḑ: 'd', // LATIN SMALL LETTER D WITH CEDILLA + ḓ: 'd', // LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW + ȡ: 'd', // LATIN SMALL LETTER D WITH CURL + ḋ: 'd', // LATIN SMALL LETTER D WITH DOT ABOVE + ḍ: 'd', // LATIN SMALL LETTER D WITH DOT BELOW + ɗ: 'd', // LATIN SMALL LETTER D WITH HOOK + ᶑ: 'd', // LATIN SMALL LETTER D WITH HOOK AND TAIL + ḏ: 'd', // LATIN SMALL LETTER D WITH LINE BELOW + ᵭ: 'd', // LATIN SMALL LETTER D WITH MIDDLE TILDE + ᶁ: 'd', // LATIN SMALL LETTER D WITH PALATAL HOOK + đ: 'd', // LATIN SMALL LETTER D WITH STROKE + ɖ: 'd', // LATIN SMALL LETTER D WITH TAIL + ƌ: 'd', // LATIN SMALL LETTER D WITH TOPBAR + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER DB DIGRAPH' (ȸ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER DELTA' (ẟ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER DEZH DIGRAPH' (ʤ) + ı: 'i', // LATIN SMALL LETTER DOTLESS I + ȷ: 'j', // LATIN SMALL LETTER DOTLESS J + ɟ: 'j', // LATIN SMALL LETTER DOTLESS J WITH STROKE + ʄ: 'j', // LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER DUM' (ꝱ) + dz: 'dz', // LATIN SMALL LETTER DZ + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER DZ DIGRAPH' (ʣ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER DZ DIGRAPH WITH CURL' (ʥ) + dž: 'dz', // LATIN SMALL LETTER DZ WITH CARON + é: 'e', // LATIN SMALL LETTER E WITH ACUTE + ĕ: 'e', // LATIN SMALL LETTER E WITH BREVE + ě: 'e', // LATIN SMALL LETTER E WITH CARON + ȩ: 'e', // LATIN SMALL LETTER E WITH CEDILLA + ḝ: 'e', // LATIN SMALL LETTER E WITH CEDILLA AND BREVE + ê: 'e', // LATIN SMALL LETTER E WITH CIRCUMFLEX + ế: 'e', // LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE + ệ: 'e', // LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW + ề: 'e', // LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE + ể: 'e', // LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE + ễ: 'e', // LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE + ḙ: 'e', // LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW + ë: 'e', // LATIN SMALL LETTER E WITH DIAERESIS + ė: 'e', // LATIN SMALL LETTER E WITH DOT ABOVE + ẹ: 'e', // LATIN SMALL LETTER E WITH DOT BELOW + ȅ: 'e', // LATIN SMALL LETTER E WITH DOUBLE GRAVE + è: 'e', // LATIN SMALL LETTER E WITH GRAVE + ẻ: 'e', // LATIN SMALL LETTER E WITH HOOK ABOVE + ȇ: 'e', // LATIN SMALL LETTER E WITH INVERTED BREVE + ē: 'e', // LATIN SMALL LETTER E WITH MACRON + ḗ: 'e', // LATIN SMALL LETTER E WITH MACRON AND ACUTE + ḕ: 'e', // LATIN SMALL LETTER E WITH MACRON AND GRAVE + ⱸ: 'e', // LATIN SMALL LETTER E WITH NOTCH + ę: 'e', // LATIN SMALL LETTER E WITH OGONEK + ᶒ: 'e', // LATIN SMALL LETTER E WITH RETROFLEX HOOK + ɇ: 'e', // LATIN SMALL LETTER E WITH STROKE + ẽ: 'e', // LATIN SMALL LETTER E WITH TILDE + ḛ: 'e', // LATIN SMALL LETTER E WITH TILDE BELOW + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER EGYPTOLOGICAL AIN' (ꜥ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER EGYPTOLOGICAL ALEF' (ꜣ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER ENG' (ŋ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER ESH' (ʃ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER ESH WITH CURL' (ʆ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER ESH WITH PALATAL HOOK' (ᶋ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER ESH WITH RETROFLEX HOOK' (ᶘ) + ꝫ: 'et', // LATIN SMALL LETTER ET + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER ETH' (ð) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER EZH' (ʒ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER EZH REVERSED' (ƹ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER EZH WITH CARON' (ǯ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER EZH WITH CURL' (ʓ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER EZH WITH RETROFLEX HOOK' (ᶚ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER EZH WITH TAIL' (ƺ) + ḟ: 'f', // LATIN SMALL LETTER F WITH DOT ABOVE + ƒ: 'f', // LATIN SMALL LETTER F WITH HOOK + ᵮ: 'f', // LATIN SMALL LETTER F WITH MIDDLE TILDE + ᶂ: 'f', // LATIN SMALL LETTER F WITH PALATAL HOOK + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER FENG DIGRAPH' (ʩ) + ǵ: 'g', // LATIN SMALL LETTER G WITH ACUTE + ğ: 'g', // LATIN SMALL LETTER G WITH BREVE + ǧ: 'g', // LATIN SMALL LETTER G WITH CARON + ģ: 'g', // LATIN SMALL LETTER G WITH CEDILLA + ĝ: 'g', // LATIN SMALL LETTER G WITH CIRCUMFLEX + ġ: 'g', // LATIN SMALL LETTER G WITH DOT ABOVE + ɠ: 'g', // LATIN SMALL LETTER G WITH HOOK + ḡ: 'g', // LATIN SMALL LETTER G WITH MACRON + ᶃ: 'g', // LATIN SMALL LETTER G WITH PALATAL HOOK + ǥ: 'g', // LATIN SMALL LETTER G WITH STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER GAMMA' (ɣ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER GLOTTAL STOP' (ɂ) + ḫ: 'h', // LATIN SMALL LETTER H WITH BREVE BELOW + ȟ: 'h', // LATIN SMALL LETTER H WITH CARON + ḩ: 'h', // LATIN SMALL LETTER H WITH CEDILLA + ĥ: 'h', // LATIN SMALL LETTER H WITH CIRCUMFLEX + ⱨ: 'h', // LATIN SMALL LETTER H WITH DESCENDER + ḧ: 'h', // LATIN SMALL LETTER H WITH DIAERESIS + ḣ: 'h', // LATIN SMALL LETTER H WITH DOT ABOVE + ḥ: 'h', // LATIN SMALL LETTER H WITH DOT BELOW + ɦ: 'h', // LATIN SMALL LETTER H WITH HOOK + ẖ: 'h', // LATIN SMALL LETTER H WITH LINE BELOW + ħ: 'h', // LATIN SMALL LETTER H WITH STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER HALF H' (ⱶ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER HENG' (ꜧ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER HENG WITH HOOK' (ɧ) + ƕ: 'hv', // LATIN SMALL LETTER HV + í: 'i', // LATIN SMALL LETTER I WITH ACUTE + ĭ: 'i', // LATIN SMALL LETTER I WITH BREVE + ǐ: 'i', // LATIN SMALL LETTER I WITH CARON + î: 'i', // LATIN SMALL LETTER I WITH CIRCUMFLEX + ï: 'i', // LATIN SMALL LETTER I WITH DIAERESIS + ḯ: 'i', // LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE + ị: 'i', // LATIN SMALL LETTER I WITH DOT BELOW + ȉ: 'i', // LATIN SMALL LETTER I WITH DOUBLE GRAVE + ì: 'i', // LATIN SMALL LETTER I WITH GRAVE + ỉ: 'i', // LATIN SMALL LETTER I WITH HOOK ABOVE + ȋ: 'i', // LATIN SMALL LETTER I WITH INVERTED BREVE + ī: 'i', // LATIN SMALL LETTER I WITH MACRON + į: 'i', // LATIN SMALL LETTER I WITH OGONEK + ᶖ: 'i', // LATIN SMALL LETTER I WITH RETROFLEX HOOK + ɨ: 'i', // LATIN SMALL LETTER I WITH STROKE + ĩ: 'i', // LATIN SMALL LETTER I WITH TILDE + ḭ: 'i', // LATIN SMALL LETTER I WITH TILDE BELOW + ꝺ: 'd', // LATIN SMALL LETTER INSULAR D + ꝼ: 'f', // LATIN SMALL LETTER INSULAR F + ᵹ: 'g', // LATIN SMALL LETTER INSULAR G + ꞃ: 'r', // LATIN SMALL LETTER INSULAR R + ꞅ: 's', // LATIN SMALL LETTER INSULAR S + ꞇ: 't', // LATIN SMALL LETTER INSULAR T + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER IOTA' (ɩ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER IOTA WITH STROKE' (ᵼ) + ꝭ: 'is', // LATIN SMALL LETTER IS + ǰ: 'j', // LATIN SMALL LETTER J WITH CARON + ĵ: 'j', // LATIN SMALL LETTER J WITH CIRCUMFLEX + ʝ: 'j', // LATIN SMALL LETTER J WITH CROSSED-TAIL + ɉ: 'j', // LATIN SMALL LETTER J WITH STROKE + ḱ: 'k', // LATIN SMALL LETTER K WITH ACUTE + ǩ: 'k', // LATIN SMALL LETTER K WITH CARON + ķ: 'k', // LATIN SMALL LETTER K WITH CEDILLA + ⱪ: 'k', // LATIN SMALL LETTER K WITH DESCENDER + ꝃ: 'k', // LATIN SMALL LETTER K WITH DIAGONAL STROKE + ḳ: 'k', // LATIN SMALL LETTER K WITH DOT BELOW + ƙ: 'k', // LATIN SMALL LETTER K WITH HOOK + ḵ: 'k', // LATIN SMALL LETTER K WITH LINE BELOW + ᶄ: 'k', // LATIN SMALL LETTER K WITH PALATAL HOOK + ꝁ: 'k', // LATIN SMALL LETTER K WITH STROKE + ꝅ: 'k', // LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER KRA' (ĸ) + ĺ: 'l', // LATIN SMALL LETTER L WITH ACUTE + ƚ: 'l', // LATIN SMALL LETTER L WITH BAR + ɬ: 'l', // LATIN SMALL LETTER L WITH BELT + ľ: 'l', // LATIN SMALL LETTER L WITH CARON + ļ: 'l', // LATIN SMALL LETTER L WITH CEDILLA + ḽ: 'l', // LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW + ȴ: 'l', // LATIN SMALL LETTER L WITH CURL + ḷ: 'l', // LATIN SMALL LETTER L WITH DOT BELOW + ḹ: 'l', // LATIN SMALL LETTER L WITH DOT BELOW AND MACRON + ⱡ: 'l', // LATIN SMALL LETTER L WITH DOUBLE BAR + ꝉ: 'l', // LATIN SMALL LETTER L WITH HIGH STROKE + ḻ: 'l', // LATIN SMALL LETTER L WITH LINE BELOW + ŀ: 'l', // LATIN SMALL LETTER L WITH MIDDLE DOT + ɫ: 'l', // LATIN SMALL LETTER L WITH MIDDLE TILDE + ᶅ: 'l', // LATIN SMALL LETTER L WITH PALATAL HOOK + ɭ: 'l', // LATIN SMALL LETTER L WITH RETROFLEX HOOK + ł: 'l', // LATIN SMALL LETTER L WITH STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER LAMBDA WITH STROKE' (ƛ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER LEZH' (ɮ) + lj: 'lj', // LATIN SMALL LETTER LJ + ſ: 's', // LATIN SMALL LETTER LONG S + ẜ: 's', // LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE + ẛ: 's', // LATIN SMALL LETTER LONG S WITH DOT ABOVE + ẝ: 's', // LATIN SMALL LETTER LONG S WITH HIGH STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER LS DIGRAPH' (ʪ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER LUM' (ꝲ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER LZ DIGRAPH' (ʫ) + ḿ: 'm', // LATIN SMALL LETTER M WITH ACUTE + ṁ: 'm', // LATIN SMALL LETTER M WITH DOT ABOVE + ṃ: 'm', // LATIN SMALL LETTER M WITH DOT BELOW + ɱ: 'm', // LATIN SMALL LETTER M WITH HOOK + ᵯ: 'm', // LATIN SMALL LETTER M WITH MIDDLE TILDE + ᶆ: 'm', // LATIN SMALL LETTER M WITH PALATAL HOOK + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER MIDDLE-WELSH LL' (ỻ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER MIDDLE-WELSH V' (ỽ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER MUM' (ꝳ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER N PRECEDED BY APOSTROPHE' (ʼn) + ń: 'n', // LATIN SMALL LETTER N WITH ACUTE + ň: 'n', // LATIN SMALL LETTER N WITH CARON + ņ: 'n', // LATIN SMALL LETTER N WITH CEDILLA + ṋ: 'n', // LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW + ȵ: 'n', // LATIN SMALL LETTER N WITH CURL + ṅ: 'n', // LATIN SMALL LETTER N WITH DOT ABOVE + ṇ: 'n', // LATIN SMALL LETTER N WITH DOT BELOW + ǹ: 'n', // LATIN SMALL LETTER N WITH GRAVE + ɲ: 'n', // LATIN SMALL LETTER N WITH LEFT HOOK + ṉ: 'n', // LATIN SMALL LETTER N WITH LINE BELOW + ƞ: 'n', // LATIN SMALL LETTER N WITH LONG RIGHT LEG + ᵰ: 'n', // LATIN SMALL LETTER N WITH MIDDLE TILDE + ᶇ: 'n', // LATIN SMALL LETTER N WITH PALATAL HOOK + ɳ: 'n', // LATIN SMALL LETTER N WITH RETROFLEX HOOK + ñ: 'n', // LATIN SMALL LETTER N WITH TILDE + nj: 'nj', // LATIN SMALL LETTER NJ + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER NUM' (ꝴ) + ó: 'o', // LATIN SMALL LETTER O WITH ACUTE + ŏ: 'o', // LATIN SMALL LETTER O WITH BREVE + ǒ: 'o', // LATIN SMALL LETTER O WITH CARON + ô: 'o', // LATIN SMALL LETTER O WITH CIRCUMFLEX + ố: 'o', // LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE + ộ: 'o', // LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW + ồ: 'o', // LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE + ổ: 'o', // LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE + ỗ: 'o', // LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE + ö: 'o', // LATIN SMALL LETTER O WITH DIAERESIS + ȫ: 'o', // LATIN SMALL LETTER O WITH DIAERESIS AND MACRON + ȯ: 'o', // LATIN SMALL LETTER O WITH DOT ABOVE + ȱ: 'o', // LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON + ọ: 'o', // LATIN SMALL LETTER O WITH DOT BELOW + ő: 'o', // LATIN SMALL LETTER O WITH DOUBLE ACUTE + ȍ: 'o', // LATIN SMALL LETTER O WITH DOUBLE GRAVE + ò: 'o', // LATIN SMALL LETTER O WITH GRAVE + ỏ: 'o', // LATIN SMALL LETTER O WITH HOOK ABOVE + ơ: 'o', // LATIN SMALL LETTER O WITH HORN + ớ: 'o', // LATIN SMALL LETTER O WITH HORN AND ACUTE + ợ: 'o', // LATIN SMALL LETTER O WITH HORN AND DOT BELOW + ờ: 'o', // LATIN SMALL LETTER O WITH HORN AND GRAVE + ở: 'o', // LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE + ỡ: 'o', // LATIN SMALL LETTER O WITH HORN AND TILDE + ȏ: 'o', // LATIN SMALL LETTER O WITH INVERTED BREVE + ꝋ: 'o', // LATIN SMALL LETTER O WITH LONG STROKE OVERLAY + ꝍ: 'o', // LATIN SMALL LETTER O WITH LOOP + ⱺ: 'o', // LATIN SMALL LETTER O WITH LOW RING INSIDE + ō: 'o', // LATIN SMALL LETTER O WITH MACRON + ṓ: 'o', // LATIN SMALL LETTER O WITH MACRON AND ACUTE + ṑ: 'o', // LATIN SMALL LETTER O WITH MACRON AND GRAVE + ǫ: 'o', // LATIN SMALL LETTER O WITH OGONEK + ǭ: 'o', // LATIN SMALL LETTER O WITH OGONEK AND MACRON + ø: 'o', // LATIN SMALL LETTER O WITH STROKE + ǿ: 'o', // LATIN SMALL LETTER O WITH STROKE AND ACUTE + õ: 'o', // LATIN SMALL LETTER O WITH TILDE + ṍ: 'o', // LATIN SMALL LETTER O WITH TILDE AND ACUTE + ṏ: 'o', // LATIN SMALL LETTER O WITH TILDE AND DIAERESIS + ȭ: 'o', // LATIN SMALL LETTER O WITH TILDE AND MACRON + ƣ: 'oi', // LATIN SMALL LETTER OI + ꝏ: 'oo', // LATIN SMALL LETTER OO + ɛ: 'e', // LATIN SMALL LETTER OPEN E + ᶓ: 'e', // LATIN SMALL LETTER OPEN E WITH RETROFLEX HOOK + ɔ: 'o', // LATIN SMALL LETTER OPEN O + ᶗ: 'o', // LATIN SMALL LETTER OPEN O WITH RETROFLEX HOOK + ȣ: 'ou', // LATIN SMALL LETTER OU + ṕ: 'p', // LATIN SMALL LETTER P WITH ACUTE + ṗ: 'p', // LATIN SMALL LETTER P WITH DOT ABOVE + ꝓ: 'p', // LATIN SMALL LETTER P WITH FLOURISH + ƥ: 'p', // LATIN SMALL LETTER P WITH HOOK + ᵱ: 'p', // LATIN SMALL LETTER P WITH MIDDLE TILDE + ᶈ: 'p', // LATIN SMALL LETTER P WITH PALATAL HOOK + ꝕ: 'p', // LATIN SMALL LETTER P WITH SQUIRREL TAIL + ᵽ: 'p', // LATIN SMALL LETTER P WITH STROKE + ꝑ: 'p', // LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER PHI' (ɸ) + ꝙ: 'q', // LATIN SMALL LETTER Q WITH DIAGONAL STROKE + ʠ: 'q', // LATIN SMALL LETTER Q WITH HOOK + ɋ: 'q', // LATIN SMALL LETTER Q WITH HOOK TAIL + ꝗ: 'q', // LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER QP DIGRAPH' (ȹ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER R ROTUNDA' (ꝛ) + ŕ: 'r', // LATIN SMALL LETTER R WITH ACUTE + ř: 'r', // LATIN SMALL LETTER R WITH CARON + ŗ: 'r', // LATIN SMALL LETTER R WITH CEDILLA + ṙ: 'r', // LATIN SMALL LETTER R WITH DOT ABOVE + ṛ: 'r', // LATIN SMALL LETTER R WITH DOT BELOW + ṝ: 'r', // LATIN SMALL LETTER R WITH DOT BELOW AND MACRON + ȑ: 'r', // LATIN SMALL LETTER R WITH DOUBLE GRAVE + ɾ: 'r', // LATIN SMALL LETTER R WITH FISHHOOK + ᵳ: 'r', // LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE + ȓ: 'r', // LATIN SMALL LETTER R WITH INVERTED BREVE + ṟ: 'r', // LATIN SMALL LETTER R WITH LINE BELOW + ɼ: 'r', // LATIN SMALL LETTER R WITH LONG LEG + ᵲ: 'r', // LATIN SMALL LETTER R WITH MIDDLE TILDE + ᶉ: 'r', // LATIN SMALL LETTER R WITH PALATAL HOOK + ɍ: 'r', // LATIN SMALL LETTER R WITH STROKE + ɽ: 'r', // LATIN SMALL LETTER R WITH TAIL + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER RAMS HORN' (ɤ) + ↄ: 'c', // LATIN SMALL LETTER REVERSED C + ꜿ: 'c', // LATIN SMALL LETTER REVERSED C WITH DOT + ɘ: 'e', // LATIN SMALL LETTER REVERSED E + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER REVERSED OPEN E' (ɜ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER REVERSED OPEN E WITH HOOK' (ɝ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER REVERSED OPEN E WITH RETROFLEX HOOK' (ᶔ) + ɿ: 'r', // LATIN SMALL LETTER REVERSED R WITH FISHHOOK + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER RUM' (ꝵ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER RUM ROTUNDA' (ꝝ) + ś: 's', // LATIN SMALL LETTER S WITH ACUTE + ṥ: 's', // LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE + š: 's', // LATIN SMALL LETTER S WITH CARON + ṧ: 's', // LATIN SMALL LETTER S WITH CARON AND DOT ABOVE + ş: 's', // LATIN SMALL LETTER S WITH CEDILLA + ŝ: 's', // LATIN SMALL LETTER S WITH CIRCUMFLEX + ș: 's', // LATIN SMALL LETTER S WITH COMMA BELOW + ṡ: 's', // LATIN SMALL LETTER S WITH DOT ABOVE + ṣ: 's', // LATIN SMALL LETTER S WITH DOT BELOW + ṩ: 's', // LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE + ʂ: 's', // LATIN SMALL LETTER S WITH HOOK + ᵴ: 's', // LATIN SMALL LETTER S WITH MIDDLE TILDE + ᶊ: 's', // LATIN SMALL LETTER S WITH PALATAL HOOK + ȿ: 's', // LATIN SMALL LETTER S WITH SWASH TAIL + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER SALTILLO' (ꞌ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER SCHWA' (ə) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER SCHWA WITH HOOK' (ɚ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER SCHWA WITH RETROFLEX HOOK' (ᶕ) + ɡ: 'g', // LATIN SMALL LETTER SCRIPT G + ß: 'ss', // LATIN SMALL LETTER SHARP S + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER SIDEWAYS DIAERESIZED U' (ᴞ) + ᴑ: 'o', // LATIN SMALL LETTER SIDEWAYS O + ᴓ: 'o', // LATIN SMALL LETTER SIDEWAYS O WITH STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER SIDEWAYS OPEN O' (ᴒ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER SIDEWAYS TURNED M' (ᴟ) + ᴝ: 'u', // LATIN SMALL LETTER SIDEWAYS U + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER SQUAT REVERSED ESH' (ʅ) + ť: 't', // LATIN SMALL LETTER T WITH CARON + ţ: 't', // LATIN SMALL LETTER T WITH CEDILLA + ṱ: 't', // LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW + ț: 't', // LATIN SMALL LETTER T WITH COMMA BELOW + ȶ: 't', // LATIN SMALL LETTER T WITH CURL + ẗ: 't', // LATIN SMALL LETTER T WITH DIAERESIS + ⱦ: 't', // LATIN SMALL LETTER T WITH DIAGONAL STROKE + ṫ: 't', // LATIN SMALL LETTER T WITH DOT ABOVE + ṭ: 't', // LATIN SMALL LETTER T WITH DOT BELOW + ƭ: 't', // LATIN SMALL LETTER T WITH HOOK + ṯ: 't', // LATIN SMALL LETTER T WITH LINE BELOW + ᵵ: 't', // LATIN SMALL LETTER T WITH MIDDLE TILDE + ƫ: 't', // LATIN SMALL LETTER T WITH PALATAL HOOK + ʈ: 't', // LATIN SMALL LETTER T WITH RETROFLEX HOOK + ŧ: 't', // LATIN SMALL LETTER T WITH STROKE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TAILLESS PHI' (ⱷ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TC DIGRAPH WITH CURL' (ʨ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TESH DIGRAPH' (ʧ) + ᵺ: 'th', // LATIN SMALL LETTER TH WITH STRIKETHROUGH + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER THORN' (þ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER THORN WITH STROKE' (ꝥ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER' (ꝧ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TONE FIVE' (ƽ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TONE SIX' (ƅ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TONE TWO' (ƨ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TOP HALF O' (ᴖ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TRESILLO' (ꜫ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TS DIGRAPH' (ʦ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TUM' (ꝷ) + ɐ: 'a', // LATIN SMALL LETTER TURNED A + ᴂ: 'ae', // LATIN SMALL LETTER TURNED AE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TURNED ALPHA' (ɒ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TURNED DELTA' (ƍ) + ǝ: 'e', // LATIN SMALL LETTER TURNED E + ᵷ: 'g', // LATIN SMALL LETTER TURNED G + ɥ: 'h', // LATIN SMALL LETTER TURNED H + ʮ: 'h', // LATIN SMALL LETTER TURNED H WITH FISHHOOK + ʯ: 'h', // LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL + ᴉ: 'i', // LATIN SMALL LETTER TURNED I + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TURNED INSULAR G' (ꝿ) + ʞ: 'k', // LATIN SMALL LETTER TURNED K + ꞁ: 'l', // LATIN SMALL LETTER TURNED L + ɯ: 'm', // LATIN SMALL LETTER TURNED M + ɰ: 'm', // LATIN SMALL LETTER TURNED M WITH LONG LEG + ᴔ: 'oe', // LATIN SMALL LETTER TURNED OE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER TURNED OPEN E' (ᴈ) + ɹ: 'r', // LATIN SMALL LETTER TURNED R + ɻ: 'r', // LATIN SMALL LETTER TURNED R WITH HOOK + ɺ: 'r', // LATIN SMALL LETTER TURNED R WITH LONG LEG + ⱹ: 'r', // LATIN SMALL LETTER TURNED R WITH TAIL + ʇ: 't', // LATIN SMALL LETTER TURNED T + ʌ: 'v', // LATIN SMALL LETTER TURNED V + ʍ: 'w', // LATIN SMALL LETTER TURNED W + ʎ: 'y', // LATIN SMALL LETTER TURNED Y + ꜩ: 'tz', // LATIN SMALL LETTER TZ + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER U BAR' (ʉ) + ú: 'u', // LATIN SMALL LETTER U WITH ACUTE + ŭ: 'u', // LATIN SMALL LETTER U WITH BREVE + ǔ: 'u', // LATIN SMALL LETTER U WITH CARON + û: 'u', // LATIN SMALL LETTER U WITH CIRCUMFLEX + ṷ: 'u', // LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW + ü: 'u', // LATIN SMALL LETTER U WITH DIAERESIS + ǘ: 'u', // LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE + ǚ: 'u', // LATIN SMALL LETTER U WITH DIAERESIS AND CARON + ǜ: 'u', // LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE + ǖ: 'u', // LATIN SMALL LETTER U WITH DIAERESIS AND MACRON + ṳ: 'u', // LATIN SMALL LETTER U WITH DIAERESIS BELOW + ụ: 'u', // LATIN SMALL LETTER U WITH DOT BELOW + ű: 'u', // LATIN SMALL LETTER U WITH DOUBLE ACUTE + ȕ: 'u', // LATIN SMALL LETTER U WITH DOUBLE GRAVE + ù: 'u', // LATIN SMALL LETTER U WITH GRAVE + ủ: 'u', // LATIN SMALL LETTER U WITH HOOK ABOVE + ư: 'u', // LATIN SMALL LETTER U WITH HORN + ứ: 'u', // LATIN SMALL LETTER U WITH HORN AND ACUTE + ự: 'u', // LATIN SMALL LETTER U WITH HORN AND DOT BELOW + ừ: 'u', // LATIN SMALL LETTER U WITH HORN AND GRAVE + ử: 'u', // LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE + ữ: 'u', // LATIN SMALL LETTER U WITH HORN AND TILDE + ȗ: 'u', // LATIN SMALL LETTER U WITH INVERTED BREVE + ū: 'u', // LATIN SMALL LETTER U WITH MACRON + ṻ: 'u', // LATIN SMALL LETTER U WITH MACRON AND DIAERESIS + ų: 'u', // LATIN SMALL LETTER U WITH OGONEK + ᶙ: 'u', // LATIN SMALL LETTER U WITH RETROFLEX HOOK + ů: 'u', // LATIN SMALL LETTER U WITH RING ABOVE + ũ: 'u', // LATIN SMALL LETTER U WITH TILDE + ṹ: 'u', // LATIN SMALL LETTER U WITH TILDE AND ACUTE + ṵ: 'u', // LATIN SMALL LETTER U WITH TILDE BELOW + ᵫ: 'ue', // LATIN SMALL LETTER UE + ꝸ: 'um', // LATIN SMALL LETTER UM + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER UPSILON' (ʊ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER UPSILON WITH STROKE' (ᵿ) + ⱴ: 'v', // LATIN SMALL LETTER V WITH CURL + ꝟ: 'v', // LATIN SMALL LETTER V WITH DIAGONAL STROKE + ṿ: 'v', // LATIN SMALL LETTER V WITH DOT BELOW + ʋ: 'v', // LATIN SMALL LETTER V WITH HOOK + ᶌ: 'v', // LATIN SMALL LETTER V WITH PALATAL HOOK + ⱱ: 'v', // LATIN SMALL LETTER V WITH RIGHT HOOK + ṽ: 'v', // LATIN SMALL LETTER V WITH TILDE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER VEND' (ꝩ) + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER VISIGOTHIC Z' (ꝣ) + ꝡ: 'vy', // LATIN SMALL LETTER VY + ẃ: 'w', // LATIN SMALL LETTER W WITH ACUTE + ŵ: 'w', // LATIN SMALL LETTER W WITH CIRCUMFLEX + ẅ: 'w', // LATIN SMALL LETTER W WITH DIAERESIS + ẇ: 'w', // LATIN SMALL LETTER W WITH DOT ABOVE + ẉ: 'w', // LATIN SMALL LETTER W WITH DOT BELOW + ẁ: 'w', // LATIN SMALL LETTER W WITH GRAVE + ⱳ: 'w', // LATIN SMALL LETTER W WITH HOOK + ẘ: 'w', // LATIN SMALL LETTER W WITH RING ABOVE + ẍ: 'x', // LATIN SMALL LETTER X WITH DIAERESIS + ẋ: 'x', // LATIN SMALL LETTER X WITH DOT ABOVE + ᶍ: 'x', // LATIN SMALL LETTER X WITH PALATAL HOOK + ý: 'y', // LATIN SMALL LETTER Y WITH ACUTE + ŷ: 'y', // LATIN SMALL LETTER Y WITH CIRCUMFLEX + ÿ: 'y', // LATIN SMALL LETTER Y WITH DIAERESIS + ẏ: 'y', // LATIN SMALL LETTER Y WITH DOT ABOVE + ỵ: 'y', // LATIN SMALL LETTER Y WITH DOT BELOW + ỳ: 'y', // LATIN SMALL LETTER Y WITH GRAVE + ƴ: 'y', // LATIN SMALL LETTER Y WITH HOOK + ỷ: 'y', // LATIN SMALL LETTER Y WITH HOOK ABOVE + ỿ: 'y', // LATIN SMALL LETTER Y WITH LOOP + ȳ: 'y', // LATIN SMALL LETTER Y WITH MACRON + ẙ: 'y', // LATIN SMALL LETTER Y WITH RING ABOVE + ɏ: 'y', // LATIN SMALL LETTER Y WITH STROKE + ỹ: 'y', // LATIN SMALL LETTER Y WITH TILDE + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LETTER YOGH' (ȝ) + ź: 'z', // LATIN SMALL LETTER Z WITH ACUTE + ž: 'z', // LATIN SMALL LETTER Z WITH CARON + ẑ: 'z', // LATIN SMALL LETTER Z WITH CIRCUMFLEX + ʑ: 'z', // LATIN SMALL LETTER Z WITH CURL + ⱬ: 'z', // LATIN SMALL LETTER Z WITH DESCENDER + ż: 'z', // LATIN SMALL LETTER Z WITH DOT ABOVE + ẓ: 'z', // LATIN SMALL LETTER Z WITH DOT BELOW + ȥ: 'z', // LATIN SMALL LETTER Z WITH HOOK + ẕ: 'z', // LATIN SMALL LETTER Z WITH LINE BELOW + ᵶ: 'z', // LATIN SMALL LETTER Z WITH MIDDLE TILDE + ᶎ: 'z', // LATIN SMALL LETTER Z WITH PALATAL HOOK + ʐ: 'z', // LATIN SMALL LETTER Z WITH RETROFLEX HOOK + ƶ: 'z', // LATIN SMALL LETTER Z WITH STROKE + ɀ: 'z', // LATIN SMALL LETTER Z WITH SWASH TAIL + ff: 'ff', // LATIN SMALL LIGATURE FF + ffi: 'ffi', // LATIN SMALL LIGATURE FFI + ffl: 'ffl', // LATIN SMALL LIGATURE FFL + fi: 'fi', // LATIN SMALL LIGATURE FI + fl: 'fl', // LATIN SMALL LIGATURE FL + ij: 'ij', // LATIN SMALL LIGATURE IJ + // CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LIGATURE LONG S T' (ſt) + œ: 'oe', // LATIN SMALL LIGATURE OE + st: 'st', // LATIN SMALL LIGATURE ST + ₐ: 'a', // LATIN SUBSCRIPT SMALL LETTER A + ₑ: 'e', // LATIN SUBSCRIPT SMALL LETTER E + ᵢ: 'i', // LATIN SUBSCRIPT SMALL LETTER I + ⱼ: 'j', // LATIN SUBSCRIPT SMALL LETTER J + ₒ: 'o', // LATIN SUBSCRIPT SMALL LETTER O + ᵣ: 'r', // LATIN SUBSCRIPT SMALL LETTER R + // CANNOT FIND APPROXIMATION FOR 'LATIN SUBSCRIPT SMALL LETTER SCHWA' (ₔ) + ᵤ: 'u', // LATIN SUBSCRIPT SMALL LETTER U + ᵥ: 'v', // LATIN SUBSCRIPT SMALL LETTER V + ₓ: 'x', // LATIN SUBSCRIPT SMALL LETTER X +}; diff --git a/app/utils/url/latinise.ts b/app/utils/url/latinise.ts new file mode 100644 index 0000000000..f2526ef304 --- /dev/null +++ b/app/utils/url/latinise.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +// Credit to http://semplicewebsites.com/removing-accents-javascript + +import {latinMap} from './latin_map'; + +export function map(x: string) { + return latinMap[x] || x; +} + +export function latinise(input: string) { + return input.replace(/[^A-Za-z0-9]/g, map); +} diff --git a/assets/base/images/google.png b/assets/base/images/google.png new file mode 100644 index 0000000000..9271884323 Binary files /dev/null and b/assets/base/images/google.png differ diff --git a/assets/base/images/logo.png b/assets/base/images/logo.png new file mode 100755 index 0000000000..2ed25e06d5 Binary files /dev/null and b/assets/base/images/logo.png differ diff --git a/index.ts b/index.ts index bed214276f..07f11dc258 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,8 @@ import 'react-native-gesture-handler'; import setFontFamily from './app/utils/font_family'; import './app/mattermost'; +declare const global: { HermesInternal: null | {} }; + if (__DEV__) { const LogBox = require('react-native/Libraries/LogBox/LogBox'); LogBox.ignoreLogs([ @@ -17,6 +19,16 @@ if (__DEV__) { setFontFamily(); +if (global.HermesInternal) { + // Polyfills required to use Intl with Hermes engine + require('@formatjs/intl-getcanonicallocales/polyfill'); + require('@formatjs/intl-locale/polyfill'); + require('@formatjs/intl-pluralrules/polyfill'); + require('@formatjs/intl-numberformat/polyfill'); + require('@formatjs/intl-datetimeformat/polyfill'); + require('@formatjs/intl-datetimeformat/add-golden-tz'); +} + if (Platform.OS === 'android') { const ShareExtension = require('share_extension/index.tsx').default; const AppRegistry = require('react-native/Libraries/ReactNative/AppRegistry'); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 65f119c4be..8300d375bd 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -711,7 +711,7 @@ SPEC CHECKSUMS: EXConstants: c00cd53a17a65b2e53ddb3890e4e74d3418e406e EXFileSystem: 35769beb727d5341d1276fd222710f9704f7164e FBLazyVector: 49cbe4b43e445b06bf29199b6ad2057649e4c8f5 - FBReactNativeSpec: ebaa990b13e6f0496fd41894a824c585c4afab46 + FBReactNativeSpec: a804c9d6c798f94831713302354003ee54ea18cb glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62 jail-monkey: 80c9e34da2cd54023e5ad08bf7051ec75bd43d5b libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3 diff --git a/package-lock.json b/package-lock.json index 42e6450958..fef722b294 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2077,6 +2077,30 @@ } } }, + "@formatjs/intl-datetimeformat": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@formatjs/intl-datetimeformat/-/intl-datetimeformat-3.3.5.tgz", + "integrity": "sha512-JbjkS2OHSyrNgHBiELmaywZ9Yy03HwRj69adWrc9N6baAl/sN6INtyxU+uv3MhTjOEyMtw5FrFO5Juk9VY5o5A==", + "requires": { + "@formatjs/ecma402-abstract": "1.7.1", + "tslib": "^2.1.0" + }, + "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz", + "integrity": "sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, "@formatjs/intl-displaynames": { "version": "4.0.13", "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-4.0.13.tgz", @@ -2093,6 +2117,22 @@ } } }, + "@formatjs/intl-getcanonicallocales": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.5.10.tgz", + "integrity": "sha512-tFqGxZ9HkAzphupybyCKdWHzL1ge/sY8TtzEK57Hs3RCxrv/y+VxIPrE+Izw2oCFowQBz76cyi0zT6PjHuWArA==", + "requires": { + "cldr-core": "38", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, "@formatjs/intl-listformat": { "version": "5.0.14", "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-5.0.14.tgz", @@ -2109,6 +2149,104 @@ } } }, + "@formatjs/intl-locale": { + "version": "2.4.24", + "resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.24.tgz", + "integrity": "sha512-+JOwvBRFS/GFuJlWiWbfAzBng0A+ANoGV1LRseXK+4uzp4Sn35GD8M/dfgU1lp2R2dTWpYie2yyoHe4k4aHF6w==", + "requires": { + "@formatjs/ecma402-abstract": "1.7.1", + "@formatjs/intl-getcanonicallocales": "1.5.10", + "cldr-core": "38", + "tslib": "^2.1.0" + }, + "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz", + "integrity": "sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, + "@formatjs/intl-numberformat": { + "version": "6.2.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-numberformat/-/intl-numberformat-6.2.10.tgz", + "integrity": "sha512-b2pN56nxQ2JnYaT1ji8NYJbDv9rQmQ1BWHgyRJWIjKz6afYeeoVf/O7YIaDFawNOONgRrn5J1SFYtNdQzXJJkg==", + "requires": { + "@formatjs/ecma402-abstract": "1.7.1", + "tslib": "^2.1.0" + }, + "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz", + "integrity": "sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, + "@formatjs/intl-pluralrules": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.18.tgz", + "integrity": "sha512-qRFITPsNoeXfsiGc97pp8mVgqcC7aQNuXsiJjY9LpXVTkYNfjUP4ZpbYXflM4xoWCXMJNz3ilsrQhZWXy9td5g==", + "requires": { + "@formatjs/ecma402-abstract": "1.7.1", + "tslib": "^2.1.0" + }, + "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz", + "integrity": "sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, + "@formatjs/intl-relativetimeformat": { + "version": "8.1.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-8.1.8.tgz", + "integrity": "sha512-MIVrsgG7hvYrnes6TxJLflXhhTuxIaWCIdf6p5Iv6HguTtDJqqAFOCNRCqUnYQeYcNbgIQBgLb0Kh7djS0GU+w==", + "requires": { + "@formatjs/ecma402-abstract": "1.7.1", + "tslib": "^2.1.0" + }, + "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz", + "integrity": "sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, "@hapi/hoek": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.0.tgz", @@ -9101,6 +9239,11 @@ "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", "dev": true }, + "cldr-core": { + "version": "38.1.0", + "resolved": "https://registry.npmjs.org/cldr-core/-/cldr-core-38.1.0.tgz", + "integrity": "sha512-Da9xKjDp4qGGIX0VDsBqTan09iR5nuYD2a/KkfEaUyqKhu6wFVNRiCpPDXeRbpVwPBY6PgemV8WiHatMhcpy4A==" + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", diff --git a/package.json b/package.json index 9f501caacf..defbaeff34 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,12 @@ "private": true, "dependencies": { "@babel/runtime": "7.13.16", + "@formatjs/intl-datetimeformat": "3.3.5", + "@formatjs/intl-getcanonicallocales": "1.5.10", + "@formatjs/intl-locale": "2.4.24", + "@formatjs/intl-numberformat": "6.2.10", + "@formatjs/intl-pluralrules": "4.0.18", + "@formatjs/intl-relativetimeformat": "8.1.8", "@mattermost/react-native-emm": "1.1.1", "@mattermost/react-native-paste-input": "0.1.3", "@nozbe/watermelondb": "0.21.0", diff --git a/patches/react-native-button+3.0.1.patch b/patches/react-native-button+3.0.1.patch new file mode 100644 index 0000000000..d484eac716 --- /dev/null +++ b/patches/react-native-button+3.0.1.patch @@ -0,0 +1,28 @@ +diff --git a/node_modules/react-native-button/Button.js b/node_modules/react-native-button/Button.js +index b248176..2ee35d5 100644 +--- a/node_modules/react-native-button/Button.js ++++ b/node_modules/react-native-button/Button.js +@@ -71,7 +71,6 @@ export default class Button extends Component { + } + + return ( +- + +- +- {this._renderGroupedChildren()} ++ ++ ++ {this._renderGroupedChildren()} ++ + + +- + ); + } + } diff --git a/types/api/apps.d.ts b/types/api/apps.d.ts new file mode 100644 index 0000000000..f46ff82cdf --- /dev/null +++ b/types/api/apps.d.ts @@ -0,0 +1,211 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type AppManifest = { + app_id: string; + display_name: string; + description?: string; + homepage_url?: string; + root_url: string; +}; + +type AppModalState = { + form: AppForm; + call: AppCallRequest; +}; + +type AppsState = { + bindings: AppBinding[]; +}; + +type AppBinding = { + app_id: string; + location?: string; + icon?: string; + + // Label is the (usually short) primary text to display at the location. + // - For LocationPostMenu is the menu item text. + // - For LocationChannelHeader is the dropdown text. + // - For LocationCommand is the name of the command + label: string; + + // Hint is the secondary text to display + // - LocationPostMenu: not used + // - LocationChannelHeader: tooltip + // - LocationCommand: the "Hint" line + hint?: string; + + // Description is the (optional) extended help text, used in modals and autocomplete + description?: string; + + role_id?: string; + depends_on_team?: boolean; + depends_on_channel?: boolean; + depends_on_user?: boolean; + depends_on_post?: boolean; + + // A Binding is either to a Call, or is a "container" for other locations - + // i.e. menu sub-items or subcommands. + call?: AppCall; + bindings?: AppBinding[]; + form?: AppForm; +}; + +type AppCallValues = { + [name: string]: any; +}; + +type AppCallType = string; + +type AppCall = { + path: string; + expand?: AppExpand; + state?: any; +}; + +type AppCallRequest = AppCall & { + context: AppContext; + values?: AppCallValues; + raw_command?: string; + selected_field?: string; + query?: string; +}; + +type AppCallResponseType = string; + +type AppCallResponse = { + type: AppCallResponseType; + markdown?: string; + data?: Res; + error?: string; + navigate_to_url?: string; + use_external_browser?: boolean; + call?: AppCall; + form?: AppForm; + app_metadata?: AppMetadataForClient; +}; + +type AppMetadataForClient = { + bot_user_id: string; + bot_username: string; +}; + +type AppContext = { + app_id: string; + location?: string; + acting_user_id?: string; + user_id?: string; + channel_id?: string; + team_id?: string; + post_id?: string; + root_id?: string; + props?: AppContextProps; + user_agent?: string; +}; + +type AppContextProps = { + [name: string]: string; +}; + +type AppExpandLevel = string; + +type AppExpand = { + app?: AppExpandLevel; + acting_user?: AppExpandLevel; + channel?: AppExpandLevel; + config?: AppExpandLevel; + mentioned?: AppExpandLevel; + parent_post?: AppExpandLevel; + post?: AppExpandLevel; + root_post?: AppExpandLevel; + team?: AppExpandLevel; + user?: AppExpandLevel; +}; + +type AppForm = { + title?: string; + header?: string; + footer?: string; + icon?: string; + submit_buttons?: string; + cancel_button?: boolean; + submit_on_cancel?: boolean; + fields: AppField[]; + call?: AppCall; + depends_on?: string[]; +}; + +type AppFormValue = string | AppSelectOption | boolean | null; +type AppFormValues = {[name: string]: AppFormValue}; + +type AppSelectOption = { + label: string; + value: string; + icon_data?: string; +}; + +type AppFieldType = string; + +// This should go in mattermost-redux +type AppField = { + + // Name is the name of the JSON field to use. + name: string; + type: AppFieldType; + is_required?: boolean; + readonly?: boolean; + + // Present (default) value of the field + value?: AppFormValue; + + description?: string; + + label?: string; + hint?: string; + position?: number; + + modal_label?: string; + + // Select props + refresh?: boolean; + options?: AppSelectOption[]; + multiselect?: boolean; + + // Text props + subtype?: string; + min_length?: number; + max_length?: number; +}; + +type AutocompleteSuggestion = { + suggestion: string; + complete?: string; + description?: string; + hint?: string; + iconData?: string; +}; + +type AutocompleteSuggestionWithComplete = AutocompleteSuggestion & { + complete: string; +}; + +type AutocompleteElement = AppField; +type AutocompleteStaticSelect = AutocompleteElement & { + options: AppSelectOption[]; +}; + +type AutocompleteDynamicSelect = AutocompleteElement; + +type AutocompleteUserSelect = AutocompleteElement; + +type AutocompleteChannelSelect = AutocompleteElement; + +type FormResponseData = { + errors?: { + [field: string]: string; + }; +}; + +type AppLookupResponse = { + items: AppSelectOption[]; +}; diff --git a/types/api/bots.d.ts b/types/api/bots.d.ts new file mode 100644 index 0000000000..e96cc2647c --- /dev/null +++ b/types/api/bots.d.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type Bot = { + user_id: string ; + username: string ; + display_name?: string ; + description?: string ; + owner_id: string ; + create_at: number ; + update_at: number ; + delete_at: number ; +} + +// BotPatch is a description of what fields to update on an existing bot. +type BotPatch = { + username: string; + display_name: string; + description: string; +} diff --git a/types/api/channels.d.ts b/types/api/channels.d.ts new file mode 100644 index 0000000000..7909d3d975 --- /dev/null +++ b/types/api/channels.d.ts @@ -0,0 +1,106 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +type ChannelType = 'O' | 'P' | 'D' | 'G'; +type ChannelStats = { + channel_id: string; + member_count: number; + pinnedpost_count: number; +}; +type ChannelNotifyProps = { + desktop: 'default' | 'all' | 'mention' | 'none'; + email: 'default' | 'all' | 'mention' | 'none'; + mark_unread: 'all' | 'mention'; + push: 'default' | 'all' | 'mention' | 'none'; + ignore_channel_mentions: 'default' | 'off' | 'on'; +}; +type Channel = { + id: string; + create_at: number; + update_at: number; + delete_at: number; + team_id: string; + type: ChannelType; + display_name: string; + name: string; + header: string; + purpose: string; + last_post_at: number; + total_msg_count: number; + extra_update_at: number; + creator_id: string; + scheme_id: string; + isCurrent?: boolean; + teammate_id?: string; + status?: string; + fake?: boolean; + group_constrained: boolean; +}; +type ChannelWithTeamData = Channel & { + team_display_name: string; + team_name: string; + team_update_at: number; +} +type ChannelMembership = { + channel_id: string; + user_id: string; + roles: string; + last_viewed_at: number; + msg_count: number; + mention_count: number; + notify_props: Partial; + last_update_at: number; + scheme_user: boolean; + scheme_admin: boolean; + post_root_id?: string; +}; +type ChannelUnread = { + channel_id: string; + user_id: string; + team_id: string; + msg_count: number; + mention_count: number; + last_viewed_at: number; + deltaMsgs: number; +}; +type ChannelsState = { + currentChannelId: string; + channels: IDMappedObjects; + channelsInTeam: RelationOneToMany; + myMembers: RelationOneToOne; + membersInChannel: RelationOneToOne>; + stats: RelationOneToOne; + groupsAssociatedToChannel: any; + totalCount: number; + manuallyUnread: RelationOneToOne; + channelMemberCountsByGroup: RelationOneToOne; +}; + +type ChannelModeration = { + name: string; + roles: { + guests?: { + value: boolean; + enabled: boolean; + }; + members: { + value: boolean; + enabled: boolean; + }; + }; +}; + +type ChannelModerationPatch = { + name: string; + roles: { + guests?: boolean; + members?: boolean; + }; +}; + +type ChannelMemberCountByGroup = { + group_id: string; + channel_member_count: number; + channel_member_timezones_count: number; +}; + +type ChannelMemberCountsByGroup = Record; diff --git a/types/api/client4.d.ts b/types/api/client4.d.ts index a1683f5b0a..4bb3f85e68 100644 --- a/types/api/client4.d.ts +++ b/types/api/client4.d.ts @@ -1,5 +1,8 @@ -export declare type logLevel = 'ERROR' | 'WARNING' | 'INFO'; -export declare type GenericClientResponse = { +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +declare type logLevel = 'ERROR' | 'WARNING' | 'INFO'; +declare type GenericClientResponse = { response: any; headers: Map; data: any; @@ -14,14 +17,14 @@ declare type ErrorInvalidResponse = { defaultMessage: string; }; }; -export declare type ErrorApi = { +declare type ErrorApi = { message: string; server_error_id: string; status_code: number; url: string; }; -export declare type Client4Error = ErrorOffline | ErrorInvalidResponse | ErrorApi; -export declare type Options = { +declare type Client4Error = ErrorOffline | ErrorInvalidResponse | ErrorApi; +declare type ClientOptions = { headers?: { [x: string]: string; }; @@ -30,4 +33,3 @@ export declare type Options = { credentials?: 'omit' | 'same-origin' | 'include'; body?: any; }; -export {}; diff --git a/types/api/config.d.ts b/types/api/config.d.ts index 4ef645d0ae..1b17cf93bc 100644 --- a/types/api/config.d.ts +++ b/types/api/config.d.ts @@ -82,6 +82,7 @@ interface ClientConfig { EnableSignUpWithGitLab: string; EnableSignUpWithGoogle: string; EnableSignUpWithOffice365: string; + EnableSignUpWithOpenId: string; EnableSVGs: string; EnableTesting: string; EnableThemeSelection: string; @@ -129,6 +130,8 @@ interface ClientConfig { MaxFileSize: string; MaxNotificationsPerChannel: string; MinimumHashtagLength: string; + OpenIdButtonColor: string; + OpenIdButtonText: string; PasswordMinimumLength: string; PasswordRequireLowercase: string; PasswordRequireNumber: string; diff --git a/types/api/emojis.d.ts b/types/api/emojis.d.ts new file mode 100644 index 0000000000..7947d3ac43 --- /dev/null +++ b/types/api/emojis.d.ts @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type EmojiCategory = ( + | 'recent' + | 'people' + | 'nature' + | 'foods' + | 'activity' + | 'places' + | 'objects' + | 'symbols' + | 'flags' + | 'custom' +); + +type CustomEmoji = { + id: string; + create_at: number; + update_at: number; + delete_at: number; + creator_id: string; + name: string; + category: 'custom'; +}; + +type SystemEmoji = { + filename: string; + aliases: Array; + category: EmojiCategory; + batch: number; +}; + +type Emoji = SystemEmoji | CustomEmoji; + +type EmojisState = { + customEmoji: { + [x: string]: CustomEmoji; + }; + nonExistentEmoji: Set; +}; diff --git a/types/api/error.d.ts b/types/api/error.d.ts new file mode 100644 index 0000000000..e8adf47e29 --- /dev/null +++ b/types/api/error.d.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type ApiError = { + server_error_id?: string; + stack?: string; + message: string; + status_code?: number; +}; diff --git a/types/api/files.d.ts b/types/api/files.d.ts new file mode 100644 index 0000000000..ff094856b7 --- /dev/null +++ b/types/api/files.d.ts @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type FileInfo = { + id: string; + user_id: string; + post_id: string; + create_at: number; + update_at: number; + delete_at: number; + name: string; + extension: string; + size: number; + mime_type: string; + width: number; + height: number; + has_preview_image: boolean; + clientId: string; + localPath?: string; + uri?: string; + loading?: boolean; +}; + +type FilesState = { + files: Dictionary; + fileIdsByPostId: Dictionary>; + filePublicLink?: string; +}; + +type FileUploadResponse = { + file_infos: FileInfo[]; + client_ids: string[]; +}; diff --git a/types/api/groups.d.ts b/types/api/groups.d.ts new file mode 100644 index 0000000000..c0fbecbc30 --- /dev/null +++ b/types/api/groups.d.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type SyncableType = 'team' | 'channel'; +type SyncablePatch = { + scheme_admin: boolean; + auto_add: boolean; +}; +type Group = { + id: string; + name: string; + display_name: string; + description: string; + type: string; + remote_id: string; + create_at: number; + update_at: number; + delete_at: number; + has_syncables: boolean; + member_count: number; + scheme_admin: boolean; + allow_reference: boolean; +}; +type GroupTeam = { + team_id: string; + team_display_name: string; + team_type: string; + group_id: string; + auto_add: boolean; + scheme_admin: boolean; + create_at: number; + delete_at: number; + update_at: number; +}; +type GroupChannel = { + channel_id: string; + channel_display_name: string; + channel_type: string; + team_id: string; + team_display_name: string; + team_type: string; + group_id: string; + auto_add: boolean; + scheme_admin: boolean; + create_at: number; + delete_at: number; + update_at: number; +}; +type GroupSyncables = { + teams: Array; + channels: Array; +}; +type GroupsState = { + syncables: { + [x: string]: GroupSyncables; + }; + members: any; + groups: { + [x: string]: Group; + }; + myGroups: { + [x: string]: Group; + }; +}; +type GroupSearchOpts = { + q: string; + is_linked?: boolean; + is_configured?: boolean; +}; diff --git a/types/api/integrations.d.ts b/types/api/integrations.d.ts new file mode 100644 index 0000000000..a15e3b0cf0 --- /dev/null +++ b/types/api/integrations.d.ts @@ -0,0 +1,87 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type Command = { + 'id': string; + 'token': string; + 'create_at': number; + 'update_at': number; + 'delete_at': number; + 'creator_id': string; + 'team_id': string; + 'trigger': string; + 'method': 'P' | 'G' | ''; + 'username': string; + 'icon_url': string; + 'auto_complete': boolean; + 'auto_complete_desc': string; + 'auto_complete_hint': string; + 'display_name': string; + 'description': string; + 'url': string; +}; + +type CommandArgs = { + channel_id: string; + team_id: string; + root_id?: string; + parent_id?: string; +}; + +// AutocompleteSuggestion represents a single suggestion downloaded from the server. +type AutocompleteSuggestion = { + Complete: string; + Suggestion: string; + Hint: string; + Description: string; + IconData: string; +}; + +type DialogSubmission = { + url: string; + callback_id: string; + state: string; + user_id: string; + channel_id: string; + team_id: string; + submission: { + [x: string]: string; + }; + cancelled: boolean; +}; + +type DialogOption = { + text: string; + value: string; +}; + +type DialogElement = { + display_name: string; + name: string; + type: string; + subtype: string; + default: string; + placeholder: string; + help_text: string; + optional: boolean; + min_length: number; + max_length: number; + data_source: string; + options: Array; +}; + +type InteractiveDialogConfig = { + app_id: string; + trigger_id: string; + url: string; + dialog: { + callback_id: string; + title: string; + introduction_text: string; + icon_url?: string; + elements: DialogElement[]; + submit_label: string; + notify_on_cancel: boolean; + state: string; + }; +}; diff --git a/types/api/license.d.ts b/types/api/license.d.ts new file mode 100644 index 0000000000..72ad20f0cc --- /dev/null +++ b/types/api/license.d.ts @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +interface ClientLicense { + Announcement: string; + Cloud: string; + Cluster: string; + Company: string; + Compliance: string; + CustomPermissionsSchemes: string; + CustomTermsOfService: string; + DataRetention: string; + Elasticsearch: string; + EmailNotificationContents: string; + GoogleOAuth: string; + GuestAccounts: string; + GuestAccountsPermissions: string; + IDLoadedPushNotifications: string; + IsLicensed: string; + LDAP: string; + LDAPGroups: string; + LockTeammateNameDisplay: string; + MFA: string; + MHPNS: string; + MessageExport: string; + Metrics: string; + Office365OAuth: string; + OpenId: string; + RemoteClusterService: string; + SAML: string; + SharedChannels: string; + Users: string; + } diff --git a/types/api/posts.d.ts b/types/api/posts.d.ts new file mode 100644 index 0000000000..31a6ae2fe8 --- /dev/null +++ b/types/api/posts.d.ts @@ -0,0 +1,113 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type PostType = 'system_add_remove' | + 'system_add_to_channel' | + 'system_add_to_team' | + 'system_channel_deleted' | + 'system_channel_restored' | + 'system_displayname_change' | + 'system_convert_channel' | + 'system_ephemeral' | + 'system_header_change' | + 'system_join_channel' | + 'system_join_leave' | + 'system_leave_channel' | + 'system_purpose_change' | + 'system_remove_from_channel'; + +type PostEmbedType = 'image' | 'message_attachment' | 'opengraph'; + +type PostEmbed = { + type: PostEmbedType; + url: string; + data: Record; +}; + +type PostImage = { + height: number; + width: number; + format?: string; + frame_count?: number; +}; + +type PostMetadata = { + embeds: Array; + emojis: Array; + files: Array; + images: Dictionary; + reactions: Array; +}; + +type Post = { + id: string; + create_at: number; + update_at: number; + edit_at: number; + delete_at: number; + is_pinned: boolean; + user_id: string; + channel_id: string; + root_id: string; + parent_id: string; + original_id: string; + message: string; + type: PostType; + props: Record; + hashtags: string; + pending_post_id: string; + reply_count: number; + file_ids?: any[]; + metadata: PostMetadata; + failed?: boolean; + user_activity_posts?: Array; + state?: 'DELETED'; + ownPost?: boolean; +}; + +type PostWithFormatData = Post & { + isFirstReply: boolean; + isLastReply: boolean; + previousPostIsComment: boolean; + commentedOnPost?: Post; + consecutivePostByUser: boolean; + replyCount: number; + isCommentMention: boolean; + highlight: boolean; +}; + +type PostOrderBlock = { + order: Array; + recent?: boolean; + oldest?: boolean; +}; + +type MessageHistory = { + messages: Array; + index: { + post: number; + comment: number; + }; +}; + +type PostsState = { + posts: IDMappedObjects; + postsInChannel: Dictionary>; + postsInThread: RelationOneToMany; + reactions: RelationOneToOne>; + openGraph: RelationOneToOne; + pendingPostIds: Array; + selectedPostId: string; + currentFocusedPostId: string; + messagesHistory: MessageHistory; + expandedURLs: Dictionary; +}; + +type PostProps = { + disable_group_highlight?: boolean; + mentionHighlightDisabled: boolean; +}; + +type PostResponse = PostOrderBlock & { + posts: IDMappedObjects; +}; diff --git a/types/api/reactions.d.ts b/types/api/reactions.d.ts new file mode 100644 index 0000000000..614b6ffd06 --- /dev/null +++ b/types/api/reactions.d.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type Reaction = { + user_id: string; + post_id: string; + emoji_name: string; + create_at: number; +}; diff --git a/types/api/roles.d.ts b/types/api/roles.d.ts new file mode 100644 index 0000000000..a9d6e92c24 --- /dev/null +++ b/types/api/roles.d.ts @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type ChannelModerationRoles = 'members' | 'guests'; + +type Role = { + id: string; + name: string; + display_name: string; + description: string; + create_at: number; + update_at: number; + delete_at: number; + permissions: Array; + scheme_managed: boolean; + built_in: boolean; +}; diff --git a/types/api/teams.d.ts b/types/api/teams.d.ts new file mode 100644 index 0000000000..9e342dab8c --- /dev/null +++ b/types/api/teams.d.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type TeamMembership = { + mention_count: number; + msg_count: number; + team_id: string; + user_id: string; + roles: string; + delete_at: number; + scheme_user: boolean; + scheme_admin: boolean; +}; + +type TeamMemberWithError = { + member: TeamMembership; + user_id: string; + error: ApiError; +} + +type TeamType = 'O' | 'I'; + +type Team = { + id: string; + create_at: number; + update_at: number; + delete_at: number; + display_name: string; + name: string; + description: string; + email: string; + type: TeamType; + company_name: string; + allowed_domains: string; + invite_id: string; + allow_open_invite: boolean; + scheme_id: string; + group_constrained: boolean; +}; + +type TeamsState = { + currentTeamId: string; + teams: Dictionary; + myMembers: Dictionary; + membersInTeam: any; + stats: any; + groupsAssociatedToTeam: any; + totalCount: number; +}; + +type TeamUnread = { + team_id: string; + mention_count: number; + msg_count: number; +}; diff --git a/types/api/users.d.ts b/types/api/users.d.ts new file mode 100644 index 0000000000..00084441f4 --- /dev/null +++ b/types/api/users.d.ts @@ -0,0 +1,80 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type UserNotifyProps = { + auto_responder_active?: 'true' | 'false'; + auto_responder_message?: string; + desktop: 'default' | 'all' | 'mention' | 'none'; + desktop_notification_sound?: string; + desktop_sound: 'true' | 'false'; + email: 'true' | 'false'; + mark_unread: 'all' | 'mention'; + push: 'default' | 'all' | 'mention' | 'none'; + push_status: 'ooo' | 'offline' | 'away' | 'dnd' | 'online'; + comments: 'never' | 'root' | 'any'; + first_name: 'true' | 'false'; + channel: 'true' | 'false'; + mention_keys: string; + user_id?: string; +}; + +type UserProfile = { + id: string; + create_at: number; + update_at: number; + delete_at: number; + username: string; + auth_data: string; + auth_service: string; + email: string; + email_verified: boolean; + nickname: string; + first_name: string; + last_name: string; + position: string; + roles: string; + locale: string; + notify_props: UserNotifyProps; + terms_of_service_id: string; + terms_of_service_create_at: number; + timezone?: UserTimezone; + is_bot: boolean; + last_picture_update: number; +}; + +type UsersState = { + currentUserId: string; + isManualStatus: RelationOneToOne; + mySessions: Array; + profiles: IDMappedObjects; + profilesInTeam: RelationOneToMany; + profilesNotInTeam: RelationOneToMany; + profilesWithoutTeam: Set; + profilesInChannel: RelationOneToMany; + profilesNotInChannel: RelationOneToMany; + statuses: RelationOneToOne; + stats: any; +}; + +type UserTimezone = { + useAutomaticTimezone: boolean | string; + automaticTimezone: string; + manualTimezone: string; +}; + +type UserActivity = { + [x in PostType]: { + [y in $ID]: | { + ids: Array<$ID>; + usernames: Array; + } | Array<$ID>; + }; +}; + +type UserStatus = { + user_id: string; + status: string; + manual: boolean; + last_activity_at: number; + active_channel?: string; +}; diff --git a/types/global/preferences.d.ts b/types/global/preferences.d.ts index 543730569d..bffd2aeb50 100644 --- a/types/global/preferences.d.ts +++ b/types/global/preferences.d.ts @@ -11,32 +11,3 @@ interface PreferenceType { interface PreferencesType { [x: string]: PreferenceType; } - -interface Theme { - [key: string]: string | undefined; - type?: string; - sidebarBg: string; - sidebarText: string; - sidebarUnreadText: string; - sidebarTextHoverBg: string; - sidebarTextActiveBorder: string; - sidebarTextActiveColor: string; - sidebarHeaderBg: string; - sidebarHeaderTextColor: string; - onlineIndicator: string; - awayIndicator: string; - dndIndicator: string; - mentionBg: string; - mentionBj: string; - mentionColor: string; - centerChannelBg: string; - centerChannelColor: string; - newMessageSeparator: string; - linkColor: string; - buttonBg: string; - buttonColor: string; - errorTextColor: string; - mentionHighlightBg: string; - mentionHighlightLink: string; - codeTheme: string; -} diff --git a/types/global/utilities.d.ts b/types/global/utilities.d.ts index cd86e6d0ae..d09586477a 100644 --- a/types/global/utilities.d.ts +++ b/types/global/utilities.d.ts @@ -1,6 +1,31 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -interface Dictionary { +type $ID = E['id']; +type $UserID = E['user_id']; +type $Name = E['name']; +type $Username = E['username']; +type $Email = E['email']; +type RelationOneToOne = { + [x in $ID]: T; +}; +type RelationOneToMany = { + [x in $ID]: Array<$ID>; +}; +type IDMappedObjects = RelationOneToOne; +type UserIDMappedObjects = { + [x in $UserID]: E; +}; +type NameMappedObjects = { + [x in $Name]: E; +}; +type UsernameMappedObjects = { + [x in $Username]: E; +}; +type EmailMappedObjects = { + [x in $Email]: E; +}; + +type Dictionary = { [key: string]: T; -} +}; diff --git a/types/screens/gallery.d.ts b/types/screens/gallery.d.ts index 94e7378728..92f7fba551 100644 --- a/types/screens/gallery.d.ts +++ b/types/screens/gallery.d.ts @@ -6,9 +6,6 @@ import {intlShape} from 'react-intl'; import {StyleProp, ViewStyle} from 'react-native'; import Animated from 'react-native-reanimated'; -import type {FileInfo} from '@mm-redux/types/files'; -import type {Theme} from '@mm-redux/types/preferences'; - export interface CallbackFunctionWithoutArguments { (): void; }