From 12896bd23eeba79884245c1c29fdc568cf21a7fa Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Mon, 14 Mar 2016 08:50:46 -0400 Subject: Converting to Webpack. Stage 1. --- webapp/utils/async_client.jsx | 1086 ++++++++++++++++++++ webapp/utils/channel_intro_messages.jsx | 254 +++++ webapp/utils/client.jsx | 1651 +++++++++++++++++++++++++++++++ webapp/utils/constants.jsx | 572 +++++++++++ webapp/utils/delayed_action.jsx | 27 + webapp/utils/emoticons.jsx | 162 +++ webapp/utils/markdown.jsx | 577 +++++++++++ webapp/utils/text_formatting.jsx | 402 ++++++++ webapp/utils/utils.jsx | 1411 ++++++++++++++++++++++++++ 9 files changed, 6142 insertions(+) create mode 100644 webapp/utils/async_client.jsx create mode 100644 webapp/utils/channel_intro_messages.jsx create mode 100644 webapp/utils/client.jsx create mode 100644 webapp/utils/constants.jsx create mode 100644 webapp/utils/delayed_action.jsx create mode 100644 webapp/utils/emoticons.jsx create mode 100644 webapp/utils/markdown.jsx create mode 100644 webapp/utils/text_formatting.jsx create mode 100644 webapp/utils/utils.jsx (limited to 'webapp/utils') diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx new file mode 100644 index 000000000..9c40311cf --- /dev/null +++ b/webapp/utils/async_client.jsx @@ -0,0 +1,1086 @@ +// 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 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 ( +
+
+ +
+
+ + + +
+

+ +

+ {createSetHeaderButton(channel)} +
+ ); + } + + return ( +
+

+ +

+
+ ); +} + +export function createOffTopicIntroMessage(channel) { + return ( +
+ + {createSetHeaderButton(channel)} + {createInviteChannelMemberButton(channel, 'channel')} +
+ ); +} + +export function createDefaultIntroMessage(channel) { + const inviteModalLink = ( + + + + + ); + + return ( +
+ + {inviteModalLink} + {createSetHeaderButton(channel)} +
+
+ ); +} + +export function createStandardIntroMessage(channel) { + var uiName = channel.display_name; + var creatorName = ''; + + var uiType; + var memberMessage; + if (channel.type === 'P') { + uiType = ( + + ); + memberMessage = ( + + ); + } else { + uiType = ( + + ); + memberMessage = ( + + ); + } + + const date = ( + + ); + + var createMessage; + if (creatorName === '') { + createMessage = ( + + ); + } else { + createMessage = ( + + + + ); + } + + return ( +
+

+ +

+

+ {createMessage} + {memberMessage} +
+

+ {createSetHeaderButton(channel)} + {createInviteChannelMemberButton(channel, uiType)} +
+ ); +} + +function createInviteChannelMemberButton(channel, uiType) { + return ( + + + + + ); +} + +function createSetHeaderButton(channel) { + return ( + + + + + ); +} diff --git a/webapp/utils/client.jsx b/webapp/utils/client.jsx new file mode 100644 index 000000000..9bd62e22d --- /dev/null +++ b/webapp/utils/client.jsx @@ -0,0 +1,1651 @@ +// 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 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('//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..32123e369 --- /dev/null +++ b/webapp/utils/constants.jsx @@ -0,0 +1,572 @@ +// 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_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: " ", + AWAY_ICON_SVG: " ", + OFFLINE_ICON_SVG: " ", + MENU_ICON: " ", + COMMENT_ICON: " ", + REPLY_ICON: " ", + SCROLL_BOTTOM_ICON: " ", + 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)(:['’]-?\(|:'\(|:'\()(?=$|\s)/g, // :`( + confused: /(^|\s)(:-?\/)(?=$|\s)/g, // :/ + confounded: /(^|\s)(:-?s)(?=$|\s)/gi, // :s + neutral_face: /(^|\s)(:-?\|)(?=$|\s)/g, // :| + flushed: /(^|\s)(:-?\$)(?=$|\s)/g, // :$ + mask: /(^|\s)(:-x)(?=$|\s)/gi, // :-x + heart: /(^|\s)(<3|<3)(?=$|\s)/g, // <3 + broken_heart: /(^|\s)(<\/3|</3)(?=$|\s)/g, // `, + 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]+?(?=[\\ 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 ( + '
' + + '' + + HighlightedLanguages[usedLanguage] + + '' + + '
' +
+                        '' +
+                            parsed.value +
+                        '' +
+                    '
' + + '
' + ); + } else if (usedLanguage === 'tex' || usedLanguage === 'latex') { + try { + const html = katex.renderToString(code, {throwOnError: false, displayMode: true}); + + return '
' + html + '
'; + } catch (e) { + // fall through if latex parsing fails and handle below + } + } + + return ( + '
' +
+                '' +
+                    (escaped ? code : TextFormatting.sanitizeHtml(code)) + '\n' +
+                '' +
+            '
' + ); + } + + codespan(text) { + return '' + super.codespan(text) + ''; + } + + br() { + if (this.formattingOptions.singleline) { + return ' '; + } + + return super.br(); + } + + image(href, title, text) { + let out = '' + text + '' : '>'; + return out; + } + + heading(text, level, raw) { + const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`; + return `${text}`; + } + + 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 = ''; + + return prefix + output + suffix; + } + + paragraph(text) { + if (this.formattingOptions.singleline) { + return `

${text}

`; + } + + return super.paragraph(text); + } + + table(header, body) { + return `
${header}${body}
`; + } + + listitem(text) { + const taskListReg = /^\[([ |xX])\] /; + const isTaskList = taskListReg.exec(text); + + if (isTaskList) { + return `
  • ${' '}${text.replace(taskListReg, '')}
  • `; + } + return `
  • ${text}
  • `; + } + + 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, '&'); + output = output.replace(//g, '>'); + output = output.replace(/'/g, '''); + output = output.replace(/"/g, '"'); + + 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: `
    ${linkText}`, + 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: `${mention}`, + 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: `${alias}`, + 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: `${mention}`, + 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: `${token.originalText}`, + 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 = `${hashtag}`; + } + + 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: `${word}`, + 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: `${alias}`, + 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 ( + + ); +} + +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 = { + '&': '&', + '<': '<', + '>': '>' + }; + 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 = { + '&': '&', + '<': '<', + '>': '>' + }; + 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 = $('
    '); + 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 = $('
    '); + classContainer.appendTo(cssMainContainer); + } + + // append additional style + classContainer.html(''); +} + +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; +} -- cgit v1.2.3-1-g7c22