Server & LoginOptions

This commit is contained in:
Elias Nahum
2021-05-10 23:29:08 -04:00
parent 11c183b5ed
commit 22fec720b1
79 changed files with 3987 additions and 1684 deletions

View File

@@ -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<AppCallResponse>;

View File

@@ -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);

View File

@@ -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<Bot>;

View File

@@ -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<Channel>) => {
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)}`,

View File

@@ -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)}`,

View File

@@ -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;

View File

@@ -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<any>;
ping: () => Promise<any>;
logClientError: (message: string, level?: string) => Promise<any>;
getClientConfigOld: () => Promise<Config>;
getClientConfigOld: () => Promise<ClientConfig>;
getClientLicenseOld: () => Promise<any>;
getTimezones: () => Promise<string[]>;
getDataRetentionPolicy: () => Promise<any>;
getRolesByNames: (rolesNames: string[]) => Promise<Role[]>;
getRedirectLocation: (urlParam: string) => Promise<Dictionary<string>>;
getRedirectLocation: (urlParam: string) => Promise<Record<string, string>>;
}
const ClientGeneral = (superclass: any) => class extends superclass {

View File

@@ -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';

View File

@@ -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(() => {

View File

@@ -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)},

View File

@@ -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<Post> & {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 = {

View File

@@ -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<any>;
deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;

View File

@@ -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<Team> & {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)}`,

View File

@@ -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<UserProfile> & {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`,

View File

@@ -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(
<ErrorText {...baseProps}/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper.toJSON()).toMatchSnapshot();
});
});

View File

@@ -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 (
<FormattedText
testID={testID}
id={intl.id}
defaultMessage={intl.defaultMessage}
values={intl.values}
style={[GlobalStyles.errorLabel, style.errorLabel, textStyle]}
/>
);
}
return (
<Text
testID={testID}
style={[GlobalStyles.errorLabel, style.errorLabel, textStyle]}
>
{error.message || error}
</Text>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
errorLabel: {
color: (theme.errorTextColor || '#DA4A4A'),
},
};
});

View File

@@ -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);

View File

@@ -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<string, any>}}
type ErrorProps = {
error: ClientErrorWithIntl | string;
testID?: string;
textStyle?: StyleProp<ViewStyle> | StyleProp<TextStyle>
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 (
<FormattedText
testID={testID}
id={intl.id}
defaultMessage={intl.defaultMessage}
values={intl.values}
style={[style.errorLabel, textStyle]}
/>
);
}
return (
<Text
testID={testID}
style={[style.errorLabel, textStyle]}
>
{message}
</Text>
);
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
errorLabel: {
color: (theme.errorTextColor || '#DA4A4A'),
marginTop: 15,
marginBottom: 15,
fontSize: 12,
textAlign: 'left',
},
};
});
export default ErrorText;

View File

@@ -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<string, any>;
testID?: string;
style?: StyleProp<ViewStyle> | StyleProp<TextStyle>
}
const FormattedText = (props: FormattedTextProps) => {
const intl = useIntl();
const {formatMessage} = intl;
const {id, defaultMessage, values, ...otherProps} = props;
const tokenizedValues: Record<string, any> = {};
const elements: Record<string, any> = {};
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;

View File

@@ -3,6 +3,8 @@
export default {
CHANNEL: 'channel',
PERMALINK: 'permalink',
DM: 'dmchannel',
GM: 'groupchannel',
OTHER: 'other',
PERMALINK: 'permalink',
};

18
app/constants/files.ts Normal file
View File

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const Files: Record<string, string[]> = {
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;

72
app/constants/general.ts Normal file
View File

@@ -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',
};

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -13,7 +13,7 @@ const isSystemAdmin = (roles: string) => {
const clientMap: Record<string, Analytics> = {};
class Analytics {
export class Analytics {
analytics: RudderClient | null = null;
context: any;
diagnosticId: string | undefined;

View File

@@ -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;

View File

@@ -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);
}
},
};

View File

@@ -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,
},
});
}

View File

