forked from Ivasoft/mattermost-mobile
Server & LoginOptions
This commit is contained in:
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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)}`,
|
||||
|
||||
@@ -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)}`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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)}`,
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
59
app/components/error_text/index.tsx
Normal file
59
app/components/error_text/index.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
18
app/constants/files.ts
Normal 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
72
app/constants/general.ts
Normal 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',
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
63
app/screens/login_options/email.tsx
Normal file
63
app/screens/login_options/email.tsx
Normal 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;
|
||||
73
app/screens/login_options/gitlab.tsx
Normal file
73
app/screens/login_options/gitlab.tsx
Normal 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;
|
||||
72
app/screens/login_options/google.tsx
Normal file
72
app/screens/login_options/google.tsx
Normal 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;
|
||||
142
app/screens/login_options/index.tsx
Normal file
142
app/screens/login_options/index.tsx
Normal 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;
|
||||
78
app/screens/login_options/ldap.tsx
Normal file
78
app/screens/login_options/ldap.tsx
Normal 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;
|
||||
66
app/screens/login_options/office365.tsx
Normal file
66
app/screens/login_options/office365.tsx
Normal 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;
|
||||
64
app/screens/login_options/open_id.tsx
Normal file
64
app/screens/login_options/open_id.tsx
Normal 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;
|
||||
71
app/screens/login_options/saml.tsx
Normal file
71
app/screens/login_options/saml.tsx
Normal 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
12
app/screens/login_options/types.d.ts
vendored
Normal 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;
|
||||
};
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
8
app/utils/general/index.ts
Normal file
8
app/utils/general/index.ts
Normal 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
|
||||
}
|
||||
@@ -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
207
app/utils/markdown/index.ts
Normal 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';
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
995
app/utils/url/latin_map.ts
Normal 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: 'ff', // LATIN SMALL LIGATURE FF
|
||||
ffi: 'ffi', // LATIN SMALL LIGATURE FFI
|
||||
ffl: 'ffl', // LATIN SMALL LIGATURE FFL
|
||||
fi: 'fi', // LATIN SMALL LIGATURE FI
|
||||
fl: 'fl', // LATIN SMALL LIGATURE FL
|
||||
ij: 'ij', // LATIN SMALL LIGATURE IJ
|
||||
// CANNOT FIND APPROXIMATION FOR 'LATIN SMALL LIGATURE LONG S T' (ſt)
|
||||
œ: 'oe', // LATIN SMALL LIGATURE OE
|
||||
st: 'st', // LATIN SMALL LIGATURE ST
|
||||
ₐ: 'a', // LATIN SUBSCRIPT SMALL LETTER A
|
||||
ₑ: 'e', // LATIN SUBSCRIPT SMALL LETTER E
|
||||
ᵢ: 'i', // LATIN SUBSCRIPT SMALL LETTER I
|
||||
ⱼ: 'j', // LATIN SUBSCRIPT SMALL LETTER J
|
||||
ₒ: 'o', // LATIN SUBSCRIPT SMALL LETTER O
|
||||
ᵣ: 'r', // LATIN SUBSCRIPT SMALL LETTER R
|
||||
// CANNOT FIND APPROXIMATION FOR 'LATIN SUBSCRIPT SMALL LETTER SCHWA' (ₔ)
|
||||
ᵤ: 'u', // LATIN SUBSCRIPT SMALL LETTER U
|
||||
ᵥ: 'v', // LATIN SUBSCRIPT SMALL LETTER V
|
||||
ₓ: 'x', // LATIN SUBSCRIPT SMALL LETTER X
|
||||
};
|
||||
13
app/utils/url/latinise.ts
Normal file
13
app/utils/url/latinise.ts
Normal 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);
|
||||
}
|
||||
BIN
assets/base/images/google.png
Normal file
BIN
assets/base/images/google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 601 B |
BIN
assets/base/images/logo.png
Executable file
BIN
assets/base/images/logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
12
index.ts
12
index.ts
@@ -7,6 +7,8 @@ import 'react-native-gesture-handler';
|
||||
import setFontFamily from './app/utils/font_family';
|
||||
import './app/mattermost';
|
||||
|
||||
declare const global: { HermesInternal: null | {} };
|
||||
|
||||
if (__DEV__) {
|
||||
const LogBox = require('react-native/Libraries/LogBox/LogBox');
|
||||
LogBox.ignoreLogs([
|
||||
@@ -17,6 +19,16 @@ if (__DEV__) {
|
||||
|
||||
setFontFamily();
|
||||
|
||||
if (global.HermesInternal) {
|
||||
// Polyfills required to use Intl with Hermes engine
|
||||
require('@formatjs/intl-getcanonicallocales/polyfill');
|
||||
require('@formatjs/intl-locale/polyfill');
|
||||
require('@formatjs/intl-pluralrules/polyfill');
|
||||
require('@formatjs/intl-numberformat/polyfill');
|
||||
require('@formatjs/intl-datetimeformat/polyfill');
|
||||
require('@formatjs/intl-datetimeformat/add-golden-tz');
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
const ShareExtension = require('share_extension/index.tsx').default;
|
||||
const AppRegistry = require('react-native/Libraries/ReactNative/AppRegistry');
|
||||
|
||||
@@ -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
143
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
28
patches/react-native-button+3.0.1.patch
Normal file
28
patches/react-native-button+3.0.1.patch
Normal 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
211
types/api/apps.d.ts
vendored
Normal 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
20
types/api/bots.d.ts
vendored
Normal 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
106
types/api/channels.d.ts
vendored
Normal 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>;
|
||||
14
types/api/client4.d.ts
vendored
14
types/api/client4.d.ts
vendored
@@ -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 {};
|
||||
|
||||
3
types/api/config.d.ts
vendored
3
types/api/config.d.ts
vendored
@@ -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
41
types/api/emojis.d.ts
vendored
Normal 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
9
types/api/error.d.ts
vendored
Normal 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
33
types/api/files.d.ts
vendored
Normal 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
69
types/api/groups.d.ts
vendored
Normal 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
87
types/api/integrations.d.ts
vendored
Normal 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
33
types/api/license.d.ts
vendored
Normal 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
113
types/api/posts.d.ts
vendored
Normal 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
9
types/api/reactions.d.ts
vendored
Normal 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
17
types/api/roles.d.ts
vendored
Normal 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
55
types/api/teams.d.ts
vendored
Normal 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
80
types/api/users.d.ts
vendored
Normal 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;
|
||||
};
|
||||
29
types/global/preferences.d.ts
vendored
29
types/global/preferences.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
29
types/global/utilities.d.ts
vendored
29
types/global/utilities.d.ts
vendored
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
3
types/screens/gallery.d.ts
vendored
3
types/screens/gallery.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user