summaryrefslogtreecommitdiffstats
path: root/webapp/utils
diff options
context:
space:
mode:
author=Corey Hulen <corey@hulen.com>2016-03-16 18:13:16 -0700
committer=Corey Hulen <corey@hulen.com>2016-03-16 18:13:16 -0700
commitb9d5b4e5dcc1585397f1e1d2e53c5f040ee76220 (patch)
tree85d2c293aa3456182a754fefe6646162b516eb6c /webapp/utils
parente101b2cf7c172d1c4ff20e0df63917b5b8f923ed (diff)
parentcba59d4eb6ef0f65304bc72339c676ebfd653e2b (diff)
downloadchat-b9d5b4e5dcc1585397f1e1d2e53c5f040ee76220.tar.gz
chat-b9d5b4e5dcc1585397f1e1d2e53c5f040ee76220.tar.bz2
chat-b9d5b4e5dcc1585397f1e1d2e53c5f040ee76220.zip
merging files
Diffstat (limited to 'webapp/utils')
-rw-r--r--webapp/utils/async_client.jsx1112
-rw-r--r--webapp/utils/channel_intro_messages.jsx254
-rw-r--r--webapp/utils/client.jsx1680
-rw-r--r--webapp/utils/constants.jsx573
-rw-r--r--webapp/utils/delayed_action.jsx27
-rw-r--r--webapp/utils/emoticons.jsx162
-rw-r--r--webapp/utils/markdown.jsx577
-rw-r--r--webapp/utils/text_formatting.jsx402
-rw-r--r--webapp/utils/utils.jsx1411
9 files changed, 6198 insertions, 0 deletions
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
new file mode 100644
index 000000000..d3f91bb0e
--- /dev/null
+++ b/webapp/utils/async_client.jsx
@@ -0,0 +1,1112 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import * as client from './client.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import BrowserStore from 'stores/browser_store.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
+import PostStore from 'stores/post_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+import * as utils from './utils.jsx';
+
+import Constants from './constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+const StatTypes = Constants.StatTypes;
+
+// Used to track in progress async calls
+const callTracker = {};
+
+export function dispatchError(err, method) {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ERROR,
+ err,
+ method
+ });
+}
+
+function isCallInProgress(callName) {
+ if (!(callName in callTracker)) {
+ return false;
+ }
+
+ if (callTracker[callName] === 0) {
+ return false;
+ }
+
+ if (utils.getTimestamp() - callTracker[callName] > 5000) {
+ //console.log('AsyncClient call ' + callName + ' expired after more than 5 seconds');
+ return false;
+ }
+
+ return true;
+}
+
+export function getChannels(checkVersion) {
+ if (isCallInProgress('getChannels')) {
+ return null;
+ }
+
+ callTracker.getChannels = utils.getTimestamp();
+
+ return client.getChannels(
+ (data, textStatus, xhr) => {
+ callTracker.getChannels = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ if (checkVersion) {
+ var serverVersion = xhr.getResponseHeader('X-Version-ID');
+
+ if (serverVersion !== BrowserStore.getLastServerVersion()) {
+ if (!BrowserStore.getLastServerVersion() || BrowserStore.getLastServerVersion() === '') {
+ BrowserStore.setLastServerVersion(serverVersion);
+ } else {
+ BrowserStore.setLastServerVersion(serverVersion);
+ window.location.reload(true);
+ console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
+ }
+ }
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_CHANNELS,
+ channels: data.channels,
+ members: data.members
+ });
+ },
+ (err) => {
+ callTracker.getChannels = 0;
+ dispatchError(err, 'getChannels');
+ }
+ );
+}
+
+export function getChannel(id) {
+ if (isCallInProgress('getChannel' + id)) {
+ return;
+ }
+
+ callTracker['getChannel' + id] = utils.getTimestamp();
+
+ client.getChannel(id,
+ (data, textStatus, xhr) => {
+ callTracker['getChannel' + id] = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_CHANNEL,
+ channel: data.channel,
+ member: data.member
+ });
+ },
+ (err) => {
+ callTracker['getChannel' + id] = 0;
+ dispatchError(err, 'getChannel');
+ }
+ );
+}
+
+export function updateLastViewedAt(id) {
+ let channelId;
+ if (id) {
+ channelId = id;
+ } else {
+ channelId = ChannelStore.getCurrentId();
+ }
+
+ if (channelId == null) {
+ return;
+ }
+
+ if (isCallInProgress(`updateLastViewed${channelId}`)) {
+ return;
+ }
+
+ callTracker[`updateLastViewed${channelId}`] = utils.getTimestamp();
+ client.updateLastViewedAt(
+ channelId,
+ () => {
+ callTracker.updateLastViewed = 0;
+ },
+ (err) => {
+ callTracker.updateLastViewed = 0;
+ dispatchError(err, 'updateLastViewedAt');
+ }
+ );
+}
+
+export function getMoreChannels(force) {
+ if (isCallInProgress('getMoreChannels')) {
+ return;
+ }
+
+ if (ChannelStore.getMoreAll().loading || force) {
+ callTracker.getMoreChannels = utils.getTimestamp();
+ client.getMoreChannels(
+ function getMoreChannelsSuccess(data, textStatus, xhr) {
+ callTracker.getMoreChannels = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_MORE_CHANNELS,
+ channels: data.channels,
+ members: data.members
+ });
+ },
+ function getMoreChannelsFailure(err) {
+ callTracker.getMoreChannels = 0;
+ dispatchError(err, 'getMoreChannels');
+ }
+ );
+ }
+}
+
+export function getChannelExtraInfo(id, memberLimit) {
+ let channelId;
+ if (id) {
+ channelId = id;
+ } else {
+ channelId = ChannelStore.getCurrentId();
+ }
+
+ if (channelId != null) {
+ if (isCallInProgress('getChannelExtraInfo_' + channelId)) {
+ return;
+ }
+
+ callTracker['getChannelExtraInfo_' + channelId] = utils.getTimestamp();
+
+ client.getChannelExtraInfo(
+ channelId,
+ memberLimit,
+ (data, textStatus, xhr) => {
+ callTracker['getChannelExtraInfo_' + channelId] = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_CHANNEL_EXTRA_INFO,
+ extra_info: data
+ });
+ },
+ (err) => {
+ callTracker['getChannelExtraInfo_' + channelId] = 0;
+ dispatchError(err, 'getChannelExtraInfo');
+ }
+ );
+ }
+}
+
+export function getProfiles() {
+ if (isCallInProgress('getProfiles')) {
+ return;
+ }
+
+ callTracker.getProfiles = utils.getTimestamp();
+ client.getProfiles(
+ function getProfilesSuccess(data, textStatus, xhr) {
+ callTracker.getProfiles = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_PROFILES,
+ profiles: data
+ });
+ },
+ function getProfilesFailure(err) {
+ callTracker.getProfiles = 0;
+ dispatchError(err, 'getProfiles');
+ }
+ );
+}
+
+export function getSessions() {
+ if (isCallInProgress('getSessions')) {
+ return;
+ }
+
+ callTracker.getSessions = utils.getTimestamp();
+ client.getSessions(
+ UserStore.getCurrentId(),
+ function getSessionsSuccess(data, textStatus, xhr) {
+ callTracker.getSessions = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_SESSIONS,
+ sessions: data
+ });
+ },
+ function getSessionsFailure(err) {
+ callTracker.getSessions = 0;
+ dispatchError(err, 'getSessions');
+ }
+ );
+}
+
+export function getAudits() {
+ if (isCallInProgress('getAudits')) {
+ return;
+ }
+
+ callTracker.getAudits = utils.getTimestamp();
+ client.getAudits(
+ UserStore.getCurrentId(),
+ function getAuditsSuccess(data, textStatus, xhr) {
+ callTracker.getAudits = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_AUDITS,
+ audits: data
+ });
+ },
+ function getAuditsFailure(err) {
+ callTracker.getAudits = 0;
+ dispatchError(err, 'getAudits');
+ }
+ );
+}
+
+export function getLogs() {
+ if (isCallInProgress('getLogs')) {
+ return;
+ }
+
+ callTracker.getLogs = utils.getTimestamp();
+ client.getLogs(
+ (data, textStatus, xhr) => {
+ callTracker.getLogs = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_LOGS,
+ logs: data
+ });
+ },
+ (err) => {
+ callTracker.getLogs = 0;
+ dispatchError(err, 'getLogs');
+ }
+ );
+}
+
+export function getServerAudits() {
+ if (isCallInProgress('getServerAudits')) {
+ return;
+ }
+
+ callTracker.getServerAudits = utils.getTimestamp();
+ client.getServerAudits(
+ (data, textStatus, xhr) => {
+ callTracker.getServerAudits = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_SERVER_AUDITS,
+ audits: data
+ });
+ },
+ (err) => {
+ callTracker.getServerAudits = 0;
+ dispatchError(err, 'getServerAudits');
+ }
+ );
+}
+
+export function getComplianceReports() {
+ if (isCallInProgress('getComplianceReports')) {
+ return;
+ }
+
+ callTracker.getComplianceReports = utils.getTimestamp();
+ client.getComplianceReports(
+ (data, textStatus, xhr) => {
+ callTracker.getComplianceReports = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_SERVER_COMPLIANCE_REPORTS,
+ complianceReports: data
+ });
+ },
+ (err) => {
+ callTracker.getComplianceReports = 0;
+ dispatchError(err, 'getComplianceReports');
+ }
+ );
+}
+
+export function getConfig() {
+ if (isCallInProgress('getConfig')) {
+ return;
+ }
+
+ callTracker.getConfig = utils.getTimestamp();
+ client.getConfig(
+ (data, textStatus, xhr) => {
+ callTracker.getConfig = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_CONFIG,
+ config: data
+ });
+ },
+ (err) => {
+ callTracker.getConfig = 0;
+ dispatchError(err, 'getConfig');
+ }
+ );
+}
+
+export function getAllTeams() {
+ if (isCallInProgress('getAllTeams')) {
+ return;
+ }
+
+ callTracker.getAllTeams = utils.getTimestamp();
+ client.getAllTeams(
+ (data, textStatus, xhr) => {
+ callTracker.getAllTeams = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ALL_TEAMS,
+ teams: data
+ });
+ },
+ (err) => {
+ callTracker.getAllTeams = 0;
+ dispatchError(err, 'getAllTeams');
+ }
+ );
+}
+
+export function search(terms) {
+ if (isCallInProgress('search_' + String(terms))) {
+ return;
+ }
+
+ callTracker['search_' + String(terms)] = utils.getTimestamp();
+ client.search(
+ terms,
+ function searchSuccess(data, textStatus, xhr) {
+ callTracker['search_' + String(terms)] = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_SEARCH,
+ results: data
+ });
+ },
+ function searchFailure(err) {
+ callTracker['search_' + String(terms)] = 0;
+ dispatchError(err, 'search');
+ }
+ );
+}
+
+export function getPostsPage(id, maxPosts) {
+ let channelId = id;
+ if (channelId == null) {
+ channelId = ChannelStore.getCurrentId();
+ if (channelId == null) {
+ return;
+ }
+ }
+
+ if (isCallInProgress('getPostsPage_' + channelId)) {
+ return;
+ }
+
+ var postList = PostStore.getAllPosts(id);
+
+ var max = maxPosts;
+ if (max == null) {
+ max = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS;
+ }
+
+ // if we already have more than POST_CHUNK_SIZE posts,
+ // let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE,
+ // with a max at maxPosts
+ var numPosts = Math.min(max, Constants.POST_CHUNK_SIZE);
+ if (postList && postList.order.length > 0) {
+ numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE));
+ }
+
+ if (channelId != null) {
+ callTracker['getPostsPage_' + channelId] = utils.getTimestamp();
+
+ client.getPostsPage(
+ channelId,
+ 0,
+ numPosts,
+ (data, textStatus, xhr) => {
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_POSTS,
+ id: channelId,
+ before: true,
+ numRequested: numPosts,
+ post_list: data
+ });
+
+ getProfiles();
+ },
+ (err) => {
+ dispatchError(err, 'getPostsPage');
+ },
+ () => {
+ callTracker['getPostsPage_' + channelId] = 0;
+ }
+ );
+ }
+}
+
+export function getPosts(id) {
+ let channelId = id;
+ if (channelId == null) {
+ channelId = ChannelStore.getCurrentId();
+ if (channelId == null) {
+ return;
+ }
+ }
+
+ if (isCallInProgress('getPosts_' + channelId)) {
+ return;
+ }
+
+ const postList = PostStore.getAllPosts(channelId);
+
+ if ($.isEmptyObject(postList) || postList.order.length < Constants.POST_CHUNK_SIZE) {
+ getPostsPage(channelId, Constants.POST_CHUNK_SIZE);
+ return;
+ }
+
+ const latestPost = PostStore.getLatestPost(channelId);
+ let latestPostTime = 0;
+
+ if (latestPost != null && latestPost.update_at != null) {
+ latestPostTime = latestPost.create_at;
+ }
+
+ callTracker['getPosts_' + channelId] = utils.getTimestamp();
+
+ client.getPosts(
+ channelId,
+ latestPostTime,
+ (data, textStatus, xhr) => {
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_POSTS,
+ id: channelId,
+ before: true,
+ numRequested: 0,
+ post_list: data
+ });
+
+ getProfiles();
+ },
+ (err) => {
+ dispatchError(err, 'getPosts');
+ },
+ () => {
+ callTracker['getPosts_' + channelId] = 0;
+ }
+ );
+}
+
+export function getPostsBefore(postId, offset, numPost) {
+ const channelId = ChannelStore.getCurrentId();
+ if (channelId == null) {
+ return;
+ }
+
+ if (isCallInProgress('getPostsBefore_' + channelId)) {
+ return;
+ }
+
+ client.getPostsBefore(
+ channelId,
+ postId,
+ offset,
+ numPost,
+ (data, textStatus, xhr) => {
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_POSTS,
+ id: channelId,
+ before: true,
+ numRequested: numPost,
+ post_list: data
+ });
+
+ getProfiles();
+ },
+ (err) => {
+ dispatchError(err, 'getPostsBefore');
+ },
+ () => {
+ callTracker['getPostsBefore_' + channelId] = 0;
+ }
+ );
+}
+
+export function getPostsAfter(postId, offset, numPost) {
+ const channelId = ChannelStore.getCurrentId();
+ if (channelId == null) {
+ return;
+ }
+
+ if (isCallInProgress('getPostsAfter_' + channelId)) {
+ return;
+ }
+
+ client.getPostsAfter(
+ channelId,
+ postId,
+ offset,
+ numPost,
+ (data, textStatus, xhr) => {
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_POSTS,
+ id: channelId,
+ before: false,
+ numRequested: numPost,
+ post_list: data
+ });
+
+ getProfiles();
+ },
+ (err) => {
+ dispatchError(err, 'getPostsAfter');
+ },
+ () => {
+ callTracker['getPostsAfter_' + channelId] = 0;
+ }
+ );
+}
+
+export function getMe() {
+ if (isCallInProgress('getMe')) {
+ return null;
+ }
+
+ callTracker.getMe = utils.getTimestamp();
+ return client.getMe(
+ (data, textStatus, xhr) => {
+ callTracker.getMe = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ME,
+ me: data
+ });
+
+ GlobalActions.newLocalizationSelected(data.locale);
+ },
+ (err) => {
+ callTracker.getMe = 0;
+ dispatchError(err, 'getMe');
+ }
+ );
+}
+
+export function getStatuses() {
+ const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
+
+ const teammateIds = [];
+ for (const preference of preferences) {
+ if (preference.value === 'true') {
+ teammateIds.push(preference.name);
+ }
+ }
+
+ if (isCallInProgress('getStatuses') || teammateIds.length === 0) {
+ return;
+ }
+
+ callTracker.getStatuses = utils.getTimestamp();
+ client.getStatuses(teammateIds,
+ (data, textStatus, xhr) => {
+ callTracker.getStatuses = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_STATUSES,
+ statuses: data
+ });
+ },
+ (err) => {
+ callTracker.getStatuses = 0;
+ dispatchError(err, 'getStatuses');
+ }
+ );
+}
+
+export function getMyTeam() {
+ if (isCallInProgress('getMyTeam')) {
+ return null;
+ }
+
+ callTracker.getMyTeam = utils.getTimestamp();
+ return client.getMyTeam(
+ function getMyTeamSuccess(data, textStatus, xhr) {
+ callTracker.getMyTeam = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_MY_TEAM,
+ team: data
+ });
+ },
+ function getMyTeamFailure(err) {
+ callTracker.getMyTeam = 0;
+ dispatchError(err, 'getMyTeam');
+ }
+ );
+}
+
+export function getAllPreferences() {
+ if (isCallInProgress('getAllPreferences')) {
+ return;
+ }
+
+ callTracker.getAllPreferences = utils.getTimestamp();
+ client.getAllPreferences(
+ (data, textStatus, xhr) => {
+ callTracker.getAllPreferences = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_PREFERENCES,
+ preferences: data
+ });
+ },
+ (err) => {
+ callTracker.getAllPreferences = 0;
+ dispatchError(err, 'getAllPreferences');
+ }
+ );
+}
+
+export function savePreferences(preferences, success, error) {
+ client.savePreferences(
+ preferences,
+ (data, textStatus, xhr) => {
+ if (xhr.status !== 304) {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_PREFERENCES,
+ preferences
+ });
+ }
+
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ dispatchError(err, 'savePreferences');
+
+ if (error) {
+ error();
+ }
+ }
+ );
+}
+
+export function getSuggestedCommands(command, suggestionId, component) {
+ client.listCommands(
+ (data) => {
+ var matches = [];
+ data.forEach((cmd) => {
+ if (('/' + cmd.trigger).indexOf(command) === 0) {
+ let s = '/' + cmd.trigger;
+ let hint = '';
+ if (cmd.auto_complete_hint && cmd.auto_complete_hint.length !== 0) {
+ hint = cmd.auto_complete_hint;
+ }
+ matches.push({
+ suggestion: s,
+ hint,
+ description: cmd.auto_complete_desc
+ });
+ }
+ });
+
+ matches = matches.sort((a, b) => a.suggestion.localeCompare(b.suggestion));
+
+ // pull out the suggested commands from the returned data
+ const terms = matches.map((suggestion) => suggestion.suggestion);
+
+ if (terms.length > 0) {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
+ id: suggestionId,
+ matchedPretext: command,
+ terms,
+ items: matches,
+ component
+ });
+ }
+ },
+ (err) => {
+ dispatchError(err, 'getCommandSuggestions');
+ }
+ );
+}
+
+export function getFileInfo(filename) {
+ const callName = 'getFileInfo' + filename;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getFileInfo(
+ filename,
+ (data) => {
+ callTracker[callName] = 0;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_FILE_INFO,
+ filename,
+ info: data
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getFileInfo');
+ }
+ );
+}
+
+export function getStandardAnalytics(teamId) {
+ const callName = 'getStandardAnaytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'standard',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ const stats = {};
+
+ for (const index in data) {
+ if (data[index].name === 'channel_open_count') {
+ stats[StatTypes.TOTAL_PUBLIC_CHANNELS] = data[index].value;
+ }
+
+ if (data[index].name === 'channel_private_count') {
+ stats[StatTypes.TOTAL_PRIVATE_GROUPS] = data[index].value;
+ }
+
+ if (data[index].name === 'post_count') {
+ stats[StatTypes.TOTAL_POSTS] = data[index].value;
+ }
+
+ if (data[index].name === 'unique_user_count') {
+ stats[StatTypes.TOTAL_USERS] = data[index].value;
+ }
+
+ if (data[index].name === 'team_count' && teamId == null) {
+ stats[StatTypes.TOTAL_TEAMS] = data[index].value;
+ }
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getStandardAnalytics');
+ }
+ );
+}
+
+export function getAdvancedAnalytics(teamId) {
+ const callName = 'getAdvancedAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'extra_counts',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ const stats = {};
+
+ for (const index in data) {
+ if (data[index].name === 'file_post_count') {
+ stats[StatTypes.TOTAL_FILE_POSTS] = data[index].value;
+ }
+
+ if (data[index].name === 'hashtag_post_count') {
+ stats[StatTypes.TOTAL_HASHTAG_POSTS] = data[index].value;
+ }
+
+ if (data[index].name === 'incoming_webhook_count') {
+ stats[StatTypes.TOTAL_IHOOKS] = data[index].value;
+ }
+
+ if (data[index].name === 'outgoing_webhook_count') {
+ stats[StatTypes.TOTAL_OHOOKS] = data[index].value;
+ }
+
+ if (data[index].name === 'command_count') {
+ stats[StatTypes.TOTAL_COMMANDS] = data[index].value;
+ }
+
+ if (data[index].name === 'session_count') {
+ stats[StatTypes.TOTAL_SESSIONS] = data[index].value;
+ }
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getAdvancedAnalytics');
+ }
+ );
+}
+
+export function getPostsPerDayAnalytics(teamId) {
+ const callName = 'getPostsPerDayAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'post_counts_day',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ data.reverse();
+
+ const stats = {};
+ stats[StatTypes.POST_PER_DAY] = data;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getPostsPerDayAnalytics');
+ }
+ );
+}
+
+export function getUsersPerDayAnalytics(teamId) {
+ const callName = 'getUsersPerDayAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'user_counts_with_posts_day',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ data.reverse();
+
+ const stats = {};
+ stats[StatTypes.USERS_WITH_POSTS_PER_DAY] = data;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getUsersPerDayAnalytics');
+ }
+ );
+}
+
+export function getRecentAndNewUsersAnalytics(teamId) {
+ const callName = 'getRecentAndNewUsersAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ const stats = {};
+
+ const usersList = [];
+ for (const id in users) {
+ if (users.hasOwnProperty(id)) {
+ usersList.push(users[id]);
+ }
+ }
+
+ usersList.sort((a, b) => {
+ if (a.last_activity_at < b.last_activity_at) {
+ return 1;
+ }
+
+ if (a.last_activity_at > b.last_activity_at) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ const recentActive = [];
+ for (let i = 0; i < usersList.length; i++) {
+ if (usersList[i].last_activity_at == null) {
+ continue;
+ }
+
+ recentActive.push(usersList[i]);
+ if (i >= Constants.STAT_MAX_ACTIVE_USERS) {
+ break;
+ }
+ }
+
+ stats[StatTypes.RECENTLY_ACTIVE_USERS] = recentActive;
+
+ usersList.sort((a, b) => {
+ if (a.create_at < b.create_at) {
+ return 1;
+ }
+
+ if (a.create_at > b.create_at) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ var newlyCreated = [];
+ for (let i = 0; i < usersList.length; i++) {
+ newlyCreated.push(usersList[i]);
+ if (i >= Constants.STAT_MAX_NEW_USERS) {
+ break;
+ }
+ }
+
+ stats[StatTypes.NEWLY_CREATED_USERS] = newlyCreated;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getRecentAndNewUsersAnalytics');
+ }
+ );
+}
diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx
new file mode 100644
index 000000000..ddd615581
--- /dev/null
+++ b/webapp/utils/channel_intro_messages.jsx
@@ -0,0 +1,254 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Utils from './utils.jsx';
+import ChannelInviteModal from 'components/channel_invite_modal.jsx';
+import EditChannelHeaderModal from 'components/edit_channel_header_modal.jsx';
+import ToggleModalButton from 'components/toggle_modal_button.jsx';
+import UserProfile from 'components/user_profile.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+import Constants from 'utils/constants.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
+
+import React from 'react';
+import {FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl';
+
+export function createChannelIntroMessage(channel) {
+ if (channel.type === 'D') {
+ return createDMIntroMessage(channel);
+ } else if (ChannelStore.isDefault(channel)) {
+ return createDefaultIntroMessage(channel);
+ } else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
+ return createOffTopicIntroMessage(channel);
+ } else if (channel.type === 'O' || channel.type === 'P') {
+ return createStandardIntroMessage(channel);
+ }
+ return null;
+}
+
+export function createDMIntroMessage(channel) {
+ var teammate = Utils.getDirectTeammate(channel.id);
+
+ if (teammate) {
+ var teammateName = teammate.username;
+ if (teammate.nickname.length > 0) {
+ teammateName = teammate.nickname;
+ }
+
+ return (
+ <div className='channel-intro'>
+ <div className='post-profile-img__container channel-intro-img'>
+ <img
+ className='post-profile-img'
+ src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at}
+ height='50'
+ width='50'
+ />
+ </div>
+ <div className='channel-intro-profile'>
+ <strong>
+ <UserProfile user={teammate}/>
+ </strong>
+ </div>
+ <p className='channel-intro-text'>
+ <FormattedHTMLMessage
+ id='intro_messages.DM'
+ defaultMessage='This is the start of your direct message history with {teammate}.<br />Direct messages and files shared here are not shown to people outside this area.'
+ values={{
+ teammate: teammateName
+ }}
+ />
+ </p>
+ {createSetHeaderButton(channel)}
+ </div>
+ );
+ }
+
+ return (
+ <div className='channel-intro'>
+ <p className='channel-intro-text'>
+ <FormattedMessage
+ id='intro_messages.teammate'
+ defaultMessage='This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.'
+ />
+ </p>
+ </div>
+ );
+}
+
+export function createOffTopicIntroMessage(channel) {
+ return (
+ <div className='channel-intro'>
+ <FormattedHTMLMessage
+ id='intro_messages.offTopic'
+ defaultMessage='<h4 class="channel-intro__title">Beginning of {display_name}</h4><p class="channel-intro__content">This is the start of {display_name}, a channel for non-work-related conversations.<br/></p>'
+ values={{
+ display_name: channel.display_name
+ }}
+ />
+ {createSetHeaderButton(channel)}
+ {createInviteChannelMemberButton(channel, 'channel')}
+ </div>
+ );
+}
+
+export function createDefaultIntroMessage(channel) {
+ const inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ onClick={GlobalActions.showGetTeamInviteLinkModal}
+ >
+ <i className='fa fa-user-plus'></i>
+ <FormattedMessage
+ id='intro_messages.inviteOthers'
+ defaultMessage='Invite others to this team'
+ />
+ </a>
+ );
+
+ return (
+ <div className='channel-intro'>
+ <FormattedHTMLMessage
+ id='intro_messages.default'
+ defaultMessage="<h4 class='channel-intro__title'>Beginning of {display_name}</h4><p class='channel-intro__content'><strong>Welcome to {display_name}!</strong><br/><br/>This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.</p>"
+ values={{
+ display_name: channel.display_name
+ }}
+ />
+ {inviteModalLink}
+ {createSetHeaderButton(channel)}
+ <br/>
+ </div>
+ );
+}
+
+export function createStandardIntroMessage(channel) {
+ var uiName = channel.display_name;
+ var creatorName = '';
+
+ var uiType;
+ var memberMessage;
+ if (channel.type === 'P') {
+ uiType = (
+ <FormattedMessage
+ id='intro_messages.group'
+ defaultMessage='private group'
+ />
+ );
+ memberMessage = (
+ <FormattedMessage
+ id='intro_messages.onlyInvited'
+ defaultMessage=' Only invited members can see this private group.'
+ />
+ );
+ } else {
+ uiType = (
+ <FormattedMessage
+ id='intro_messages.channel'
+ defaultMessage='channel'
+ />
+ );
+ memberMessage = (
+ <FormattedMessage
+ id='intro_messages.anyMember'
+ defaultMessage=' Any member can join and read this channel.'
+ />
+ );
+ }
+
+ const date = (
+ <FormattedDate
+ value={channel.create_at}
+ month='long'
+ day='2-digit'
+ year='numeric'
+ />
+ );
+
+ var createMessage;
+ if (creatorName === '') {
+ createMessage = (
+ <FormattedMessage
+ id='intro_messages.noCreator'
+ defaultMessage='This is the start of the {name} {type}, created on {date}.'
+ values={{
+ name: (uiName),
+ type: (uiType),
+ date: (date)
+ }}
+ />
+ );
+ } else {
+ createMessage = (
+ <span>
+ <FormattedHTMLMessage
+ id='intro_messages.creator'
+ defaultMessage='This is the start of the <strong>{name}</strong> {type}, created by <strong>{creator}</strong> on <strong>{date}</strong>'
+ values={{
+ name: (uiName),
+ type: (uiType),
+ date: (date),
+ creator: creatorName
+ }}
+ />
+ </span>
+ );
+ }
+
+ return (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>
+ <FormattedMessage
+ id='intro_messages.beginning'
+ defaultMessage='Beginning of {name}'
+ values={{
+ name: (uiName)
+ }}
+ />
+ </h4>
+ <p className='channel-intro__content'>
+ {createMessage}
+ {memberMessage}
+ <br/>
+ </p>
+ {createSetHeaderButton(channel)}
+ {createInviteChannelMemberButton(channel, uiType)}
+ </div>
+ );
+}
+
+function createInviteChannelMemberButton(channel, uiType) {
+ return (
+ <ToggleModalButton
+ className='intro-links'
+ dialogType={ChannelInviteModal}
+ dialogProps={{channel}}
+ >
+ <i className='fa fa-user-plus'></i>
+ <FormattedMessage
+ id='intro_messages.invite'
+ defaultMessage='Invite others to this {type}'
+ values={{
+ type: (uiType)
+ }}
+ />
+ </ToggleModalButton>
+ );
+}
+
+function createSetHeaderButton(channel) {
+ return (
+ <ToggleModalButton
+ className='intro-links'
+ dialogType={EditChannelHeaderModal}
+ dialogProps={{channel}}
+ >
+ <i className='fa fa-pencil'></i>
+ <FormattedMessage
+ id='intro_messages.setHeader'
+ defaultMessage='Set a Header'
+ />
+ </ToggleModalButton>
+ );
+}
diff --git a/webapp/utils/client.jsx b/webapp/utils/client.jsx
new file mode 100644
index 000000000..e29cf71d3
--- /dev/null
+++ b/webapp/utils/client.jsx
@@ -0,0 +1,1680 @@
+// See License.txt for license information.
+
+import BrowserStore from 'stores/browser_store.jsx';
+import $ from 'jquery';
+
+import {browserHistory} from 'react-router';
+
+let translations = {
+ connectionError: 'There appears to be a problem with your internet connection.',
+ unknownError: 'We received an unexpected status code from the server.'
+};
+
+export function setTranslations(messages) {
+ translations = messages;
+}
+
+export function track(category, action, label, property, value) {
+ global.window.analytics.track(action, {category, label, property, value});
+}
+
+export function trackPage() {
+ global.window.analytics.page();
+}
+
+function handleError(methodName, xhr, status, err) {
+ var e = null;
+ try {
+ e = JSON.parse(xhr.responseText);
+ } catch (parseError) {
+ e = null;
+ }
+
+ var msg = '';
+
+ if (e) {
+ msg = 'method=' + methodName + ' msg=' + e.message + ' detail=' + e.detailed_error + ' rid=' + e.request_id;
+ } else {
+ msg = 'method=' + methodName + ' status=' + status + ' statusCode=' + xhr.status + ' err=' + err;
+
+ if (xhr.status === 0) {
+ e = {message: translations.connectionError};
+ } else {
+ e = {message: translations.unknownError + ' (' + xhr.status + ')'};
+ }
+ }
+
+ console.error(msg); //eslint-disable-line no-console
+ console.error(e); //eslint-disable-line no-console
+
+ track('api', 'api_weberror', methodName, 'message', msg);
+
+ if (xhr.status === 401) {
+ if (window.location.href.indexOf('/channels') === 0) {
+ browserHistory.push('/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search));
+ } else {
+ var teamURL = window.location.pathname.split('/channels')[0];
+ browserHistory.push(teamURL + '/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search));
+ }
+ }
+
+ return e;
+}
+
+export function getTranslations(url, success, error) {
+ $.ajax({
+ url: url,
+ dataType: 'json',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getTranslations', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function createTeamFromSignup(teamSignup, success, error) {
+ $.ajax({
+ url: '/api/v1/teams/create_from_signup',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(teamSignup),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('createTeamFromSignup', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function createTeamWithLdap(teamSignup, success, error) {
+ $.ajax({
+ url: '/api/v1/teams/create_with_ldap',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(teamSignup),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('createTeamFromSignup', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function createTeamWithSSO(team, service, success, error) {
+ $.ajax({
+ url: '/api/v1/teams/create_with_sso/' + service,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(team),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('createTeamWithSSO', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function createUser(user, data, emailHash, success, error) {
+ $.ajax({
+ url: '/api/v1/users/create?d=' + encodeURIComponent(data) + '&h=' + encodeURIComponent(emailHash),
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(user),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('createUser', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_users_create', user.team_id, 'email', user.email);
+}
+
+export function updateUser(user, success, error) {
+ $.ajax({
+ url: '/api/v1/users/update',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(user),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updateUser', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_users_update');
+}
+
+export function updatePassword(data, success, error) {
+ $.ajax({
+ url: '/api/v1/users/newpassword',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('newPassword', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_users_newpassword');
+}
+
+export function updateUserNotifyProps(data, success, error) {
+ $.ajax({
+ url: '/api/v1/users/update_notify',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updateUserNotifyProps', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function updateRoles(data, success, error) {
+ $.ajax({
+ url: '/api/v1/users/update_roles',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updateRoles', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_users_update_roles');
+}
+
+export function updateActive(userId, active, success, error) {
+ var data = {};
+ data.user_id = userId;
+ data.active = '' + active;
+
+ $.ajax({
+ url: '/api/v1/users/update_active',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updateActive', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_users_update_roles');
+}
+
+export function sendPasswordReset(data, success, error) {
+ $.ajax({
+ url: '/api/v1/users/send_password_reset',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('sendPasswordReset', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_users_send_password_reset');
+}
+
+export function resetPassword(data, success, error) {
+ $.ajax({
+ url: '/api/v1/users/reset_password',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('resetPassword', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_users_reset_password');
+}
+
+export function switchToSSO(data, success, error) {
+ $.ajax({
+ url: '/api/v1/users/switch_to_sso',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('switchToSSO', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_users_switch_to_sso');
+}
+
+export function switchToEmail(data, success, error) {
+ $.ajax({
+ url: '/api/v1/users/switch_to_email',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('switchToEmail', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_users_switch_to_email');
+}
+
+export function logout(success, error) {
+ track('api', 'api_users_logout');
+ $.ajax({
+ url: '/api/v1/users/logout',
+ type: 'POST',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('logout', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function loginByEmail(name, email, password, success, error) {
+ $.ajax({
+ url: '/api/v1/users/login',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify({name, email, password}),
+ success: function onSuccess(data, textStatus, xhr) {
+ track('api', 'api_users_login_success', data.team_id, 'email', data.email);
+ sessionStorage.removeItem(data.id + '_last_error');
+ BrowserStore.signalLogin();
+ success(data, textStatus, xhr);
+ },
+ error: function onError(xhr, status, err) {
+ track('api', 'api_users_login_fail', name, 'email', email);
+
+ var e = handleError('loginByEmail', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function loginByUsername(name, username, password, success, error) {
+ $.ajax({
+ url: '/api/v1/users/login',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify({name, username, password}),
+ success: function onSuccess(data, textStatus, xhr) {
+ track('api', 'api_users_login_success', data.team_id, 'username', data.username);
+ sessionStorage.removeItem(data.id + '_last_error');
+ BrowserStore.signalLogin();
+ success(data, textStatus, xhr);
+ },
+ error: function onError(xhr, status, err) {
+ track('api', 'api_users_login_fail', name, 'username', username);
+
+ var e = handleError('loginByUsername', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function loginByLdap(teamName, id, password, success, error) {
+ $.ajax({
+ url: '/api/v1/users/login_ldap',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify({teamName, id, password}),
+ success: function onSuccess(data, textStatus, xhr) {
+ track('api', 'api_users_loginLdap_success', data.team_id, 'id', id);
+ sessionStorage.removeItem(data.id + '_last_error');
+ BrowserStore.signalLogin();
+ success(data, textStatus, xhr);
+ },
+ error: function onError(xhr, status, err) {
+ track('api', 'api_users_loginLdap_fail', teamName, 'id', id);
+
+ var e = handleError('loginByLdap', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function revokeSession(altId, success, error) {
+ $.ajax({
+ url: '/api/v1/users/revoke_session',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify({id: altId}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('revokeSession', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getSessions(userId, success, error) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/users/' + userId + '/sessions',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getSessions', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getAudits(userId, success, error) {
+ $.ajax({
+ url: '/api/v1/users/' + userId + '/audits',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getAudits', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getComplianceReports(success, error) {
+ $.ajax({
+ url: '/api/v1/admin/compliance_reports',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getComplianceReports', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function saveComplianceReports(job, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/save_compliance_report',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(job),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('saveComplianceReports', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getLogs(success, error) {
+ $.ajax({
+ url: '/api/v1/admin/logs',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getLogs', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getServerAudits(success, error) {
+ $.ajax({
+ url: '/api/v1/admin/audits',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getServerAudits', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getConfig(success, error) {
+ return $.ajax({
+ url: '/api/v1/admin/config',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getConfig', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getAnalytics(name, teamId, success, error) {
+ let url = '/api/v1/admin/analytics/';
+ if (teamId == null) {
+ url += name;
+ } else {
+ url += teamId + '/' + name;
+ }
+ $.ajax({
+ url,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('getSystemAnalytics', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getClientConfig(success, error) {
+ return $.ajax({
+ url: '/api/v1/admin/client_props',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getClientConfig', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getTeamAnalytics(teamId, name, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/analytics/' + teamId + '/' + name,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('getTeamAnalytics', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function saveConfig(config, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/save_config',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(config),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('saveConfig', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function logClientError(msg) {
+ var l = {};
+ l.level = 'ERROR';
+ l.message = msg;
+
+ $.ajax({
+ url: '/api/v1/admin/log_client',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(l)
+ });
+}
+
+export function testEmail(config, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/test_email',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(config),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('testEmail', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getAllTeams(success, error) {
+ $.ajax({
+ url: '/api/v1/teams/all',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getAllTeams', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getMeLoggedIn(success, error) {
+ return $.ajax({
+ cache: false,
+ url: '/api/v1/users/me_logged_in',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getMeLoggedIn', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getMe(success, error) {
+ var currentUser = null;
+ $.ajax({
+ cache: false,
+ url: '/api/v1/users/me',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success: function gotUser(data, textStatus, xhr) {
+ currentUser = data;
+ if (success) {
+ success(data, textStatus, xhr);
+ }
+ },
+ error: function onError(xhr, status, err) {
+ if (error) {
+ var e = handleError('getMe', xhr, status, err);
+ error(e);
+ }
+ }
+ });
+
+ return currentUser;
+}
+
+export function inviteMembers(data, success, error) {
+ $.ajax({
+ url: '/api/v1/teams/invite_members',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('inviteMembers', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_teams_invite_members');
+}
+
+export function updateTeam(team, success, error) {
+ $.ajax({
+ url: '/api/v1/teams/update',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(team),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('updateTeam', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_teams_update_name');
+}
+
+export function signupTeam(email, success, error) {
+ $.ajax({
+ url: '/api/v1/teams/signup',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify({email: email}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('singupTeam', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_teams_signup');
+}
+
+export function createTeam(team, success, error) {
+ $.ajax({
+ url: '/api/v1/teams/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(team),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('createTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function findTeamByName(teamName, success, error) {
+ $.ajax({
+ url: '/api/v1/teams/find_team_by_name',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify({name: teamName}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('findTeamByName', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function createChannel(channel, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(channel),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('createChannel', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_create', channel.type, 'name', channel.name);
+}
+
+export function createDirectChannel(channel, userId, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/create_direct',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify({user_id: userId}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('createDirectChannel', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_create_direct', channel.type, 'name', channel.name);
+}
+
+export function updateChannel(channel, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/update',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(channel),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updateChannel', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_update');
+}
+
+export function updateChannelHeader(channelId, header, success, error) {
+ const data = {
+ channel_id: channelId,
+ channel_header: header
+ };
+
+ $.ajax({
+ url: '/api/v1/channels/update_header',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updateChannelHeader', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_header');
+}
+
+export function updateChannelPurpose(data, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/update_purpose',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updateChannelPurpose', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_purpose');
+}
+
+export function updateNotifyProps(data, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/update_notify_props',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updateNotifyProps', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function joinChannel(id, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/' + id + '/join',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('joinChannel', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_join');
+}
+
+export function leaveChannel(id, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/' + id + '/leave',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('leaveChannel', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_leave');
+}
+
+export function deleteChannel(id, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/' + id + '/delete',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('deleteChannel', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_delete');
+}
+
+export function updateLastViewedAt(channelId, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/' + channelId + '/update_last_viewed_at',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updateLastViewedAt', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getChannels(success, error) {
+ return $.ajax({
+ cache: false,
+ url: '/api/v1/channels/',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ ifModified: true,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getChannels', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getChannel(id, success, error) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/channels/' + id + '/',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getChannel', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channel_get');
+}
+
+export function getMoreChannels(success, error) {
+ $.ajax({
+ url: '/api/v1/channels/more',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ ifModified: true,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getMoreChannels', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getChannelCounts(success, error) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/channels/counts',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ ifModified: true,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getChannelCounts', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getChannelExtraInfo(id, memberLimit, success, error) {
+ let url = '/api/v1/channels/' + id + '/extra_info';
+
+ if (memberLimit) {
+ url += '/' + memberLimit;
+ }
+
+ return $.ajax({
+ url,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getChannelExtraInfo', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function executeCommand(channelId, command, suggest, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/execute',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify({channelId, command, suggest: '' + suggest}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('executeCommand', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function addCommand(cmd, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(cmd),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('addCommand', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function deleteCommand(data, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/delete',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('deleteCommand', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listTeamCommands(success, error) {
+ $.ajax({
+ url: '/api/v1/commands/list_team_commands',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('listTeamCommands', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function regenCommandToken(data, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/regen_token',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('regenCommandToken', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listCommands(success, error) {
+ $.ajax({
+ url: '/api/v1/commands/list',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('listCommands', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getPostsPage(channelId, offset, limit, success, error, complete) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/channels/' + channelId + '/posts/' + offset + '/' + limit,
+ dataType: 'json',
+ type: 'GET',
+ ifModified: true,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getPosts', xhr, status, err);
+ error(e);
+ },
+ complete: complete
+ });
+}
+
+export function getPosts(channelId, since, success, error, complete) {
+ return $.ajax({
+ url: '/api/v1/channels/' + channelId + '/posts/' + since,
+ dataType: 'json',
+ type: 'GET',
+ ifModified: true,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getPosts', xhr, status, err);
+ error(e);
+ },
+ complete: complete
+ });
+}
+
+export function getPostsBefore(channelId, post, offset, numPost, success, error, complete) {
+ $.ajax({
+ url: '/api/v1/channels/' + channelId + '/post/' + post + '/before/' + offset + '/' + numPost,
+ dataType: 'json',
+ type: 'GET',
+ ifModified: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getPostsBefore', xhr, status, err);
+ error(e);
+ },
+ complete: complete
+ });
+}
+
+export function getPostsAfter(channelId, post, offset, numPost, success, error, complete) {
+ $.ajax({
+ url: '/api/v1/channels/' + channelId + '/post/' + post + '/after/' + offset + '/' + numPost,
+ dataType: 'json',
+ type: 'GET',
+ ifModified: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getPostsAfter', xhr, status, err);
+ error(e);
+ },
+ complete: complete
+ });
+}
+
+export function getPost(channelId, postId, success, error, complete) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/channels/' + channelId + '/post/' + postId,
+ dataType: 'json',
+ type: 'GET',
+ ifModified: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getPost', xhr, status, err);
+ error(e);
+ },
+ complete
+ });
+}
+
+export function getPostById(postId, success, error, complete) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/posts/' + postId,
+ dataType: 'json',
+ type: 'GET',
+ ifModified: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getPostById', xhr, status, err);
+ error(e);
+ },
+ complete
+ });
+}
+
+export function search(terms, success, error) {
+ $.ajax({
+ url: '/api/v1/posts/search',
+ dataType: 'json',
+ type: 'GET',
+ data: {terms: terms},
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('search', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_posts_search');
+}
+
+export function deletePost(channelId, id, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/' + channelId + '/post/' + id + '/delete',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('deletePost', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_posts_delete');
+}
+
+export function createPost(post, channel, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/' + post.channel_id + '/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(post),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('createPost', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_posts_create', channel.name, 'length', post.message.length);
+
+ // global.window.analytics.track('api_posts_create', {
+ // category: 'api',
+ // channel_name: channel.name,
+ // channel_type: channel.type,
+ // length: post.message.length,
+ // files: (post.filenames || []).length,
+ // mentions: (post.message.match('/<mention>/g') || []).length
+ // });
+}
+
+export function updatePost(post, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/' + post.channel_id + '/update',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(post),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updatePost', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_posts_update');
+}
+
+export function addChannelMember(id, data, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/' + id + '/add',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('addChannelMember', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_add_member');
+}
+
+export function removeChannelMember(id, data, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/' + id + '/remove',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('removeChannelMember', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_remove_member');
+}
+
+export function getProfiles(success, error) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/users/profiles',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ ifModified: true,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getProfiles', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getProfilesForTeam(teamId, success, error) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/users/profiles/' + teamId,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getProfilesForTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function uploadFile(formData, success, error) {
+ var request = $.ajax({
+ url: '/api/v1/files/upload',
+ type: 'POST',
+ data: formData,
+ cache: false,
+ contentType: false,
+ processData: false,
+ success,
+ error: function onError(xhr, status, err) {
+ if (err !== 'abort') {
+ var e = handleError('uploadFile', xhr, status, err);
+ error(e);
+ }
+ }
+ });
+
+ track('api', 'api_files_upload');
+
+ return request;
+}
+
+export function getFileInfo(filename, success, error) {
+ $.ajax({
+ url: '/api/v1/files/get_info' + filename,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success: (data) => {
+ success(data);
+ },
+ error: function onError(xhr, status, err) {
+ var e = handleError('getFileInfo', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getPublicLink(data, success, error) {
+ $.ajax({
+ url: '/api/v1/files/get_public_link',
+ dataType: 'json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getPublicLink', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function uploadProfileImage(imageData, success, error) {
+ $.ajax({
+ url: '/api/v1/users/newimage',
+ type: 'POST',
+ data: imageData,
+ cache: false,
+ contentType: false,
+ processData: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('uploadProfileImage', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function importSlack(fileData, success, error) {
+ $.ajax({
+ url: '/api/v1/teams/import_team',
+ type: 'POST',
+ data: fileData,
+ cache: false,
+ contentType: false,
+ processData: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('importTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function exportTeam(success, error) {
+ $.ajax({
+ url: '/api/v1/teams/export_team',
+ type: 'GET',
+ dataType: 'json',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('exportTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getStatuses(ids, success, error) {
+ $.ajax({
+ url: '/api/v1/users/status',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(ids),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getStatuses', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getMyTeam(success, error) {
+ return $.ajax({
+ url: '/api/v1/teams/me',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ ifModified: true,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getMyTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function registerOAuthApp(app, success, error) {
+ $.ajax({
+ url: '/api/v1/oauth/register',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(app),
+ success: success,
+ error: (xhr, status, err) => {
+ const e = handleError('registerApp', xhr, status, err);
+ error(e);
+ }
+ });
+
+ module.exports.track('api', 'api_apps_register');
+}
+
+export function allowOAuth2(responseType, clientId, redirectUri, state, scope, success, error) {
+ $.ajax({
+ url: '/api/v1/oauth/allow?response_type=' + responseType + '&client_id=' + clientId + '&redirect_uri=' + redirectUri + '&scope=' + scope + '&state=' + state,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ const e = handleError('allowOAuth2', xhr, status, err);
+ error(e);
+ }
+ });
+
+ module.exports.track('api', 'api_users_allow_oauth2');
+}
+
+export function addIncomingHook(hook, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/incoming/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(hook),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('addIncomingHook', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function deleteIncomingHook(data, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/incoming/delete',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('deleteIncomingHook', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listIncomingHooks(success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/incoming/list',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('listIncomingHooks', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getAllPreferences(success, error) {
+ return $.ajax({
+ url: '/api/v1/preferences/',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('getAllPreferences', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getPreferenceCategory(category, success, error) {
+ $.ajax({
+ url: `/api/v1/preferences/${category}`,
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('getPreferenceCategory', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function savePreferences(preferences, success, error) {
+ $.ajax({
+ url: '/api/v1/preferences/save',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(preferences),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('savePreferences', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function addOutgoingHook(hook, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(hook),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('addOutgoingHook', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function deleteOutgoingHook(data, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/delete',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('deleteOutgoingHook', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listOutgoingHooks(success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/list',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('listOutgoingHooks', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function regenOutgoingHookToken(data, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/regen_token',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('regenOutgoingHookToken', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function uploadLicenseFile(formData, success, error) {
+ $.ajax({
+ url: '/api/v1/license/add',
+ type: 'POST',
+ data: formData,
+ cache: false,
+ contentType: false,
+ processData: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('uploadLicenseFile', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_license_upload');
+}
+
+export function removeLicenseFile(success, error) {
+ $.ajax({
+ url: '/api/v1/license/remove',
+ type: 'POST',
+ cache: false,
+ contentType: false,
+ processData: false,
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('removeLicenseFile', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_license_upload');
+}
+
+export function getClientLicenceConfig(success, error) {
+ return $.ajax({
+ url: '/api/v1/license/client_config',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getClientLicenceConfig', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getInviteInfo(success, error, id) {
+ $.ajax({
+ url: '/api/v1/teams/get_invite_info',
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json',
+ data: JSON.stringify({invite_id: id}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getInviteInfo', xhr, status, err);
+ if (error) {
+ error(e);
+ }
+ }
+ });
+}
+
+export function verifyEmail(success, error, uid, hid) {
+ $.ajax({
+ url: '/api/v1/users/verify_email',
+ type: 'POST',
+ contentType: 'application/json',
+ dataType: 'text',
+ data: JSON.stringify({uid, hid}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('verifyEmail', xhr, status, err);
+ if (error) {
+ error(e);
+ }
+ }
+ });
+}
+
+export function resendVerification(success, error, teamName, email) {
+ $.ajax({
+ url: '/api/v1/users/resend_verification',
+ type: 'POST',
+ contentType: 'application/json',
+ dataType: 'text',
+ data: JSON.stringify({team_name: teamName, email}),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('resendVerification', xhr, status, err);
+ if (error) {
+ error(e);
+ }
+ }
+ });
+}
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
new file mode 100644
index 000000000..0f7c9857d
--- /dev/null
+++ b/webapp/utils/constants.jsx
@@ -0,0 +1,573 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import keyMirror from 'keymirror';
+
+import audioIcon from 'images/icons/audio.png';
+import videoIcon from 'images/icons/video.png';
+import excelIcon from 'images/icons/excel.png';
+import pptIcon from 'images/icons/ppt.png';
+import pdfIcon from 'images/icons/pdf.png';
+import codeIcon from 'images/icons/code.png';
+import wordIcon from 'images/icons/word.png';
+import patchIcon from 'images/icons/patch.png';
+import genericIcon from 'images/icons/generic.png';
+
+import logoImage from 'images/logo_compact.png';
+
+import solarizedDarkCSS from '!!file?name=files/code_themes/[hash].[ext]!highlight.js/styles/solarized-dark.css';
+import solarizedDarkIcon from 'images/themes/code_themes/solarized-dark.png';
+
+import solarizedLightCSS from '!!file?name=files/code_themes/[hash].[ext]!highlight.js/styles/solarized-light.css';
+import solarizedLightIcon from 'images/themes/code_themes/solarized-light.png';
+
+import githubCSS from '!!file?name=files/code_themes/[hash].[ext]!highlight.js/styles/github.css';
+import githubIcon from 'images/themes/code_themes/github.png';
+
+import monokaiCSS from '!!file?name=files/code_themes/[hash].[ext]!highlight.js/styles/monokai.css';
+import monokaiIcon from 'images/themes/code_themes/monokai.png';
+
+import defaultThemeImage from 'images/themes/organization.png';
+import mattermostDarkThemeImage from 'images/themes/mattermost_dark.png';
+import mattermostThemeImage from 'images/themes/mattermost.png';
+import windows10ThemeImage from 'images/themes/windows_dark.png';
+
+export default {
+ ActionTypes: keyMirror({
+ RECEIVED_ERROR: null,
+
+ CLICK_CHANNEL: null,
+ CREATE_CHANNEL: null,
+ LEAVE_CHANNEL: null,
+ CREATE_POST: null,
+ POST_DELETED: null,
+ REMOVE_POST: null,
+
+ RECEIVED_CHANNELS: null,
+ RECEIVED_CHANNEL: null,
+ RECEIVED_MORE_CHANNELS: null,
+ RECEIVED_CHANNEL_EXTRA_INFO: null,
+
+ FOCUS_POST: null,
+ RECEIVED_POSTS: null,
+ RECEIVED_FOCUSED_POST: null,
+ RECEIVED_POST: null,
+ RECEIVED_EDIT_POST: null,
+ RECEIVED_SEARCH: null,
+ RECEIVED_SEARCH_TERM: null,
+ RECEIVED_POST_SELECTED: null,
+ RECEIVED_MENTION_DATA: null,
+ RECEIVED_ADD_MENTION: null,
+
+ RECEIVED_PROFILES: null,
+ RECEIVED_ME: null,
+ RECEIVED_SESSIONS: null,
+ RECEIVED_AUDITS: null,
+ RECEIVED_TEAMS: null,
+ RECEIVED_STATUSES: null,
+ RECEIVED_PREFERENCE: null,
+ RECEIVED_PREFERENCES: null,
+ RECEIVED_FILE_INFO: null,
+
+ RECEIVED_MSG: null,
+
+ RECEIVED_MY_TEAM: null,
+
+ RECEIVED_CONFIG: null,
+ RECEIVED_LOGS: null,
+ RECEIVED_SERVER_AUDITS: null,
+ RECEIVED_SERVER_COMPLIANCE_REPORTS: null,
+ RECEIVED_ALL_TEAMS: null,
+
+ RECEIVED_LOCALE: null,
+
+ SHOW_SEARCH: null,
+
+ TOGGLE_IMPORT_THEME_MODAL: null,
+ TOGGLE_INVITE_MEMBER_MODAL: null,
+ TOGGLE_DELETE_POST_MODAL: null,
+ TOGGLE_GET_POST_LINK_MODAL: null,
+ TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
+ TOGGLE_REGISTER_APP_MODAL: null,
+
+ SUGGESTION_PRETEXT_CHANGED: null,
+ SUGGESTION_RECEIVED_SUGGESTIONS: null,
+ SUGGESTION_CLEAR_SUGGESTIONS: null,
+ SUGGESTION_COMPLETE_WORD: null,
+ SUGGESTION_SELECT_NEXT: null,
+ SUGGESTION_SELECT_PREVIOUS: null
+ }),
+
+ PayloadSources: keyMirror({
+ SERVER_ACTION: null,
+ VIEW_ACTION: null
+ }),
+
+ StatTypes: keyMirror({
+ TOTAL_USERS: null,
+ TOTAL_PUBLIC_CHANNELS: null,
+ TOTAL_PRIVATE_GROUPS: null,
+ TOTAL_POSTS: null,
+ TOTAL_TEAMS: null,
+ TOTAL_FILE_POSTS: null,
+ TOTAL_HASHTAG_POSTS: null,
+ TOTAL_IHOOKS: null,
+ TOTAL_OHOOKS: null,
+ TOTAL_COMMANDS: null,
+ TOTAL_SESSIONS: null,
+ POST_PER_DAY: null,
+ USERS_WITH_POSTS_PER_DAY: null,
+ RECENTLY_ACTIVE_USERS: null,
+ NEWLY_CREATED_USERS: null
+ }),
+ STAT_MAX_ACTIVE_USERS: 20,
+ STAT_MAX_NEW_USERS: 20,
+
+ SocketEvents: {
+ POSTED: 'posted',
+ POST_EDITED: 'post_edited',
+ POST_DELETED: 'post_deleted',
+ CHANNEL_VIEWED: 'channel_viewed',
+ NEW_USER: 'new_user',
+ USER_ADDED: 'user_added',
+ USER_REMOVED: 'user_removed',
+ TYPING: 'typing',
+ PREFERENCE_CHANGED: 'preference_changed',
+ EPHEMERAL_MESSAGE: 'ephemeral_message'
+ },
+
+ //SPECIAL_MENTIONS: ['all', 'channel'],
+ SPECIAL_MENTIONS: ['channel'],
+ CHARACTER_LIMIT: 4000,
+ IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
+ AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'],
+ VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'],
+ PRESENTATION_TYPES: ['ppt', 'pptx'],
+ SPREADSHEET_TYPES: ['xlsx', 'csv'],
+ WORD_TYPES: ['doc', 'docx'],
+ CODE_TYPES: ['css', 'html', 'js', 'php', 'rb'],
+ PDF_TYPES: ['pdf'],
+ PATCH_TYPES: ['patch'],
+ ICON_FROM_TYPE: {
+ audio: audioIcon,
+ video: videoIcon,
+ spreadsheet: excelIcon,
+ presentation: pptIcon,
+ pdf: pdfIcon,
+ code: codeIcon,
+ word: wordIcon,
+ patch: patchIcon,
+ other: genericIcon
+ },
+ ICON_NAME_FROM_TYPE: {
+ audio: 'audio',
+ video: 'video',
+ spreadsheet: 'excel',
+ presentation: 'ppt',
+ pdf: 'pdf',
+ code: 'code',
+ word: 'word',
+ patch: 'patch',
+ other: 'generic'
+ },
+ MAX_DISPLAY_FILES: 5,
+ MAX_UPLOAD_FILES: 5,
+ MAX_FILE_SIZE: 50000000, // 50 MB
+ THUMBNAIL_WIDTH: 128,
+ THUMBNAIL_HEIGHT: 100,
+ WEB_VIDEO_WIDTH: 640,
+ WEB_VIDEO_HEIGHT: 480,
+ MOBILE_VIDEO_WIDTH: 480,
+ MOBILE_VIDEO_HEIGHT: 360,
+ DEFAULT_CHANNEL: 'town-square',
+ OFFTOPIC_CHANNEL: 'off-topic',
+ GITLAB_SERVICE: 'gitlab',
+ GOOGLE_SERVICE: 'google',
+ EMAIL_SERVICE: 'email',
+ SIGNIN_CHANGE: 'signin_change',
+ SIGNIN_VERIFIED: 'verified',
+ SESSION_EXPIRED: 'expired',
+ POST_CHUNK_SIZE: 60,
+ MAX_POST_CHUNKS: 3,
+ POST_FOCUS_CONTEXT_RADIUS: 10,
+ POST_LOADING: 'loading',
+ POST_FAILED: 'failed',
+ POST_DELETED: 'deleted',
+ POST_TYPE_EPHEMERAL: 'system_ephemeral',
+ POST_TYPE_JOIN_LEAVE: 'system_join_leave',
+ SYSTEM_MESSAGE_PREFIX: 'system_',
+ SYSTEM_MESSAGE_PROFILE_NAME: 'System',
+ SYSTEM_MESSAGE_PROFILE_IMAGE: logoImage,
+ RESERVED_TEAM_NAMES: [
+ 'www',
+ 'web',
+ 'admin',
+ 'support',
+ 'notify',
+ 'test',
+ 'demo',
+ 'mail',
+ 'team',
+ 'channel',
+ 'internal',
+ 'localhost',
+ 'dockerhost',
+ 'stag',
+ 'post',
+ 'cluster',
+ 'api'
+ ],
+ RESERVED_USERNAMES: [
+ 'valet',
+ 'all',
+ 'channel'
+ ],
+ MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ MAX_DMS: 20,
+ MAX_CHANNEL_POPOVER_COUNT: 100,
+ DM_CHANNEL: 'D',
+ OPEN_CHANNEL: 'O',
+ PRIVATE_CHANNEL: 'P',
+ INVITE_TEAM: 'I',
+ OPEN_TEAM: 'O',
+ MAX_POST_LEN: 4000,
+ EMOJI_SIZE: 16,
+ ONLINE_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-243 245 12 12'style='enable-background:new -243 245 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <path class='online--icon' d='M-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5z'/> <ellipse class='online--icon' cx='-238.5' cy='248' rx='2.5' ry='2.5'/> </g> <path class='online--icon' d='M-238.9,253.8c0-0.4,0.1-0.9,0.2-1.3c-2.2-0.2-2.2-2-2.2-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5c0,0.1-0.1,0.5,0,0.6 c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0C-238.7,255.7-238.9,254.8-238.9,253.8z'/> <g> <g> <path class='online--icon' d='M-232.3,250.1l1.3,1.3c0,0,0,0.1,0,0.1l-4.1,4.1c0,0,0,0-0.1,0c0,0,0,0,0,0l-2.7-2.7c0,0,0-0.1,0-0.1l1.2-1.2 c0,0,0.1,0,0.1,0l1.4,1.4l2.9-2.9C-232.4,250.1-232.3,250.1-232.3,250.1z'/> </g> </g> </svg>",
+ AWAY_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-299 391 12 12'style='enable-background:new -299 391 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <ellipse class='away--icon' cx='-294.6' cy='394' rx='2.5' ry='2.5'/> <path class='away--icon' d='M-293.8,399.4c0-0.4,0.1-0.7,0.2-1c-0.3,0.1-0.6,0.2-1,0.2c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0.7,0,1.4-0.1,2-0.3 C-293.3,401.5-293.8,400.5-293.8,399.4z'/> </g> <path class='away--icon' d='M-287,400c0,0.1-0.1,0.1-0.1,0.1l-4.9,0c-0.1,0-0.1-0.1-0.1-0.1v-1.6c0-0.1,0.1-0.1,0.1-0.1l4.9,0c0.1,0,0.1,0.1,0.1,0.1 V400z'/> </svg>",
+ OFFLINE_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-299 391 12 12'style='enable-background:new -299 391 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <g> <ellipse class='offline--icon' cx='-294.5' cy='394' rx='2.5' ry='2.5'/> <path class='offline--icon' d='M-294.3,399.7c0-0.4,0.1-0.8,0.2-1.2c-0.1,0-0.2,0-0.4,0c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4h0.1h0.1c0.3,0,0.7,0,1-0.1C-293.9,401.6-294.3,400.7-294.3,399.7z'/> </g> </g> <g> <path class='offline--icon' d='M-288.9,399.4l1.8-1.8c0.1-0.1,0.1-0.3,0-0.3l-0.7-0.7c-0.1-0.1-0.3-0.1-0.3,0l-1.8,1.8l-1.8-1.8c-0.1-0.1-0.3-0.1-0.3,0 l-0.7,0.7c-0.1,0.1-0.1,0.3,0,0.3l1.8,1.8l-1.8,1.8c-0.1,0.1-0.1,0.3,0,0.3l0.7,0.7c0.1,0.1,0.3,0.1,0.3,0l1.8-1.8l1.8,1.8 c0.1,0.1,0.3,0.1,0.3,0l0.7-0.7c0.1-0.1,0.1-0.3,0-0.3L-288.9,399.4z'/> </g> </svg>",
+ MENU_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>",
+ COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>",
+ REPLY_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='-158 242 18 18' style='enable-background:new -158 242 18 18;' xml:space='preserve'> <path d='M-142.2,252.6c-2-3-4.8-4.7-8.3-4.8v-3.3c0-0.2-0.1-0.3-0.2-0.3s-0.3,0-0.4,0.1l-6.9,6.2c-0.1,0.1-0.1,0.2-0.1,0.3 c0,0.1,0,0.2,0.1,0.3l6.9,6.4c0.1,0.1,0.3,0.1,0.4,0.1c0.1-0.1,0.2-0.2,0.2-0.4v-3.8c4.2,0,7.4,0.4,9.6,4.4c0.1,0.1,0.2,0.2,0.3,0.2 c0,0,0.1,0,0.1,0c0.2-0.1,0.3-0.3,0.2-0.4C-140.2,257.3-140.6,255-142.2,252.6z M-150.8,252.5c-0.2,0-0.4,0.2-0.4,0.4v3.3l-6-5.5 l6-5.3v2.8c0,0.2,0.2,0.4,0.4,0.4c3.3,0,6,1.5,8,4.5c0.5,0.8,0.9,1.6,1.2,2.3C-144,252.8-147.1,252.5-150.8,252.5z'/> </svg>",
+ SCROLL_BOTTOM_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='-239 239 21 23' style='enable-background:new -239 239 21 23;' xml:space='preserve'> <path d='M-239,241.4l2.4-2.4l8.1,8.2l8.1-8.2l2.4,2.4l-10.5,10.6L-239,241.4z M-228.5,257.2l8.1-8.2l2.4,2.4l-10.5,10.6l-10.5-10.6 l2.4-2.4L-228.5,257.2z'/> </svg>",
+ UPDATE_TYPING_MS: 5000,
+ THEMES: {
+ default: {
+ type: 'Organization',
+ sidebarBg: '#2071a7',
+ sidebarText: '#fff',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#136197',
+ sidebarTextActiveBorder: '#7AB0D6',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#2f81b7',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#7DBE00',
+ awayIndicator: '#DCBD4E',
+ mentionBj: '#FBFBFB',
+ mentionColor: '#2071A7',
+ centerChannelBg: '#f2f4f8',
+ centerChannelColor: '#333333',
+ newMessageSeparator: '#FF8800',
+ linkColor: '#2f81b7',
+ buttonBg: '#1dacfc',
+ buttonColor: '#FFFFFF',
+ mentionHighlightBg: '#fff2bb',
+ mentionHighlightLink: '#2f81b7',
+ codeTheme: 'github',
+ image: defaultThemeImage
+ },
+ mattermost: {
+ type: 'Mattermost',
+ sidebarBg: '#fafafa',
+ sidebarText: '#333333',
+ sidebarUnreadText: '#333333',
+ sidebarTextHoverBg: '#e6f2fa',
+ sidebarTextActiveBorder: '#378FD2',
+ sidebarTextActiveColor: '#111111',
+ sidebarHeaderBg: '#2389d7',
+ sidebarHeaderTextColor: '#ffffff',
+ onlineIndicator: '#7DBE00',
+ awayIndicator: '#DCBD4E',
+ mentionBj: '#2389d7',
+ mentionColor: '#ffffff',
+ centerChannelBg: '#ffffff',
+ centerChannelColor: '#333333',
+ newMessageSeparator: '#FF8800',
+ linkColor: '#2389d7',
+ buttonBg: '#2389d7',
+ buttonColor: '#FFFFFF',
+ mentionHighlightBg: '#fff2bb',
+ mentionHighlightLink: '#2f81b7',
+ codeTheme: 'github',
+ image: mattermostThemeImage
+ },
+ mattermostDark: {
+ type: 'Mattermost Dark',
+ sidebarBg: '#1B2C3E',
+ sidebarText: '#fff',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#4A5664',
+ sidebarTextActiveBorder: '#39769C',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#1B2C3E',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#55C5B2',
+ awayIndicator: '#A9A14C',
+ mentionBj: '#B74A4A',
+ mentionColor: '#FFFFFF',
+ centerChannelBg: '#2F3E4E',
+ centerChannelColor: '#DDDDDD',
+ newMessageSeparator: '#5de5da',
+ linkColor: '#A4FFEB',
+ buttonBg: '#4CBBA4',
+ buttonColor: '#FFFFFF',
+ mentionHighlightBg: '#984063',
+ mentionHighlightLink: '#A4FFEB',
+ codeTheme: 'solarized-dark',
+ image: mattermostDarkThemeImage
+ },
+ windows10: {
+ type: 'Windows Dark',
+ sidebarBg: '#171717',
+ sidebarText: '#fff',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#302e30',
+ sidebarTextActiveBorder: '#196CAF',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#1f1f1f',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#0177e7',
+ awayIndicator: '#A9A14C',
+ mentionBj: '#0177e7',
+ mentionColor: '#FFFFFF',
+ centerChannelBg: '#1F1F1F',
+ centerChannelColor: '#DDDDDD',
+ newMessageSeparator: '#CC992D',
+ linkColor: '#0D93FF',
+ buttonBg: '#0177e7',
+ buttonColor: '#FFFFFF',
+ mentionHighlightBg: '#784098',
+ mentionHighlightLink: '#A4FFEB',
+ codeTheme: 'monokai',
+ image: windows10ThemeImage
+ }
+ },
+ THEME_ELEMENTS: [
+ {
+ group: 'sidebarElements',
+ id: 'sidebarBg',
+ uiName: 'Sidebar BG'
+ },
+ {
+ group: 'sidebarElements',
+ id: 'sidebarText',
+ uiName: 'Sidebar Text'
+ },
+ {
+ group: 'sidebarElements',
+ id: 'sidebarHeaderBg',
+ uiName: 'Sidebar Header BG'
+ },
+ {
+ group: 'sidebarElements',
+ id: 'sidebarHeaderTextColor',
+ uiName: 'Sidebar Header Text'
+ },
+ {
+ group: 'sidebarElements',
+ id: 'sidebarUnreadText',
+ uiName: 'Sidebar Unread Text'
+ },
+ {
+ group: 'sidebarElements',
+ id: 'sidebarTextHoverBg',
+ uiName: 'Sidebar Text Hover BG'
+ },
+ {
+ group: 'sidebarElements',
+ id: 'sidebarTextActiveBorder',
+ uiName: 'Sidebar Text Active Border'
+ },
+ {
+ group: 'sidebarElements',
+ id: 'sidebarTextActiveColor',
+ uiName: 'Sidebar Text Active Color'
+ },
+ {
+ group: 'sidebarElements',
+ id: 'onlineIndicator',
+ uiName: 'Online Indicator'
+ },
+ {
+ group: 'sidebarElements',
+ id: 'awayIndicator',
+ uiName: 'Away Indicator'
+ },
+ {
+ group: 'sidebarElements',
+ id: 'mentionBj',
+ uiName: 'Mention Jewel BG'
+ },
+ {
+ group: 'sidebarElements',
+ id: 'mentionColor',
+ uiName: 'Mention Jewel Text'
+ },
+ {
+ group: 'centerChannelElements',
+ id: 'centerChannelBg',
+ uiName: 'Center Channel BG'
+ },
+ {
+ group: 'centerChannelElements',
+ id: 'centerChannelColor',
+ uiName: 'Center Channel Text'
+ },
+ {
+ group: 'centerChannelElements',
+ id: 'newMessageSeparator',
+ uiName: 'New Message Separator'
+ },
+ {
+ group: 'centerChannelElements',
+ id: 'mentionHighlightBg',
+ uiName: 'Mention Highlight BG'
+ },
+ {
+ group: 'centerChannelElements',
+ id: 'mentionHighlightLink',
+ uiName: 'Mention Highlight Link'
+ },
+ {
+ group: 'centerChannelElements',
+ id: 'codeTheme',
+ uiName: 'Code Theme',
+ themes: [
+ {
+ id: 'solarized-dark',
+ uiName: 'Solarized Dark',
+ cssURL: solarizedDarkCSS,
+ iconURL: solarizedDarkIcon
+ },
+ {
+ id: 'solarized-light',
+ uiName: 'Solarized Light',
+ cssURL: solarizedLightCSS,
+ iconURL: solarizedLightIcon
+ },
+ {
+ id: 'github',
+ uiName: 'GitHub',
+ cssURL: githubCSS,
+ iconURL: githubIcon
+ },
+ {
+ id: 'monokai',
+ uiName: 'Monokai',
+ cssURL: monokaiCSS,
+ iconURL: monokaiIcon
+ }
+ ]
+ },
+ {
+ group: 'linkAndButtonElements',
+ id: 'linkColor',
+ uiName: 'Link Color'
+ },
+ {
+ group: 'linkAndButtonElements',
+ id: 'buttonBg',
+ uiName: 'Button BG'
+ },
+ {
+ group: 'linkAndButtonElements',
+ id: 'buttonColor',
+ uiName: 'Button Text'
+ }
+ ],
+ DEFAULT_CODE_THEME: 'github',
+ FONTS: {
+ 'Droid Serif': 'font--droid_serif',
+ 'Roboto Slab': 'font--roboto_slab',
+ Lora: 'font--lora',
+ Arvo: 'font--arvo',
+ 'Open Sans': 'font--open_sans',
+ Roboto: 'font--roboto',
+ 'PT Sans': 'font--pt_sans',
+ Lato: 'font--lato',
+ 'Source Sans Pro': 'font--source_sans_pro',
+ 'Exo 2': 'font--exo_2',
+ Ubuntu: 'font--ubuntu'
+ },
+ DEFAULT_FONT: 'Open Sans',
+ Preferences: {
+ CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
+ CATEGORY_DISPLAY_SETTINGS: 'display_settings',
+ DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
+ DISPLAY_PREFER_FULL_NAME: 'full_name',
+ CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
+ TUTORIAL_STEP: 'tutorial_step'
+ },
+ TutorialSteps: {
+ INTRO_SCREENS: 0,
+ POST_POPOVER: 1,
+ CHANNEL_POPOVER: 2,
+ MENU_POPOVER: 3
+ },
+ KeyCodes: {
+ UP: 38,
+ DOWN: 40,
+ LEFT: 37,
+ RIGHT: 39,
+ BACKSPACE: 8,
+ ENTER: 13,
+ ESCAPE: 27,
+ SPACE: 32,
+ TAB: 9
+ },
+ HighlightedLanguages: {
+ diff: 'Diff',
+ apache: 'Apache',
+ makefile: 'Makefile',
+ http: 'HTTP',
+ json: 'JSON',
+ markdown: 'Markdown',
+ javascript: 'JavaScript',
+ css: 'CSS',
+ nginx: 'nginx',
+ objectivec: 'Objective-C',
+ python: 'Python',
+ xml: 'XML',
+ perl: 'Perl',
+ bash: 'Bash',
+ php: 'PHP',
+ coffeescript: 'CoffeeScript',
+ cs: 'C#',
+ cpp: 'C++',
+ sql: 'SQL',
+ go: 'Go',
+ ruby: 'Ruby',
+ java: 'Java',
+ ini: 'ini'
+ },
+ PostsViewJumpTypes: {
+ BOTTOM: 1,
+ POST: 2,
+ SIDEBAR_OPEN: 3
+ },
+ NotificationPrefs: {
+ MENTION: 'mention'
+ },
+ FeatureTogglePrefix: 'feature_enabled_',
+ PRE_RELEASE_FEATURES: {
+ MARKDOWN_PREVIEW: {
+ label: 'markdown_preview', // github issue: https://github.com/mattermost/platform/pull/1389
+ description: 'Show markdown preview option in message input box'
+ },
+ EMBED_PREVIEW: {
+ label: 'embed_preview',
+ description: 'Show preview snippet of links below message'
+ },
+ EMBED_TOGGLE: {
+ label: 'embed_toggle',
+ description: 'Show toggle for all embed previews'
+ }
+ },
+ OVERLAY_TIME_DELAY: 400,
+ MIN_USERNAME_LENGTH: 3,
+ MAX_USERNAME_LENGTH: 64,
+ MIN_PASSWORD_LENGTH: 5,
+ MAX_PASSWORD_LENGTH: 50,
+ TIME_SINCE_UPDATE_INTERVAL: 30000,
+ MIN_HASHTAG_LINK_LENGTH: 3
+};
diff --git a/webapp/utils/delayed_action.jsx b/webapp/utils/delayed_action.jsx
new file mode 100644
index 000000000..4f6239ad0
--- /dev/null
+++ b/webapp/utils/delayed_action.jsx
@@ -0,0 +1,27 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export default class DelayedAction {
+ constructor(action) {
+ this.action = action;
+
+ this.timer = -1;
+
+ // bind fire since it doesn't get passed the correct this value with setTimeout
+ this.fire = this.fire.bind(this);
+ }
+
+ fire() {
+ this.action();
+
+ this.timer = -1;
+ }
+
+ fireAfter(timeout) {
+ if (this.timer >= 0) {
+ window.clearTimeout(this.timer);
+ }
+
+ this.timer = window.setTimeout(this.fire, timeout);
+ }
+}
diff --git a/webapp/utils/emoticons.jsx b/webapp/utils/emoticons.jsx
new file mode 100644
index 000000000..b675ca3cc
--- /dev/null
+++ b/webapp/utils/emoticons.jsx
@@ -0,0 +1,162 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+const emoticonPatterns = {
+ slightly_smiling_face: /(^|\s)(:-?\))(?=$|\s)/g, // :)
+ wink: /(^|\s)(;-?\))(?=$|\s)/g, // ;)
+ open_mouth: /(^|\s)(:o)(?=$|\s)/gi, // :o
+ scream: /(^|\s)(:-o)(?=$|\s)/gi, // :-o
+ smirk: /(^|\s)(:-?])(?=$|\s)/g, // :]
+ smile: /(^|\s)(:-?d)(?=$|\s)/gi, // :D
+ stuck_out_tongue_closed_eyes: /(^|\s)(x-d)(?=$|\s)/gi, // x-d
+ stuck_out_tongue: /(^|\s)(:-?p)(?=$|\s)/gi, // :p
+ rage: /(^|\s)(:-?[\[@])(?=$|\s)/g, // :@
+ slightly_frowning_face: /(^|\s)(:-?\()(?=$|\s)/g, // :(
+ cry: /(^|\s)(:['’]-?\(|:&#x27;\(|:&#39;\()(?=$|\s)/g, // :`(
+ confused: /(^|\s)(:-?\/)(?=$|\s)/g, // :/
+ confounded: /(^|\s)(:-?s)(?=$|\s)/gi, // :s
+ neutral_face: /(^|\s)(:-?\|)(?=$|\s)/g, // :|
+ flushed: /(^|\s)(:-?\$)(?=$|\s)/g, // :$
+ mask: /(^|\s)(:-x)(?=$|\s)/gi, // :-x
+ heart: /(^|\s)(<3|&lt;3)(?=$|\s)/g, // <3
+ broken_heart: /(^|\s)(<\/3|&lt;&#x2F;3)(?=$|\s)/g, // </3
+ thumbsup: /(^|\s)(:\+1:)(?=$|\s)/g, // :+1:
+ thumbsdown: /(^|\s)(:\-1:)(?=$|\s)/g // :-1:
+};
+
+function initializeEmoticonMap() {
+ const emoticonNames =
+ ('+1,-1,100,1234,8ball,a,ab,abc,abcd,accept,aerial_tramway,airplane,alarm_clock,alien,ambulance,anchor,angel,' +
+ 'anger,angry,anguished,ant,apple,aquarius,aries,arrow_backward,arrow_double_down,arrow_double_up,arrow_down,' +
+ 'arrow_down_small,arrow_forward,arrow_heading_down,arrow_heading_up,arrow_left,arrow_lower_left,' +
+ 'arrow_lower_right,arrow_right,arrow_right_hook,arrow_up,arrow_up_down,arrow_up_small,arrow_upper_left,' +
+ 'arrow_upper_right,arrows_clockwise,arrows_counterclockwise,art,articulated_lorry,astonished,atm,b,baby,' +
+ 'baby_bottle,baby_chick,baby_symbol,back,baggage_claim,balloon,ballot_box_with_check,bamboo,banana,bangbang,' +
+ 'bank,bar_chart,barber,baseball,basketball,bath,bathtub,battery,bear,bee,beer,beers,beetle,beginner,bell,bento,' +
+ 'bicyclist,bike,bikini,bird,birthday,black_circle,black_joker,black_medium_small_square,black_medium_square,' +
+ 'black_large_square,black_nib,black_small_square,black_square,black_square_button,blossom,blowfish,blue_book,' +
+ 'blue_car,blue_heart,blush,boar,boat,bomb,book,bookmark,bookmark_tabs,books,boom,boot,bouquet,bow,bowling,bowtie,' +
+ 'boy,bread,bride_with_veil,bridge_at_night,briefcase,broken_heart,bug,bulb,bullettrain_front,bullettrain_side,bus,' +
+ 'busstop,bust_in_silhouette,busts_in_silhouette,cactus,cake,calendar,calling,camel,camera,cancer,candy,capital_abcd,' +
+ 'capricorn,car,card_index,carousel_horse,cat,cat2,cd,chart,chart_with_downwards_trend,chart_with_upwards_trend,' +
+ 'checkered_flag,cherries,cherry_blossom,chestnut,chicken,children_crossing,chocolate_bar,christmas_tree,church,' +
+ 'cinema,circus_tent,city_sunrise,city_sunset,cl,clap,clapper,clipboard,clock1,clock10,clock1030,clock11,' +
+ 'clock1130,clock12,clock1230,clock130,clock2,clock230,clock3,clock330,clock4,clock430,clock5,clock530,clock6,' +
+ 'clock630,clock7,clock730,clock8,clock830,clock9,clock930,closed_book,closed_lock_with_key,closed_umbrella,cloud,' +
+ 'clubs,cn,cocktail,coffee,cold_sweat,collision,computer,confetti_ball,confounded,confused,congratulations,' +
+ 'construction,construction_worker,convenience_store,cookie,cool,cop,copyright,corn,couple,couple_with_heart,' +
+ 'couplekiss,cow,cow2,credit_card,crescent_moon,crocodile,crossed_flags,crown,cry,crying_cat_face,crystal_ball,' +
+ 'cupid,curly_loop,currency_exchange,curry,custard,customs,cyclone,dancer,dancers,dango,dart,dash,date,de,' +
+ 'deciduous_tree,department_store,diamond_shape_with_a_dot_inside,diamonds,disappointed,disappointed_relieved,' +
+ 'dizzy,dizzy_face,do_not_litter,dog,dog2,dollar,dolls,dolphin,donut,door,doughnut,dragon,dragon_face,dress,' +
+ 'dromedary_camel,droplet,dvd,e-mail,ear,ear_of_rice,earth_africa,earth_americas,earth_asia,egg,eggplant,eight,' +
+ 'eight_pointed_black_star,eight_spoked_asterisk,electric_plug,elephant,email,end,envelope,es,euro,' +
+ 'european_castle,european_post_office,evergreen_tree,exclamation,expressionless,eyeglasses,eyes,facepunch,' +
+ 'factory,fallen_leaf,family,fast_forward,fax,fearful,feelsgood,feet,ferris_wheel,file_folder,finnadie,fire,' +
+ 'fire_engine,fireworks,first_quarter_moon,first_quarter_moon_with_face,fish,fish_cake,fishing_pole_and_fish,fist,' +
+ 'five,flags,flashlight,floppy_disk,flower_playing_cards,flushed,foggy,football,fork_and_knife,fountain,four,' +
+ 'four_leaf_clover,fr,free,fried_shrimp,fries,frog,frowning,fu,fuelpump,full_moon,full_moon_with_face,game_die,gb,' +
+ 'gem,gemini,ghost,gift,gift_heart,girl,globe_with_meridians,goat,goberserk,godmode,golf,grapes,green_apple,' +
+ 'green_book,green_heart,grey_exclamation,grey_question,grimacing,grin,grinning,guardsman,guitar,gun,haircut,' +
+ 'hamburger,hammer,hamster,hand,handbag,hankey,hash,hatched_chick,hatching_chick,headphones,hear_no_evil,heart,' +
+ 'heart_decoration,heart_eyes,heart_eyes_cat,heartbeat,heartpulse,hearts,heavy_check_mark,heavy_division_sign,' +
+ 'heavy_dollar_sign,heavy_exclamation_mark,heavy_minus_sign,heavy_multiplication_x,heavy_plus_sign,helicopter,' +
+ 'herb,hibiscus,high_brightness,high_heel,hocho,honey_pot,honeybee,horse,horse_racing,hospital,hotel,hotsprings,' +
+ 'hourglass,hourglass_flowing_sand,house,house_with_garden,hurtrealbad,hushed,ice_cream,icecream,id,' +
+ 'ideograph_advantage,imp,inbox_tray,incoming_envelope,information_desk_person,information_source,innocent,' +
+ 'interrobang,iphone,it,izakaya_lantern,jack_o_lantern,japan,japanese_castle,japanese_goblin,japanese_ogre,jeans,' +
+ 'joy,joy_cat,jp,key,keycap_ten,kimono,kiss,kissing,kissing_cat,kissing_closed_eyes,kissing_face,kissing_heart,' +
+ 'kissing_smiling_eyes,koala,koko,kr,large_blue_circle,large_blue_diamond,large_orange_diamond,last_quarter_moon,' +
+ 'last_quarter_moon_with_face,laughing,leaves,ledger,left_luggage,left_right_arrow,leftwards_arrow_with_hook,' +
+ 'lemon,leo,leopard,libra,light_rail,link,lips,lipstick,lock,lock_with_ink_pen,lollipop,loop,loudspeaker,' +
+ 'love_hotel,love_letter,low_brightness,m,mag,mag_right,mahjong,mailbox,mailbox_closed,mailbox_with_mail,' +
+ 'mailbox_with_no_mail,man,man_with_gua_pi_mao,man_with_turban,mans_shoe,maple_leaf,mask,massage,meat_on_bone,' +
+ 'mega,melon,memo,mens,metal,metro,microphone,microscope,milky_way,minibus,minidisc,mobile_phone_off,' +
+ 'money_with_wings,moneybag,monkey,monkey_face,monorail,mortar_board,mount_fuji,mountain_bicyclist,' +
+ 'mountain_cableway,mountain_railway,mouse,mouse2,movie_camera,moyai,muscle,mushroom,musical_keyboard,' +
+ 'musical_note,musical_score,mute,nail_care,name_badge,neckbeard,necktie,negative_squared_cross_mark,' +
+ 'neutral_face,new,new_moon,new_moon_with_face,newspaper,ng,nine,no_bell,no_bicycles,no_entry,no_entry_sign,' +
+ 'no_good,no_mobile_phones,no_mouth,no_pedestrians,no_smoking,non-potable_water,nose,notebook,' +
+ 'notebook_with_decorative_cover,notes,nut_and_bolt,o,o2,ocean,octocat,octopus,oden,office,ok,ok_hand,' +
+ 'ok_woman,older_man,older_woman,on,oncoming_automobile,oncoming_bus,oncoming_police_car,oncoming_taxi,one,' +
+ 'open_file_folder,open_hands,open_mouth,ophiuchus,orange_book,outbox_tray,ox,package,page_facing_up,' +
+ 'page_with_curl,pager,palm_tree,panda_face,paperclip,parking,part_alternation_mark,partly_sunny,' +
+ 'passport_control,paw_prints,peach,pear,pencil,pencil2,penguin,pensive,performing_arts,persevere,' +
+ 'person_frowning,person_with_blond_hair,person_with_pouting_face,phone,pig,pig2,pig_nose,pill,pineapple,pisces,' +
+ 'pizza,plus1,point_down,point_left,point_right,point_up,point_up_2,police_car,poodle,poop,post_office,' +
+ 'postal_horn,postbox,potable_water,pouch,poultry_leg,pound,pouting_cat,pray,princess,punch,purple_heart,purse,' +
+ 'pushpin,put_litter_in_its_place,question,rabbit,rabbit2,racehorse,radio,radio_button,rage,rage1,rage2,rage3,' +
+ 'rage4,railway_car,rainbow,raised_hand,raised_hands,raising_hand,ram,ramen,rat,recycle,red_car,red_circle,' +
+ 'registered,relaxed,relieved,repeat,repeat_one,restroom,revolving_hearts,rewind,ribbon,rice,rice_ball,' +
+ 'rice_cracker,rice_scene,ring,rocket,roller_coaster,rooster,rose,rotating_light,round_pushpin,rowboat,ru,' +
+ 'rugby_football,runner,running,running_shirt_with_sash,sa,sagittarius,sailboat,sake,sandal,santa,satellite,' +
+ 'satisfied,saxophone,school,school_satchel,scissors,scorpius,scream,scream_cat,scroll,seat,secret,see_no_evil,' +
+ 'seedling,seven,shaved_ice,sheep,shell,ship,shipit,shirt,shit,shoe,shower,signal_strength,six,six_pointed_star,' +
+ 'ski,skull,sleeping,sleepy,slightly_smiling_face,slightly_frowning_face,slot_machine,small_blue_diamond,' +
+ 'small_orange_diamond,small_red_triangle,small_red_triangle_down,smile,smile_cat,smiley,smiley_cat,smiling_imp,' +
+ 'smirk,smirk_cat,smoking,snail,snake,snowboarder,snowflake,snowman,sob,soccer,soon,sos,sound,space_invader,spades,' +
+ 'spaghetti,sparkle,sparkler,sparkles,sparkling_heart,speak_no_evil,speaker,speech_balloon,speedboat,squirrel,star,' +
+ 'star2,stars,station,statue_of_liberty,steam_locomotive,stew,straight_ruler,strawberry,stuck_out_tongue,' +
+ 'stuck_out_tongue_closed_eyes,stuck_out_tongue_winking_eye,sun_with_face,sunflower,sunglasses,sunny,sunrise,' +
+ 'sunrise_over_mountains,surfer,sushi,suspect,suspension_railway,sweat,sweat_drops,sweat_smile,sweet_potato,swimmer,' +
+ 'symbols,syringe,tada,tanabata_tree,tangerine,taurus,taxi,tea,telephone,telephone_receiver,telescope,tennis,tent,' +
+ 'thought_balloon,three,thumbsdown,thumbsup,ticket,tiger,tiger2,tired_face,tm,toilet,tokyo_tower,tomato,tongue,top,' +
+ 'tophat,tractor,traffic_light,train,train2,tram,triangular_flag_on_post,triangular_ruler,trident,triumph,trolleybus,' +
+ 'trollface,trophy,tropical_drink,tropical_fish,truck,trumpet,tshirt,tulip,turtle,tv,twisted_rightwards_arrows,' +
+ 'two,two_hearts,two_men_holding_hands,two_women_holding_hands,u5272,u5408,u55b6,u6307,u6708,u6709,u6e80,u7121,' +
+ 'u7533,u7981,u7a7a,uk,umbrella,unamused,underage,unlock,up,us,v,vertical_traffic_light,vhs,vibration_mode,' +
+ 'video_camera,video_game,violin,virgo,volcano,vs,walking,waning_crescent_moon,waning_gibbous_moon,warning,watch,' +
+ 'water_buffalo,watermelon,wave,wavy_dash,waxing_crescent_moon,waxing_gibbous_moon,wc,weary,wedding,whale,whale2,' +
+ 'wheelchair,white_check_mark,white_circle,white_flower,white_large_square,white_medium_small_square,' +
+ 'white_medium_square,white_small_square,white_square_button,wind_chime,wine_glass,wink,wolf,woman,' +
+ 'womans_clothes,womans_hat,womens,worried,wrench,x,yellow_heart,yen,yum,zap,zero,zzz').split(',');
+
+ // use a map to help make lookups faster instead of having to use indexOf on an array
+ const out = new Map();
+
+ for (let i = 0; i < emoticonNames.length; i++) {
+ out.set(emoticonNames[i], true);
+ }
+
+ return out;
+}
+
+export const emoticonMap = initializeEmoticonMap();
+
+export function handleEmoticons(text, tokens) {
+ let output = text;
+
+ function replaceEmoticonWithToken(fullMatch, prefix, matchText, name) {
+ if (emoticonMap.has(name)) {
+ const index = tokens.size;
+ const alias = `MM_EMOTICON${index}`;
+
+ tokens.set(alias, {
+ value: `<img align="absmiddle" alt="${matchText}" class="emoticon" src="${getImagePathForEmoticon(name)}" title="${matchText}" />`,
+ originalText: fullMatch
+ });
+
+ return prefix + alias;
+ }
+
+ return fullMatch;
+ }
+
+ output = output.replace(/(^|\s)(:([a-zA-Z0-9_-]+):)(?=$|\s)/g, (fullMatch, prefix, matchText, name) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name));
+
+ $.each(emoticonPatterns, (name, pattern) => {
+ // this might look a bit funny, but since the name isn't contained in the actual match
+ // like with the named emoticons, we need to add it in manually
+ output = output.replace(pattern, (fullMatch, prefix, matchText) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name));
+ });
+
+ return output;
+}
+
+export function getImagePathForEmoticon(name) {
+ if (name) {
+ return `/static/emoji/${name}.png`;
+ }
+ return '/static/emoji';
+}
diff --git a/webapp/utils/markdown.jsx b/webapp/utils/markdown.jsx
new file mode 100644
index 000000000..635a39290
--- /dev/null
+++ b/webapp/utils/markdown.jsx
@@ -0,0 +1,577 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import highlightJs from 'highlight.js/lib/highlight.js';
+import highlightJsDiff from 'highlight.js/lib/languages/diff.js';
+import highlightJsApache from 'highlight.js/lib/languages/apache.js';
+import highlightJsMakefile from 'highlight.js/lib/languages/makefile.js';
+import highlightJsHttp from 'highlight.js/lib/languages/http.js';
+import highlightJsJson from 'highlight.js/lib/languages/json.js';
+import highlightJsMarkdown from 'highlight.js/lib/languages/markdown.js';
+import highlightJsJavascript from 'highlight.js/lib/languages/javascript.js';
+import highlightJsCss from 'highlight.js/lib/languages/css.js';
+import highlightJsNginx from 'highlight.js/lib/languages/nginx.js';
+import highlightJsObjectivec from 'highlight.js/lib/languages/objectivec.js';
+import highlightJsPython from 'highlight.js/lib/languages/python.js';
+import highlightJsXml from 'highlight.js/lib/languages/xml.js';
+import highlightJsPerl from 'highlight.js/lib/languages/perl.js';
+import highlightJsBash from 'highlight.js/lib/languages/bash.js';
+import highlightJsPhp from 'highlight.js/lib/languages/php.js';
+import highlightJsCoffeescript from 'highlight.js/lib/languages/coffeescript.js';
+import highlightJsCs from 'highlight.js/lib/languages/cs.js';
+import highlightJsCpp from 'highlight.js/lib/languages/cpp.js';
+import highlightJsSql from 'highlight.js/lib/languages/sql.js';
+import highlightJsGo from 'highlight.js/lib/languages/go.js';
+import highlightJsRuby from 'highlight.js/lib/languages/ruby.js';
+import highlightJsJava from 'highlight.js/lib/languages/java.js';
+import highlightJsIni from 'highlight.js/lib/languages/ini.js';
+
+highlightJs.registerLanguage('diff', highlightJsDiff);
+highlightJs.registerLanguage('apache', highlightJsApache);
+highlightJs.registerLanguage('makefile', highlightJsMakefile);
+highlightJs.registerLanguage('http', highlightJsHttp);
+highlightJs.registerLanguage('json', highlightJsJson);
+highlightJs.registerLanguage('markdown', highlightJsMarkdown);
+highlightJs.registerLanguage('javascript', highlightJsJavascript);
+highlightJs.registerLanguage('css', highlightJsCss);
+highlightJs.registerLanguage('nginx', highlightJsNginx);
+highlightJs.registerLanguage('objectivec', highlightJsObjectivec);
+highlightJs.registerLanguage('python', highlightJsPython);
+highlightJs.registerLanguage('xml', highlightJsXml);
+highlightJs.registerLanguage('perl', highlightJsPerl);
+highlightJs.registerLanguage('bash', highlightJsBash);
+highlightJs.registerLanguage('php', highlightJsPhp);
+highlightJs.registerLanguage('coffeescript', highlightJsCoffeescript);
+highlightJs.registerLanguage('cs', highlightJsCs);
+highlightJs.registerLanguage('cpp', highlightJsCpp);
+highlightJs.registerLanguage('sql', highlightJsSql);
+highlightJs.registerLanguage('go', highlightJsGo);
+highlightJs.registerLanguage('ruby', highlightJsRuby);
+highlightJs.registerLanguage('java', highlightJsJava);
+highlightJs.registerLanguage('ini', highlightJsIni);
+
+import * as TextFormatting from './text_formatting.jsx';
+import * as Utils from './utils.jsx';
+
+import marked from 'marked';
+import katex from 'katex';
+import 'katex/dist/katex.min.css';
+
+import Constants from 'utils/constants.jsx';
+const HighlightedLanguages = Constants.HighlightedLanguages;
+
+function markdownImageLoaded(image) {
+ image.style.height = 'auto';
+}
+window.markdownImageLoaded = markdownImageLoaded;
+
+class MattermostInlineLexer extends marked.InlineLexer {
+ constructor(links, options) {
+ super(links, options);
+
+ this.rules = Object.assign({}, this.rules);
+
+ // modified version of the regex that allows for links starting with www and those surrounded by parentheses
+ // the original is /^[\s\S]+?(?=[\\<!\[_*`~]|https?:\/\/| {2,}\n|$)/
+ this.rules.text = /^[\s\S]+?(?=[\\<!\[_*`~]|https?:\/\/|www\.|\(| {2,}\n|$)/;
+
+ // modified version of the regex that allows links starting with www and those surrounded by parentheses
+ // the original is /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/
+ this.rules.url = /^(\(?(?:https?:\/\/|www\.)[^\s<.][^\s<]*[^<.,:;"'\]\s])/;
+
+ // modified version of the regex that allows <links> starting with www.
+ // the original is /^<([^ >]+(@|:\/)[^ >]+)>/
+ this.rules.autolink = /^<((?:[^ >]+(@|:\/)|www\.)[^ >]+)>/;
+ }
+}
+
+class MattermostParser extends marked.Parser {
+ parse(src) {
+ this.inline = new MattermostInlineLexer(src.links, this.options, this.renderer);
+ this.tokens = src.reverse();
+
+ var out = '';
+ while (this.next()) {
+ out += this.tok();
+ }
+
+ return out;
+ }
+}
+
+class MattermostMarkdownRenderer extends marked.Renderer {
+ constructor(options, formattingOptions = {}) {
+ super(options);
+
+ this.heading = this.heading.bind(this);
+ this.paragraph = this.paragraph.bind(this);
+ this.text = this.text.bind(this);
+
+ this.formattingOptions = formattingOptions;
+ }
+
+ code(code, language, escaped) {
+ let usedLanguage = language || '';
+ usedLanguage = usedLanguage.toLowerCase();
+
+ // treat html as xml to prevent injection attacks
+ if (usedLanguage === 'html') {
+ usedLanguage = 'xml';
+ }
+
+ if (HighlightedLanguages[usedLanguage]) {
+ const parsed = highlightJs.highlight(usedLanguage, code);
+
+ return (
+ '<div class="post-body--code">' +
+ '<span class="post-body--code__language">' +
+ HighlightedLanguages[usedLanguage] +
+ '</span>' +
+ '<pre>' +
+ '<code class="hljs">' +
+ parsed.value +
+ '</code>' +
+ '</pre>' +
+ '</div>'
+ );
+ } else if (usedLanguage === 'tex' || usedLanguage === 'latex') {
+ try {
+ const html = katex.renderToString(code, {throwOnError: false, displayMode: true});
+
+ return '<div class="post-body--code tex">' + html + '</div>';
+ } catch (e) {
+ // fall through if latex parsing fails and handle below
+ }
+ }
+
+ return (
+ '<pre>' +
+ '<code class="hljs">' +
+ (escaped ? code : TextFormatting.sanitizeHtml(code)) + '\n' +
+ '</code>' +
+ '</pre>'
+ );
+ }
+
+ codespan(text) {
+ return '<span class="codespan__pre-wrap">' + super.codespan(text) + '</span>';
+ }
+
+ br() {
+ if (this.formattingOptions.singleline) {
+ return ' ';
+ }
+
+ return super.br();
+ }
+
+ image(href, title, text) {
+ let out = '<img src="' + href + '" alt="' + text + '"';
+ if (title) {
+ out += ' title="' + title + '"';
+ }
+ out += ' onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img"';
+ out += this.options.xhtml ? '/>' : '>';
+ return out;
+ }
+
+ heading(text, level, raw) {
+ const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`;
+ return `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`;
+ }
+
+ link(href, title, text) {
+ let outHref = href;
+ let outText = text;
+ let prefix = '';
+ let suffix = '';
+
+ // some links like https://en.wikipedia.org/wiki/Rendering_(computer_graphics) contain brackets
+ // and we try our best to differentiate those from ones just wrapped in brackets when autolinking
+ if (outHref.startsWith('(') && outHref.endsWith(')') && text === outHref) {
+ prefix = '(';
+ suffix = ')';
+ outText = text.substring(1, text.length - 1);
+ outHref = outHref.substring(1, outHref.length - 1);
+ }
+
+ try {
+ const unescaped = decodeURIComponent(unescape(href)).replace(/[^\w:]/g, '').toLowerCase();
+
+ if (unescaped.indexOf('javascript:') === 0 || unescaped.indexOf('vbscript:') === 0) { // eslint-disable-line no-script-url
+ return '';
+ }
+ } catch (e) {
+ return '';
+ }
+
+ if (!(/[a-z+.-]+:/i).test(outHref)) {
+ outHref = `http://${outHref}`;
+ }
+
+ let output = '<a class="theme markdown__link" href="' + outHref + '"';
+ if (title) {
+ output += ' title="' + title + '"';
+ }
+
+ if (outHref.lastIndexOf(Utils.getTeamURLFromAddressBar(), 0) === 0) {
+ output += '>';
+ } else {
+ output += ' target="_blank">';
+ }
+
+ output += outText + '</a>';
+
+ return prefix + output + suffix;
+ }
+
+ paragraph(text) {
+ if (this.formattingOptions.singleline) {
+ return `<p class="markdown__paragraph-inline">${text}</p>`;
+ }
+
+ return super.paragraph(text);
+ }
+
+ table(header, body) {
+ return `<div class="table-responsive"><table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table></div>`;
+ }
+
+ listitem(text) {
+ const taskListReg = /^\[([ |xX])\] /;
+ const isTaskList = taskListReg.exec(text);
+
+ if (isTaskList) {
+ return `<li class="list-item--task-list">${'<input type="checkbox" disabled="disabled" ' + (isTaskList[1] === ' ' ? '' : 'checked="checked" ') + '/> '}${text.replace(taskListReg, '')}</li>`;
+ }
+ return `<li>${text}</li>`;
+ }
+
+ text(txt) {
+ return TextFormatting.doFormatText(txt, this.formattingOptions);
+ }
+}
+
+class MattermostLexer extends marked.Lexer {
+ token(originalSrc, top, bq) {
+ let src = originalSrc.replace(/^ +$/gm, '');
+
+ while (src) {
+ // newline
+ let cap = this.rules.newline.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ if (cap[0].length > 1) {
+ this.tokens.push({
+ type: 'space'
+ });
+ }
+ }
+
+ // code
+ cap = this.rules.code.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ cap = cap[0].replace(/^ {4}/gm, '');
+ this.tokens.push({
+ type: 'code',
+ text: this.options.pedantic ? cap : cap.replace(/\n+$/, '')
+ });
+ continue;
+ }
+
+ // fences (gfm)
+ cap = this.rules.fences.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'code',
+ lang: cap[2],
+ text: cap[3] || ''
+ });
+ continue;
+ }
+
+ // heading
+ cap = this.rules.heading.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'heading',
+ depth: cap[1].length,
+ text: cap[2]
+ });
+ continue;
+ }
+
+ // table no leading pipe (gfm)
+ cap = this.rules.nptable.exec(src);
+ if (top && cap) {
+ src = src.substring(cap[0].length);
+
+ const item = {
+ type: 'table',
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+ cells: cap[3].replace(/\n$/, '').split('\n')
+ };
+
+ for (let i = 0; i < item.align.length; i++) {
+ if (/^ *-+: *$/.test(item.align[i])) {
+ item.align[i] = 'right';
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
+ item.align[i] = 'center';
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
+ item.align[i] = 'left';
+ } else {
+ item.align[i] = null;
+ }
+ }
+
+ for (let i = 0; i < item.cells.length; i++) {
+ item.cells[i] = item.cells[i].split(/ *\| */);
+ }
+
+ this.tokens.push(item);
+
+ continue;
+ }
+
+ // lheading
+ cap = this.rules.lheading.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'heading',
+ depth: cap[2] === '=' ? 1 : 2,
+ text: cap[1]
+ });
+ continue;
+ }
+
+ // hr
+ cap = this.rules.hr.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'hr'
+ });
+ continue;
+ }
+
+ // blockquote
+ cap = this.rules.blockquote.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+
+ this.tokens.push({
+ type: 'blockquote_start'
+ });
+
+ cap = cap[0].replace(/^ *> ?/gm, '');
+
+ // Pass `top` to keep the current
+ // "toplevel" state. This is exactly
+ // how markdown.pl works.
+ this.token(cap, top, true);
+
+ this.tokens.push({
+ type: 'blockquote_end'
+ });
+
+ continue;
+ }
+
+ // list
+ cap = this.rules.list.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ const bull = cap[2];
+
+ this.tokens.push({
+ type: 'list_start',
+ ordered: bull.length > 1
+ });
+
+ // Get each top-level item.
+ cap = cap[0].match(this.rules.item);
+
+ let next = false;
+ const l = cap.length;
+ let i = 0;
+
+ for (; i < l; i++) {
+ let item = cap[i];
+
+ // Remove the list item's bullet
+ // so it is seen as the next token.
+ let space = item.length;
+ item = item.replace(/^ *([*+-]|\d+\.) +/, '');
+
+ // Outdent whatever the
+ // list item contains. Hacky.
+ if (~item.indexOf('\n ')) {
+ space -= item.length;
+ item = this.options.pedantic ?
+ item.replace(/^ {1,4}/gm, '') :
+ item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '');
+ }
+
+ // Determine whether the next list item belongs here.
+ // Backpedal if it does not belong in this list.
+ if (this.options.smartLists && i !== l - 1) {
+ const b = this.rules.bullet.exec(cap[i + 1])[0];
+ if (bull !== b && !(bull.length > 1 && b.length > 1)) {
+ src = cap.slice(i + 1).join('\n') + src;
+ i = l - 1;
+ }
+ }
+
+ // Determine whether item is loose or not.
+ // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
+ // for discount behavior.
+ let loose = next || (/\n\n(?!\s*$)/).test(item);
+ if (i !== l - 1) {
+ next = item.charAt(item.length - 1) === '\n';
+ if (!loose) {
+ loose = next;
+ }
+ }
+
+ this.tokens.push({
+ type: loose ?
+ 'loose_item_start' :
+ 'list_item_start'
+ });
+
+ // Recurse.
+ this.token(item, false, bq);
+
+ this.tokens.push({
+ type: 'list_item_end'
+ });
+ }
+
+ this.tokens.push({
+ type: 'list_end'
+ });
+
+ continue;
+ }
+
+ // html
+ cap = this.rules.html.exec(src);
+ if (cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: this.options.sanitize ? 'paragraph' : 'html',
+ pre: !this.options.sanitizer && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
+ text: cap[0]
+ });
+ continue;
+ }
+
+ // def
+ cap = this.rules.def.exec(src);
+ if ((!bq && top) && cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.links[cap[1].toLowerCase()] = {
+ href: cap[2],
+ title: cap[3]
+ };
+ continue;
+ }
+
+ // table (gfm)
+ cap = this.rules.table.exec(src);
+ if (top && cap) {
+ src = src.substring(cap[0].length);
+
+ const item = {
+ type: 'table',
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+ cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
+ };
+
+ for (let i = 0; i < item.align.length; i++) {
+ if (/^ *-+: *$/.test(item.align[i])) {
+ item.align[i] = 'right';
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
+ item.align[i] = 'center';
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
+ item.align[i] = 'left';
+ } else {
+ item.align[i] = null;
+ }
+ }
+
+ for (let i = 0; i < item.cells.length; i++) {
+ item.cells[i] = item.cells[i].replace(/^ *\| *| *\| *$/g, '').split(/ *\| */);
+ }
+
+ this.tokens.push(item);
+
+ continue;
+ }
+
+ // top-level paragraph
+ cap = this.rules.paragraph.exec(src);
+ if (top && cap) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'paragraph',
+ text: cap[1].charAt(cap[1].length - 1) === '\n' ? cap[1].slice(0, -1) : cap[1]
+ });
+ continue;
+ }
+
+ // text
+ cap = this.rules.text.exec(src);
+ if (cap) {
+ // Top-level should never reach here.
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'text',
+ text: cap[0]
+ });
+ continue;
+ }
+
+ if (src) {
+ throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
+ }
+ }
+
+ return this.tokens;
+ }
+}
+
+export function format(text, options) {
+ const markdownOptions = {
+ renderer: new MattermostMarkdownRenderer(null, options),
+ sanitize: true,
+ gfm: true,
+ tables: true
+ };
+
+ const tokens = new MattermostLexer(markdownOptions).lex(text);
+
+ return new MattermostParser(markdownOptions).parse(tokens);
+}
+
+// Marked helper functions that should probably just be exported
+
+function unescape(html) {
+ return html.replace(/&([#\w]+);/g, (_, m) => {
+ const n = m.toLowerCase();
+ if (n === 'colon') {
+ return ':';
+ } else if (n.charAt(0) === '#') {
+ return n.charAt(1) === 'x' ?
+ String.fromCharCode(parseInt(n.substring(2), 16)) :
+ String.fromCharCode(+n.substring(1));
+ }
+ return '';
+ });
+}
diff --git a/webapp/utils/text_formatting.jsx b/webapp/utils/text_formatting.jsx
new file mode 100644
index 000000000..9833b995c
--- /dev/null
+++ b/webapp/utils/text_formatting.jsx
@@ -0,0 +1,402 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Autolinker from 'autolinker';
+import Constants from './constants.jsx';
+import * as Emoticons from './emoticons.jsx';
+import * as Markdown from './markdown.jsx';
+import UserStore from 'stores/user_store.jsx';
+import * as Utils from './utils.jsx';
+
+// Performs formatting of user posts including highlighting mentions and search terms and converting urls, hashtags, and
+// @mentions to links by taking a user's message and returning a string of formatted html. Also takes a number of options
+// as part of the second parameter:
+// - searchTerm - If specified, this word is highlighted in the resulting html. Defaults to nothing.
+// - mentionHighlight - Specifies whether or not to highlight mentions of the current user. Defaults to true.
+// - singleline - Specifies whether or not to remove newlines. Defaults to false.
+// - emoticons - Enables emoticon parsing. Defaults to true.
+// - markdown - Enables markdown parsing. Defaults to true.
+export function formatText(text, options = {}) {
+ let output;
+
+ if (!('markdown' in options) || options.markdown) {
+ // the markdown renderer will call doFormatText as necessary
+ output = Markdown.format(text, options);
+ } else {
+ output = sanitizeHtml(text);
+ output = doFormatText(output, options);
+ }
+
+ // replace newlines with spaces if necessary
+ if (options.singleline) {
+ output = replaceNewlines(output);
+ }
+
+ return output;
+}
+
+// Performs most of the actual formatting work for formatText. Not intended to be called normally.
+export function doFormatText(text, options) {
+ let output = text;
+
+ const tokens = new Map();
+
+ // replace important words and phrases with tokens
+ output = autolinkAtMentions(output, tokens);
+ output = autolinkEmails(output, tokens);
+ output = autolinkHashtags(output, tokens);
+
+ if (!('emoticons' in options) || options.emoticon) {
+ output = Emoticons.handleEmoticons(output, tokens);
+ }
+
+ if (options.searchTerm) {
+ output = highlightSearchTerm(output, tokens, options.searchTerm);
+ }
+
+ if (!('mentionHighlight' in options) || options.mentionHighlight) {
+ output = highlightCurrentMentions(output, tokens);
+ }
+
+ // reinsert tokens with formatted versions of the important words and phrases
+ output = replaceTokens(output, tokens);
+
+ return output;
+}
+
+export function sanitizeHtml(text) {
+ let output = text;
+
+ // normal string.replace only does a single occurrance so use a regex instead
+ output = output.replace(/&/g, '&amp;');
+ output = output.replace(/</g, '&lt;');
+ output = output.replace(/>/g, '&gt;');
+ output = output.replace(/'/g, '&apos;');
+ output = output.replace(/"/g, '&quot;');
+
+ return output;
+}
+
+// Convert emails into tokens
+function autolinkEmails(text, tokens) {
+ function replaceEmailWithToken(autolinker, match) {
+ const linkText = match.getMatchedText();
+ let url = linkText;
+
+ if (match.getType() === 'email') {
+ url = `mailto:${url}`;
+ }
+
+ const index = tokens.size;
+ const alias = `MM_EMAIL${index}`;
+
+ tokens.set(alias, {
+ value: `<a class="theme" href="${url}">${linkText}</a>`,
+ originalText: linkText
+ });
+
+ return alias;
+ }
+
+ // we can't just use a static autolinker because we need to set replaceFn
+ const autolinker = new Autolinker({
+ urls: false,
+ email: true,
+ phone: false,
+ twitter: false,
+ hashtag: false,
+ replaceFn: replaceEmailWithToken
+ });
+
+ return autolinker.link(text);
+}
+
+function autolinkAtMentions(text, tokens) {
+ // Return true if provided character is punctuation
+ function isPunctuation(character) {
+ const re = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]/g;
+ return re.test(character);
+ }
+
+ // Test if provided text needs to be highlighted, special mention or current user
+ function mentionExists(u) {
+ return (Constants.SPECIAL_MENTIONS.indexOf(u) !== -1 || UserStore.getProfileByUsername(u));
+ }
+
+ function addToken(username, mention) {
+ const index = tokens.size;
+ const alias = `MM_ATMENTION${index}`;
+
+ tokens.set(alias, {
+ value: `<a class='mention-link' href='#' data-mention='${username}'>${mention}</a>`,
+ originalText: mention
+ });
+ return alias;
+ }
+
+ function replaceAtMentionWithToken(fullMatch, mention, username) {
+ let usernameLower = username.toLowerCase();
+
+ if (mentionExists(usernameLower)) {
+ // Exact match
+ const alias = addToken(usernameLower, mention, '');
+ return alias;
+ }
+
+ // Not an exact match, attempt to truncate any punctuation to see if we can find a user
+ const originalUsername = usernameLower;
+
+ for (let c = usernameLower.length; c > 0; c--) {
+ if (isPunctuation(usernameLower[c - 1])) {
+ usernameLower = usernameLower.substring(0, c - 1);
+
+ if (mentionExists(usernameLower)) {
+ const suffix = originalUsername.substr(c - 1);
+ const alias = addToken(usernameLower, '@' + usernameLower);
+ return alias + suffix;
+ }
+ } else {
+ // If the last character is not punctuation, no point in going any further
+ break;
+ }
+ }
+
+ return fullMatch;
+ }
+
+ let output = text;
+ output = output.replace(/(@([a-z0-9.\-_]*))/gi, replaceAtMentionWithToken);
+
+ return output;
+}
+
+function escapeRegex(text) {
+ return text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+}
+
+function highlightCurrentMentions(text, tokens) {
+ let output = text;
+
+ const mentionKeys = UserStore.getCurrentMentionKeys();
+
+ // look for any existing tokens which are self mentions and should be highlighted
+ var newTokens = new Map();
+ for (const [alias, token] of tokens) {
+ if (mentionKeys.indexOf(token.originalText) !== -1) {
+ const index = tokens.size + newTokens.size;
+ const newAlias = `MM_SELFMENTION${index}`;
+
+ newTokens.set(newAlias, {
+ value: `<span class='mention--highlight'>${alias}</span>`,
+ originalText: token.originalText
+ });
+ output = output.replace(alias, newAlias);
+ }
+ }
+
+ // the new tokens are stashed in a separate map since we can't add objects to a map during iteration
+ for (const newToken of newTokens) {
+ tokens.set(newToken[0], newToken[1]);
+ }
+
+ // look for self mentions in the text
+ function replaceCurrentMentionWithToken(fullMatch, prefix, mention) {
+ const index = tokens.size;
+ const alias = `MM_SELFMENTION${index}`;
+
+ tokens.set(alias, {
+ value: `<span class='mention--highlight'>${mention}</span>`,
+ originalText: mention
+ });
+
+ return prefix + alias;
+ }
+
+ for (const mention of UserStore.getCurrentMentionKeys()) {
+ output = output.replace(new RegExp(`(^|\\W)(${escapeRegex(mention)})\\b`, 'gi'), replaceCurrentMentionWithToken);
+ }
+
+ return output;
+}
+
+function autolinkHashtags(text, tokens) {
+ let output = text;
+
+ var newTokens = new Map();
+ for (const [alias, token] of tokens) {
+ if (token.originalText.lastIndexOf('#', 0) === 0) {
+ const index = tokens.size + newTokens.size;
+ const newAlias = `MM_HASHTAG${index}`;
+
+ newTokens.set(newAlias, {
+ value: `<a class='mention-link' href='#' data-hashtag='${token.originalText}'>${token.originalText}</a>`,
+ originalText: token.originalText
+ });
+
+ output = output.replace(alias, newAlias);
+ }
+ }
+
+ // the new tokens are stashed in a separate map since we can't add objects to a map during iteration
+ for (const newToken of newTokens) {
+ tokens.set(newToken[0], newToken[1]);
+ }
+
+ // look for hashtags in the text
+ function replaceHashtagWithToken(fullMatch, prefix, hashtag) {
+ const index = tokens.size;
+ const alias = `MM_HASHTAG${index}`;
+
+ let value = hashtag;
+
+ if (hashtag.length > Constants.MIN_HASHTAG_LINK_LENGTH) {
+ value = `<a class='mention-link' href='#' data-hashtag='${hashtag}'>${hashtag}</a>`;
+ }
+
+ tokens.set(alias, {
+ value,
+ originalText: hashtag
+ });
+
+ return prefix + alias;
+ }
+
+ return output.replace(/(^|\W)(#[a-zA-ZäöüÄÖÜß][a-zA-Z0-9äöüÄÖÜß.\-_]*)\b/g, replaceHashtagWithToken);
+}
+
+const puncStart = /^[.,()&$!\[\]{}':;\\]+/;
+const puncEnd = /[.,()&$#!\[\]{}':;\\]+$/;
+
+function parseSearchTerms(searchTerm) {
+ let terms = [];
+
+ let termString = searchTerm;
+
+ while (termString) {
+ let captured;
+
+ // check for a quoted string
+ captured = (/^"(.*?)"/).exec(termString);
+ if (captured) {
+ termString = termString.substring(captured[0].length);
+ terms.push(captured[1]);
+ continue;
+ }
+
+ // check for a search flag (and don't add it to terms)
+ captured = (/^(?:in|from|channel): ?\S+/).exec(termString);
+ if (captured) {
+ termString = termString.substring(captured[0].length);
+ continue;
+ }
+
+ // capture any plain text up until the next quote or search flag
+ captured = (/^.+?(?=\bin|\bfrom|\bchannel|"|$)/).exec(termString);
+ if (captured) {
+ termString = termString.substring(captured[0].length);
+
+ // break the text up into words based on how the server splits them in SqlPostStore.SearchPosts and then discard empty terms
+ terms.push(...captured[0].split(/[ <>+\-\(\)\~\@]/).filter((term) => !!term));
+ continue;
+ }
+
+ // we should never reach this point since at least one of the regexes should match something in the remaining text
+ throw new Error('Infinite loop in search term parsing: ' + termString);
+ }
+
+ // remove punctuation from each term
+ terms = terms.map((term) => term.replace(puncStart, '').replace(puncEnd, ''));
+
+ return terms;
+}
+
+function convertSearchTermToRegex(term) {
+ let pattern;
+ if (term.endsWith('*')) {
+ pattern = '\\b' + escapeRegex(term.substring(0, term.length - 1));
+ } else {
+ pattern = '\\b' + escapeRegex(term) + '\\b';
+ }
+
+ return new RegExp(pattern, 'gi');
+}
+
+function highlightSearchTerm(text, tokens, searchTerm) {
+ const terms = parseSearchTerms(searchTerm);
+
+ if (terms.length === 0) {
+ return text;
+ }
+
+ let output = text;
+
+ function replaceSearchTermWithToken(word) {
+ const index = tokens.size;
+ const alias = `MM_SEARCHTERM${index}`;
+
+ tokens.set(alias, {
+ value: `<span class='search-highlight'>${word}</span>`,
+ originalText: word
+ });
+
+ return alias;
+ }
+
+ for (const term of terms) {
+ // highlight existing tokens matching search terms
+ var newTokens = new Map();
+ for (const [alias, token] of tokens) {
+ if (token.originalText === term.replace(/\*$/, '')) {
+ const index = tokens.size + newTokens.size;
+ const newAlias = `MM_SEARCHTERM${index}`;
+
+ newTokens.set(newAlias, {
+ value: `<span class='search-highlight'>${alias}</span>`,
+ originalText: token.originalText
+ });
+
+ output = output.replace(alias, newAlias);
+ }
+ }
+
+ // the new tokens are stashed in a separate map since we can't add objects to a map during iteration
+ for (const newToken of newTokens) {
+ tokens.set(newToken[0], newToken[1]);
+ }
+
+ output = output.replace(convertSearchTermToRegex(term), replaceSearchTermWithToken);
+ }
+
+ return output;
+}
+
+function replaceTokens(text, tokens) {
+ let output = text;
+
+ // iterate backwards through the map so that we do replacement in the opposite order that we added tokens
+ const aliases = [...tokens.keys()];
+ for (let i = aliases.length - 1; i >= 0; i--) {
+ const alias = aliases[i];
+ const token = tokens.get(alias);
+ output = output.replace(alias, token.value);
+ }
+
+ return output;
+}
+
+function replaceNewlines(text) {
+ return text.replace(/\n/g, ' ');
+}
+
+// A click handler that can be used with the results of TextFormatting.formatText to add default functionality
+// to clicked hashtags and @mentions.
+export function handleClick(e) {
+ const mentionAttribute = e.target.getAttributeNode('data-mention');
+ const hashtagAttribute = e.target.getAttributeNode('data-hashtag');
+
+ if (mentionAttribute) {
+ Utils.searchForTerm(mentionAttribute.value);
+ } else if (hashtagAttribute) {
+ Utils.searchForTerm(hashtagAttribute.value);
+ }
+}
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
new file mode 100644
index 000000000..686630a9b
--- /dev/null
+++ b/webapp/utils/utils.jsx
@@ -0,0 +1,1411 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+import LocalizationStore from 'stores/localization_store.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+import Constants from 'utils/constants.jsx';
+var ActionTypes = Constants.ActionTypes;
+import * as Client from './client.jsx';
+import * as AsyncClient from './async_client.jsx';
+import * as client from './client.jsx';
+import Autolinker from 'autolinker';
+
+import React from 'react';
+import {FormattedTime} from 'react-intl';
+
+import icon50 from 'images/icon50x50.png';
+
+export function isEmail(email) {
+ // writing a regex to match all valid email addresses is really, really hard (see http://stackoverflow.com/a/201378)
+ // so we just do a simple check and rely on a verification email to tell if it's a real address
+ return (/^.+@.+$/).test(email);
+}
+
+export function cleanUpUrlable(input) {
+ var cleaned = input.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-');
+ cleaned = cleaned.replace(/-{2,}/, '-');
+ cleaned = cleaned.replace(/^\-+/, '');
+ cleaned = cleaned.replace(/\-+$/, '');
+ return cleaned;
+}
+
+export function isTestDomain() {
+ if ((/^localhost/).test(window.location.hostname)) {
+ return true;
+ }
+
+ if ((/^dockerhost/).test(window.location.hostname)) {
+ return true;
+ }
+
+ if ((/^test/).test(window.location.hostname)) {
+ return true;
+ }
+
+ if ((/^127.0./).test(window.location.hostname)) {
+ return true;
+ }
+
+ if ((/^192.168./).test(window.location.hostname)) {
+ return true;
+ }
+
+ if ((/^10./).test(window.location.hostname)) {
+ return true;
+ }
+
+ if ((/^176./).test(window.location.hostname)) {
+ return true;
+ }
+
+ return false;
+}
+
+export function isChrome() {
+ if (navigator.userAgent.indexOf('Chrome') > -1) {
+ return true;
+ }
+ return false;
+}
+
+export function isSafari() {
+ if (navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) {
+ return true;
+ }
+ return false;
+}
+
+export function isIosChrome() {
+ // https://developer.chrome.com/multidevice/user-agent
+ return navigator.userAgent.indexOf('CriOS') !== -1;
+}
+
+export function isMobileApp() {
+ const userAgent = navigator.userAgent;
+
+ // the mobile app has different user agents for the native api calls and the shim, so handle them both
+ const isApi = userAgent.indexOf('Mattermost') !== -1;
+ const isShim = userAgent.indexOf('iPhone') !== -1 && userAgent.indexOf('Safari') === -1 && userAgent.indexOf('Chrome') === -1;
+
+ return isApi || isShim;
+}
+
+export function isInRole(roles, inRole) {
+ var parts = roles.split(' ');
+ for (var i = 0; i < parts.length; i++) {
+ if (parts[i] === inRole) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+export function isAdmin(roles) {
+ if (isInRole(roles, 'admin')) {
+ return true;
+ }
+
+ if (isInRole(roles, 'system_admin')) {
+ return true;
+ }
+
+ return false;
+}
+
+export function isSystemAdmin(roles) {
+ if (isInRole(roles, 'system_admin')) {
+ return true;
+ }
+
+ return false;
+}
+
+export function getDomainWithOutSub() {
+ var parts = window.location.host.split('.');
+
+ if (parts.length === 1) {
+ if (parts[0].indexOf('dockerhost') > -1) {
+ return 'dockerhost:8065';
+ }
+
+ return 'localhost:8065';
+ }
+
+ return parts[1] + '.' + parts[2];
+}
+
+export function getCookie(name) {
+ var value = '; ' + document.cookie;
+ var parts = value.split('; ' + name + '=');
+ if (parts.length === 2) {
+ return parts.pop().split(';').shift();
+ }
+ return '';
+}
+
+var requestedNotificationPermission = false;
+
+export function notifyMe(title, body, channel) {
+ if (!('Notification' in window)) {
+ return;
+ }
+
+ if (Notification.permission === 'granted' || (Notification.permission === 'default' && !requestedNotificationPermission)) {
+ requestedNotificationPermission = true;
+
+ Notification.requestPermission((permission) => {
+ if (permission === 'granted') {
+ try {
+ var notification = new Notification(title, {body: body, tag: body, icon: icon50});
+ notification.onclick = () => {
+ window.focus();
+ if (channel) {
+ switchChannel(channel);
+ } else {
+ window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
+ }
+ };
+ setTimeout(() => {
+ notification.close();
+ }, 5000);
+ } catch (e) {
+ console.error(e); //eslint-disable-line no-console
+ }
+ }
+ });
+ }
+}
+
+var canDing = true;
+
+export function ding() {
+ if (!isBrowserFirefox() && canDing) {
+ var audio = new Audio('/static/images/bing.mp3');
+ audio.play();
+ canDing = false;
+ setTimeout(() => {
+ canDing = true;
+ return;
+ }, 3000);
+ }
+}
+
+export function getUrlParameter(sParam) {
+ var sPageURL = window.location.search.substring(1);
+ var sURLVariables = sPageURL.split('&');
+ for (var i = 0; i < sURLVariables.length; i++) {
+ var sParameterName = sURLVariables[i].split('=');
+ if (sParameterName[0] === sParam) {
+ return sParameterName[1];
+ }
+ }
+ return null;
+}
+
+export function getDateForUnixTicks(ticks) {
+ return new Date(ticks);
+}
+
+export function displayDate(ticks) {
+ var d = new Date(ticks);
+ var monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+
+ return monthNames[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear();
+}
+
+export function displayTime(ticks, utc) {
+ const d = new Date(ticks);
+ let hours;
+ let minutes;
+ let ampm = '';
+ let timezone = '';
+
+ if (utc) {
+ hours = d.getUTCHours();
+ minutes = d.getUTCMinutes();
+ timezone = ' UTC';
+ } else {
+ hours = d.getHours();
+ minutes = d.getMinutes();
+ }
+
+ if (minutes <= 9) {
+ minutes = '0' + minutes;
+ }
+
+ const useMilitaryTime = PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time');
+ if (!useMilitaryTime) {
+ ampm = ' AM';
+ if (hours >= 12) {
+ ampm = ' PM';
+ }
+
+ hours = hours % 12;
+ if (!hours) {
+ hours = '12';
+ }
+ }
+
+ return hours + ':' + minutes + ampm + timezone;
+}
+
+export function displayTimeFormatted(ticks) {
+ const useMilitaryTime = PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time');
+
+ return (
+ <FormattedTime
+ value={ticks}
+ hour='numeric'
+ minute='numeric'
+ hour12={!useMilitaryTime}
+ />
+ );
+}
+
+export function isMilitaryTime() {
+ return PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time');
+}
+
+export function displayDateTime(ticks) {
+ var seconds = Math.floor((Date.now() - ticks) / 1000);
+
+ var interval = Math.floor(seconds / 3600);
+
+ if (interval > 24) {
+ return this.displayTime(ticks);
+ }
+
+ if (interval > 1) {
+ return interval + ' hours ago';
+ }
+
+ if (interval === 1) {
+ return interval + ' hour ago';
+ }
+
+ interval = Math.floor(seconds / 60);
+ if (interval >= 2) {
+ return interval + ' minutes ago';
+ }
+
+ if (interval >= 1) {
+ return '1 minute ago';
+ }
+
+ return 'just now';
+}
+
+export function displayCommentDateTime(ticks) {
+ return displayDate(ticks) + ' ' + displayTime(ticks);
+}
+
+// returns Unix timestamp in milliseconds
+export function getTimestamp() {
+ return Date.now();
+}
+
+// extracts links not styled by Markdown
+export function extractLinks(text) {
+ const links = [];
+ let inText = text;
+
+ // strip out code blocks
+ inText = inText.replace(/`[^`]*`/g, '');
+
+ // strip out inline markdown images
+ inText = inText.replace(/!\[[^\]]*\]\([^\)]*\)/g, '');
+
+ function replaceFn(autolinker, match) {
+ let link = '';
+ const matchText = match.getMatchedText();
+
+ if (matchText.trim().indexOf('http') === 0) {
+ link = matchText;
+ } else {
+ link = 'http://' + matchText;
+ }
+
+ links.push(link);
+ }
+
+ Autolinker.link(
+ inText,
+ {
+ replaceFn,
+ urls: {schemeMatches: true, wwwMatches: true, tldMatches: false},
+ emails: false,
+ twitter: false,
+ phone: false,
+ hashtag: false
+ }
+ );
+
+ return links;
+}
+
+export function escapeRegExp(string) {
+ return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
+}
+
+// Taken from http://stackoverflow.com/questions/1068834/object-comparison-in-javascript and modified slightly
+export function areObjectsEqual(x, y) {
+ let p;
+ const leftChain = [];
+ const rightChain = [];
+
+ // Remember that NaN === NaN returns false
+ // and isNaN(undefined) returns true
+ if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
+ return true;
+ }
+
+ // Compare primitives and functions.
+ // Check if both arguments link to the same object.
+ // Especially useful on step when comparing prototypes
+ if (x === y) {
+ return true;
+ }
+
+ // Works in case when functions are created in constructor.
+ // Comparing dates is a common scenario. Another built-ins?
+ // We can even handle functions passed across iframes
+ if ((typeof x === 'function' && typeof y === 'function') ||
+ (x instanceof Date && y instanceof Date) ||
+ (x instanceof RegExp && y instanceof RegExp) ||
+ (x instanceof String && y instanceof String) ||
+ (x instanceof Number && y instanceof Number)) {
+ return x.toString() === y.toString();
+ }
+
+ if (x instanceof Map && y instanceof Map) {
+ return areMapsEqual(x, y);
+ }
+
+ // At last checking prototypes as good a we can
+ if (!(x instanceof Object && y instanceof Object)) {
+ return false;
+ }
+
+ if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) {
+ return false;
+ }
+
+ if (x.constructor !== y.constructor) {
+ return false;
+ }
+
+ if (x.prototype !== y.prototype) {
+ return false;
+ }
+
+ // Check for infinitive linking loops
+ if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {
+ return false;
+ }
+
+ // Quick checking of one object beeing a subset of another.
+ for (p in y) {
+ if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
+ return false;
+ } else if (typeof y[p] !== typeof x[p]) {
+ return false;
+ }
+ }
+
+ for (p in x) {
+ if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
+ return false;
+ } else if (typeof y[p] !== typeof x[p]) {
+ return false;
+ }
+
+ switch (typeof (x[p])) {
+ case 'object':
+ case 'function':
+
+ leftChain.push(x);
+ rightChain.push(y);
+
+ if (!areObjectsEqual(x[p], y[p])) {
+ return false;
+ }
+
+ leftChain.pop();
+ rightChain.pop();
+ break;
+
+ default:
+ if (x[p] !== y[p]) {
+ return false;
+ }
+ break;
+ }
+ }
+
+ return true;
+}
+
+export function areMapsEqual(a, b) {
+ if (a.size !== b.size) {
+ return false;
+ }
+
+ for (const [key, value] of a) {
+ if (!b.has(key)) {
+ return false;
+ }
+
+ if (!areObjectsEqual(value, b.get(key))) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+export function replaceHtmlEntities(text) {
+ var tagsToReplace = {
+ '&amp;': '&',
+ '&lt;': '<',
+ '&gt;': '>'
+ };
+ var newtext = text;
+ for (var tag in tagsToReplace) {
+ if ({}.hasOwnProperty.call(tagsToReplace, tag)) {
+ var regex = new RegExp(tag, 'g');
+ newtext = newtext.replace(regex, tagsToReplace[tag]);
+ }
+ }
+ return newtext;
+}
+
+export function insertHtmlEntities(text) {
+ var tagsToReplace = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;'
+ };
+ var newtext = text;
+ for (var tag in tagsToReplace) {
+ if ({}.hasOwnProperty.call(tagsToReplace, tag)) {
+ var regex = new RegExp(tag, 'g');
+ newtext = newtext.replace(regex, tagsToReplace[tag]);
+ }
+ }
+ return newtext;
+}
+
+export function searchForTerm(term) {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_SEARCH_TERM,
+ term: term,
+ do_search: true
+ });
+}
+
+export function getFileType(extin) {
+ var ext = extin.toLowerCase();
+ if (Constants.IMAGE_TYPES.indexOf(ext) > -1) {
+ return 'image';
+ }
+
+ if (Constants.AUDIO_TYPES.indexOf(ext) > -1) {
+ return 'audio';
+ }
+
+ if (Constants.VIDEO_TYPES.indexOf(ext) > -1) {
+ return 'video';
+ }
+
+ if (Constants.SPREADSHEET_TYPES.indexOf(ext) > -1) {
+ return 'spreadsheet';
+ }
+
+ if (Constants.CODE_TYPES.indexOf(ext) > -1) {
+ return 'code';
+ }
+
+ if (Constants.WORD_TYPES.indexOf(ext) > -1) {
+ return 'word';
+ }
+
+ if (Constants.PRESENTATION_TYPES.indexOf(ext) > -1) {
+ return 'presentation';
+ }
+
+ if (Constants.PDF_TYPES.indexOf(ext) > -1) {
+ return 'pdf';
+ }
+
+ if (Constants.PATCH_TYPES.indexOf(ext) > -1) {
+ return 'patch';
+ }
+
+ return 'other';
+}
+
+export function getPreviewImagePathForFileType(fileTypeIn) {
+ var fileType = fileTypeIn.toLowerCase();
+
+ var icon;
+ if (fileType in Constants.ICON_FROM_TYPE) {
+ icon = Constants.ICON_FROM_TYPE[fileType];
+ } else {
+ icon = Constants.ICON_FROM_TYPE.other;
+ }
+
+ return icon;
+}
+
+export function getIconClassName(fileTypeIn) {
+ var fileType = fileTypeIn.toLowerCase();
+
+ if (fileType in Constants.ICON_NAME_FROM_TYPE) {
+ return Constants.ICON_NAME_FROM_TYPE[fileType];
+ }
+
+ return 'glyphicon-file';
+}
+
+export function splitFileLocation(fileLocation) {
+ var fileSplit = fileLocation.split('.');
+
+ var ext = '';
+ if (fileSplit.length > 1) {
+ ext = fileSplit[fileSplit.length - 1];
+ fileSplit.splice(fileSplit.length - 1, 1);
+ }
+
+ var filePath = fileSplit.join('.');
+ var filename = filePath.split('/')[filePath.split('/').length - 1];
+
+ return {ext: ext, name: filename, path: filePath};
+}
+
+export function getPreviewImagePath(filename) {
+ // Returns the path to a preview image that can be used to represent a file.
+ const fileInfo = splitFileLocation(filename);
+ const fileType = getFileType(fileInfo.ext);
+
+ if (fileType === 'image') {
+ return getFileUrl(fileInfo.path + '_preview.jpg');
+ }
+
+ // only images have proper previews, so just use a placeholder icon for non-images
+ return getPreviewImagePathForFileType(fileType);
+}
+
+export function toTitleCase(str) {
+ function doTitleCase(txt) {
+ return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
+ }
+ return str.replace(/\w\S*/g, doTitleCase);
+}
+
+export function applyTheme(theme) {
+ if (theme.sidebarBg) {
+ changeCss('.sidebar--left, .modal .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1);
+ changeCss('body', 'scrollbar-face-color:' + theme.sidebarBg, 3);
+ }
+
+ if (theme.sidebarText) {
+ changeCss('.sidebar--left .nav-pills__container li>a, .sidebar--right, .modal .settings-modal .nav-pills>li a, .sidebar--menu', 'color:' + changeOpacity(theme.sidebarText, 0.6), 1);
+ changeCss('@media(max-width: 768px){.modal .settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1);
+ changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.6), 1);
+ changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
+ changeCss('.sidebar--left .status .offline--icon, .sidebar--left .status .offline--icon', 'fill:' + theme.sidebarText, 1);
+ changeCss('@media(max-width: 768px){.modal .settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
+ }
+
+ if (theme.sidebarUnreadText) {
+ changeCss('.sidebar--left .nav-pills__container li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 2);
+ }
+
+ if (theme.sidebarTextHoverBg) {
+ changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .modal .settings-modal .nav-pills>li:hover a, .modal .settings-modal .nav-pills>li:focus a', 'background:' + theme.sidebarTextHoverBg, 1);
+ changeCss('@media(max-width: 768px){.modal .settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1);
+ }
+
+ if (theme.sidebarTextActiveBorder) {
+ changeCss('.sidebar--left .nav li.active a:before, .modal .settings-modal .nav-pills>li.active a:before', 'background:' + theme.sidebarTextActiveBorder, 1);
+ }
+
+ if (theme.sidebarTextActiveColor) {
+ changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .modal .settings-modal .nav-pills>li.active a, .modal .settings-modal .nav-pills>li.active a:hover, .modal .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2);
+ changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.1), 1);
+ }
+
+ if (theme.sidebarHeaderBg) {
+ changeCss('.sidebar--left .team__header, .sidebar--menu .team__header, .post-list__timestamp', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('.attachment .attachment__container', 'border-left-color:' + theme.sidebarHeaderBg, 1);
+ }
+
+ if (theme.sidebarHeaderTextColor) {
+ changeCss('.sidebar--left .team__header .header__info, .sidebar--menu .team__header .header__info, .post-list__timestamp', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('.sidebar--left .team__header .navbar-right .dropdown__icon, .sidebar--menu .team__header .navbar-right .dropdown__icon', 'fill:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('.sidebar--left .team__header .user__name, .sidebar--menu .team__header .user__name', 'color:' + changeOpacity(theme.sidebarHeaderTextColor, 0.8), 1);
+ changeCss('.sidebar--left .team__header:hover .user__name, .sidebar--menu .team__header:hover .user__name', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('.modal .modal-header .modal-title, .modal .modal-header .modal-title .name, .modal .modal-header button.close', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('#navbar .navbar-default .navbar-brand .heading', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('#navbar .navbar-default .navbar-toggle .icon-bar, ', 'background:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('@media(max-width: 768px){.search-bar__container', 'color:' + theme.sidebarHeaderTextColor, 2);
+ }
+
+ if (theme.onlineIndicator) {
+ changeCss('.sidebar--left .status .online--icon', 'fill:' + theme.onlineIndicator, 1);
+ }
+
+ if (theme.awayIndicator) {
+ changeCss('.sidebar--left .status .away--icon', 'fill:' + theme.awayIndicator, 1);
+ }
+
+ if (theme.mentionBj) {
+ changeCss('.sidebar--left .nav-pills__unread-indicator', 'background:' + theme.mentionBj, 1);
+ changeCss('.sidebar--left .badge', 'background:' + theme.mentionBj + '!important;', 1);
+ }
+
+ if (theme.mentionColor) {
+ changeCss('.sidebar--left .nav-pills__unread-indicator', 'color:' + theme.mentionColor, 2);
+ changeCss('.sidebar--left .badge', 'color:' + theme.mentionColor + '!important;', 2);
+ }
+
+ if (theme.centerChannelBg) {
+ changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .suggestion-list__content, .modal .modal-content', 'background:' + theme.centerChannelBg, 1);
+ changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
+ changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.post-image__details, .search-help-popover .search-autocomplete__divider span', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.sidebar--right, .dropdown-menu, .popover, .tip-overlay', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.popover.bottom>.arrow:after', 'border-bottom-color:' + theme.centerChannelBg, 1);
+ changeCss('.popover.right>.arrow:after, .tip-overlay.tip-overlay--sidebar .arrow, .tip-overlay.tip-overlay--header .arrow', 'border-right-color:' + theme.centerChannelBg, 1);
+ changeCss('.popover.left>.arrow:after', 'border-left-color:' + theme.centerChannelBg, 1);
+ changeCss('.popover.top>.arrow:after, .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg, 1);
+ changeCss('@media(min-width: 768px){.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.attachment__content', 'background:' + theme.centerChannelBg, 1);
+ changeCss('body', 'scrollbar-face-color:' + theme.centerChannelBg, 2);
+ changeCss('body', 'scrollbar-track-color:' + theme.centerChannelBg, 2);
+ }
+
+ if (theme.centerChannelColor) {
+ changeCss('.post-list__arrows', 'fill:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
+ changeCss('.sidebar--left, .sidebar--right .sidebar--right__header, .suggestion-list__content .command', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .suggestion-list__content .command, .modal .modal-content, .dropdown-menu, .popover, .mentions__name, .tip-overlay', 'color:' + theme.centerChannelColor, 1);
+ changeCss('#archive-link-home', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
+ changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.mentions--top, .suggestion-list', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
+ changeCss('.mentions--top, .suggestion-list', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
+ changeCss('.mentions--top, .suggestion-list', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1);
+ changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3);
+ changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2);
+ changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1);
+ changeCss('.post__body hr, .loading-screen .loading__content .round, .tutorial__circles .circle', 'background:' + theme.centerChannelColor, 1);
+ changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
+ changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
+ changeCss('.custom-textarea, .custom-textarea:focus, .file-preview, .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .suggestion-list__content, .modal .modal-content, .modal .settings-modal .settings-table .settings-content .divider-light, .webhooks__container, .dropdown-menu, .modal .modal-header, .popover', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
+ changeCss('.search-help-popover .search-autocomplete__divider span', 'color:' + changeOpacity(theme.centerChannelColor, 0.7), 1);
+ changeCss('.popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
+ changeCss('.popover.left>.arrow', 'border-left-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
+ changeCss('.popover.top>.arrow', 'border-top-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
+ changeCss('.suggestion-list__content .command, .popover .popover-title', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.dropdown-menu .divider, .search-help-popover .search-autocomplete__divider:before', 'background:' + theme.centerChannelColor, 1);
+ changeCss('.custom-textarea', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2);
+ changeCss('.post-image__details', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1);
+ changeCss('@media(min-width: 768px){.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
+ changeCss('.attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
+ changeCss('.channel-intro .channel-intro__content, .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
+ changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.search-item-container, .post-right__container .post.post--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
+ changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
+ changeCss('.channel-intro, .modal .settings-modal .settings-table .settings-content .divider-dark, hr, .modal .settings-modal .settings-table .settings-links, .modal .settings-modal .settings-table .settings-content .appearance-section .theme-elements__header', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.post.current--user .post__body, .post.post--comment.other--root.current--user .post-comment, pre, .post-right__container .post.post--root', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post.current--user .post__body, .post.post--comment.other--root.current--user .post-comment, .post.same--root.post--comment .post__body, .more-modal__list .more-modal__row, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2);
+ changeCss('@media(max-width: 1800px){.inner-wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ changeCss('.post:hover, .more-modal__list .more-modal__row:hover, .modal .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.suggestion-list__content .command:hover, .mentions__name:hover, .suggestion--selected, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
+ changeCss('code, .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
+ changeCss('@media(min-width: 960px){.post.current--user:hover .post__body ', 'background: none;', 1);
+ changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.search-help-popover .search-autocomplete__item:hover, .modal .settings-modal .settings-table .settings-content .appearance-section .theme-elements__body', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
+ changeCss('.search-help-popover .search-autocomplete__item.selected', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
+ changeCss('::-webkit-scrollbar-thumb', 'background:' + changeOpacity(theme.centerChannelColor, 0.4), 1);
+ changeCss('body', 'scrollbar-arrow-color:' + theme.centerChannelColor, 4);
+ }
+
+ if (theme.newMessageSeparator) {
+ changeCss('.new-separator .separator__text', 'color:' + theme.newMessageSeparator, 1);
+ changeCss('.new-separator .separator__hr', 'border-color:' + changeOpacity(theme.newMessageSeparator, 0.5), 1);
+ }
+
+ if (theme.linkColor) {
+ changeCss('a, a:focus, a:hover, .btn, .btn:focus, .btn:hover', 'color:' + theme.linkColor, 1);
+ changeCss('.post .comment-icon__container, .post .post__reply', 'fill:' + theme.linkColor, 1);
+ }
+
+ if (theme.buttonBg) {
+ changeCss('.btn.btn-primary, .tutorial__circles .circle.active', 'background:' + theme.buttonBg, 1);
+ changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1);
+ changeCss('.file-playback__controls', 'color:' + changeColor(theme.buttonBg, -0.25), 1);
+ }
+
+ if (theme.buttonColor) {
+ changeCss('.btn.btn-primary', 'color:' + theme.buttonColor, 2);
+ }
+
+ if (theme.mentionHighlightBg) {
+ changeCss('.mention--highlight, .search-highlight', 'background:' + theme.mentionHighlightBg, 1);
+ }
+
+ if (theme.mentionHighlightBg) {
+ changeCss('.post.post--highlight', 'background:' + changeOpacity(theme.mentionHighlightBg, 0.5), 1);
+ }
+
+ if (theme.mentionHighlightLink) {
+ changeCss('.mention--highlight .mention-link', 'color:' + theme.mentionHighlightLink, 1);
+ }
+
+ if (!theme.codeTheme) {
+ theme.codeTheme = Constants.DEFAULT_CODE_THEME;
+ }
+ updateCodeTheme(theme.codeTheme);
+}
+
+export function applyFont(fontName) {
+ const body = $('body');
+
+ for (const key of Reflect.ownKeys(Constants.FONTS)) {
+ const className = Constants.FONTS[key];
+
+ if (fontName === key) {
+ if (!body.hasClass(className)) {
+ body.addClass(className);
+ }
+ } else {
+ body.removeClass(className);
+ }
+ }
+}
+
+export function changeCss(className, classValue, classRepeat) {
+ // we need invisible container to store additional css definitions
+ var cssMainContainer = $('#css-modifier-container');
+ if (cssMainContainer.length === 0) {
+ cssMainContainer = $('<div id="css-modifier-container"></div>');
+ cssMainContainer.hide();
+ cssMainContainer.appendTo($('body'));
+ }
+
+ // and we need one div for each class
+ var classContainer = cssMainContainer.find('div[data-class="' + className + classRepeat + '"]');
+ if (classContainer.length === 0) {
+ classContainer = $('<div data-class="' + className + classRepeat + '"></div>');
+ classContainer.appendTo(cssMainContainer);
+ }
+
+ // append additional style
+ classContainer.html('<style>' + className + ' {' + classValue + '}</style>');
+}
+
+export function rgb2hex(rgbIn) {
+ if (/^#[0-9A-F]{6}$/i.test(rgbIn)) {
+ return rgbIn;
+ }
+
+ var rgb = rgbIn.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
+ function hex(x) {
+ return ('0' + parseInt(x, 10).toString(16)).slice(-2);
+ }
+ return '#' + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
+}
+
+export function updateCodeTheme(userTheme) {
+ let cssPath = '';
+ Constants.THEME_ELEMENTS.forEach((element) => {
+ if (element.id === 'codeTheme') {
+ element.themes.forEach((theme) => {
+ if (userTheme === theme.id) {
+ cssPath = theme.cssURL;
+ return;
+ }
+ });
+ }
+ });
+ const $link = $('link.code_theme');
+ if (cssPath !== $link.attr('href')) {
+ changeCss('code.hljs', 'visibility: hidden');
+ var xmlHTTP = new XMLHttpRequest();
+ xmlHTTP.open('GET', cssPath, true);
+ xmlHTTP.onload = function onLoad() {
+ $link.attr('href', cssPath);
+ if (isBrowserFirefox()) {
+ $link.one('load', () => {
+ changeCss('code.hljs', 'visibility: visible');
+ });
+ } else {
+ changeCss('code.hljs', 'visibility: visible');
+ }
+ };
+ xmlHTTP.send();
+ }
+}
+
+export function placeCaretAtEnd(el) {
+ el.focus();
+ el.selectionStart = el.value.length;
+ el.selectionEnd = el.value.length;
+
+ return;
+}
+
+export function getCaretPosition(el) {
+ if (el.selectionStart) {
+ return el.selectionStart;
+ } else if (document.selection) {
+ el.focus();
+
+ var r = document.selection.createRange();
+ if (r == null) {
+ return 0;
+ }
+
+ var re = el.createTextRange();
+ var rc = re.duplicate();
+ re.moveToBookmark(r.getBookmark());
+ rc.setEndPoint('EndToStart', re);
+
+ return rc.text.length;
+ }
+ return 0;
+}
+
+export function setSelectionRange(input, selectionStart, selectionEnd) {
+ if (input.setSelectionRange) {
+ input.focus();
+ input.setSelectionRange(selectionStart, selectionEnd);
+ } else if (input.createTextRange) {
+ var range = input.createTextRange();
+ range.collapse(true);
+ range.moveEnd('character', selectionEnd);
+ range.moveStart('character', selectionStart);
+ range.select();
+ }
+}
+
+export function setCaretPosition(input, pos) {
+ setSelectionRange(input, pos, pos);
+}
+
+export function getSelectedText(input) {
+ var selectedText;
+ if (typeof document.selection !== 'undefined') {
+ input.focus();
+ var sel = document.selection.createRange();
+ selectedText = sel.text;
+ } else if (typeof input.selectionStart !== 'undefined') {
+ var startPos = input.selectionStart;
+ var endPos = input.selectionEnd;
+ selectedText = input.value.substring(startPos, endPos);
+ }
+
+ return selectedText;
+}
+
+export function isValidUsername(name) {
+ var error = '';
+ if (!name) {
+ error = 'This field is required';
+ } else if (name.length < Constants.MIN_USERNAME_LENGTH || name.length > Constants.MAX_USERNAME_LENGTH) {
+ error = 'Must be between ' + Constants.MIN_USERNAME_LENGTH + ' and ' + Constants.MAX_USERNAME_LENGTH + ' characters';
+ } else if (!(/^[a-z0-9\.\-\_]+$/).test(name)) {
+ error = "Must contain only letters, numbers, and the symbols '.', '-', and '_'.";
+ } else if (!(/[a-z]/).test(name.charAt(0))) { //eslint-disable-line no-negated-condition
+ error = 'First character must be a letter.';
+ } else {
+ for (var i = 0; i < Constants.RESERVED_USERNAMES.length; i++) {
+ if (name === Constants.RESERVED_USERNAMES[i]) {
+ error = 'Cannot use a reserved word as a username.';
+ break;
+ }
+ }
+ }
+
+ return error;
+}
+
+export function updateAddressBar(channelName) {
+ const teamURL = TeamStore.getCurrentTeamUrl();
+ history.replaceState('data', '', teamURL + '/channels/' + channelName);
+}
+
+export function switchChannel(channel) {
+ GlobalActions.emitChannelClickEvent(channel);
+
+ updateAddressBar(channel.name);
+
+ $('.inner-wrap').removeClass('move--right');
+ $('.sidebar--left').removeClass('move--right');
+
+ client.trackPage();
+
+ return false;
+}
+
+export function isMobile() {
+ return screen.width <= 768;
+}
+
+export function isComment(post) {
+ if ('root_id' in post) {
+ return post.root_id !== '' && post.root_id != null;
+ }
+ return false;
+}
+
+export function getDirectTeammate(channelId) {
+ var userIds = ChannelStore.get(channelId).name.split('__');
+ var curUserId = UserStore.getCurrentId();
+ var teammate = {};
+
+ if (userIds.length !== 2 || userIds.indexOf(curUserId) === -1) {
+ return teammate;
+ }
+
+ for (var idx in userIds) {
+ if (userIds[idx] !== curUserId) {
+ teammate = UserStore.getProfile(userIds[idx]);
+ break;
+ }
+ }
+
+ return teammate;
+}
+
+Image.prototype.load = function imageLoad(url, progressCallback) {
+ var self = this;
+ var xmlHTTP = new XMLHttpRequest();
+ xmlHTTP.open('GET', url, true);
+ xmlHTTP.responseType = 'arraybuffer';
+ xmlHTTP.onload = function onLoad() {
+ var h = xmlHTTP.getAllResponseHeaders();
+ var m = h.match(/^Content-Type\:\s*(.*?)$/mi);
+ var mimeType = m[1] || 'image/png';
+
+ var blob = new Blob([this.response], {type: mimeType});
+ self.src = window.URL.createObjectURL(blob);
+ };
+ xmlHTTP.onprogress = function onprogress(e) {
+ parseInt(self.completedPercentage = (e.loaded / e.total) * 100, 10);
+ if (progressCallback) {
+ progressCallback();
+ }
+ };
+ xmlHTTP.onloadstart = function onloadstart() {
+ self.completedPercentage = 0;
+ };
+ xmlHTTP.send();
+};
+
+Image.prototype.completedPercentage = 0;
+
+export function changeColor(colourIn, amt) {
+ var hex = colourIn;
+ var lum = amt;
+
+ // validate hex string
+ hex = String(hex).replace(/[^0-9a-f]/gi, '');
+ if (hex.length < 6) {
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
+ }
+ lum = lum || 0;
+
+ // convert to decimal and change luminosity
+ var rgb = '#';
+ var c;
+ var i;
+ for (i = 0; i < 3; i++) {
+ c = parseInt(hex.substr(i * 2, 2), 16);
+ c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
+ rgb += ('00' + c).substr(c.length);
+ }
+
+ return rgb;
+}
+
+export function changeOpacity(oldColor, opacity) {
+ var color = oldColor;
+ 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];
+ }
+
+ var r = parseInt(color.substring(0, 2), 16);
+ var g = parseInt(color.substring(2, 4), 16);
+ var b = parseInt(color.substring(4, 6), 16);
+
+ return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
+}
+
+export function getFullName(user) {
+ if (user.first_name && user.last_name) {
+ return user.first_name + ' ' + user.last_name;
+ } else if (user.first_name) {
+ return user.first_name;
+ } else if (user.last_name) {
+ return user.last_name;
+ }
+
+ return '';
+}
+
+export function getDisplayName(user) {
+ if (user.nickname && user.nickname.trim().length > 0) {
+ return user.nickname;
+ }
+ var fullName = getFullName(user);
+
+ if (fullName) {
+ return fullName;
+ }
+
+ return user.username;
+}
+
+export function displayUsername(userId) {
+ const user = UserStore.getProfile(userId);
+ const nameFormat = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false');
+
+ let username = '';
+ if (user) {
+ if (nameFormat === Constants.Preferences.DISPLAY_PREFER_NICKNAME) {
+ username = user.nickname || getFullName(user);
+ } else if (nameFormat === Constants.Preferences.DISPLAY_PREFER_FULL_NAME) {
+ username = getFullName(user);
+ }
+ if (!username.trim().length) {
+ username = user.username;
+ }
+ }
+
+ return username;
+}
+
+//IE10 does not set window.location.origin automatically so this must be called instead when using it
+export function getWindowLocationOrigin() {
+ var windowLocationOrigin = window.location.origin;
+ if (!windowLocationOrigin) {
+ windowLocationOrigin = window.location.protocol + '//' + window.location.hostname;
+ if (window.location.port) {
+ windowLocationOrigin += ':' + window.location.port;
+ }
+ }
+ return windowLocationOrigin;
+}
+
+// Converts a file size in bytes into a human-readable string of the form '123MB'.
+export function fileSizeToString(bytes) {
+ // it's unlikely that we'll have files bigger than this
+ if (bytes > 1024 * 1024 * 1024 * 1024) {
+ return Math.floor(bytes / (1024 * 1024 * 1024 * 1024)) + 'TB';
+ } else if (bytes > 1024 * 1024 * 1024) {
+ return Math.floor(bytes / (1024 * 1024 * 1024)) + 'GB';
+ } else if (bytes > 1024 * 1024) {
+ return Math.floor(bytes / (1024 * 1024)) + 'MB';
+ } else if (bytes > 1024) {
+ return Math.floor(bytes / 1024) + 'KB';
+ }
+
+ return bytes + 'B';
+}
+
+// Converts a filename (like those attached to Post objects) to a url that can be used to retrieve attachments from the server.
+export function getFileUrl(filename, isDownload) {
+ const downloadParam = isDownload ? '?download=1' : '';
+ return getWindowLocationOrigin() + '/api/v1/files/get' + filename + downloadParam;
+}
+
+// Gets the name of a file (including extension) from a given url or file path.
+export function getFileName(path) {
+ var split = path.split('/');
+ return split[split.length - 1];
+}
+
+// Gets the websocket port to use. Configurable on the server.
+export function getWebsocketPort(protocol) {
+ if ((/^wss:/).test(protocol)) { // wss://
+ return ':' + global.window.mm_config.WebsocketSecurePort;
+ }
+ if ((/^ws:/).test(protocol)) {
+ return ':' + global.window.mm_config.WebsocketPort;
+ }
+ return '';
+}
+
+// Generates a RFC-4122 version 4 compliant globally unique identifier.
+export function generateId() {
+ // implementation taken from http://stackoverflow.com/a/2117523
+ var id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
+
+ id = id.replace(/[xy]/g, function replaceRandom(c) {
+ var r = Math.floor(Math.random() * 16);
+
+ var v;
+ if (c === 'x') {
+ v = r;
+ } else {
+ v = r & 0x3 | 0x8;
+ }
+
+ return v.toString(16);
+ });
+
+ return id;
+}
+
+export function isBrowserFirefox() {
+ return navigator && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
+}
+
+// Checks if browser is IE10 or IE11
+export function isBrowserIE() {
+ if (window.navigator && window.navigator.userAgent) {
+ var ua = window.navigator.userAgent;
+
+ return ua.indexOf('Trident/7.0') > 0 || ua.indexOf('Trident/6.0') > 0;
+ }
+
+ return false;
+}
+
+export function isBrowserEdge() {
+ return window.navigator && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('edge') > -1;
+}
+
+export function getDirectChannelName(id, otherId) {
+ let handle;
+
+ if (otherId > id) {
+ handle = id + '__' + otherId;
+ } else {
+ handle = otherId + '__' + id;
+ }
+
+ return handle;
+}
+
+// Used to get the id of the other user from a DM channel
+export function getUserIdFromChannelName(channel) {
+ var ids = channel.name.split('__');
+ var otherUserId = '';
+ if (ids[0] === UserStore.getCurrentId()) {
+ otherUserId = ids[1];
+ } else {
+ otherUserId = ids[0];
+ }
+
+ return otherUserId;
+}
+
+// Returns true if the given channel is a direct channel between the current user and the given one
+export function isDirectChannelForUser(otherUserId, channel) {
+ return channel.type === Constants.DM_CHANNEL && getUserIdFromChannelName(channel) === otherUserId;
+}
+
+export function importSlack(file, success, error) {
+ var formData = new FormData();
+ formData.append('file', file, file.name);
+ formData.append('filesize', file.size);
+ formData.append('importFrom', 'slack');
+
+ client.importSlack(formData, success, error);
+}
+
+export function getTeamURLFromAddressBar() {
+ return window.location.href.split('/channels')[0];
+}
+
+export function getShortenedTeamURL() {
+ const teamURL = getTeamURLFromAddressBar();
+ if (teamURL.length > 35) {
+ return teamURL.substring(0, 10) + '...' + teamURL.substring(teamURL.length - 12, teamURL.length) + '/';
+ }
+ return teamURL + '/';
+}
+
+export function windowWidth() {
+ return $(window).width();
+}
+
+export function windowHeight() {
+ return $(window).height();
+}
+
+export function openDirectChannelToUser(user, successCb, errorCb) {
+ const channelName = this.getDirectChannelName(UserStore.getCurrentId(), user.id);
+ let channel = ChannelStore.getByName(channelName);
+
+ const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true');
+ AsyncClient.savePreferences([preference]);
+
+ if (channel) {
+ if ($.isFunction(successCb)) {
+ successCb(channel, true);
+ }
+ } else {
+ channel = {
+ name: channelName,
+ last_post_at: 0,
+ total_msg_count: 0,
+ type: 'D',
+ display_name: user.username,
+ teammate_id: user.id,
+ status: UserStore.getStatus(user.id)
+ };
+
+ Client.createDirectChannel(
+ channel,
+ user.id,
+ (data) => {
+ AsyncClient.getChannel(data.id);
+ if ($.isFunction(successCb)) {
+ successCb(data, false);
+ }
+ },
+ () => {
+ window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
+ if ($.isFunction(errorCb)) {
+ errorCb();
+ }
+ }
+ );
+ }
+}
+
+// Use when sorting multiple channels or teams by their `display_name` field
+export function sortByDisplayName(a, b) {
+ let aDisplayName = '';
+ let bDisplayName = '';
+
+ if (a && a.display_name) {
+ aDisplayName = a.display_name.toLowerCase();
+ }
+ if (b && b.display_name) {
+ bDisplayName = b.display_name.toLowerCase();
+ }
+
+ if (aDisplayName < bDisplayName) {
+ return -1;
+ }
+ if (aDisplayName > bDisplayName) {
+ return 1;
+ }
+ return 0;
+}
+
+export function getChannelTerm(channelType) {
+ let channelTerm = 'Channel';
+ if (channelType === Constants.PRIVATE_CHANNEL) {
+ channelTerm = 'Group';
+ }
+
+ return channelTerm;
+}
+
+export function getPostTerm(post) {
+ let postTerm = 'Post';
+ if (post.root_id) {
+ postTerm = 'Comment';
+ }
+
+ return postTerm;
+}
+
+export function isFeatureEnabled(feature) {
+ return PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, Constants.FeatureTogglePrefix + feature.label);
+}
+
+export function isSystemMessage(post) {
+ return post.type && (post.type.lastIndexOf(Constants.SYSTEM_MESSAGE_PREFIX) === 0);
+}
+
+export function fillArray(value, length) {
+ const arr = [];
+
+ for (let i = 0; i < length; i++) {
+ arr.push(value);
+ }
+
+ return arr;
+}
+
+// Checks if a data transfer contains files not text, folders, etc..
+// Slightly modified from http://stackoverflow.com/questions/6848043/how-do-i-detect-a-file-is-being-dragged-rather-than-a-draggable-element-on-my-pa
+export function isFileTransfer(files) {
+ if (isBrowserIE()) {
+ return files.types != null && files.types.contains('Files');
+ }
+
+ return files.types != null && (files.types.indexOf ? files.types.indexOf('Files') !== -1 : files.types.contains('application/x-moz-file'));
+}
+
+export function clearFileInput(elm) {
+ // clear file input for all modern browsers
+ try {
+ elm.value = '';
+ if (elm.value) {
+ elm.type = 'text';
+ elm.type = 'file';
+ }
+ } catch (e) {
+ // Do nothing
+ }
+}
+
+export function isPostEphemeral(post) {
+ return post.type === Constants.POST_TYPE_EPHEMERAL || post.state === Constants.POST_DELETED;
+}
+
+export function getRootId(post) {
+ return post.root_id === '' ? post.id : post.root_id;
+}
+
+export function localizeMessage(id, defaultMessage) {
+ const translations = LocalizationStore.getTranslations();
+ if (translations) {
+ const value = translations[id];
+ if (value) {
+ return value;
+ }
+ }
+
+ if (defaultMessage) {
+ return defaultMessage;
+ }
+
+ return id;
+}