@@ -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<ClientConfig, ClientLicense>([
Client4.getClientConfigOld(),
Client4.getClientLicenseOld(),
]);
return {config, license};
} catch (error) {
return {error};
}
};

View File

@@ -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<any>, styles: StyleProp<ViewStyle>) => {
const withGestures = (screen: NavigationFunctionComponent, styles: StyleProp<ViewStyle>) => {
if (Platform.OS === 'android') {
return gestureHandlerRootHOC(screen, styles);
}
@@ -21,10 +23,23 @@ const withGestures = (screen: React.ComponentType<any>, styles: StyleProp<ViewSt
return screen;
};
const withIntl = (Screen: React.ComponentType) => {
return function IntlEnabledComponent(props: any) {
return (
<IntlProvider
locale={DEFAULT_LOCALE}
messages={getTranslations()}
>
<Screen {...props}/>
</IntlProvider>
);
}
}
Navigation.setLazyComponentRegistrator((screenName) => {
// let screen: any;
// let extraStyles: StyleProp<ViewStyle>;
// switch (screenName) {
let screen: any|undefined;
let extraStyles: StyleProp<ViewStyle>;
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)));
}

View File

@@ -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<ViewStyle> = {
backgroundColor,
};
if (config.EmailLoginButtonBorderColor) {
additionalStyle.borderColor = config.EmailLoginButtonBorderColor;
}
const textColor = config.EmailLoginButtonTextColor || 'white';
return (
<Button
key='email'
onPress={onPress}
containerStyle={[styles.button, additionalStyle]}
>
<FormattedText
id='signup.email'
defaultMessage='Email and Password'
style={[styles.buttonText, {color: textColor}]}
/>
</Button>
);
}
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;

View File

@@ -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 (
<Button
key='gitlab'
onPress={handlePress}
containerStyle={[styles.button, additionalButtonStyle]}
>
<Image
source={require('@assets/images/gitlab.png')}
style={logoStyle}
/>
<Text
style={[styles.buttonText, {color: textColor}]}
>
{'GitLab'}
</Text>
</Button>
);
}
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;

View File

@@ -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 (
<Button
key='google'
onPress={handlePress}
containerStyle={[styles.button, additionalButtonStyle]}
>
<Image
source={require('@assets/images/google.png')}
style={logoStyle}
/>
<FormattedText
id='signup.google'
defaultMessage='Google Apps'
style={[styles.buttonText, {color: textColor}]}
/>
</Button>
);
}
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;

View File

@@ -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 (
<SafeAreaView style={styles.container}>
<ScrollView
style={styles.container}
contentContainerStyle={styles.innerContainer}
>
<StatusBar/>
<Image
source={require('@assets/images/logo.png')}
style={{height: 72, resizeMode: 'contain'}}
/>
<Text style={styles.header}>
{config.SiteName}
</Text>
<FormattedText
style={styles.subheader}
id='web.root.signup_info'
defaultMessage='All team communication in one place, searchable and accessible anywhere'
/>
<FormattedText
style={[styles.subheader, {fontWeight: 'bold', marginTop: 10}]}
id='mobile.login_options.choose_title'
defaultMessage='Choose your login method'
/>
<EmailOption
config={config}
onPress={displayLogin}
theme={theme}
/>
<LdapOption
config={config}
license={license}
onPress={displayLogin}
theme={theme}
/>
<GitLabOption
config={config}
onPress={displaySSO}
theme={theme}
/>
<GoogleOption
config={config}
onPress={displaySSO}
theme={theme}
/>
<Office365Option
config={config}
license={license}
onPress={displaySSO}
theme={theme}
/>
<OpenIdOption
config={config}
license={license}
onPress={displaySSO}
theme={theme}
/>
<SamlOption
config={config}
license={license}
onPress={displaySSO}
theme={theme}
/>
</ScrollView>
</SafeAreaView>
);
};
export default LoginOptions;

View File

@@ -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 = (
<Text style={[styles.buttonText, {color: textColor}]}>
{config.LdapLoginFieldName}
</Text>
);
} else {
buttonText = (
<FormattedText
id='login.ldapUsernameLower'
defaultMessage='AD/LDAP username'
style={[styles.buttonText, {color: textColor}]}
/>
);
}
return (
<Button
key='ldap'
onPress={onPress}
containerStyle={[styles.button, additionalButtonStyle]}
>
{buttonText}
</Button>
);
}
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;

View File

@@ -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 (
<Button
key='o365'
onPress={handlePress}
containerStyle={[styles.button, additionalButtonStyle]}
>
<FormattedText
id='signup.office365'
defaultMessage='Office 365'
style={[styles.buttonText, {color: textColor}]}
/>
</Button>
);
}
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;

View File

@@ -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 (
<Button
key='openId'
onPress={handlePress}
containerStyle={[styles.button, additionalButtonStyle]}
>
<FormattedText
id='signup.openid'
defaultMessage={config.OpenIdButtonText || 'OpenID'}
style={[styles.buttonText, {color: textColor}]}
/>
</Button>
);
}
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;

View File

@@ -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 (
<Button
key='saml'
onPress={handlePress}
containerStyle={[styles.button, additionalStyle]}
>
<Text
style={[styles.buttonText, {color: textColor}]}
>
{config.SamlLoginButtonText}
</Text>
</Button>
);
}
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;

12
app/screens/login_options/types.d.ts vendored Normal file
View File

@@ -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;
};

View File

@@ -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: {

View File

@@ -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);

View File

@@ -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(
<SelectServer {...baseProps}
/>;
)
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(
<SelectServer {...baseProps}
/>;
)
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(
<SelectServer {...baseProps}
/>;
)
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(
<SelectServer {...baseProps}
/>;
)
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());
});
});

View File

@@ -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<SelectServerProps, SelectServerState> {
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 = (
<ActivityIndicator
animating={true}
size='small'
style={style.connectingIndicator}
/>
);
buttonText = (
<FormattedText
id='mobile.components.select_server_view.connecting'
defaultMessage='Connecting...'
/>
);
} else {
buttonText = (
<FormattedText
id='mobile.components.select_server_view.connect'
defaultMessage='Connect'
/>
);
}
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 (
<SafeAreaView
testID='select_server.screen'
style={style.container}
>
<KeyboardAvoidingView
behavior='padding'
style={style.container}
keyboardVerticalOffset={0}
enabled={Platform.OS === 'ios'}
>
<StatusBar barStyle={barStyle}/>
<TouchableWithoutFeedback
onPress={this.blur}
accessible={false}
>
<View
style={[GlobalStyles.container, GlobalStyles.signupContainer]}
>
<Image
source={require('@assets/images/logo.png')}
style={{height: 72, resizeMode: 'contain'}}
/>
<View testID='select_server.header.text'>
<FormattedText
style={StyleSheet.flatten([
GlobalStyles.header,
GlobalStyles.label,
])}
id='mobile.components.select_server_view.enterServerUrl'
defaultMessage='Enter Server URL'
/>
</View>
<TextInput
testID='select_server.server_url.input'
ref={this.inputRef}
value={url}
editable={!inputDisabled}
onChangeText={this.handleTextChanged}
onSubmitEditing={this.handleConnect}
style={StyleSheet.flatten(inputStyle)}
autoCapitalize='none'
autoCorrect={false}
keyboardType='url'
placeholder={formatMessage({
id: 'mobile.components.select_server_view.siteUrlPlaceholder',
defaultMessage: 'https://mattermost.example.com',
})}
placeholderTextColor={changeOpacity('#000', 0.5)}
returnKeyType='go'
underlineColorAndroid='transparent'
disableFullscreenUI={true}
/>
<Button
testID='select_server.connect.button'
onPress={this.handleConnect}
containerStyle={[
GlobalStyles.signupButton,
style.connectButton,
]}
>
{buttonIcon}
<Text style={GlobalStyles.signupButtonText}>{buttonText}</Text>
</Button>
<View>
<ErrorText
testID='select_server.error.text'
error={error}
/>
</View>
</View>
</TouchableWithoutFeedback>
<AppVersion/>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
}
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);

View File

@@ -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<TextInput>(null);
const [connecting, setConnecting] = useState(false);
const [error, setError] = useState<ClientErrorWithIntl|string|undefined>();
const [url, setUrl] = useState<string>('');
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 = (
<ActivityIndicator
animating={true}
size='small'
color={theme.buttonBg}
style={styles.connectingIndicator}
/>
);
buttonText = (
<FormattedText
id='mobile.components.select_server_view.connecting'
defaultMessage='Connecting...'
style={styles.connectText}
/>
);
} else {
buttonText = (
<FormattedText
id='mobile.components.select_server_view.connect'
defaultMessage='Connect'
style={styles.connectText}
/>
);
}
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 (
<>
<StatusBar barStyle='dark-content'/>
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior='automatic'
style={styles.scrollView}
<SafeAreaView
testID='select_server.screen'
style={styles.container}
>
<KeyboardAvoidingView
behavior='padding'
style={styles.flex}
keyboardVerticalOffset={0}
enabled={Platform.OS === 'ios'}
>
<StatusBar barStyle={barStyle}/>
<TouchableWithoutFeedback
onPress={blur}
accessible={false}
>
{global.HermesInternal == null ? null : (
<View style={styles.engine}>
<Text style={styles.footer}>{'Engine: Hermes'}</Text>
<View style={styles.formContainer}>
<Image
source={require('@assets/images/logo.png')}
style={styles.logo}
/>
<View testID='select_server.header.text'>
<FormattedText
style={styles.header}
id='mobile.components.select_server_view.enterServerUrl'
defaultMessage='Enter Server URL'
/>
</View>
)}
<View style={styles.body}>
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{'Step One'}</Text>
<Text style={styles.sectionDescription}>
{'Edit '}
<Text
style={styles.highlight}
>{'screens/server/index.tsx'}</Text>{' to change this'}
{'XXXXXscreen and then come back to see your edits.'}
</Text>
<TextInput
testID='select_server.server_url.input'
ref={input}
value={url}
editable={!inputDisabled}
onChangeText={handleTextChanged}
onSubmitEditing={handleConnect}
style={StyleSheet.flatten(inputStyle)}
autoCapitalize='none'
autoCorrect={false}
keyboardType='url'
placeholder={formatMessage({
id: 'mobile.components.select_server_view.siteUrlPlaceholder',
defaultMessage: 'https://mattermost.example.com',
})}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
returnKeyType='go'
underlineColorAndroid='transparent'
disableFullscreenUI={true}
/>
<Button
testID='select_server.connect.button'
onPress={handleConnect}
containerStyle={styles.connectButton}
>
{buttonIcon}
{buttonText}
</Button>
{Boolean(error) &&
<View>
<ErrorText
testID='select_server.error.text'
error={error!}
theme={theme}
/>
</View>
<View style={styles.sectionContainer}>
<Text
style={styles.sectionTitle}
onPress={() => goToScreen(Screens.CHANNEL, 'Channel')}
>{'See Your Changes'}</Text>
<Text style={styles.sectionDescription}>
<ReloadInstructions/>
</Text>
</View>
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{'Debug'}</Text>
<Text style={styles.sectionDescription}>
<DebugInstructions/>
</Text>
</View>
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{'Learn More'}</Text>
<Text style={styles.sectionDescription}>
{'Read the docs to discover what to do next:'}
</Text>
</View>
<LearnMoreLinks/>
}
</View>
</ScrollView>
</SafeAreaView>
</>
</TouchableWithoutFeedback>
<AppVersion/>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
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;

View File

@@ -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
}

View File

@@ -43,3 +43,22 @@ export const isMinimumServerVersion = (currentVersion: string, minMajorVersion =
// Dot version is equal
return true;
};
export function buildQueryString(parameters: Dictionary<any>): 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;
}

207
app/utils/markdown/index.ts Normal file
View File

@@ -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<string, string> = {
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';
}

View File

@@ -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;

View File

@@ -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]|<br\/?>)((?: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=<id>
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 '<filtered>';
}
return part;
}).join('/');
if (index !== -1) {
// Add this on afterwards since it wouldn't pass the whitelist
url += '?<filtered>';
}
return url;
}

995
app/utils/url/latin_map.ts Normal file
View File

@@ -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<string, string> = {
Á: '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', // LATIN SMALL LIGATURE FF
: 'ffi', // LATIN SMALL LIGATURE FFI
: 'ffl', // LATIN SMALL LIGATURE FFL
: 'fi', // LATIN SMALL LIGATURE FI
: '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', // 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
};

13
app/utils/url/latinise.ts Normal file
View File

@@ -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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

BIN
assets/base/images/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -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');

View File

@@ -711,7 +711,7 @@ SPEC CHECKSUMS:
EXConstants: c00cd53a17a65b2e53ddb3890e4e74d3418e406e
EXFileSystem: 35769beb727d5341d1276fd222710f9704f7164e
FBLazyVector: 49cbe4b43e445b06bf29199b6ad2057649e4c8f5
FBReactNativeSpec: ebaa990b13e6f0496fd41894a824c585c4afab46
FBReactNativeSpec: a804c9d6c798f94831713302354003ee54ea18cb
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
jail-monkey: 80c9e34da2cd54023e5ad08bf7051ec75bd43d5b
libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3

143
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 (
- <View style={containerStyle}>
<TouchableNativeFeedback
{...touchableProps}
style={{flex: 1}}
@@ -79,11 +78,12 @@ export default class Button extends Component {
accessibilityLabel={this.props.accessibilityLabel}
accessibilityRole="button"
background={background}>
- <View style={{padding: padding}}>
- {this._renderGroupedChildren()}
+ <View style={containerStyle}>
+ <View style={{padding: padding}}>
+ {this._renderGroupedChildren()}
+ </View>
</View>
</TouchableNativeFeedback>
- </View>
);
}
}

211
types/api/apps.d.ts vendored Normal file
View File

@@ -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<Res = unknown> = {
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[];
};

20
types/api/bots.d.ts vendored Normal file
View File

@@ -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;
}

106
types/api/channels.d.ts vendored Normal file
View File

@@ -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<ChannelNotifyProps>;
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<Channel>;
channelsInTeam: RelationOneToMany<Team, Channel>;
myMembers: RelationOneToOne<Channel, ChannelMembership>;
membersInChannel: RelationOneToOne<Channel, UserIDMappedObjects<ChannelMembership>>;
stats: RelationOneToOne<Channel, ChannelStats>;
groupsAssociatedToChannel: any;
totalCount: number;
manuallyUnread: RelationOneToOne<Channel, boolean>;
channelMemberCountsByGroup: RelationOneToOne<Channel, ChannelMemberCountsByGroup>;
};
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<string, ChannelMemberCountByGroup>;

View File

@@ -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<string, string>;
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 {};

View File

@@ -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;

41
types/api/emojis.d.ts vendored Normal file
View File

@@ -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<string>;
category: EmojiCategory;
batch: number;
};
type Emoji = SystemEmoji | CustomEmoji;
type EmojisState = {
customEmoji: {
[x: string]: CustomEmoji;
};
nonExistentEmoji: Set<string>;
};

9
types/api/error.d.ts vendored Normal file
View File

@@ -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;
};

33
types/api/files.d.ts vendored Normal file
View File

@@ -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<FileInfo>;
fileIdsByPostId: Dictionary<Array<string>>;
filePublicLink?: string;
};
type FileUploadResponse = {
file_infos: FileInfo[];
client_ids: string[];
};

69
types/api/groups.d.ts vendored Normal file
View File

@@ -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<GroupTeam>;
channels: Array<GroupChannel>;
};
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;
};

87
types/api/integrations.d.ts vendored Normal file
View File

@@ -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<DialogOption>;
};
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;
};
};

33
types/api/license.d.ts vendored Normal file
View File

@@ -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;
}

113
types/api/posts.d.ts vendored Normal file
View File

@@ -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<string, any>;
};
type PostImage = {
height: number;
width: number;
format?: string;
frame_count?: number;
};
type PostMetadata = {
embeds: Array<PostEmbed>;
emojis: Array<CustomEmoji>;
files: Array<FileInfo>;
images: Dictionary<PostImage>;
reactions: Array<Reaction>;
};
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<string, any>;
hashtags: string;
pending_post_id: string;
reply_count: number;
file_ids?: any[];
metadata: PostMetadata;
failed?: boolean;
user_activity_posts?: Array<Post>;
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<string>;
recent?: boolean;
oldest?: boolean;
};
type MessageHistory = {
messages: Array<string>;
index: {
post: number;
comment: number;
};
};
type PostsState = {
posts: IDMappedObjects<Post>;
postsInChannel: Dictionary<Array<PostOrderBlock>>;
postsInThread: RelationOneToMany<Post, Post>;
reactions: RelationOneToOne<Post, Dictionary<Reaction>>;
openGraph: RelationOneToOne<Post, any>;
pendingPostIds: Array<string>;
selectedPostId: string;
currentFocusedPostId: string;
messagesHistory: MessageHistory;
expandedURLs: Dictionary<string>;
};
type PostProps = {
disable_group_highlight?: boolean;
mentionHighlightDisabled: boolean;
};
type PostResponse = PostOrderBlock & {
posts: IDMappedObjects<Post>;
};

9
types/api/reactions.d.ts vendored Normal file
View File

@@ -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;
};

17
types/api/roles.d.ts vendored Normal file
View File

@@ -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<string>;
scheme_managed: boolean;
built_in: boolean;
};

55
types/api/teams.d.ts vendored Normal file
View File

@@ -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<Team>;
myMembers: Dictionary<TeamMembership>;
membersInTeam: any;
stats: any;
groupsAssociatedToTeam: any;
totalCount: number;
};
type TeamUnread = {
team_id: string;
mention_count: number;
msg_count: number;
};

80
types/api/users.d.ts vendored Normal file
View File

@@ -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<UserProfile, boolean>;
mySessions: Array<any>;
profiles: IDMappedObjects<UserProfile>;
profilesInTeam: RelationOneToMany<Team, UserProfile>;
profilesNotInTeam: RelationOneToMany<Team, UserProfile>;
profilesWithoutTeam: Set<string>;
profilesInChannel: RelationOneToMany<Channel, UserProfile>;
profilesNotInChannel: RelationOneToMany<Channel, UserProfile>;
statuses: RelationOneToOne<UserProfile, string>;
stats: any;
};
type UserTimezone = {
useAutomaticTimezone: boolean | string;
automaticTimezone: string;
manualTimezone: string;
};
type UserActivity = {
[x in PostType]: {
[y in $ID<UserProfile>]: | {
ids: Array<$ID<UserProfile>>;
usernames: Array<UserProfile['username']>;
} | Array<$ID<UserProfile>>;
};
};
type UserStatus = {
user_id: string;
status: string;
manual: boolean;
last_activity_at: number;
active_channel?: string;
};

View File

@@ -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;
}

View File

@@ -1,6 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
interface Dictionary<T> {
type $ID<E extends {id: string}> = E['id'];
type $UserID<E extends {user_id: string}> = E['user_id'];
type $Name<E extends {name: string}> = E['name'];
type $Username<E extends {username: string}> = E['username'];
type $Email<E extends {email: string}> = E['email'];
type RelationOneToOne<E extends {id: string}, T> = {
[x in $ID<E>]: T;
};
type RelationOneToMany<E1 extends {id: string}, E2 extends {id: string}> = {
[x in $ID<E1>]: Array<$ID<E2>>;
};
type IDMappedObjects<E extends {id: string}> = RelationOneToOne<E, E>;
type UserIDMappedObjects<E extends {user_id: string}> = {
[x in $UserID<E>]: E;
};
type NameMappedObjects<E extends {name: string}> = {
[x in $Name<E>]: E;
};
type UsernameMappedObjects<E extends {username: string}> = {
[x in $Username<E>]: E;
};
type EmailMappedObjects<E extends {email: string}> = {
[x in $Email<E>]: E;
};
type Dictionary<T> = {
[key: string]: T;
}
};

View File

@@ -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;
}