summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
Diffstat (limited to 'webapp')
-rw-r--r--webapp/.eslintrc.json13
-rw-r--r--webapp/Makefile2
-rw-r--r--webapp/actions/channel_actions.jsx26
-rw-r--r--webapp/actions/global_actions.jsx4
-rw-r--r--webapp/actions/post_actions.jsx54
-rw-r--r--webapp/actions/user_actions.jsx56
-rw-r--r--webapp/actions/websocket_actions.jsx149
-rw-r--r--webapp/components/about_build_modal.jsx7
-rw-r--r--webapp/components/admin_console/admin_settings.jsx5
-rw-r--r--webapp/components/admin_console/custom_brand_settings.jsx2
-rw-r--r--webapp/components/admin_console/external_service_settings.jsx2
-rw-r--r--webapp/components/admin_console/team_users.jsx2
-rw-r--r--webapp/components/admin_console/text_setting.jsx6
-rw-r--r--webapp/components/admin_console/user_item.jsx7
-rw-r--r--webapp/components/admin_console/webhook_settings.jsx6
-rw-r--r--webapp/components/channel_info_modal.jsx46
-rw-r--r--webapp/components/channel_switch_modal.jsx8
-rw-r--r--webapp/components/create_comment.jsx69
-rw-r--r--webapp/components/create_post.jsx58
-rw-r--r--webapp/components/edit_post_modal.jsx125
-rw-r--r--webapp/components/emoji/components/add_emoji.jsx5
-rw-r--r--webapp/components/file_upload.jsx3
-rw-r--r--webapp/components/form_error.jsx11
-rw-r--r--webapp/components/integrations/components/add_command.jsx7
-rw-r--r--webapp/components/integrations/components/add_incoming_webhook.jsx5
-rw-r--r--webapp/components/integrations/components/add_outgoing_webhook.jsx5
-rw-r--r--webapp/components/invite_member_modal.jsx10
-rw-r--r--webapp/components/logged_in.jsx12
-rw-r--r--webapp/components/msg_typing.jsx2
-rw-r--r--webapp/components/navbar.jsx2
-rw-r--r--webapp/components/needs_team.jsx37
-rw-r--r--webapp/components/new_channel_flow.jsx6
-rw-r--r--webapp/components/new_channel_modal.jsx44
-rw-r--r--webapp/components/post_view/components/post.jsx9
-rw-r--r--webapp/components/post_view/components/post_list.jsx35
-rw-r--r--webapp/components/post_view/post_view_controller.jsx14
-rw-r--r--webapp/components/removed_from_channel_modal.jsx2
-rw-r--r--webapp/components/rename_channel_modal.jsx2
-rw-r--r--webapp/components/search_bar.jsx51
-rw-r--r--webapp/components/setting_item_max.jsx4
-rw-r--r--webapp/components/suggestion/suggestion_box.jsx52
-rw-r--r--webapp/components/suggestion/suggestion_list.jsx2
-rw-r--r--webapp/components/suggestion/switch_channel_provider.jsx8
-rw-r--r--webapp/components/team_export_tab.jsx127
-rw-r--r--webapp/components/team_general_tab.jsx2
-rw-r--r--webapp/components/team_members_dropdown.jsx2
-rw-r--r--webapp/components/team_settings.jsx8
-rw-r--r--webapp/components/team_settings_modal.jsx7
-rw-r--r--webapp/components/textbox.jsx14
-rw-r--r--webapp/components/user_settings/import_theme_modal.jsx91
-rw-r--r--webapp/components/user_settings/premade_theme_chooser.jsx16
-rw-r--r--webapp/components/user_settings/user_settings_general.jsx13
-rw-r--r--webapp/components/user_settings/user_settings_theme.jsx142
-rw-r--r--webapp/dispatcher/app_dispatcher.jsx4
-rw-r--r--webapp/i18n/en.json11
-rw-r--r--webapp/i18n/i18n.jsx10
-rw-r--r--webapp/package.json70
-rw-r--r--webapp/root.html3
-rw-r--r--webapp/sass/components/_modal.scss2
-rw-r--r--webapp/sass/layout/_headers.scss4
-rw-r--r--webapp/sass/layout/_post-right.scss234
-rw-r--r--webapp/sass/layout/_post.scss2
-rw-r--r--webapp/sass/layout/_sidebar-left.scss4
-rw-r--r--webapp/sass/responsive/_mobile.scss54
-rw-r--r--webapp/sass/routes/_settings.scss16
-rw-r--r--webapp/stores/channel_store.jsx16
-rw-r--r--webapp/stores/preference_store.jsx32
-rw-r--r--webapp/utils/async_client.jsx60
-rw-r--r--webapp/utils/channel_intro_messages.jsx37
-rw-r--r--webapp/utils/constants.jsx237
-rw-r--r--webapp/utils/text_formatting.jsx21
-rw-r--r--webapp/utils/utils.jsx10
-rw-r--r--webapp/utils/websocket_client.jsx7
-rw-r--r--webapp/webpack.config.js29
74 files changed, 1282 insertions, 978 deletions
diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json
index dada7f164..ad28561f1 100644
--- a/webapp/.eslintrc.json
+++ b/webapp/.eslintrc.json
@@ -59,6 +59,7 @@
"keyword-spacing": [2, {"before": true, "after": true, "overrides": {}}],
"linebreak-style": 2,
"lines-around-comment": [2, { "beforeBlockComment": true, "beforeLineComment": true, "allowBlockStart": true, "allowBlockEnd": true }],
+ "max-lines": [1, {"max": 450, "skipBlankLines": true, "skipComments": false}],
"max-nested-callbacks": [1, {"max":1}],
"max-nested-callbacks": [2, {"max":2}],
"max-statements-per-line": [2, {"max": 1}],
@@ -92,6 +93,7 @@
"no-extend-native": 2,
"no-extra-bind": 2,
"no-extra-label": 2,
+ "no-extra-parens": 0,
"no-extra-semi": 2,
"no-fallthrough": 2,
"no-floating-decimal": 2,
@@ -107,6 +109,7 @@
"no-lonely-if": 2,
"no-loop-func": 2,
"no-magic-numbers": [1, { "ignore": [-1, 0, 1, 2], "enforceConst": true, "detectObjects": true } ],
+ "no-mixed-operators": [2, {"allowSamePrecedence": false}],
"no-mixed-spaces-and-tabs": 2,
"no-multi-spaces": [2, { "exceptions": { "Property": false } }],
"no-multi-str": 0,
@@ -152,13 +155,16 @@
"no-useless-concat": 2,
"no-useless-constructor": 2,
"no-useless-escape": 2,
+ "no-useless-rename": 2,
"no-var": 0,
"no-void": 2,
"no-warning-comments": 1,
"no-whitespace-before-property": 2,
"no-with": 2,
+ "object-curly-newline": 0,
"object-curly-spacing": [2, "never"],
- "object-shorthand": [1, "always"],
+ "object-property-newline": [2, {"allowMultiplePropertiesPerLine": true}],
+ "object-shorthand": [2, "always"],
"one-var": [2, "never"],
"one-var-declaration-per-line": 0,
"operator-linebreak": [2, "after"],
@@ -189,9 +195,11 @@
"react/jsx-no-target-blank": 2,
"react/jsx-no-undef": 2,
"react/jsx-pascal-case": 2,
+ "react/jsx-filename-extension": 2,
"react/jsx-space-before-closing": [2, "never"],
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
+ "react/no-comment-textnodes": 2,
"react/no-danger": 0,
"react/no-deprecated": 2,
"react/no-did-mount-set-state": 2,
@@ -199,17 +207,20 @@
"react/no-direct-mutation-state": 2,
"react/no-is-mounted": 2,
"react/no-multi-comp": [2, { "ignoreStateless": true }],
+ "react/no-render-return-value": 2,
"react/no-set-state": 0,
"react/no-string-refs": 0,
"react/no-unknown-property": 2,
"react/prefer-es6-class": 2,
"react/prefer-stateless-function": 0,
"react/prop-types": 2,
+ "react/require-optimization": 1,
"react/require-render-return": 2,
"react/self-closing-comp": 2,
"react/sort-comp": 0,
"react/wrap-multilines": 2,
"require-yield": 2,
+ "rest-spread-spacing": [2, "never"],
"semi": [2, "always"],
"semi-spacing": [2, {"before": false, "after": true}],
"sort-imports": 0,
diff --git a/webapp/Makefile b/webapp/Makefile
index 81e00aec6..10712deff 100644
--- a/webapp/Makefile
+++ b/webapp/Makefile
@@ -20,6 +20,8 @@ test: .npminstall
build: .npminstall
@echo Building mattermost Webapp
+ rm -rf dist
+
npm run build
run: .npminstall
diff --git a/webapp/actions/channel_actions.jsx b/webapp/actions/channel_actions.jsx
index 9e5ecb03b..a590f9a9b 100644
--- a/webapp/actions/channel_actions.jsx
+++ b/webapp/actions/channel_actions.jsx
@@ -5,6 +5,8 @@ import {browserHistory} from 'react-router/es6';
import * as Utils from 'utils/utils.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'utils/web_client.jsx';
export function goToChannel(channel) {
@@ -22,5 +24,27 @@ export function goToChannel(channel) {
}
export function executeCommand(channelId, message, suggest, success, error) {
- Client.executeCommand(channelId, message, suggest, success, error);
+ let msg = message;
+
+ msg = msg.substring(0, msg.indexOf(' ')).toLowerCase() + msg.substring(msg.indexOf(' '), msg.length);
+
+ if (message.indexOf('/shortcuts') !== -1 && Utils.isMac()) {
+ msg += ' mac';
+ }
+
+ if (!Utils.isMac() && message.indexOf('/shortcuts') !== -1 && message.indexOf('mac') !== -1) {
+ msg = '/shortcuts';
+ }
+
+ Client.executeCommand(channelId, msg, suggest, success, error);
+}
+
+export function setChannelAsRead(channelIdParam) {
+ const channelId = channelIdParam || ChannelStore.getCurrentId();
+ AsyncClient.updateLastViewedAt();
+ ChannelStore.resetCounts(channelId);
+ ChannelStore.emitChange();
+ if (channelId === ChannelStore.getCurrentId()) {
+ ChannelStore.emitLastViewed(Number.MAX_VALUE, false);
+ }
}
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx
index d9b89f987..8d90b226d 100644
--- a/webapp/actions/global_actions.jsx
+++ b/webapp/actions/global_actions.jsx
@@ -12,7 +12,6 @@ import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import SearchStore from 'stores/search_store.jsx';
-import * as Websockets from 'actions/websocket_actions.jsx';
import {handleNewPost} from 'actions/post_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -20,6 +19,7 @@ const ActionTypes = Constants.ActionTypes;
import Client from 'utils/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
+import WebSocketClient from 'utils/websocket_client.jsx';
import * as Utils from 'utils/utils.jsx';
import en from 'i18n/en.json';
@@ -439,7 +439,7 @@ var lastTimeTypingSent = 0;
export function emitLocalUserTypingEvent(channelId, parentId) {
const t = Date.now();
if ((t - lastTimeTypingSent) > Constants.UPDATE_TYPING_MS) {
- Websockets.sendMessage({channel_id: channelId, action: 'typing', props: {parent_id: parentId}, state: {}});
+ WebSocketClient.userTyping(channelId, parentId);
lastTimeTypingSent = t;
}
}
diff --git a/webapp/actions/post_actions.jsx b/webapp/actions/post_actions.jsx
index 2b55e31ef..a6b464a24 100644
--- a/webapp/actions/post_actions.jsx
+++ b/webapp/actions/post_actions.jsx
@@ -6,7 +6,9 @@ import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+import * as PostUtils from 'utils/post_utils.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
@@ -26,7 +28,7 @@ export function handleNewPost(post, msg) {
var websocketMessageProps = null;
if (msg) {
- websocketMessageProps = msg.props;
+ websocketMessageProps = msg.data;
}
if (post.root_id && PostStore.getPost(post.channel_id, post.root_id) == null) {
@@ -62,3 +64,53 @@ export function handleNewPost(post, msg) {
websocketMessageProps
});
}
+
+export function setUnreadPost(channelId, postId) {
+ let lastViewed = 0;
+ let ownNewMessage = false;
+ const post = PostStore.getPost(channelId, postId);
+ const posts = PostStore.getVisiblePosts(channelId).posts;
+ var currentUsedId = UserStore.getCurrentId();
+ if (currentUsedId === post.user_id || PostUtils.isSystemMessage(post)) {
+ for (const otherPostId in posts) {
+ if (lastViewed < posts[otherPostId].create_at && currentUsedId !== posts[otherPostId].user_id && !PostUtils.isSystemMessage(posts[otherPostId])) {
+ lastViewed = posts[otherPostId].create_at;
+ }
+ }
+ if (lastViewed === 0) {
+ lastViewed = Number.MAX_VALUE;
+ } else if (lastViewed > post.create_at) {
+ lastViewed = post.create_at - 1;
+ ownNewMessage = true;
+ } else {
+ lastViewed -= 1;
+ }
+ } else {
+ lastViewed = post.create_at - 1;
+ }
+
+ if (lastViewed === Number.MAX_VALUE) {
+ AsyncClient.updateLastViewedAt();
+ ChannelStore.resetCounts(ChannelStore.getCurrentId());
+ ChannelStore.emitChange();
+ } else {
+ let unreadPosts = 0;
+ for (const otherPostId in posts) {
+ if (posts[otherPostId].create_at > lastViewed) {
+ unreadPosts += 1;
+ }
+ }
+ const member = ChannelStore.getMember(channelId);
+ const channel = ChannelStore.get(channelId);
+ member.last_viewed_at = lastViewed;
+ member.msg_count = channel.total_msg_count - unreadPosts;
+ member.mention_count = 0;
+ ChannelStore.setChannelMember(member);
+ ChannelStore.setUnreadCount(channelId);
+ AsyncClient.setLastViewedAt(lastViewed, channelId);
+ }
+
+ if (channelId === ChannelStore.getCurrentId()) {
+ ChannelStore.emitLastViewed(lastViewed, ownNewMessage);
+ }
+}
diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx
index 2f6eb9942..6d14e9fba 100644
--- a/webapp/actions/user_actions.jsx
+++ b/webapp/actions/user_actions.jsx
@@ -1,10 +1,15 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import Client from 'utils/web_client.jsx';
+import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
+import Client from 'utils/web_client.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+
+import {ActionTypes, Preferences} from 'utils/constants.jsx';
export function switchFromLdapToEmail(email, password, ldapPassword, onSuccess, onError) {
Client.ldapToEmail(
@@ -28,3 +33,52 @@ export function getMoreDmList() {
AsyncClient.getProfilesForDirectMessageList();
AsyncClient.getTeamMembers(TeamStore.getCurrentId());
}
+
+export function saveTheme(teamId, theme, onSuccess, onError) {
+ AsyncClient.savePreference(
+ Preferences.CATEGORY_THEME,
+ teamId,
+ JSON.stringify(theme),
+ () => {
+ onThemeSaved(teamId, theme, onSuccess);
+ },
+ (err) => {
+ onError(err);
+ }
+ );
+}
+
+function onThemeSaved(teamId, theme, onSuccess) {
+ const themePreferences = PreferenceStore.getCategory(Preferences.CATEGORY_THEME);
+
+ if (teamId !== '' && themePreferences.size > 1) {
+ // no extra handling to be done to delete team-specific themes
+ onSuccess();
+ return;
+ }
+
+ const toDelete = [];
+
+ for (const [name] of themePreferences) {
+ if (name === '') {
+ continue;
+ }
+
+ toDelete.push({
+ user_id: UserStore.getCurrentId(),
+ category: Preferences.CATEGORY_THEME,
+ name
+ });
+ }
+
+ // we're saving a new global theme so delete any team-specific ones
+ AsyncClient.deletePreferences(toDelete);
+
+ // delete them locally before we hear from the server so that the UI flow is smoother
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.DELETED_PREFERENCES,
+ preferences: toDelete
+ });
+
+ onSuccess();
+} \ No newline at end of file
diff --git a/webapp/actions/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx
index 7be9d84f3..f7e6adf5d 100644
--- a/webapp/actions/websocket_actions.jsx
+++ b/webapp/actions/websocket_actions.jsx
@@ -11,6 +11,7 @@ import ErrorStore from 'stores/error_store.jsx';
import NotificationStore from 'stores/notification_store.jsx'; //eslint-disable-line no-unused-vars
import Client from 'utils/web_client.jsx';
+import WebSocketClient from 'utils/websocket_client.jsx';
import * as Utils from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
@@ -23,16 +24,9 @@ const SocketEvents = Constants.SocketEvents;
import {browserHistory} from 'react-router/es6';
const MAX_WEBSOCKET_FAILS = 7;
-const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
-const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
-
-var conn = null;
-var connectFailCount = 0;
-var pastFirstInit = false;
-var manuallyClosed = false;
export function initialize() {
- if (window.WebSocket && !conn) {
+ if (window.WebSocket) {
let protocol = 'ws://';
if (window.location.protocol === 'https:') {
protocol = 'wss://';
@@ -40,85 +34,35 @@ export function initialize() {
const connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + Client.getUsersRoute() + '/websocket';
- if (connectFailCount === 0) {
- console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console
- }
-
- manuallyClosed = false;
-
- conn = new WebSocket(connUrl);
-
- conn.onopen = () => {
- if (connectFailCount > 0) {
- console.log('websocket re-established connection'); //eslint-disable-line no-console
- AsyncClient.getChannels();
- AsyncClient.getPosts(ChannelStore.getCurrentId());
- }
-
- if (pastFirstInit) {
- ErrorStore.clearLastError();
- ErrorStore.emitChange();
- }
-
- pastFirstInit = true;
- connectFailCount = 0;
- };
-
- conn.onclose = () => {
- conn = null;
-
- if (connectFailCount === 0) {
- console.log('websocket closed'); //eslint-disable-line no-console
- }
-
- if (manuallyClosed) {
- return;
- }
-
- connectFailCount = connectFailCount + 1;
-
- var retryTime = MIN_WEBSOCKET_RETRY_TIME;
-
- if (connectFailCount > MAX_WEBSOCKET_FAILS) {
- ErrorStore.storeLastError({message: Utils.localizeMessage('channel_loader.socketError', 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.')});
-
- // If we've failed a bunch of connections then start backing off
- retryTime = MIN_WEBSOCKET_RETRY_TIME * connectFailCount * connectFailCount;
- if (retryTime > MAX_WEBSOCKET_RETRY_TIME) {
- retryTime = MAX_WEBSOCKET_RETRY_TIME;
- }
- }
-
- ErrorStore.setConnectionErrorCount(connectFailCount);
- ErrorStore.emitChange();
-
- setTimeout(
- () => {
- initialize();
- },
- retryTime
- );
- };
-
- conn.onerror = (evt) => {
- if (connectFailCount <= 1) {
- console.log('websocket error'); //eslint-disable-line no-console
- console.log(evt); //eslint-disable-line no-console
- }
- };
-
- conn.onmessage = (evt) => {
- const msg = JSON.parse(evt.data);
- handleMessage(msg);
- };
+ WebSocketClient.initialize(connUrl);
+ WebSocketClient.setEventCallback(handleEvent);
+ WebSocketClient.setReconnectCallback(handleReconnect);
+ WebSocketClient.setCloseCallback(handleClose);
}
}
-function handleMessage(msg) {
- // Let the store know we are online. This probably shouldn't be here.
- UserStore.setStatus(msg.user_id, 'online');
+export function close() {
+ WebSocketClient.close();
+}
+
+function handleReconnect() {
+ AsyncClient.getChannels();
+ AsyncClient.getPosts(ChannelStore.getCurrentId());
+ ErrorStore.clearLastError();
+ ErrorStore.emitChange();
+}
+
+function handleClose(failCount) {
+ if (failCount > MAX_WEBSOCKET_FAILS) {
+ ErrorStore.storeLastError({message: Utils.localizeMessage('channel_loader.socketError', 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.')});
+ }
+
+ ErrorStore.setConnectionErrorCount(failCount);
+ ErrorStore.emitChange();
+}
- switch (msg.action) {
+function handleEvent(msg) {
+ switch (msg.event) {
case SocketEvents.POSTED:
case SocketEvents.EPHEMERAL_MESSAGE:
handleNewPostEvent(msg);
@@ -172,36 +116,14 @@ function handleMessage(msg) {
}
}
-export function sendMessage(msg) {
- if (conn && conn.readyState === WebSocket.OPEN) {
- var teamId = TeamStore.getCurrentId();
- if (teamId && teamId.length > 0) {
- msg.team_id = teamId;
- }
-
- conn.send(JSON.stringify(msg));
- } else if (!conn || conn.readyState === WebSocket.Closed) {
- conn = null;
- initialize();
- }
-}
-
-export function close() {
- manuallyClosed = true;
- connectFailCount = 0;
- if (conn && conn.readyState === WebSocket.OPEN) {
- conn.close();
- }
-}
-
function handleNewPostEvent(msg) {
- const post = JSON.parse(msg.props.post);
+ const post = JSON.parse(msg.data.post);
handleNewPost(post, msg);
}
function handlePostEditEvent(msg) {
// Store post
- const post = JSON.parse(msg.props.post);
+ const post = JSON.parse(msg.data.post);
PostStore.storePost(post);
PostStore.emitChange();
@@ -214,7 +136,7 @@ function handlePostEditEvent(msg) {
}
function handlePostDeleteEvent(msg) {
- const post = JSON.parse(msg.props.post);
+ const post = JSON.parse(msg.data.post);
GlobalActions.emitPostDeletedEvent(post);
}
@@ -234,7 +156,6 @@ function handleLeaveTeamEvent(msg) {
}
} else if (TeamStore.getCurrentId() === msg.team_id) {
UserActions.getMoreDmList();
- GlobalActions.emitProfilesForDmList();
}
}
@@ -257,12 +178,12 @@ function handleUserRemovedEvent(msg) {
if (UserStore.getCurrentId() === msg.user_id) {
AsyncClient.getChannels();
- if (msg.props.remover_id !== msg.user_id &&
+ if (msg.data.remover_id !== msg.user_id &&
msg.channel_id === ChannelStore.getCurrentId() &&
$('#removed_from_channel').length > 0) {
var sentState = {};
sentState.channelName = ChannelStore.getCurrent().display_name;
- sentState.remover = UserStore.getProfile(msg.props.remover_id).username;
+ sentState.remover = UserStore.getProfile(msg.data.remover_id).username;
BrowserStore.setItem('channel-removed-state', sentState);
$('#removed_from_channel').modal('show');
@@ -290,12 +211,10 @@ function handleChannelDeletedEvent(msg) {
}
function handlePreferenceChangedEvent(msg) {
- const preference = JSON.parse(msg.props.preference);
+ const preference = JSON.parse(msg.data.preference);
GlobalActions.emitPreferenceChangedEvent(preference);
}
function handleUserTypingEvent(msg) {
- if (TeamStore.getCurrentId() === msg.team_id) {
- GlobalActions.emitRemoteUserTypingEvent(msg.channel_id, msg.user_id, msg.props.parent_id);
- }
+ GlobalActions.emitRemoteUserTypingEvent(msg.channel_id, msg.user_id, msg.data.parent_id);
}
diff --git a/webapp/components/about_build_modal.jsx b/webapp/components/about_build_modal.jsx
index 197179191..1f41d76f9 100644
--- a/webapp/components/about_build_modal.jsx
+++ b/webapp/components/about_build_modal.jsx
@@ -104,6 +104,11 @@ export default class AboutBuildModal extends React.Component {
}
}
+ let version = '\u00a0' + config.Version;
+ if (config.BuildNumber !== config.Version) {
+ version += '\u00a0 (' + config.BuildNumber + ')';
+ }
+
return (
<Modal
dialogClassName='about-modal'
@@ -135,7 +140,7 @@ export default class AboutBuildModal extends React.Component {
id='about.version'
defaultMessage='Version:'
/>
- {'\u00a0' + config.Version + '\u00a0 (' + config.BuildNumber + ')'}
+ {version}
</div>
<div>
<FormattedMessage
diff --git a/webapp/components/admin_console/admin_settings.jsx b/webapp/components/admin_console/admin_settings.jsx
index e11d843a7..e29be33d1 100644
--- a/webapp/components/admin_console/admin_settings.jsx
+++ b/webapp/components/admin_console/admin_settings.jsx
@@ -106,11 +106,6 @@ export default class AdminSettings extends React.Component {
}
render() {
- let saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass += 'btn-primary';
- }
-
return (
<div className='wrapper--fixed'>
{this.renderTitle()}
diff --git a/webapp/components/admin_console/custom_brand_settings.jsx b/webapp/components/admin_console/custom_brand_settings.jsx
index 193889ea9..b4026c4a9 100644
--- a/webapp/components/admin_console/custom_brand_settings.jsx
+++ b/webapp/components/admin_console/custom_brand_settings.jsx
@@ -11,6 +11,7 @@ import BrandImageSetting from './brand_image_setting.jsx';
import {FormattedMessage} from 'react-intl';
import SettingsGroup from './settings_group.jsx';
import TextSetting from './text_setting.jsx';
+import Constants from 'utils/constants.jsx';
export default class CustomBrandSettings extends AdminSettings {
constructor(props) {
@@ -115,6 +116,7 @@ export default class CustomBrandSettings extends AdminSettings {
defaultMessage='Site Name:'
/>
}
+ maxLength={Constants.MAX_SITENAME_LENGTH}
placeholder={Utils.localizeMessage('admin.team.siteNameExample', 'Ex "Mattermost"')}
helpText={
<FormattedMessage
diff --git a/webapp/components/admin_console/external_service_settings.jsx b/webapp/components/admin_console/external_service_settings.jsx
index ebeb78332..59a129fc0 100644
--- a/webapp/components/admin_console/external_service_settings.jsx
+++ b/webapp/components/admin_console/external_service_settings.jsx
@@ -77,7 +77,7 @@ export default class ExternalServiceSettings extends AdminSettings {
helpText={
<FormattedHTMLMessage
id='admin.service.googleDescription'
- defaultMessage='Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at <a href="https://www.youtube.com/watch?v=Im69kzhpR3I" target="_blank">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Leaving the field blank disables the automatic generation of YouTube video previews from links.'
+ defaultMessage='Set this key to enable the display of titles for embedded YouTube video previews. Without the key, YouTube previews will still be created based on hyperlinks appearing in messages or comments but they will not show the video title. View a <a href="https://www.youtube.com/watch?v=Im69kzhpR3I" target="_blank">Google Developers Tutorial</a> for instructions on how to obtain a key.'
/>
}
value={this.state.googleDeveloperKey}
diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx
index 3ec375627..f82c20a86 100644
--- a/webapp/components/admin_console/team_users.jsx
+++ b/webapp/components/admin_console/team_users.jsx
@@ -186,7 +186,7 @@ export default class UserList extends React.Component {
var memberList = this.state.users.map((user) => {
var teamMember = this.getTeamMemberForUser(user.id);
- if (teamMember.delete_at > 0) {
+ if (!teamMember || teamMember.delete_at > 0) {
return null;
}
diff --git a/webapp/components/admin_console/text_setting.jsx b/webapp/components/admin_console/text_setting.jsx
index bb37f8e29..a5844aca7 100644
--- a/webapp/components/admin_console/text_setting.jsx
+++ b/webapp/components/admin_console/text_setting.jsx
@@ -4,6 +4,7 @@
import React from 'react';
import Setting from './setting.jsx';
+import Constants from 'utils/constants.jsx';
export default class TextSetting extends React.Component {
static get propTypes() {
@@ -16,6 +17,7 @@ export default class TextSetting extends React.Component {
React.PropTypes.string,
React.PropTypes.number
]).isRequired,
+ maxLength: React.PropTypes.number,
onChange: React.PropTypes.func.isRequired,
disabled: React.PropTypes.bool,
type: React.PropTypes.oneOf([
@@ -27,7 +29,8 @@ export default class TextSetting extends React.Component {
static get defaultProps() {
return {
- type: 'input'
+ type: 'input',
+ maxLength: Constants.MAX_TEXTSETTING_LENGTH
};
}
@@ -51,6 +54,7 @@ export default class TextSetting extends React.Component {
type='text'
placeholder={this.props.placeholder}
value={this.props.value}
+ maxLength={this.props.maxLength}
onChange={this.handleChange}
disabled={this.props.disabled}
/>
diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx
index e6c4f637c..79dbd5639 100644
--- a/webapp/components/admin_console/user_item.jsx
+++ b/webapp/components/admin_console/user_item.jsx
@@ -505,6 +505,11 @@ export default class UserItem extends React.Component {
);
}
+ let displayedName = Utils.getDisplayName(user);
+ if (displayedName !== user.username) {
+ displayedName += ' (@' + user.username + ')';
+ }
+
return (
<div className='more-modal__row'>
<img
@@ -514,7 +519,7 @@ export default class UserItem extends React.Component {
width='36'
/>
<div className='more-modal__details'>
- <div className='more-modal__name'>{Utils.getDisplayName(user)}</div>
+ <div className='more-modal__name'>{displayedName}</div>
<div className='more-modal__description'>
<FormattedHTMLMessage
id='admin.user_item.emailTitle'
diff --git a/webapp/components/admin_console/webhook_settings.jsx b/webapp/components/admin_console/webhook_settings.jsx
index 18a3ed7ad..3c8ea5466 100644
--- a/webapp/components/admin_console/webhook_settings.jsx
+++ b/webapp/components/admin_console/webhook_settings.jsx
@@ -64,7 +64,7 @@ export default class WebhookSettings extends AdminSettings {
helpText={
<FormattedMessage
id='admin.service.webhooksDescription'
- defaultMessage='When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.'
+ defaultMessage='When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag. See <a href="http://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">documentation</a> to learn more.'
/>
}
value={this.state.enableIncomingWebhooks}
@@ -81,7 +81,7 @@ export default class WebhookSettings extends AdminSettings {
helpText={
<FormattedMessage
id='admin.service.outWebhooksDesc'
- defaultMessage='When true, outgoing webhooks will be allowed.'
+ defaultMessage='When true, outgoing webhooks will be allowed. See <a href="http://docs.mattermost.com/developer/webhooks-outgoing.html" target="_blank">documentation</a> to learn more.'
/>
}
value={this.state.enableOutgoingWebhooks}
@@ -98,7 +98,7 @@ export default class WebhookSettings extends AdminSettings {
helpText={
<FormattedMessage
id='admin.service.cmdsDesc'
- defaultMessage='When true, user created slash commands will be allowed.'
+ defaultMessage='When true, custom slash commands will be allowed. See <a href="http://docs.mattermost.com/developer/slash-commands.html" target="_blank">documentation</a> to learn more.'
/>
}
value={this.state.enableCommands}
diff --git a/webapp/components/channel_info_modal.jsx b/webapp/components/channel_info_modal.jsx
index 7bd004411..b0e2c63fa 100644
--- a/webapp/components/channel_info_modal.jsx
+++ b/webapp/components/channel_info_modal.jsx
@@ -5,6 +5,7 @@ import * as Utils from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
import {Modal} from 'react-bootstrap';
+import * as TextFormatting from 'utils/text_formatting.jsx';
import React from 'react';
@@ -32,6 +33,7 @@ export default class ChannelInfoModal extends React.Component {
display_name: notFound,
name: notFound,
purpose: notFound,
+ header: notFound,
id: notFound
};
}
@@ -44,6 +46,39 @@ export default class ChannelInfoModal extends React.Component {
const channelURL = Utils.getTeamURLFromAddressBar() + '/channels/' + channel.name;
+ let channelPurpose = null;
+ if (channel.purpose) {
+ channelPurpose = (
+ <div className='form-group'>
+ <div className='info__label'>
+ <FormattedMessage
+ id='channel_info.purpose'
+ defaultMessage='Purpose:'
+ />
+ </div>
+ <div className='info__value'>{channel.purpose}</div>
+ </div>
+ );
+ }
+
+ let channelHeader = null;
+ if (channel.header) {
+ channelHeader = (
+ <div className='form-group'>
+ <div className='info__label'>
+ <FormattedMessage
+ id='channel_info.header'
+ defaultMessage='Header:'
+ />
+ </div>
+ <div
+ className='info__value'
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: false, mentionHighlight: false})}}
+ />
+ </div>
+ );
+ }
+
return (
<Modal
dialogClassName='about-modal'
@@ -60,15 +95,8 @@ export default class ChannelInfoModal extends React.Component {
</Modal.Title>
</Modal.Header>
<Modal.Body ref='modalBody'>
- <div className='form-group'>
- <div className='info__label'>
- <FormattedMessage
- id='channel_info.purpose'
- defaultMessage='Purpose:'
- />
- </div>
- <div className='info__value'>{channel.purpose}</div>
- </div>
+ {channelPurpose}
+ {channelHeader}
<div className='form-group'>
<div className='info__label'>
<FormattedMessage
diff --git a/webapp/components/channel_switch_modal.jsx b/webapp/components/channel_switch_modal.jsx
index 9bb98d74d..18e0f9f59 100644
--- a/webapp/components/channel_switch_modal.jsx
+++ b/webapp/components/channel_switch_modal.jsx
@@ -21,7 +21,7 @@ export default class SwitchChannelModal extends React.Component {
constructor() {
super();
- this.onUserInput = this.onUserInput.bind(this);
+ this.onInput = this.onInput.bind(this);
this.onShow = this.onShow.bind(this);
this.onHide = this.onHide.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
@@ -57,8 +57,8 @@ export default class SwitchChannelModal extends React.Component {
this.props.onHide();
}
- onUserInput(message) {
- this.setState({text: message});
+ onInput(e) {
+ this.setState({text: e.target.value});
}
handleKeyDown(e) {
@@ -122,7 +122,7 @@ export default class SwitchChannelModal extends React.Component {
ref='search'
className='form-control focused'
type='input'
- onUserInput={this.onUserInput}
+ onInput={this.onInput}
value={this.state.text}
onKeyDown={this.handleKeyDown}
listComponent={SuggestionList}
diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx
index f7564f396..bf23f7b44 100644
--- a/webapp/components/create_comment.jsx
+++ b/webapp/components/create_comment.jsx
@@ -19,33 +19,14 @@ import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
+import {FormattedMessage} from 'react-intl';
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
-const holders = defineMessages({
- commentLength: {
- id: 'create_comment.commentLength',
- defaultMessage: 'Comment length must be less than {max} characters.'
- },
- comment: {
- id: 'create_comment.comment',
- defaultMessage: 'Add Comment'
- },
- addComment: {
- id: 'create_comment.addComment',
- defaultMessage: 'Add a comment...'
- },
- commentTitle: {
- id: 'create_comment.commentTitle',
- defaultMessage: 'Comment'
- }
-});
-
import React from 'react';
-class CreateComment extends React.Component {
+export default class CreateComment extends React.Component {
constructor(props) {
super(props);
@@ -53,7 +34,7 @@ class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
- this.handleUserInput = this.handleUserInput.bind(this);
+ this.handleInput = this.handleInput.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleUploadClick = this.handleUploadClick.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
@@ -76,8 +57,7 @@ class CreateComment extends React.Component {
previews: draft.previews,
submitting: false,
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
- showPostDeletedModal: false,
- typing: false
+ showPostDeletedModal: false
};
}
@@ -126,7 +106,15 @@ class CreateComment extends React.Component {
}
if (post.message.length > Constants.CHARACTER_LIMIT) {
- this.setState({postError: this.props.intl.formatMessage(holders.commentLength, {max: Constants.CHARACTER_LIMIT})});
+ this.setState({
+ postError: (
+ <FormattedMessage
+ id='create_comment.commentLength'
+ defaultMessage='Comment length must be less than {max} characters.'
+ values={{max: Constants.CHARACTER_LIMIT}}
+ />
+ )
+ });
return;
}
@@ -175,13 +163,12 @@ class CreateComment extends React.Component {
submitting: false,
postError: null,
previews: [],
- serverError: null,
- typing: false
+ serverError: null
});
}
commentMsgKeyPress(e) {
- if (this.state.ctrlSend && e.ctrlKey || !this.state.ctrlSend) {
+ if ((this.state.ctrlSend && e.ctrlKey) || !this.state.ctrlSend) {
if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
ReactDOM.findDOMNode(this.refs.textbox).blur();
@@ -192,15 +179,16 @@ class CreateComment extends React.Component {
GlobalActions.emitLocalUserTypingEvent(this.props.channelId, this.props.rootId);
}
- handleUserInput(messageText) {
+ handleInput(e) {
+ const messageText = e.target.value;
+
const draft = PostStore.getCommentDraft(this.props.rootId);
draft.message = messageText;
PostStore.storeCommentDraft(this.props.rootId, draft);
$('.post-right__scroll').parent().scrollTop($('.post-right__scroll')[0].scrollHeight);
- const typing = messageText !== '';
- this.setState({messageText, typing});
+ this.setState({messageText});
}
handleKeyDown(e) {
@@ -220,7 +208,7 @@ class CreateComment extends React.Component {
AppDispatcher.handleViewAction({
type: ActionTypes.RECEIVED_EDIT_POST,
refocusId: '#reply_textbox',
- title: this.props.intl.formatMessage(holders.commentTitle),
+ title: Utils.localizeMessage('create_comment.commentTitle', 'Comment'),
message: lastPost.message,
postId: lastPost.id,
channelId: lastPost.channel_id,
@@ -313,13 +301,13 @@ class CreateComment extends React.Component {
draft.uploadsInProgress = uploadsInProgress;
PostStore.storeCommentDraft(this.props.rootId, draft);
- this.setState({previews: previews, uploadsInProgress: uploadsInProgress});
+ this.setState({previews, uploadsInProgress});
}
componentWillReceiveProps(newProps) {
if (newProps.rootId !== this.props.rootId) {
const draft = PostStore.getCommentDraft(newProps.rootId);
- this.setState({messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, previews: draft.previews, typing: false});
+ this.setState({messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
}
}
@@ -395,7 +383,6 @@ class CreateComment extends React.Component {
);
}
- const {formatMessage} = this.props.intl;
return (
<form onSubmit={this.handleSubmit}>
<div className='post-create'>
@@ -405,12 +392,11 @@ class CreateComment extends React.Component {
>
<div className='post-body__cell'>
<Textbox
- onUserInput={this.handleUserInput}
+ onInput={this.handleInput}
onKeyPress={this.commentMsgKeyPress}
onKeyDown={this.handleKeyDown}
messageText={this.state.messageText}
- typing={this.state.typing}
- createMessage={formatMessage(holders.addComment)}
+ createMessage={Utils.localizeMessage('create_comment.addComment', 'Add a comment...')}
initialText=''
supportsCommands={false}
id='reply_textbox'
@@ -436,7 +422,7 @@ class CreateComment extends React.Component {
<input
type='button'
className='btn btn-primary comment-btn pull-right'
- value={formatMessage(holders.comment)}
+ value={Utils.localizeMessage('create_comment.comment', 'Add Comment')}
onClick={this.handleSubmit}
/>
{uploadsInProgressText}
@@ -455,9 +441,6 @@ class CreateComment extends React.Component {
}
CreateComment.propTypes = {
- intl: intlShape.isRequired,
channelId: React.PropTypes.string.isRequired,
rootId: React.PropTypes.string.isRequired
-};
-
-export default injectIntl(CreateComment);
+}; \ No newline at end of file
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index 508fb36cb..9b61cca24 100644
--- a/webapp/components/create_post.jsx
+++ b/webapp/components/create_post.jsx
@@ -23,7 +23,7 @@ import PreferenceStore from 'stores/preference_store.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedHTMLMessage} from 'react-intl';
+import {FormattedHTMLMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
const Preferences = Constants.Preferences;
@@ -31,24 +31,9 @@ const TutorialSteps = Constants.TutorialSteps;
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
-const holders = defineMessages({
- comment: {
- id: 'create_post.comment',
- defaultMessage: 'Comment'
- },
- post: {
- id: 'create_post.post',
- defaultMessage: 'Post'
- },
- write: {
- id: 'create_post.write',
- defaultMessage: 'Write a message...'
- }
-});
-
import React from 'react';
-class CreatePost extends React.Component {
+export default class CreatePost extends React.Component {
constructor(props) {
super(props);
@@ -57,7 +42,7 @@ class CreatePost extends React.Component {
this.getCurrentDraft = this.getCurrentDraft.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.postMsgKeyPress = this.postMsgKeyPress.bind(this);
- this.handleUserInput = this.handleUserInput.bind(this);
+ this.handleInput = this.handleInput.bind(this);
this.handleUploadClick = this.handleUploadClick.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
@@ -87,8 +72,7 @@ class CreatePost extends React.Component {
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
fullWidthTextBox: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
showTutorialTip: false,
- showPostDeletedModal: false,
- typing: false
+ showPostDeletedModal: false
};
}
@@ -133,7 +117,7 @@ class CreatePost extends React.Component {
MessageHistoryStore.storeMessageInHistory(this.state.messageText);
- this.setState({submitting: true, serverError: null, typing: false});
+ this.setState({submitting: true, serverError: null});
if (post.message.indexOf('/') === 0) {
ChannelActions.executeCommand(
@@ -212,7 +196,7 @@ class CreatePost extends React.Component {
}
postMsgKeyPress(e) {
- if (this.state.ctrlSend && e.ctrlKey || !this.state.ctrlSend) {
+ if ((this.state.ctrlSend && e.ctrlKey) || !this.state.ctrlSend) {
if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
ReactDOM.findDOMNode(this.refs.textbox).blur();
@@ -223,9 +207,9 @@ class CreatePost extends React.Component {
GlobalActions.emitLocalUserTypingEvent(this.state.channelId, '');
}
- handleUserInput(messageText) {
- const typing = messageText !== '';
- this.setState({messageText, typing});
+ handleInput(e) {
+ const messageText = e.target.value;
+ this.setState({messageText});
const draft = PostStore.getCurrentDraft();
draft.message = messageText;
@@ -372,7 +356,7 @@ class CreatePost extends React.Component {
if (this.state.channelId !== channelId) {
const draft = this.getCurrentDraft();
- this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, typing: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
+ this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
}
}
@@ -408,8 +392,13 @@ class CreatePost extends React.Component {
if (!lastPost) {
return;
}
- const {formatMessage} = this.props.intl;
- var type = (lastPost.root_id && lastPost.root_id.length > 0) ? formatMessage(holders.comment) : formatMessage(holders.post);
+
+ let type;
+ if (lastPost.root_id && lastPost.root_id.length > 0) {
+ type = Utils.localizeMessage('create_post.comment', 'Comment');
+ } else {
+ type = Utils.localizeMessage('create_post.post', 'Post');
+ }
AppDispatcher.handleViewAction({
type: ActionTypes.RECEIVED_EDIT_POST,
@@ -519,12 +508,11 @@ class CreatePost extends React.Component {
<div className='post-create-body'>
<div className='post-body__cell'>
<Textbox
- onUserInput={this.handleUserInput}
+ onInput={this.handleInput}
onKeyPress={this.postMsgKeyPress}
onKeyDown={this.handleKeyDown}
messageText={this.state.messageText}
- typing={this.state.typing}
- createMessage={this.props.intl.formatMessage(holders.write)}
+ createMessage={Utils.localizeMessage('create_post.write', 'Write a message...')}
channelId={this.state.channelId}
id='post_textbox'
ref='textbox'
@@ -565,10 +553,4 @@ class CreatePost extends React.Component {
</form>
);
}
-}
-
-CreatePost.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(CreatePost);
+} \ No newline at end of file
diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx
index 4bd23a26d..8be0ba243 100644
--- a/webapp/components/edit_post_modal.jsx
+++ b/webapp/components/edit_post_modal.jsx
@@ -11,35 +11,35 @@ import BrowserStore from 'stores/browser_store.jsx';
import PostStore from 'stores/post_store.jsx';
import MessageHistoryStore from 'stores/message_history_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
+import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
+import {FormattedMessage} from 'react-intl';
var KeyCodes = Constants.KeyCodes;
-const holders = defineMessages({
- editPost: {
- id: 'edit_post.editPost',
- defaultMessage: 'Edit the post...'
- }
-});
-
import React from 'react';
-class EditPostModal extends React.Component {
+export default class EditPostModal extends React.Component {
constructor(props) {
super(props);
this.handleEdit = this.handleEdit.bind(this);
- this.handleEditInput = this.handleEditInput.bind(this);
this.handleEditKeyPress = this.handleEditKeyPress.bind(this);
this.handleEditPostEvent = this.handleEditPostEvent.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.handleInput = this.handleInput.bind(this);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
+ this.onModalHidden = this.onModalHidden.bind(this);
+ this.onModalShow = this.onModalShow.bind(this);
+ this.onModalShown = this.onModalShown.bind(this);
+ this.onModalHide = this.onModalHide.bind(this);
+ this.onModalKeyDown = this.onModalKeyDown.bind(this);
- this.state = {editText: '', originalText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: '', typing: false};
+ this.state = {editText: '', originalText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''};
}
+
handleEdit() {
var updatedPost = {};
updatedPost.message = this.state.editText.trim();
@@ -77,10 +77,13 @@ class EditPostModal extends React.Component {
$('#edit_post').modal('hide');
}
- handleEditInput(editMessage) {
- const typing = editMessage !== '';
- this.setState({editText: editMessage, typing});
+
+ handleInput(e) {
+ this.setState({
+ editText: e.target.value
+ });
}
+
handleEditKeyPress(e) {
if (!this.state.ctrlSend && e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
@@ -92,6 +95,7 @@ class EditPostModal extends React.Component {
this.handleSubmit(e);
}
}
+
handleEditPostEvent(options) {
this.setState({
editText: options.message || '',
@@ -100,65 +104,83 @@ class EditPostModal extends React.Component {
post_id: options.postId || '',
channel_id: options.channelId || '',
comments: options.comments || 0,
- refocusId: options.refocusId || '',
- typing: false
+ refocusId: options.refocusId || ''
});
$(ReactDOM.findDOMNode(this.refs.modal)).modal('show');
}
+
handleKeyDown(e) {
if (this.state.ctrlSend && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
this.handleEdit(e);
}
}
+
onPreferenceChange() {
this.setState({
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter')
});
}
- componentDidMount() {
- var self = this;
- $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', () => {
- self.setState({editText: '', originalText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: '', typing: false});
- });
+ onModalHidden() {
+ this.setState({editText: '', originalText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: '', typing: false});
+ }
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => {
- var button = e.relatedTarget;
- if (!button) {
- return;
- }
- self.setState({
- editText: $(button).attr('data-message'),
- originalText: $(button).attr('data-message'),
- title: $(button).attr('data-title'),
- channel_id: $(button).attr('data-channelid'),
- post_id: $(button).attr('data-postid'),
- comments: $(button).attr('data-comments'),
- refocusId: $(button).attr('data-refocusid'),
- typing: false
- });
+ onModalShow(e) {
+ var button = e.relatedTarget;
+ if (!button) {
+ return;
+ }
+ this.setState({
+ editText: $(button).attr('data-message'),
+ originalText: $(button).attr('data-message'),
+ title: $(button).attr('data-title'),
+ channel_id: $(button).attr('data-channelid'),
+ post_id: $(button).attr('data-postid'),
+ comments: $(button).attr('data-comments'),
+ refocusId: $(button).attr('data-refocusid'),
+ typing: false
});
+ }
- $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', () => {
- self.refs.editbox.focus();
- });
+ onModalShown() {
+ this.refs.editbox.focus();
+ }
- $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', () => {
- if (self.state.refocusId !== '') {
- setTimeout(() => {
- $(self.state.refocusId).get(0).focus();
- });
- }
- });
+ onModalHide() {
+ if (this.state.refocusId !== '') {
+ setTimeout(() => {
+ $(this.state.refocusId).get(0).focus();
+ });
+ }
+ }
+ onModalKeyDown(e) {
+ if (e.which === Constants.KeyCodes.ESCAPE) {
+ e.stopPropagation();
+ }
+ }
+
+ componentDidMount() {
+ $(this.refs.modal).on('hidden.bs.modal', this.onModalHidden);
+ $(this.refs.modal).on('show.bs.modal', this.onModalShow);
+ $(this.refs.modal).on('shown.bs.modal', this.onModalShown);
+ $(this.refs.modal).on('hide.bs.modal', this.onModalHide);
+ $(this.refs.modal).on('keydown', this.onModalKeyDown);
PostStore.addEditPostListener(this.handleEditPostEvent);
PreferenceStore.addChangeListener(this.onPreferenceChange);
}
+
componentWillUnmount() {
+ $(this.refs.modal).off('hidden.bs.modal', this.onModalHidden);
+ $(this.refs.modal).off('show.bs.modal', this.onModalShow);
+ $(this.refs.modal).off('shown.bs.modal', this.onModalShown);
+ $(this.refs.modal).off('hide.bs.modal', this.onModalHide);
+ $(this.refs.modal).off('keydown', this.onModalKeyDown);
PostStore.removeEditPostListner(this.handleEditPostEvent);
PreferenceStore.removeChangeListener(this.onPreferenceChange);
}
+
render() {
var error = (<div className='form-group'><br/></div>);
if (this.state.error) {
@@ -198,12 +220,11 @@ class EditPostModal extends React.Component {
</div>
<div className='edit-modal-body modal-body'>
<Textbox
- onUserInput={this.handleEditInput}
+ onInput={this.handleInput}
onKeyPress={this.handleEditKeyPress}
onKeyDown={this.handleKeyDown}
messageText={this.state.editText}
- typing={this.state.typing}
- createMessage={this.props.intl.formatMessage(holders.editPost)}
+ createMessage={Utils.localizeMessage('edit_post.editPost', 'Edit the post...')}
supportsCommands={false}
id='edit_textbox'
ref='editbox'
@@ -238,9 +259,3 @@ class EditPostModal extends React.Component {
);
}
}
-
-EditPostModal.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(EditPostModal);
diff --git a/webapp/components/emoji/components/add_emoji.jsx b/webapp/components/emoji/components/add_emoji.jsx
index 46f345476..c3d61d32c 100644
--- a/webapp/components/emoji/components/add_emoji.jsx
+++ b/webapp/components/emoji/components/add_emoji.jsx
@@ -277,7 +277,10 @@ export default class AddEmoji extends React.Component {
</div>
{preview}
<div className='backstage-form__footer'>
- <FormError error={this.state.error}/>
+ <FormError
+ type='backstage'
+ error={this.state.error}
+ />
<Link
className='btn btn-sm'
to={'/' + this.props.team.name + '/emoji'}
diff --git a/webapp/components/file_upload.jsx b/webapp/components/file_upload.jsx
index 2f485d4d3..1a3c6eadc 100644
--- a/webapp/components/file_upload.jsx
+++ b/webapp/components/file_upload.jsx
@@ -282,7 +282,8 @@ class FileUpload extends React.Component {
keyUpload(e) {
if (Utils.cmdOrCtrlPressed(e) && e.keyCode === Constants.KeyCodes.U) {
e.preventDefault();
- if (this.props.postType === 'post' && document.activeElement.id === 'post_textbox' || this.props.postType === 'comment' && document.activeElement.id === 'reply_textbox') {
+ if ((this.props.postType === 'post' && document.activeElement.id === 'post_textbox') ||
+ (this.props.postType === 'comment' && document.activeElement.id === 'reply_textbox')) {
$(this.refs.fileInput).focus().trigger('click');
}
}
diff --git a/webapp/components/form_error.jsx b/webapp/components/form_error.jsx
index 047595ef2..df6fa3ab0 100644
--- a/webapp/components/form_error.jsx
+++ b/webapp/components/form_error.jsx
@@ -7,6 +7,7 @@ export default class FormError extends React.Component {
static get propTypes() {
// accepts either a single error or an array of errors
return {
+ type: React.PropTypes.node,
error: React.PropTypes.node,
margin: React.PropTypes.node,
errors: React.PropTypes.arrayOf(React.PropTypes.node)
@@ -40,6 +41,16 @@ export default class FormError extends React.Component {
return null;
}
+ if (this.props.type === 'backstage') {
+ return (
+ <div className='pull-left has-error'>
+ <label className='control-label'>
+ {message}
+ </label>
+ </div>
+ );
+ }
+
if (this.props.margin) {
return (
<div className='form-group has-error'>
diff --git a/webapp/components/integrations/components/add_command.jsx b/webapp/components/integrations/components/add_command.jsx
index e72670e47..d24acd70d 100644
--- a/webapp/components/integrations/components/add_command.jsx
+++ b/webapp/components/integrations/components/add_command.jsx
@@ -72,7 +72,7 @@ export default class AddCommand extends React.Component {
const command = {
display_name: this.state.displayName,
description: this.state.description,
- trigger: this.state.trigger.trim(),
+ trigger: this.state.trigger.trim().toLowerCase(),
url: this.state.url.trim(),
method: this.state.method,
username: this.state.username,
@@ -537,7 +537,10 @@ export default class AddCommand extends React.Component {
</div>
{autocompleteFields}
<div className='backstage-form__footer'>
- <FormError errors={[this.state.serverError, this.state.clientError]}/>
+ <FormError
+ type='backstage'
+ errors={[this.state.serverError, this.state.clientError]}
+ />
<Link
className='btn btn-sm'
to={'/' + this.props.team.name + '/integrations/commands'}
diff --git a/webapp/components/integrations/components/add_incoming_webhook.jsx b/webapp/components/integrations/components/add_incoming_webhook.jsx
index 122600c90..a213a805f 100644
--- a/webapp/components/integrations/components/add_incoming_webhook.jsx
+++ b/webapp/components/integrations/components/add_incoming_webhook.jsx
@@ -186,7 +186,10 @@ export default class AddIncomingWebhook extends React.Component {
</div>
</div>
<div className='backstage-form__footer'>
- <FormError errors={[this.state.serverError, this.state.clientError]}/>
+ <FormError
+ type='backstage'
+ errors={[this.state.serverError, this.state.clientError]}
+ />
<Link
className='btn btn-sm'
to={'/' + this.props.team.name + '/integrations/incoming_webhooks'}
diff --git a/webapp/components/integrations/components/add_outgoing_webhook.jsx b/webapp/components/integrations/components/add_outgoing_webhook.jsx
index bd49fedc9..d6c0242a5 100644
--- a/webapp/components/integrations/components/add_outgoing_webhook.jsx
+++ b/webapp/components/integrations/components/add_outgoing_webhook.jsx
@@ -319,7 +319,10 @@ export default class AddOutgoingWebhook extends React.Component {
</div>
</div>
<div className='backstage-form__footer'>
- <FormError errors={[this.state.serverError, this.state.clientError]}/>
+ <FormError
+ type='backstage'
+ errors={[this.state.serverError, this.state.clientError]}
+ />
<Link
className='btn btn-sm'
to={'/' + this.props.team.name + '/integrations/outgoing_webhooks'}
diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx
index 265a421b6..68a7b7b15 100644
--- a/webapp/components/invite_member_modal.jsx
+++ b/webapp/components/invite_member_modal.jsx
@@ -131,7 +131,7 @@ class InviteMemberModal extends React.Component {
invites.push(invite);
}
- this.setState({emailErrors: emailErrors, firstNameErrors: firstNameErrors, lastNameErrors: lastNameErrors});
+ this.setState({emailErrors, firstNameErrors, lastNameErrors});
if (!valid || invites.length === 0) {
return;
@@ -151,7 +151,7 @@ class InviteMemberModal extends React.Component {
(err) => {
if (err.id === 'api.team.invite_members.already.app_error') {
emailErrors[err.detailed_error] = err.message;
- this.setState({emailErrors: emailErrors});
+ this.setState({emailErrors});
} else {
this.setState({serverError: err.message});
}
@@ -193,7 +193,7 @@ class InviteMemberModal extends React.Component {
var count = this.state.idCount + 1;
var inviteIds = this.state.inviteIds;
inviteIds.push(count);
- this.setState({inviteIds: inviteIds, idCount: count});
+ this.setState({inviteIds, idCount: count});
}
clearFields() {
@@ -225,7 +225,7 @@ class InviteMemberModal extends React.Component {
if (!inviteIds.length) {
inviteIds.push(++count);
}
- this.setState({inviteIds: inviteIds, idCount: count});
+ this.setState({inviteIds, idCount: count});
}
showGetTeamInviteLinkModal() {
@@ -435,7 +435,7 @@ class InviteMemberModal extends React.Component {
id='invite_member.teamInviteLink'
defaultMessage='You can also invite people using the {link}.'
values={{
- link: (link)
+ link
}}
/>
</p>
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index 484164e56..2ac858dfb 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -9,6 +9,7 @@ import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as Websockets from 'actions/websocket_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
import {browserHistory} from 'react-router/es6';
@@ -71,6 +72,8 @@ export default class LoggedIn extends React.Component {
if (this.state.user) {
this.setupUser(this.state.user);
+ } else {
+ GlobalActions.emitUserLoggedOutEvent('/login');
}
}
@@ -89,15 +92,6 @@ export default class LoggedIn extends React.Component {
id: user.id
});
}
-
- // Update CSS classes to match user theme
- if (user) {
- if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
- Utils.applyTheme(user.theme_props);
- } else {
- Utils.applyTheme(Constants.THEMES.default);
- }
- }
}
onUserChanged() {
diff --git a/webapp/components/msg_typing.jsx b/webapp/components/msg_typing.jsx
index 631eea78d..f6a6d12b2 100644
--- a/webapp/components/msg_typing.jsx
+++ b/webapp/components/msg_typing.jsx
@@ -71,7 +71,7 @@ class MsgTyping extends React.Component {
defaultMessage='{users} and {last} are typing...'
values={{
users: (users.join(', ')),
- last: (last)
+ last
}}
/>
);
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index 44730f4e2..c2d262819 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -636,7 +636,7 @@ export default class Navbar extends React.Component {
defaultMessage='No channel header yet.{newline}{link} to add one.'
values={{
newline: (<br/>),
- link: (link)
+ link
}}
/>
</div>
diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx
index 07b90636d..a8c7b3508 100644
--- a/webapp/components/needs_team.jsx
+++ b/webapp/components/needs_team.jsx
@@ -41,19 +41,34 @@ export default class NeedsTeam extends React.Component {
constructor(params) {
super(params);
- this.onChanged = this.onChanged.bind(this);
+ this.onTeamChanged = this.onTeamChanged.bind(this);
+ this.onPreferencesChanged = this.onPreferencesChanged.bind(this);
+
+ const team = TeamStore.getCurrent();
this.state = {
- team: TeamStore.getCurrent()
+ team,
+ theme: PreferenceStore.getTheme(team.id)
};
}
- onChanged() {
+ onTeamChanged() {
+ const team = TeamStore.getCurrent();
+
this.setState({
- team: TeamStore.getCurrent()
+ team,
+ theme: PreferenceStore.getTheme(team.id)
});
}
+ onPreferencesChanged(category) {
+ if (!category || category === Preferences.CATEGORY_THEME) {
+ this.setState({
+ theme: PreferenceStore.getTheme(this.state.team.id)
+ });
+ }
+ }
+
componentWillMount() {
// Go to tutorial if we are first arriving
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
@@ -63,7 +78,8 @@ export default class NeedsTeam extends React.Component {
}
componentDidMount() {
- TeamStore.addChangeListener(this.onChanged);
+ TeamStore.addChangeListener(this.onTeamChanged);
+ PreferenceStore.addChangeListener(this.onPreferencesChanged);
// Emit view action
GlobalActions.viewLoggedIn();
@@ -80,10 +96,19 @@ export default class NeedsTeam extends React.Component {
$(window).on('blur', () => {
window.isActive = false;
});
+
+ Utils.applyTheme(this.state.theme);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (!Utils.areObjectsEqual(prevState.theme, this.state.theme)) {
+ Utils.applyTheme(this.state.theme);
+ }
}
componentWillUnmount() {
- TeamStore.removeChangeListener(this.onChanged);
+ TeamStore.removeChangeListener(this.onTeamChanged);
+ PreferenceStore.removeChangeListener(this.onPreferencesChanged);
$(window).off('focus');
$(window).off('blur');
}
diff --git a/webapp/components/new_channel_flow.jsx b/webapp/components/new_channel_flow.jsx
index db06cf0be..f6e91afc4 100644
--- a/webapp/components/new_channel_flow.jsx
+++ b/webapp/components/new_channel_flow.jsx
@@ -65,6 +65,7 @@ class NewChannelFlow extends React.Component {
channelDisplayName: '',
channelName: '',
channelPurpose: '',
+ channelHeader: '',
nameModified: false
};
}
@@ -78,6 +79,7 @@ class NewChannelFlow extends React.Component {
channelDisplayName: '',
channelName: '',
channelPurpose: '',
+ channelHeader: '',
nameModified: false
});
}
@@ -99,6 +101,7 @@ class NewChannelFlow extends React.Component {
name: this.state.channelName,
display_name: this.state.channelDisplayName,
purpose: this.state.channelPurpose,
+ header: this.state.channelHeader,
type: this.state.channelType
};
Client.createChannel(
@@ -153,7 +156,8 @@ class NewChannelFlow extends React.Component {
channelDataChanged(data) {
this.setState({
channelDisplayName: data.displayName,
- channelPurpose: data.purpose
+ channelPurpose: data.purpose,
+ channelHeader: data.header
});
if (!this.state.nameModified) {
this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
diff --git a/webapp/components/new_channel_modal.jsx b/webapp/components/new_channel_modal.jsx
index 23eee625d..1198335ca 100644
--- a/webapp/components/new_channel_modal.jsx
+++ b/webapp/components/new_channel_modal.jsx
@@ -89,8 +89,9 @@ class NewChannelModal extends React.Component {
handleChange() {
const newData = {
- displayName: ReactDOM.findDOMNode(this.refs.display_name).value,
- purpose: ReactDOM.findDOMNode(this.refs.channel_purpose).value
+ displayName: this.refs.display_name.value,
+ header: this.refs.channel_header.value,
+ purpose: this.refs.channel_purpose.value
};
this.props.onDataChanged(newData);
}
@@ -258,7 +259,7 @@ class NewChannelModal extends React.Component {
</p>
</div>
</div>
- <div className='form-group less'>
+ <div className='form-group'>
<div className='col-sm-3'>
<label className='form__label control-label'>
<FormattedMessage
@@ -293,6 +294,43 @@ class NewChannelModal extends React.Component {
}}
/>
</p>
+ </div>
+ </div>
+ <div className='form-group less'>
+ <div className='col-sm-3'>
+ <label className='form__label control-label'>
+ <FormattedMessage
+ id='channel_modal.header'
+ defaultMessage='Header'
+ />
+ </label>
+ <label className='form__label light'>
+ <FormattedMessage
+ id='channel_modal.optional'
+ defaultMessage='(optional)'
+ />
+ </label>
+ </div>
+ <div className='col-sm-9'>
+ <textarea
+ className='form-control no-resize'
+ ref='channel_header'
+ rows='4'
+ placeholder={this.props.intl.formatMessage({id: 'channel_modal.header'})}
+ maxLength='128'
+ value={this.props.channelData.header}
+ onChange={this.handleChange}
+ tabIndex='2'
+ />
+ <p className='input__help'>
+ <FormattedMessage
+ id='channel_modal.headerHelp'
+ defaultMessage='Set text that will appear in the header of the {term} beside the {term} name. For example, include frequently used links by typing [Link Title](http://example.com).'
+ values={{
+ term: (channelTerm)
+ }}
+ />
+ </p>
{serverError}
</div>
</div>
diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx
index 21d335a51..ff443e355 100644
--- a/webapp/components/post_view/components/post.jsx
+++ b/webapp/components/post_view/components/post.jsx
@@ -10,6 +10,7 @@ const ActionTypes = Constants.ActionTypes;
import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
+import * as PostActions from 'actions/post_actions.jsx';
import React from 'react';
@@ -20,6 +21,7 @@ export default class Post extends React.Component {
this.handleCommentClick = this.handleCommentClick.bind(this);
this.handleDropdownOpened = this.handleDropdownOpened.bind(this);
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
+ this.handlePostClick = this.handlePostClick.bind(this);
this.state = {
dropdownOpened: false
@@ -47,6 +49,12 @@ export default class Post extends React.Component {
this.refs.info.forceUpdate();
this.refs.header.forceUpdate();
}
+ handlePostClick(e) {
+ if (e.altKey) {
+ e.preventDefault();
+ PostActions.setUnreadPost(this.props.post.channel_id, this.props.post.id);
+ }
+ }
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true;
@@ -213,6 +221,7 @@ export default class Post extends React.Component {
<div
id={'post_' + post.id}
className={'post ' + sameUserClass + ' ' + compactClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass + ' ' + hideControls + ' ' + dropdownOpenedClass}
+ onClick={this.handlePostClick}
>
<div className={'post__content ' + centerClass}>
{profilePicContainer}
diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx
index 17e29da2e..70107c838 100644
--- a/webapp/components/post_view/components/post_list.jsx
+++ b/webapp/components/post_view/components/post_list.jsx
@@ -15,14 +15,20 @@ import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import DelayedAction from 'utils/delayed_action.jsx';
+import * as ChannelActions from 'actions/channel_actions.jsx';
+
import Constants from 'utils/constants.jsx';
const ScrollTypes = Constants.ScrollTypes;
+import PreferenceStore from 'stores/preference_store.jsx';
+
import {FormattedDate, FormattedMessage} from 'react-intl';
import React from 'react';
import ReactDOM from 'react-dom';
+const Preferences = Constants.Preferences;
+
export default class PostList extends React.Component {
constructor(props) {
super(props);
@@ -37,6 +43,7 @@ export default class PostList extends React.Component {
this.handleResize = this.handleResize.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this);
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
this.jumpToPostNode = null;
this.wasAtBottom = true;
@@ -44,16 +51,24 @@ export default class PostList extends React.Component {
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
+ this.state = {
+ isScrolling: false,
+ fullWidthIntro: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
+ topPostId: null
+ };
+
if (props.channel) {
- this.introText = createChannelIntroMessage(props.channel);
+ this.introText = createChannelIntroMessage(props.channel, this.state.fullWidthIntro);
} else {
this.introText = this.getArchivesIntroMessage();
}
+ }
- this.state = {
- isScrolling: false,
- topPostId: null
- };
+ handleKeyDown(e) {
+ if (e.which === Constants.KeyCodes.ESCAPE && $('.popover.in,.modal.in').length === 0) {
+ e.preventDefault();
+ ChannelActions.setChannelAsRead();
+ }
}
isAtBottom() {
@@ -292,7 +307,7 @@ export default class PostList extends React.Component {
);
}
- if (postUserId !== userId &&
+ if ((postUserId !== userId || this.props.ownNewMessage) &&
this.props.lastViewed !== 0 &&
post.create_at > this.props.lastViewed &&
!renderedLastViewed) {
@@ -395,7 +410,7 @@ export default class PostList extends React.Component {
getArchivesIntroMessage() {
return (
- <div className='channel-intro'>
+ <div className={'channel-intro'}>
<h4 className='channel-intro__title'>
<FormattedMessage
id='post_focus_view.beginning'
@@ -412,10 +427,12 @@ export default class PostList extends React.Component {
}
window.addEventListener('resize', this.handleResize);
+ window.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
+ window.removeEventListener('keydown', this.handleKeyDown);
this.scrollStopAction.cancel();
}
@@ -510,7 +527,8 @@ export default class PostList extends React.Component {
}
PostList.defaultProps = {
- lastViewed: 0
+ lastViewed: 0,
+ ownNewMessage: false
};
PostList.propTypes = {
@@ -524,6 +542,7 @@ PostList.propTypes = {
showMoreMessagesTop: React.PropTypes.bool,
showMoreMessagesBottom: React.PropTypes.bool,
lastViewed: React.PropTypes.number,
+ ownNewMessage: React.PropTypes.bool,
postsToHighlight: React.PropTypes.object,
displayNameType: React.PropTypes.string,
displayPostsInCenter: React.PropTypes.bool,
diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx
index e5743e657..a7583fa38 100644
--- a/webapp/components/post_view/post_view_controller.jsx
+++ b/webapp/components/post_view/post_view_controller.jsx
@@ -27,6 +27,7 @@ export default class PostViewController extends React.Component {
this.onPostsChange = this.onPostsChange.bind(this);
this.onEmojisChange = this.onEmojisChange.bind(this);
this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this);
+ this.onSetNewMessageIndicator = this.onSetNewMessageIndicator.bind(this);
this.onPostListScroll = this.onPostListScroll.bind(this);
this.onActivate = this.onActivate.bind(this);
this.onDeactivate = this.onDeactivate.bind(this);
@@ -50,6 +51,7 @@ export default class PostViewController extends React.Component {
profiles,
atTop: PostStore.getVisibilityAtTop(channel.id),
lastViewed,
+ ownNewMessage: false,
scrollType: ScrollTypes.NEW_MESSAGE,
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
@@ -117,6 +119,7 @@ export default class PostViewController extends React.Component {
PostStore.addChangeListener(this.onPostsChange);
PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest);
EmojiStore.addChangeListener(this.onEmojisChange);
+ ChannelStore.addLastViewedListener(this.onSetNewMessageIndicator);
}
onDeactivate() {
@@ -125,6 +128,7 @@ export default class PostViewController extends React.Component {
PostStore.removeChangeListener(this.onPostsChange);
PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest);
EmojiStore.removeChangeListener(this.onEmojisChange);
+ ChannelStore.removeLastViewedListener(this.onSetNewMessageIndicator);
}
componentWillReceiveProps(nextProps) {
@@ -149,6 +153,7 @@ export default class PostViewController extends React.Component {
this.setState({
channel,
lastViewed,
+ ownNewMessage: false,
profiles: JSON.parse(JSON.stringify(profiles)),
postList: JSON.parse(JSON.stringify(PostStore.getVisiblePosts(channel.id))),
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
@@ -178,6 +183,10 @@ export default class PostViewController extends React.Component {
}
}
+ onSetNewMessageIndicator(lastViewed, ownNewMessage) {
+ this.setState({lastViewed, ownNewMessage});
+ }
+
onPostListScroll(atBottom) {
if (atBottom) {
this.setState({scrollType: ScrollTypes.BOTTOM});
@@ -219,6 +228,10 @@ export default class PostViewController extends React.Component {
return true;
}
+ if (nextState.ownNewMessage !== this.state.ownNewMessage) {
+ return true;
+ }
+
if (nextState.showMoreMessagesTop !== this.state.showMoreMessagesTop) {
return true;
}
@@ -281,6 +294,7 @@ export default class PostViewController extends React.Component {
useMilitaryTime={this.state.useMilitaryTime}
lastViewed={this.state.lastViewed}
emojis={this.state.emojis}
+ ownNewMessage={this.state.ownNewMessage}
/>
);
}
diff --git a/webapp/components/removed_from_channel_modal.jsx b/webapp/components/removed_from_channel_modal.jsx
index 3164e4e3f..3bdceadf7 100644
--- a/webapp/components/removed_from_channel_modal.jsx
+++ b/webapp/components/removed_from_channel_modal.jsx
@@ -116,7 +116,7 @@ export default class RemovedFromChannelModal extends React.Component {
id='removed_channel.remover'
defaultMessage='{remover} removed you from {channel}'
values={{
- remover: (remover),
+ remover,
channel: (channelName)
}}
/>
diff --git a/webapp/components/rename_channel_modal.jsx b/webapp/components/rename_channel_modal.jsx
index df08bdbc6..4dc84d971 100644
--- a/webapp/components/rename_channel_modal.jsx
+++ b/webapp/components/rename_channel_modal.jsx
@@ -203,7 +203,7 @@ export default class RenameChannelModal extends React.Component {
const displayName = ReactDOM.findDOMNode(this.refs.displayName).value.trim();
const channelName = Utils.cleanUpUrlable(displayName);
ReactDOM.findDOMNode(this.refs.channelName).value = channelName;
- this.setState({channelName: channelName});
+ this.setState({channelName});
}
}
diff --git a/webapp/components/search_bar.jsx b/webapp/components/search_bar.jsx
index d8725a7aa..290572612 100644
--- a/webapp/components/search_bar.jsx
+++ b/webapp/components/search_bar.jsx
@@ -3,7 +3,7 @@
import $ from 'jquery';
import ReactDOM from 'react-dom';
-import client from 'utils/web_client.jsx';
+import Client from 'utils/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import SearchStore from 'stores/search_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
@@ -11,30 +11,23 @@ import SuggestionBox from './suggestion/suggestion_box.jsx';
import SearchChannelProvider from './suggestion/search_channel_provider.jsx';
import SearchSuggestionList from './suggestion/search_suggestion_list.jsx';
import SearchUserProvider from './suggestion/search_user_provider.jsx';
-import * as utils from 'utils/utils.jsx';
+import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
var ActionTypes = Constants.ActionTypes;
import {Popover} from 'react-bootstrap';
-const holders = defineMessages({
- search: {
- id: 'search_bar.search',
- defaultMessage: 'Search'
- }
-});
-
import React from 'react';
-class SearchBar extends React.Component {
+export default class SearchBar extends React.Component {
constructor() {
super();
this.mounted = false;
this.onListenerChange = this.onListenerChange.bind(this);
- this.handleUserInput = this.handleUserInput.bind(this);
+ this.handleInput = this.handleInput.bind(this);
this.handleUserFocus = this.handleUserFocus.bind(this);
this.handleUserBlur = this.handleUserBlur.bind(this);
this.performSearch = this.performSearch.bind(this);
@@ -46,24 +39,28 @@ class SearchBar extends React.Component {
this.suggestionProviders = [new SearchChannelProvider(), new SearchUserProvider()];
}
+
getSearchTermStateFromStores() {
var term = SearchStore.getSearchTerm() || '';
return {
searchTerm: term
};
}
+
componentDidMount() {
SearchStore.addSearchTermChangeListener(this.onListenerChange);
this.mounted = true;
}
+
componentWillUnmount() {
SearchStore.removeSearchTermChangeListener(this.onListenerChange);
this.mounted = false;
}
+
onListenerChange(doSearch, isMentionSearch) {
if (this.mounted) {
var newState = this.getSearchTermStateFromStores();
- if (!utils.areObjectsEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
if (doSearch) {
@@ -71,9 +68,11 @@ class SearchBar extends React.Component {
}
}
}
+
clearFocus() {
$('.search-bar__container').removeClass('focused');
}
+
handleClose(e) {
e.preventDefault();
@@ -94,30 +93,34 @@ class SearchBar extends React.Component {
postId: null
});
}
- handleUserInput(text) {
- var term = text;
+
+ handleInput(e) {
+ var term = e.target.value;
SearchStore.storeSearchTerm(term);
SearchStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
}
+
handleUserBlur() {
this.setState({focused: false});
}
+
handleUserFocus() {
$('.search-bar__container').addClass('focused');
this.setState({focused: true});
}
+
performSearch(terms, isMentionSearch) {
if (terms.length) {
this.setState({isSearching: true});
- client.search(
+ Client.search(
terms,
isMentionSearch,
(data) => {
this.setState({isSearching: false});
- if (utils.isMobile()) {
+ if (Utils.isMobile()) {
ReactDOM.findDOMNode(this.refs.search).value = '';
}
@@ -134,6 +137,7 @@ class SearchBar extends React.Component {
);
}
}
+
handleSubmit(e) {
e.preventDefault();
this.performSearch(this.state.searchTerm.trim());
@@ -178,11 +182,11 @@ class SearchBar extends React.Component {
<SuggestionBox
ref='search'
className='form-control search-bar'
- placeholder={this.props.intl.formatMessage(holders.search)}
+ placeholder={Utils.localizeMessage('search_bar.search', 'Search')}
value={this.state.searchTerm}
onFocus={this.handleUserFocus}
onBlur={this.handleUserBlur}
- onUserInput={this.handleUserInput}
+ onInput={this.handleInput}
listComponent={SearchSuggestionList}
providers={this.suggestionProviders}
type='search'
@@ -202,11 +206,4 @@ class SearchBar extends React.Component {
</div>
);
}
-}
-
-SearchBar.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(SearchBar);
-
+} \ No newline at end of file
diff --git a/webapp/components/setting_item_max.jsx b/webapp/components/setting_item_max.jsx
index ec496a765..ad765a7d6 100644
--- a/webapp/components/setting_item_max.jsx
+++ b/webapp/components/setting_item_max.jsx
@@ -84,6 +84,7 @@ export default class SettingItemMax extends React.Component {
</li>
<li className='setting-list-item'>
<hr/>
+ {this.props.submitExtra}
{serverError}
{clientError}
{submit}
@@ -113,5 +114,6 @@ SettingItemMax.propTypes = {
updateSection: React.PropTypes.func,
submit: React.PropTypes.func,
title: React.PropTypes.node,
- width: React.PropTypes.string
+ width: React.PropTypes.string,
+ submitExtra: React.PropTypes.node
};
diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx
index 2184b9fab..d4b150787 100644
--- a/webapp/components/suggestion/suggestion_box.jsx
+++ b/webapp/components/suggestion/suggestion_box.jsx
@@ -21,8 +21,8 @@ export default class SuggestionBox extends React.Component {
this.handleDocumentClick = this.handleDocumentClick.bind(this);
- this.handleChange = this.handleChange.bind(this);
this.handleCompleteWord = this.handleCompleteWord.bind(this);
+ this.handleInput = this.handleInput.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handlePretextChanged = this.handlePretextChanged.bind(this);
@@ -70,27 +70,24 @@ export default class SuggestionBox extends React.Component {
}
}
- handleChange(e) {
+ handleInput(e) {
const textbox = ReactDOM.findDOMNode(this.refs.textbox);
const caret = Utils.getCaretPosition(textbox);
const pretext = textbox.value.substring(0, caret);
GlobalActions.emitSuggestionPretextChanged(this.suggestionId, pretext);
- if (this.props.onUserInput) {
- this.props.onUserInput(textbox.value);
- }
-
- if (this.props.onChange) {
- this.props.onChange(e);
+ if (this.props.onInput) {
+ this.props.onInput(e);
}
}
handleCompleteWord(term, matchedPretext) {
- const textbox = ReactDOM.findDOMNode(this.refs.textbox);
+ const textbox = this.refs.textbox;
const caret = Utils.getCaretPosition(textbox);
const text = textbox.value;
const pretext = text.substring(0, caret);
+
let prefix;
if (pretext.endsWith(matchedPretext)) {
prefix = pretext.substring(0, pretext.length - matchedPretext.length);
@@ -104,10 +101,17 @@ export default class SuggestionBox extends React.Component {
const suffix = text.substring(caret);
- if (this.props.onUserInput) {
- this.props.onUserInput(prefix + term + ' ' + suffix);
+ this.refs.textbox.value = prefix + term + ' ' + suffix;
+
+ if (this.props.onInput) {
+ // fake an input event to send back to parent components
+ const e = {
+ target: this.refs.textbox
+ };
+
+ // don't call handleInput or we'll get into an event loop
+ this.props.onInput(e);
}
- this.refs.textbox.value = (prefix + term + ' ' + suffix);
// set the caret position after the next rendering
window.requestAnimationFrame(() => {
@@ -128,6 +132,7 @@ export default class SuggestionBox extends React.Component {
e.preventDefault();
} else if (e.which === KeyCodes.ESCAPE) {
GlobalActions.emitClearSuggestions(this.suggestionId);
+ e.stopPropagation();
} else if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
@@ -143,18 +148,15 @@ export default class SuggestionBox extends React.Component {
}
render() {
- const newProps = Object.assign({}, this.props, {
- onChange: this.handleChange,
- onKeyDown: this.handleKeyDown
- });
-
let textbox = null;
if (this.props.type === 'input') {
textbox = (
<input
ref='textbox'
type='text'
- {...newProps}
+ {...this.props}
+ onInput={this.handleInput}
+ onKeyDown={this.handleKeyDown}
/>
);
} else if (this.props.type === 'search') {
@@ -162,7 +164,9 @@ export default class SuggestionBox extends React.Component {
<input
ref='textbox'
type='search'
- {...newProps}
+ {...this.props}
+ onInput={this.handleInput}
+ onKeyDown={this.handleKeyDown}
/>
);
} else if (this.props.type === 'textarea') {
@@ -170,7 +174,9 @@ export default class SuggestionBox extends React.Component {
<TextareaAutosize
id={this.suggestionId}
ref='textbox'
- {...newProps}
+ {...this.props}
+ onInput={this.handleInput}
+ onKeyDown={this.handleKeyDown}
/>
);
}
@@ -212,12 +218,10 @@ SuggestionBox.propTypes = {
listComponent: React.PropTypes.func.isRequired,
type: React.PropTypes.oneOf(['input', 'textarea', 'search']).isRequired,
value: React.PropTypes.string.isRequired,
- onUserInput: React.PropTypes.func,
providers: React.PropTypes.arrayOf(React.PropTypes.object),
listStyle: React.PropTypes.string,
// explicitly name any input event handlers we override and need to manually call
- onChange: React.PropTypes.func,
- onKeyDown: React.PropTypes.func,
- onHeightChange: React.PropTypes.func
+ onInput: React.PropTypes.func,
+ onKeyDown: React.PropTypes.func
};
diff --git a/webapp/components/suggestion/suggestion_list.jsx b/webapp/components/suggestion/suggestion_list.jsx
index f1cccf8aa..52b85b2f5 100644
--- a/webapp/components/suggestion/suggestion_list.jsx
+++ b/webapp/components/suggestion/suggestion_list.jsx
@@ -87,7 +87,7 @@ export default class SuggestionList extends React.Component {
content.scrollTop(itemTop - contentTopPadding);
} else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) {
// the item has gone off the bottom of the visible space
- content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding);
+ content.scrollTop((itemBottom - visibleContentHeight) + contentTopPadding + contentBottomPadding);
}
}
}
diff --git a/webapp/components/suggestion/switch_channel_provider.jsx b/webapp/components/suggestion/switch_channel_provider.jsx
index c12918c51..e092d9b5c 100644
--- a/webapp/components/suggestion/switch_channel_provider.jsx
+++ b/webapp/components/suggestion/switch_channel_provider.jsx
@@ -57,7 +57,13 @@ export default class SwitchChannelProvider {
}
}
- channels.sort((a, b) => a.display_name.localeCompare(b.display_name));
+ channels.sort((a, b) => {
+ if (a.display_name === b.display_name) {
+ return a.name.localeCompare(b.name);
+ }
+ return a.display_name.localeCompare(b.display_name);
+ });
+
const channelNames = channels.map((channel) => channel.name);
SuggestionStore.addSuggestions(suggestionId, channelNames, channels, SwitchChannelSuggestion, channelPrefix);
diff --git a/webapp/components/team_export_tab.jsx b/webapp/components/team_export_tab.jsx
deleted file mode 100644
index 15c131489..000000000
--- a/webapp/components/team_export_tab.jsx
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import Client from 'utils/web_client.jsx';
-
-import {FormattedMessage} from 'react-intl';
-
-import React from 'react';
-import {Link} from 'react-router/es6';
-
-export default class TeamExportTab extends React.Component {
- constructor(props) {
- super(props);
- this.state = {status: 'request', link: '', err: ''};
-
- this.onExportSuccess = this.onExportSuccess.bind(this);
- this.onExportFailure = this.onExportFailure.bind(this);
- this.doExport = this.doExport.bind(this);
- }
- onExportSuccess(data) {
- this.setState({status: 'ready', link: data.link, err: ''});
- }
- onExportFailure(e) {
- this.setState({status: 'failure', link: '', err: e.message});
- }
- doExport() {
- if (this.state.status === 'in-progress') {
- return;
- }
- this.setState({status: 'in-progress'});
- Client.exportTeam(this.onExportSuccess, this.onExportFailure);
- }
- render() {
- var messageSection = '';
- switch (this.state.status) {
- case 'request':
- messageSection = '';
- break;
- case 'in-progress':
- messageSection = (
- <p className='confirm-import alert alert-warning'>
- <i className='fa fa-spinner fa-pulse'/>
- <FormattedMessage
- id='team_export_tab.exporting'
- defaultMessage=' Exporting...'
- />
- </p>
- );
- break;
- case 'ready':
- messageSection = (
- <p className='confirm-import alert alert-success'>
- <i className='fa fa-check'/>
- <FormattedMessage
- id='team_export_tab.ready'
- defaultMessage=' Ready for '
- />
- <Link
- to={this.state.link}
- download={true}
- >
- <FormattedMessage
- id='team_export_tab.download'
- defaultMessage='download'
- />
- </Link>
- </p>
- );
- break;
- case 'failure':
- messageSection = (
- <p className='confirm-import alert alert-warning'>
- <i className='fa fa-warning'/>
- <FormattedMessage
- id='team_export_tab.unable'
- defaultMessage=' Unable to export: {error}'
- values={{
- error: this.state.err
- }}
- />
- </p>
- );
- break;
- }
-
- return (
- <div
- ref='wrapper'
- className='user-settings'
- >
- <h3 className='tab-header'>
- <FormattedMessage
- id='team_export_tab.export'
- defaultMessage='Export'
- />
- </h3>
- <div className='divider-dark first'/>
- <ul className='section-max'>
- <li className='col-xs-12 section-title'>
- <FormattedMessage
- id='team_export_tab.exportTeam'
- defaultMessage='Export your team'
- />
- </li>
- <li className='col-xs-offset-3 col-xs-8'>
- <ul className='setting-list'>
- <li className='setting-list-item'>
- <a
- className='btn btn-sm btn-primary btn-file sel-btn'
- href='#'
- onClick={this.doExport}
- >
- <FormattedMessage
- id='team_export_tab.export'
- defaultMessage='Export'
- />
- </a>
- </li>
- </ul>
- </li>
- </ul>
- <div className='divider-dark'/>
- {messageSection}
- </div>
- );
- }
-}
diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx
index 2814119c6..ac50c69a0 100644
--- a/webapp/components/team_general_tab.jsx
+++ b/webapp/components/team_general_tab.jsx
@@ -255,12 +255,10 @@ class GeneralTab extends React.Component {
}
updateName(e) {
- e.preventDefault();
this.setState({name: e.target.value});
}
updateInviteId(e) {
- e.preventDefault();
this.setState({invite_id: e.target.value});
}
diff --git a/webapp/components/team_members_dropdown.jsx b/webapp/components/team_members_dropdown.jsx
index 43449635d..f8c217aed 100644
--- a/webapp/components/team_members_dropdown.jsx
+++ b/webapp/components/team_members_dropdown.jsx
@@ -186,7 +186,7 @@ export default class TeamMembersDropdown extends React.Component {
}
const me = UserStore.getCurrentUser();
- let showMakeMember = teamMember.roles === 'admin' || user.roles === 'system_admin';
+ let showMakeMember = teamMember.roles === 'admin' && user.roles !== 'system_admin';
let showMakeAdmin = teamMember.roles === '' && user.roles !== 'system_admin';
let showMakeActive = false;
let showMakeNotActive = user.roles !== 'system_admin';
diff --git a/webapp/components/team_settings.jsx b/webapp/components/team_settings.jsx
index 210d1f541..0725f9fe5 100644
--- a/webapp/components/team_settings.jsx
+++ b/webapp/components/team_settings.jsx
@@ -3,7 +3,6 @@
import TeamStore from 'stores/team_store.jsx';
import ImportTab from './team_import_tab.jsx';
-import ExportTab from './team_export_tab.jsx';
import GeneralTab from './team_general_tab.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -58,13 +57,6 @@ export default class TeamSettings extends React.Component {
</div>
);
break;
- case 'export':
- result = (
- <div>
- <ExportTab/>
- </div>
- );
- break;
default:
result = (
<div/>
diff --git a/webapp/components/team_settings_modal.jsx b/webapp/components/team_settings_modal.jsx
index 8ac924cf8..aa7b0831e 100644
--- a/webapp/components/team_settings_modal.jsx
+++ b/webapp/components/team_settings_modal.jsx
@@ -17,10 +17,6 @@ const holders = defineMessages({
importTab: {
id: 'team_settings_modal.importTab',
defaultMessage: 'Import'
- },
- exportTab: {
- id: 'team_settings_modal.exportTab',
- defaultMessage: 'Export'
}
});
@@ -71,9 +67,6 @@ class TeamSettingsModal extends React.Component {
tabs.push({name: 'general', uiName: formatMessage(holders.generalTab), icon: 'icon fa fa-cog'});
tabs.push({name: 'import', uiName: formatMessage(holders.importTab), icon: 'icon fa fa-upload'});
- // To enable export uncomment this line
- //tabs.push({name: 'export', uiName: formatMessage(holders.exportTab), icon: 'fa fa-download'});
-
return (
<div
className='modal fade'
diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx
index 40e6aec4a..24f58f43e 100644
--- a/webapp/components/textbox.jsx
+++ b/webapp/components/textbox.jsx
@@ -176,11 +176,6 @@ export default class Textbox extends React.Component {
</div>
);
- const otherProps = {};
- if (!this.props.typing) {
- otherProps.value = this.props.messageText;
- }
-
return (
<div
ref='wrapper'
@@ -194,7 +189,7 @@ export default class Textbox extends React.Component {
spellCheck='true'
maxLength={Constants.MAX_POST_LEN}
placeholder={this.props.createMessage}
- onUserInput={this.props.onUserInput}
+ onInput={this.props.onInput}
onKeyPress={this.handleKeyPress}
onKeyDown={this.handleKeyDown}
onHeightChange={this.handleHeightChange}
@@ -202,7 +197,7 @@ export default class Textbox extends React.Component {
listComponent={SuggestionList}
providers={this.suggestionProviders}
channelId={this.props.channelId}
- {...otherProps}
+ value={this.props.messageText}
/>
<div
ref='preview'
@@ -239,10 +234,9 @@ Textbox.propTypes = {
id: React.PropTypes.string.isRequired,
channelId: React.PropTypes.string,
messageText: React.PropTypes.string.isRequired,
- onUserInput: React.PropTypes.func.isRequired,
+ onInput: React.PropTypes.func.isRequired,
onKeyPress: React.PropTypes.func.isRequired,
createMessage: React.PropTypes.string.isRequired,
onKeyDown: React.PropTypes.func,
- supportsCommands: React.PropTypes.bool.isRequired,
- typing: React.PropTypes.bool.isRequired
+ supportsCommands: React.PropTypes.bool.isRequired
};
diff --git a/webapp/components/user_settings/import_theme_modal.jsx b/webapp/components/user_settings/import_theme_modal.jsx
index 552659c4c..32c6837e8 100644
--- a/webapp/components/user_settings/import_theme_modal.jsx
+++ b/webapp/components/user_settings/import_theme_modal.jsx
@@ -1,30 +1,18 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import ReactDOM from 'react-dom';
import ModalStore from 'stores/modal_store.jsx';
-import UserStore from 'stores/user_store.jsx';
-import * as Utils from 'utils/utils.jsx';
-import Client from 'utils/web_client.jsx';
import {Modal} from 'react-bootstrap';
-import AppDispatcher from '../../dispatcher/app_dispatcher.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
-
-const holders = defineMessages({
- submitError: {
- id: 'user.settings.import_theme.submitError',
- defaultMessage: 'Invalid format, please try copying and pasting in again.'
- }
-});
+import {FormattedMessage} from 'react-intl';
const ActionTypes = Constants.ActionTypes;
import React from 'react';
-class ImportThemeModal extends React.Component {
+export default class ImportThemeModal extends React.Component {
constructor(props) {
super(props);
@@ -33,26 +21,42 @@ class ImportThemeModal extends React.Component {
this.handleChange = this.handleChange.bind(this);
this.state = {
+ value: '',
inputError: '',
- show: false
+ show: false,
+ callback: null
};
}
+
componentDidMount() {
ModalStore.addModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow);
}
+
componentWillUnmount() {
ModalStore.removeModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow);
}
- updateShow(show) {
- this.setState({show});
+
+ updateShow(show, args) {
+ this.setState({
+ show,
+ callback: args.callback
+ });
}
+
handleSubmit(e) {
e.preventDefault();
- const text = ReactDOM.findDOMNode(this.refs.input).value;
+ const text = this.state.value;
if (!this.isInputValid(text)) {
- this.setState({inputError: this.props.intl.formatMessage(holders.submitError)});
+ this.setState({
+ inputError: (
+ <FormattedMessage
+ id='user.settings.import_theme.submitError'
+ defaultMessage='Invalid format, please try copying and pasting in again.'
+ />
+ )
+ });
return;
}
@@ -81,26 +85,13 @@ class ImportThemeModal extends React.Component {
theme.mentionHighlightLink = '#2f81b7';
theme.codeTheme = 'github';
- const user = UserStore.getCurrentUser();
- user.theme_props = theme;
-
- Client.updateUser(user, Constants.UserUpdateEvents.THEME,
- (data) => {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_ME,
- me: data
- });
-
- this.setState({show: false});
- Utils.applyTheme(theme);
- },
- (err) => {
- var state = this.getStateFromStores();
- state.serverError = err;
- this.setState(state);
- }
- );
+ this.state.callback(theme);
+ this.setState({
+ show: false,
+ callback: null
+ });
}
+
isInputValid(text) {
if (text.length === 0) {
return false;
@@ -134,13 +125,25 @@ class ImportThemeModal extends React.Component {
return true;
}
+
handleChange(e) {
- if (this.isInputValid(e.target.value)) {
+ const value = e.target.value;
+ this.setState({value});
+
+ if (this.isInputValid(value)) {
this.setState({inputError: null});
} else {
- this.setState({inputError: this.props.intl.formatMessage(holders.submitError)});
+ this.setState({
+ inputError: (
+ <FormattedMessage
+ id='user.settings.import_theme.submitError'
+ defaultMessage='Invalid format, please try copying and pasting in again.'
+ />
+ )
+ });
}
}
+
render() {
return (
<span>
@@ -170,9 +173,9 @@ class ImportThemeModal extends React.Component {
<div className='form-group less'>
<div className='col-sm-9'>
<input
- ref='input'
type='text'
className='form-control'
+ value={this.state.value}
onChange={this.handleChange}
/>
<div className='input__help'>
@@ -210,9 +213,3 @@ class ImportThemeModal extends React.Component {
);
}
}
-
-ImportThemeModal.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(ImportThemeModal);
diff --git a/webapp/components/user_settings/premade_theme_chooser.jsx b/webapp/components/user_settings/premade_theme_chooser.jsx
index 9552c686d..03ea56449 100644
--- a/webapp/components/user_settings/premade_theme_chooser.jsx
+++ b/webapp/components/user_settings/premade_theme_chooser.jsx
@@ -7,8 +7,6 @@ import Constants from 'utils/constants.jsx';
import React from 'react';
-import {FormattedMessage} from 'react-intl';
-
export default class PremadeThemeChooser extends React.Component {
constructor(props) {
super(props);
@@ -54,20 +52,6 @@ export default class PremadeThemeChooser extends React.Component {
<div className='clearfix'>
{premadeThemes}
</div>
- <div className='clearfix'>
- <div className='col-sm-12 padding-bottom x2'>
- <a
- href='http://docs.mattermost.com/help/settings/theme-colors.html#custom-theme-examples'
- target='_blank'
- rel='noopener noreferrer'
- >
- <FormattedMessage
- id='user.settings.display.theme.otherThemes'
- defaultMessage='See other themes'
- />
- </a>
- </div>
- </div>
</div>
);
}
diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx
index a449c7d01..9b0e6a204 100644
--- a/webapp/components/user_settings/user_settings_general.jsx
+++ b/webapp/components/user_settings/user_settings_general.jsx
@@ -295,8 +295,17 @@ class UserSettingsGeneralTab extends React.Component {
setupInitialState(props) {
const user = props.user;
- return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname,
- email: user.email, confirmEmail: '', picture: null, loadingPicture: false, emailChangeInProgress: false};
+ return {
+ username: user.username,
+ firstName: user.first_name,
+ lastName: user.last_name,
+ nickname: user.nickname,
+ email: user.email,
+ confirmEmail: '',
+ picture: null,
+ loadingPicture: false,
+ emailChangeInProgress: false
+ };
}
createEmailSection() {
diff --git a/webapp/components/user_settings/user_settings_theme.jsx b/webapp/components/user_settings/user_settings_theme.jsx
index 94516ec8c..d12a7689a 100644
--- a/webapp/components/user_settings/user_settings_theme.jsx
+++ b/webapp/components/user_settings/user_settings_theme.jsx
@@ -8,28 +8,18 @@ import PremadeThemeChooser from './premade_theme_chooser.jsx';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import AppDispatcher from '../../dispatcher/app_dispatcher.jsx';
-import Client from 'utils/web_client.jsx';
-import * as Utils from 'utils/utils.jsx';
-
-import Constants from 'utils/constants.jsx';
+import * as UserActions from 'actions/user_actions.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
+import * as Utils from 'utils/utils.jsx';
-const ActionTypes = Constants.ActionTypes;
+import {FormattedMessage} from 'react-intl';
-const holders = defineMessages({
- themeTitle: {
- id: 'user.settings.display.theme.title',
- defaultMessage: 'Theme'
- },
- themeDescribe: {
- id: 'user.settings.display.theme.describe',
- defaultMessage: 'Open to manage your theme'
- }
-});
+import {ActionTypes, Constants, Preferences} from 'utils/constants.jsx';
import React from 'react';
@@ -47,6 +37,7 @@ export default class ThemeSetting extends React.Component {
this.originalTheme = Object.assign({}, this.state.theme);
}
+
componentDidMount() {
UserStore.addChangeListener(this.onChange);
@@ -54,17 +45,20 @@ export default class ThemeSetting extends React.Component {
$(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
}
}
+
componentDidUpdate() {
if (this.props.selected) {
$('.color-btn').removeClass('active-border');
$(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
}
}
+
componentWillReceiveProps(nextProps) {
if (this.props.selected && !nextProps.selected) {
this.resetFields();
}
}
+
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
@@ -73,27 +67,35 @@ export default class ThemeSetting extends React.Component {
Utils.applyTheme(state.theme);
}
}
+
getStateFromStores() {
- const user = UserStore.getCurrentUser();
- let theme = null;
+ const teamId = TeamStore.getCurrentId();
- if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
- theme = Object.assign({}, user.theme_props);
- } else {
- theme = $.extend(true, {}, Constants.THEMES.default);
+ const theme = PreferenceStore.getTheme(teamId);
+ if (!theme.codeTheme) {
+ theme.codeTheme = Constants.DEFAULT_CODE_THEME;
}
- let type = 'premade';
- if (theme.type === 'custom') {
- type = 'custom';
- }
+ let showAllTeamsCheckbox = false;
+ let applyToAllTeams = true;
- if (!theme.codeTheme) {
- theme.codeTheme = Constants.DEFAULT_CODE_THEME;
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true') {
+ // show the "apply to all teams" checkbox if the user is on more than one team
+ showAllTeamsCheckbox = Object.keys(TeamStore.getAll()).length > 1;
+
+ // check the "apply to all teams" checkbox by default if the user has any team-specific themes
+ applyToAllTeams = PreferenceStore.getCategory(Preferences.CATEGORY_THEME).size <= 1;
}
- return {theme, type};
+ return {
+ teamId: TeamStore.getCurrentId(),
+ theme,
+ type: theme.type || 'premade',
+ showAllTeamsCheckbox,
+ applyToAllTeams
+ };
}
+
onChange() {
const newState = this.getStateFromStores();
@@ -103,21 +105,20 @@ export default class ThemeSetting extends React.Component {
this.props.setEnforceFocus(true);
}
+
scrollToTop() {
$('.ps-container.modal-body').scrollTop(0);
}
+
submitTheme(e) {
e.preventDefault();
- var user = UserStore.getCurrentUser();
- user.theme_props = this.state.theme;
- Client.updateUser(user, Constants.UserUpdateEvents.THEME,
- (data) => {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_ME,
- me: data
- });
+ const teamId = this.state.applyToAllTeams ? '' : this.state.teamId;
+ UserActions.saveTheme(
+ teamId,
+ this.state.theme,
+ () => {
this.props.setRequireConfirm(false);
this.originalTheme = Object.assign({}, this.state.theme);
this.scrollToTop();
@@ -130,6 +131,7 @@ export default class ThemeSetting extends React.Component {
}
);
}
+
updateTheme(theme) {
let themeChanged = this.state.theme.length === theme.length;
if (!themeChanged) {
@@ -148,9 +150,11 @@ export default class ThemeSetting extends React.Component {
this.setState({theme});
Utils.applyTheme(theme);
}
+
updateType(type) {
this.setState({type});
}
+
resetFields() {
const state = this.getStateFromStores();
state.serverError = null;
@@ -161,17 +165,18 @@ export default class ThemeSetting extends React.Component {
this.props.setRequireConfirm(false);
}
+
handleImportModal() {
AppDispatcher.handleViewAction({
type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL,
- value: true
+ value: true,
+ callback: this.updateTheme
});
this.props.setEnforceFocus(false);
}
- render() {
- const {formatMessage} = this.props.intl;
+ render() {
var serverError;
if (this.state.serverError) {
serverError = this.state.serverError;
@@ -252,9 +257,27 @@ export default class ThemeSetting extends React.Component {
inputs.push(custom);
inputs.push(
- <div key='importSlackThemeButton'>
+ <div>
<br/>
<a
+ href='http://docs.mattermost.com/help/settings/theme-colors.html#custom-theme-examples'
+ target='_blank'
+ rel='noopener noreferrer'
+ >
+ <FormattedMessage
+ id='user.settings.display.theme.otherThemes'
+ defaultMessage='See other themes'
+ />
+ </a>
+ </div>
+ );
+
+ inputs.push(
+ <div
+ key='importSlackThemeButton'
+ className='padding-top'
+ >
+ <a
className='theme'
onClick={this.handleImportModal}
>
@@ -266,9 +289,29 @@ export default class ThemeSetting extends React.Component {
</div>
);
+ let allTeamsCheckbox = null;
+ if (this.state.showAllTeamsCheckbox) {
+ allTeamsCheckbox = (
+ <div className='checkbox user-settings__submit-checkbox'>
+ <label>
+ <input
+ type='checkbox'
+ checked={this.state.applyToAllTeams}
+ onChange={(e) => this.setState({applyToAllTeams: e.target.checked})}
+ />
+ <FormattedMessage
+ id='user.settings.display.theme.applyToAllTeams'
+ defaultMessage='Apply New Theme to All Teams'
+ />
+ </label>
+ </div>
+ );
+ }
+
themeUI = (
<SettingItemMax
inputs={inputs}
+ submitExtra={allTeamsCheckbox}
submit={this.submitTheme}
server_error={serverError}
width='full'
@@ -281,8 +324,18 @@ export default class ThemeSetting extends React.Component {
} else {
themeUI = (
<SettingItemMin
- title={formatMessage(holders.themeTitle)}
- describe={formatMessage(holders.themeDescribe)}
+ title={
+ <FormattedMessage
+ id='user.settings.display.theme.title'
+ defaultMessage='Theme'
+ />
+ }
+ describe={
+ <FormattedMessage
+ id='user.settings.display.theme.describe'
+ defaultMessage='Open to manage your theme'
+ />
+ }
updateSection={() => {
this.props.updateSection('theme');
}}
@@ -295,11 +348,8 @@ export default class ThemeSetting extends React.Component {
}
ThemeSetting.propTypes = {
- intl: intlShape.isRequired,
selected: React.PropTypes.bool.isRequired,
updateSection: React.PropTypes.func.isRequired,
setRequireConfirm: React.PropTypes.func.isRequired,
setEnforceFocus: React.PropTypes.func.isRequired
};
-
-export default injectIntl(ThemeSetting);
diff --git a/webapp/dispatcher/app_dispatcher.jsx b/webapp/dispatcher/app_dispatcher.jsx
index 5e43d3ad7..8ab38563b 100644
--- a/webapp/dispatcher/app_dispatcher.jsx
+++ b/webapp/dispatcher/app_dispatcher.jsx
@@ -9,7 +9,7 @@ const PayloadSources = Constants.PayloadSources;
const AppDispatcher = Object.assign(new Flux.Dispatcher(), {
handleServerAction: function performServerAction(action) {
if (!action.type) {
- console.warning('handleServerAction called with undefined action type'); // eslint-disable-line no-console
+ console.warn('handleServerAction called with undefined action type'); // eslint-disable-line no-console
}
var payload = {
@@ -21,7 +21,7 @@ const AppDispatcher = Object.assign(new Flux.Dispatcher(), {
handleViewAction: function performViewAction(action) {
if (!action.type) {
- console.warning('handleViewAction called with undefined action type'); // eslint-disable-line no-console
+ console.warn('handleViewAction called with undefined action type'); // eslint-disable-line no-console
}
var payload = {
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 0d3080d03..27c5b7da4 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -521,14 +521,14 @@
"admin.service.attemptDescription": "Number of login attempts allowed before a user is locked out and required to reset their password via email.",
"admin.service.attemptExample": "Ex \"10\"",
"admin.service.attemptTitle": "Maximum Login Attempts:",
- "admin.service.cmdsDesc": "When true, user created slash commands will be allowed.",
+ "admin.service.cmdsDesc": "When true, custom slash commands will be allowed. See <a href='http://docs.mattermost.com/developer/slash-commands.html' target='_blank'>documentation</a> to learn more.",
"admin.service.cmdsTitle": "Enable Custom Slash Commands: ",
"admin.service.corsDescription": "Enable HTTP Cross origin request from a specific domain. Use \"*\" if you want to allow CORS from any domain or leave it blank to disable it.",
"admin.service.corsEx": "http://example.com",
"admin.service.corsTitle": "Enable cross-origin requests from:",
"admin.service.developerDesc": "When true, Javascript errors are shown in a red bar at the top of the user interface. Not recommended for use in production. ",
"admin.service.developerTitle": "Enable Developer Mode: ",
- "admin.service.googleDescription": "Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at <a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Leaving the field blank disables the automatic generation of YouTube video previews from links.",
+ "admin.service.googleDescription": "Set this key to enable the display of titles for embedded YouTube video previews. Without the key, YouTube previews will still be created based on hyperlinks appearing in messages or comments but they will not show the video title. View a <a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">Google Developers Tutorial</a> for instructions on how to obtain a key.",
"admin.service.googleExample": "Ex \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"",
"admin.service.googleTitle": "Google API Key:",
"admin.service.iconDescription": "When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.",
@@ -544,7 +544,7 @@
"admin.service.mfaTitle": "Enable Multi-factor Authentication:",
"admin.service.mobileSessionDays": "Session length for mobile apps (days):",
"admin.service.mobileSessionDaysDesc": "Mobile sessions will expire after the number of days specified and will require users to sign in again.",
- "admin.service.outWebhooksDesc": "When true, outgoing webhooks will be allowed.",
+ "admin.service.outWebhooksDesc": "When true, outgoing webhooks will be allowed. See <a href='http://docs.mattermost.com/developer/webhooks-outgoing.html' target='_blank'>documentation</a> to learn more.",
"admin.service.outWebhooksTitle": "Enable Outgoing Webhooks: ",
"admin.service.overrideDescription": "When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.",
"admin.service.overrideTitle": "Enable webhooks and slash commands to override usernames:",
@@ -562,7 +562,7 @@
"admin.service.testingTitle": "Enable Testing Commands: ",
"admin.service.webSessionDays": "Session length for email and LDAP authentication (days):",
"admin.service.webSessionDaysDesc": "Email or LDAP sessions will expire after the number of days specified and will require users to sign in again.",
- "admin.service.webhooksDescription": "When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.",
+ "admin.service.webhooksDescription": "When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag. See <a href='http://docs.mattermost.com/developer/webhooks-incoming.html' target='_blank'>documentation</a> to learn more.",
"admin.service.webhooksTitle": "Enable Incoming Webhooks: ",
"admin.sidebar.addTeamSidebar": "Add team from sidebar menu",
"admin.sidebar.advanced": "Advanced",
@@ -842,6 +842,7 @@
"channel_header.viewInfo": "View Info",
"channel_info.about": "About",
"channel_info.close": "Close",
+ "channel_info.header": "Header:",
"channel_info.id": "ID: ",
"channel_info.name": "Name:",
"channel_info.notFound": "No Channel Found",
@@ -870,6 +871,8 @@
"channel_modal.displayNameError": "This field is required",
"channel_modal.edit": "Edit",
"channel_modal.group": "Group",
+ "channel_modal.header": "Header",
+ "channel_modal.headerHelp": "Set text that will appear in the header of the {term} beside the {term} name. For example, include frequently used links by typing [Link Title](http://example.com).",
"channel_modal.modalTitle": "New ",
"channel_modal.name": "Name",
"channel_modal.nameEx": "E.g.: \"Bugs\", \"Marketing\", \"客户支持\"",
diff --git a/webapp/i18n/i18n.jsx b/webapp/i18n/i18n.jsx
index 118aa0ee2..1a6efd000 100644
--- a/webapp/i18n/i18n.jsx
+++ b/webapp/i18n/i18n.jsx
@@ -1,11 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const de = require('!!file?name=i18n/[name].[ext]!./de.json');
-const es = require('!!file?name=i18n/[name].[ext]!./es.json');
-const fr = require('!!file?name=i18n/[name].[ext]!./fr.json');
-const ja = require('!!file?name=i18n/[name].[ext]!./ja.json');
-const pt_BR = require('!!file?name=i18n/[name].[ext]!./pt-BR.json'); //eslint-disable-line camelcase
+const de = require('!!file?name=i18n/[name].[hash].[ext]!./de.json');
+const es = require('!!file?name=i18n/[name].[hash].[ext]!./es.json');
+const fr = require('!!file?name=i18n/[name].[hash].[ext]!./fr.json');
+const ja = require('!!file?name=i18n/[name].[hash].[ext]!./ja.json');
+const pt_BR = require('!!file?name=i18n/[name].[hash].[ext]!./pt-BR.json'); //eslint-disable-line camelcase
import {addLocaleData} from 'react-intl';
import deLocaleData from 'react-intl/locale-data/de';
diff --git a/webapp/package.json b/webapp/package.json
index 3db9d0794..f16def242 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -3,66 +3,68 @@
"version": "0.0.1",
"private": true,
"dependencies": {
- "autolinker": "mattermost/Autolinker.js#9689831109e104d7b545318e54199e6de8fd9b87",
+ "autolinker": "0.27.0",
"bootstrap": "3.3.6",
"bootstrap-colorpicker": "2.3.3",
- "chart.js": "2.1.2",
- "compass-mixins": "0.12.7",
+ "chart.js": "2.1.6",
+ "compass-mixins": "0.12.10",
"fastclick": "1.0.6",
"flux": "2.1.1",
- "font-awesome": "4.6.1",
- "highlight.js": "9.3.0",
- "intl": "1.1.0",
+ "font-awesome": "4.6.3",
+ "highlight.js": "9.5.0",
+ "intl": "1.2.4",
"jasny-bootstrap": "3.1.3",
- "jquery": "2.2.3",
+ "jquery": "3.1.0",
"keymirror": "0.1.1",
"marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b",
"match-at": "0.1.0",
- "mattermost": "mattermost/mattermost-javascript#5815f14f0d1960aa4c99797b09d949d2959eb24f",
+ "mattermost": "mattermost/mattermost-javascript#4cdaeba22ff82bf93dc417af1ab4e89e3248d624",
"object-assign": "4.1.0",
- "perfect-scrollbar": "0.6.11",
- "react": "15.0.2",
- "react-addons-pure-render-mixin": "15.0.2",
- "react-bootstrap": "0.29.3",
- "react-custom-scrollbars": "4.0.0-beta.1",
- "react-dom": "15.0.2",
- "react-intl": "2.1.2",
- "react-router": "2.4.0",
+ "perfect-scrollbar": "0.6.12",
+ "react": "15.2.1",
+ "react-addons-pure-render-mixin": "15.2.1",
+ "react-bootstrap": "0.29.5",
+ "react-custom-scrollbars": "4.0.0",
+ "react-dom": "15.2.1",
+ "react-intl": "2.1.3",
+ "react-router": "2.5.2",
"react-select": "1.0.0-beta13",
- "react-textarea-autosize": "4.0.1",
- "superagent": "1.8.3",
+ "react-textarea-autosize": "4.0.3",
+ "superagent": "2.1.0",
"twemoji": "2.0.5",
"velocity-animate": "1.2.3"
},
"devDependencies": {
- "babel-eslint": "6.0.4",
+ "babel-core": "6.10.4",
+ "babel-eslint": "6.1.2",
"babel-loader": "6.2.4",
- "babel-plugin-transform-runtime": "6.8.0",
- "babel-polyfill": "6.8.0",
+ "babel-plugin-transform-runtime": "6.9.0",
+ "babel-polyfill": "6.9.1",
"babel-preset-es2015-webpack": "6.4.1",
- "babel-preset-react": "6.5.0",
+ "babel-preset-react": "6.11.1",
"babel-preset-stage-0": "6.5.0",
- "copy-webpack-plugin": "2.1.3",
+ "copy-webpack-plugin": "3.0.1",
"css-loader": "0.23.1",
- "eslint": "2.9.0",
- "eslint-plugin-react": "5.1.1",
+ "eslint": "3.0.1",
+ "eslint-plugin-react": "5.2.2",
"exports-loader": "0.6.3",
"extract-text-webpack-plugin": "1.0.1",
- "file-loader": "0.8.5",
+ "file-loader": "0.9.0",
"html-loader": "0.4.3",
+ "html-webpack-plugin": "2.22.0",
"imports-loader": "0.6.5",
- "image-webpack-loader": "1.8.0",
+ "image-webpack-loader": "2.0.0",
"jquery-deferred": "0.3.0",
- "jsdom": "9.0.0",
- "jsdom-global": "1.7.0",
+ "jsdom": "9.4.1",
+ "jsdom-global": "2.0.0",
"json-loader": "0.5.4",
- "mocha": "2.4.5",
+ "mocha": "2.5.3",
"mocha-jsdom": "1.1.0",
- "mocha-webpack": "0.3.0",
- "node-sass": "3.4.2",
+ "mocha-webpack": "0.4.0",
+ "node-sass": "3.8.0",
"raw-loader": "0.5.1",
- "react-addons-test-utils": "15.0.2",
- "sass-loader": "3.2.0",
+ "react-addons-test-utils": "15.2.1",
+ "sass-loader": "4.0.0",
"style-loader": "0.13.1",
"url-loader": "0.5.7",
"webpack": "2.1.0-beta.13",
diff --git a/webapp/root.html b/webapp/root.html
index d3c569361..b48712e46 100644
--- a/webapp/root.html
+++ b/webapp/root.html
@@ -34,11 +34,8 @@
<!-- CSS Should always go first -->
<link rel='stylesheet' class='code_theme'>
- <!--<link rel='stylesheet' href='/static/css/styles.css'>-->
<style id='antiClickjack'>body{display:none !important;}</style>
- <script src='/static/bundle.js'></script>
-
<script type='text/javascript'>
if (self === top) {
var blocker = document.getElementById('antiClickjack');
diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss
index 73651a320..6e5ff5d06 100644
--- a/webapp/sass/components/_modal.scss
+++ b/webapp/sass/components/_modal.scss
@@ -384,7 +384,7 @@
.loader-percent {
bottom: 0;
- color: $dark-gray;
+ color: alpha-color($white, 0.8);
height: 20px;
left: 0;
margin: auto;
diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss
index 56f7cd6e8..1e7046340 100644
--- a/webapp/sass/layout/_headers.scss
+++ b/webapp/sass/layout/_headers.scss
@@ -162,6 +162,10 @@
margin: 0 auto 15px;
padding: 0 15px;
+ &.channel-intro--centered {
+ max-width: 1020px;
+ }
+
.intro-links {
display: inline-block;
margin: 0 1.5em 10px 0;
diff --git a/webapp/sass/layout/_post-right.scss b/webapp/sass/layout/_post-right.scss
index f1fe0cac3..3ee4fe025 100644
--- a/webapp/sass/layout/_post-right.scss
+++ b/webapp/sass/layout/_post-right.scss
@@ -1,156 +1,154 @@
@charset 'UTF-8';
-.app__body {
- .post-right__container {
- @include display-flex;
- @include flex-direction(column);
- height: 100%;
-
- .post-right-root-message {
- padding: 1em 1em 0;
- }
-
- .post-right-comments-container {
- position: relative;
+.post-right__container {
+ @include display-flex;
+ @include flex-direction(column);
+ height: 100%;
- .post {
- &:first-child {
- padding-top: 15px;
- }
- }
- }
+ .post-right-root-message {
+ padding: 1em 1em 0;
+ }
+ .post-right-comments-container {
+ position: relative;
- .help_format_text {
- bottom: -63px;
- right: auto;
+ .post {
+ &:first-child {
+ padding-top: 15px;
+ }
}
+ }
- .post {
- &.post--root {
- border-bottom: 1px solid #ddd;
- padding-bottom: 1em;
- .post__body {
- background: transparent !important;
- }
- }
+ .help_format_text {
+ bottom: -63px;
+ right: auto;
+ }
- .post__header {
- .col__reply {
- background: transparent !important;
- border: none !important;
- text-align: right;
- top: 0;
- }
- }
+ .post {
+ &.post--root {
+ border-bottom: 1px solid #ddd;
+ padding-bottom: 1em;
.post__body {
- width: 100%;
+ background: transparent !important;
}
}
- hr {
- border: none;
- margin-bottom: 0;
+ .post__header {
+ .col__reply {
+ background: transparent !important;
+ border: none !important;
+ text-align: right;
+ top: 0;
+ }
}
- .post-create__container {
+ .post__body {
width: 100%;
+ }
+ }
- .textarea-wrapper {
- min-height: 100px;
- }
-
- .btn {
- margin-bottom: 10px;
- }
+ hr {
+ border: none;
+ margin-bottom: 0;
+ }
- .custom-textarea {
- min-height: 100px;
- }
+ .post-create__container {
+ width: 100%;
- .msg-typing {
- @include opacity(.7);
- display: block;
- float: left;
- font-size: 13px;
- height: 20px;
- line-height: 20px;
- margin-top: 3px;
- max-width: 230px;
- min-width: 1px;
- }
+ .textarea-wrapper {
+ min-height: 100px;
+ }
- .post-create-footer {
- padding: 5px 0 10px;
- width: 100%;
- }
+ .btn {
+ margin-bottom: 10px;
+ }
- .post-right-comments-upload-in-progress {
- color: #a8adb7;
- margin-right: 10px;
- padding: 6px 0;
- }
+ .custom-textarea {
+ min-height: 100px;
}
- }
- .post-right-header {
- border-bottom: $border-gray;
- color: #999;
- font-size: 1em;
- font-weight: 400;
- height: 39px;
- padding: 10px 10px 0 15px;
- text-transform: uppercase;
- }
+ .msg-typing {
+ @include opacity(.7);
+ display: block;
+ float: left;
+ font-size: 13px;
+ height: 20px;
+ line-height: 20px;
+ margin-top: 3px;
+ max-width: 230px;
+ min-width: 1px;
+ }
- .post-right-root-container {
- border-bottom: $border-gray;
- padding: 5px 10px;
+ .post-create-footer {
+ padding: 5px 0 10px;
+ width: 100%;
+ }
- ul {
- margin-bottom: 2px;
- padding-left: 0;
+ .post-right-comments-upload-in-progress {
+ color: #a8adb7;
+ margin-right: 10px;
+ padding: 6px 0;
}
}
+}
- .post-right-channel__name {
- font-weight: 600;
- margin: 0 0 15px;
- }
+.post-right-header {
+ border-bottom: $border-gray;
+ color: #999;
+ font-size: 1em;
+ font-weight: 400;
+ height: 39px;
+ padding: 10px 10px 0 15px;
+ text-transform: uppercase;
+}
- .post-right-root-container li {
- display: inline;
- list-style-type: none;
- padding-right: 3px;
- }
+.post-right-root-container {
+ border-bottom: $border-gray;
+ padding: 5px 10px;
- .post-right-root-time {
- color: #a8adb7;
+ ul {
+ margin-bottom: 2px;
+ padding-left: 0;
}
+}
- .post-right-create-comment-container {
- bottom: 0;
- left: 0;
- margin-bottom: 18px;
- padding: 5px;
- position: absolute;
- width: 100%;
- }
+.post-right-channel__name {
+ font-weight: 600;
+ margin: 0 0 15px;
+}
- .post-right__scroll {
- @include flex(1 1 auto);
- -webkit-overflow-scrolling: touch;
- overflow: auto;
- position: relative;
+.post-right-root-container li {
+ display: inline;
+ list-style-type: none;
+ padding-right: 3px;
+}
- .file-preview__container {
- margin-top: 5px;
- }
- }
+.post-right-root-time {
+ color: #a8adb7;
+}
+
+.post-right-create-comment-container {
+ bottom: 0;
+ left: 0;
+ margin-bottom: 18px;
+ padding: 5px;
+ position: absolute;
+ width: 100%;
+}
- .post-right-comment-time {
- color: #a8adb7;
+.post-right__scroll {
+ -webkit-overflow-scrolling: touch;
+ @include flex(1 1 auto);
+ overflow: auto;
+ position: relative;
+
+ .file-preview__container {
+ margin-top: 5px;
}
}
+
+.post-right-comment-time {
+ color: #a8adb7;
+}
diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss
index 50158ccc2..f95bb3e59 100644
--- a/webapp/sass/layout/_post.scss
+++ b/webapp/sass/layout/_post.scss
@@ -458,7 +458,7 @@ body.ios {
.post-list__table {
display: table;
height: 100%;
- min-height: 100%;
+ min-height: 350px;
table-layout: fixed;
width: 100%;
diff --git a/webapp/sass/layout/_sidebar-left.scss b/webapp/sass/layout/_sidebar-left.scss
index d4d01c865..4c718327e 100644
--- a/webapp/sass/layout/_sidebar-left.scss
+++ b/webapp/sass/layout/_sidebar-left.scss
@@ -178,6 +178,10 @@
border-radius: 0;
font-weight: 400;
position: relative;
+
+ &.unread-title {
+ font-weight: 600;
+ }
}
}
}
diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss
index 9db962ec1..fcbf23b1c 100644
--- a/webapp/sass/responsive/_mobile.scss
+++ b/webapp/sass/responsive/_mobile.scss
@@ -493,14 +493,6 @@
}
.post-create__container {
- .post-right__container & {
- padding: 0 1em;
-
- .msg-typing {
- display: none;
- }
- }
-
form {
padding: .5em 0;
}
@@ -524,6 +516,7 @@
.post-body__cell {
display: table-cell;
padding-left: 45px;
+
.sidebar--right & {
padding-left: 0;
}
@@ -531,12 +524,12 @@
.app__content & {
.btn-file {
- width: 45px;
- padding: 0;
- line-height: 36px;
bottom: -2px;
left: 0;
+ line-height: 36px;
+ padding: 0;
top: auto;
+ width: 45px;
}
}
@@ -576,6 +569,24 @@
}
}
+ // Since system console is not responsive we're overriding bootstrap styles for it
+ .admin-sidebar {
+ .navbar-nav {
+ margin-top: 0;
+
+ > li {
+ float: left;
+ }
+
+ .dropdown-menu {
+ background: $white;
+ left: auto;
+ position: absolute;
+ right: 0;
+ }
+ }
+ }
+
#navbar {
.navbar-default {
.navbar-header {
@@ -802,6 +813,25 @@
right: 0;
width: 100%;
+ .post-create__container {
+ form {
+ padding: .5em 1em;
+ }
+
+ .msg-typing:empty {
+ display: none;
+ }
+
+ .post-create-footer {
+ padding: 1em 0;
+
+ .control-label {
+ margin: .5em 0;
+ top: 0;
+ }
+ }
+ }
+
.sidebar__collapse,
.sidebar__search-icon {
display: block;
@@ -871,8 +901,8 @@
}
.app__content {
- padding-top: 45px;
margin: 0;
+ padding-top: 45px;
.channel__wrap & {
padding-top: 45px;
diff --git a/webapp/sass/routes/_settings.scss b/webapp/sass/routes/_settings.scss
index f67d1b49b..36a1acf76 100644
--- a/webapp/sass/routes/_settings.scss
+++ b/webapp/sass/routes/_settings.scss
@@ -171,6 +171,9 @@
.theme-label {
font-weight: 400;
margin-top: 5px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
img {
@@ -343,7 +346,10 @@
a {
border-radius: 0;
color: $gray;
- padding: 8px 5px 8px 15px;
+ overflow: hidden;
+ padding: 8px 15px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.icon {
@@ -382,10 +388,7 @@
@include alpha-property(background-color, $black, .1);
border-radius: 0;
font-weight: 400;
- overflow: hidden;
position: relative;
- text-overflow: ellipsis;
- white-space: nowrap;
}
}
}
@@ -475,3 +478,8 @@
.no-resize {
resize: none;
}
+
+.user-settings__submit-checkbox {
+ padding-top: 0px;
+ padding-bottom: 20px;
+}
diff --git a/webapp/stores/channel_store.jsx b/webapp/stores/channel_store.jsx
index dc2577811..542fbdf6b 100644
--- a/webapp/stores/channel_store.jsx
+++ b/webapp/stores/channel_store.jsx
@@ -13,6 +13,7 @@ const CHANGE_EVENT = 'change';
const LEAVE_EVENT = 'leave';
const MORE_CHANGE_EVENT = 'change';
const EXTRA_INFO_EVENT = 'extra_info';
+const LAST_VIEVED_EVENT = 'last_viewed';
class ChannelStoreClass extends EventEmitter {
constructor(props) {
@@ -32,6 +33,9 @@ class ChannelStoreClass extends EventEmitter {
this.emitLeave = this.emitLeave.bind(this);
this.addLeaveListener = this.addLeaveListener.bind(this);
this.removeLeaveListener = this.removeLeaveListener.bind(this);
+ this.emitLastViewed = this.emitLastViewed.bind(this);
+ this.addLastViewedListener = this.addLastViewedListener.bind(this);
+ this.removeLastViewedListener = this.removeLastViewedListener.bind(this);
this.findFirstBy = this.findFirstBy.bind(this);
this.get = this.get.bind(this);
this.getMember = this.getMember.bind(this);
@@ -109,6 +113,18 @@ class ChannelStoreClass extends EventEmitter {
this.removeListener(LEAVE_EVENT, callback);
}
+ emitLastViewed(lastViewed, ownNewMessage) {
+ this.emit(LAST_VIEVED_EVENT, lastViewed, ownNewMessage);
+ }
+
+ addLastViewedListener(callback) {
+ this.on(LAST_VIEVED_EVENT, callback);
+ }
+
+ removeLastViewedListener(callback) {
+ this.removeListener(LAST_VIEVED_EVENT, callback);
+ }
+
findFirstBy(field, value) {
return this.doFindFirst(field, value, this.getChannels());
}
diff --git a/webapp/stores/preference_store.jsx b/webapp/stores/preference_store.jsx
index 324ec4864..654036ae8 100644
--- a/webapp/stores/preference_store.jsx
+++ b/webapp/stores/preference_store.jsx
@@ -54,6 +54,16 @@ class PreferenceStoreClass extends EventEmitter {
return parseInt(this.preferences.get(key), 10);
}
+ getObject(category, name, defaultValue = null) {
+ const key = this.getKey(category, name);
+
+ if (!this.preferences.has(key)) {
+ return defaultValue;
+ }
+
+ return JSON.parse(this.preferences.get(key));
+ }
+
getCategory(category) {
const prefix = category + '--';
@@ -78,6 +88,10 @@ class PreferenceStoreClass extends EventEmitter {
}
}
+ deletePreference(preference) {
+ this.preferences.delete(this.getKey(preference.category, preference.name));
+ }
+
clear() {
this.preferences.clear();
}
@@ -94,6 +108,18 @@ class PreferenceStoreClass extends EventEmitter {
this.removeListener(CHANGE_EVENT, callback);
}
+ getTheme(teamId) {
+ if (this.preferences.has(this.getKey(Constants.Preferences.CATEGORY_THEME, teamId))) {
+ return this.getObject(Constants.Preferences.CATEGORY_THEME, teamId);
+ }
+
+ if (this.preferences.has(this.getKey(Constants.Preferences.CATEGORY_THEME, ''))) {
+ return this.getObject(Constants.Preferences.CATEGORY_THEME, '');
+ }
+
+ return Constants.THEMES.default;
+ }
+
handleEventPayload(payload) {
const action = payload.action;
@@ -108,6 +134,12 @@ class PreferenceStoreClass extends EventEmitter {
this.setPreferencesFromServer(action.preferences);
this.emitChange();
break;
+ case ActionTypes.DELETED_PREFERENCES:
+ for (const preference of action.preferences) {
+ this.deletePreference(preference);
+ }
+ this.emitChange();
+ break;
}
}
}
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index e55742140..b31a2a6b9 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -145,6 +145,43 @@ export function updateLastViewedAt(id) {
);
}
+export function setLastViewedAt(lastViewedAt, id) {
+ let channelId;
+ if (id) {
+ channelId = id;
+ } else {
+ channelId = ChannelStore.getCurrentId();
+ }
+
+ if (channelId == null) {
+ return;
+ }
+
+ if (lastViewedAt == null) {
+ return;
+ }
+
+ if (isCallInProgress(`setLastViewedAt${channelId}${lastViewedAt}`)) {
+ return;
+ }
+
+ callTracker[`setLastViewedAt${channelId}${lastViewedAt}`] = utils.getTimestamp();
+ Client.setLastViewedAt(
+ channelId,
+ lastViewedAt,
+ () => {
+ callTracker.setLastViewedAt = 0;
+ ErrorStore.clearLastError();
+ },
+ (err) => {
+ callTracker.setLastViewedAt = 0;
+ var count = ErrorStore.getConnectionErrorCount();
+ ErrorStore.setConnectionErrorCount(count + 1);
+ dispatchError(err, 'setLastViewedAt');
+ }
+ );
+}
+
export function getMoreChannels(force) {
if (isCallInProgress('getMoreChannels')) {
return;
@@ -815,6 +852,29 @@ export function savePreferences(preferences, success, error) {
);
}
+export function deletePreferences(preferences, success, error) {
+ Client.deletePreferences(
+ preferences,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.DELETED_PREFERENCES,
+ preferences
+ });
+
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ dispatchError(err, 'deletePreferences');
+
+ if (error) {
+ error();
+ }
+ }
+ );
+}
+
export function getSuggestedCommands(command, suggestionId, component) {
Client.listCommands(
(data) => {
diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx
index 50d12ed42..6418615a4 100644
--- a/webapp/utils/channel_intro_messages.jsx
+++ b/webapp/utils/channel_intro_messages.jsx
@@ -16,20 +16,25 @@ import Client from 'utils/web_client.jsx';
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl';
-export function createChannelIntroMessage(channel) {
+export function createChannelIntroMessage(channel, fullWidthIntro) {
+ let centeredIntro = '';
+ if (!fullWidthIntro) {
+ centeredIntro = 'channel-intro--centered';
+ }
+
if (channel.type === 'D') {
- return createDMIntroMessage(channel);
+ return createDMIntroMessage(channel, centeredIntro);
} else if (ChannelStore.isDefault(channel)) {
- return createDefaultIntroMessage(channel);
+ return createDefaultIntroMessage(channel, centeredIntro);
} else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
- return createOffTopicIntroMessage(channel);
+ return createOffTopicIntroMessage(channel, centeredIntro);
} else if (channel.type === 'O' || channel.type === 'P') {
- return createStandardIntroMessage(channel);
+ return createStandardIntroMessage(channel, centeredIntro);
}
return null;
}
-export function createDMIntroMessage(channel) {
+export function createDMIntroMessage(channel, centeredIntro) {
var teammate = Utils.getDirectTeammate(channel.id);
if (teammate) {
@@ -39,7 +44,7 @@ export function createDMIntroMessage(channel) {
}
return (
- <div className='channel-intro'>
+ <div className={'channel-intro ' + centeredIntro}>
<div className='post-profile-img__container channel-intro-img'>
<img
className='post-profile-img'
@@ -68,7 +73,7 @@ export function createDMIntroMessage(channel) {
}
return (
- <div className='channel-intro'>
+ <div className={'channel-intro ' + centeredIntro}>
<p className='channel-intro-text'>
<FormattedMessage
id='intro_messages.teammate'
@@ -79,9 +84,9 @@ export function createDMIntroMessage(channel) {
);
}
-export function createOffTopicIntroMessage(channel) {
+export function createOffTopicIntroMessage(channel, centeredIntro) {
return (
- <div className='channel-intro'>
+ <div className={'channel-intro ' + centeredIntro}>
<FormattedHTMLMessage
id='intro_messages.offTopic'
defaultMessage='<h4 class="channel-intro__title">Beginning of {display_name}</h4><p class="channel-intro__content">This is the start of {display_name}, a channel for non-work-related conversations.<br/></p>'
@@ -95,7 +100,7 @@ export function createOffTopicIntroMessage(channel) {
);
}
-export function createDefaultIntroMessage(channel) {
+export function createDefaultIntroMessage(channel, centeredIntro) {
let inviteModalLink = (
<a
className='intro-links'
@@ -122,7 +127,7 @@ export function createDefaultIntroMessage(channel) {
}
return (
- <div className='channel-intro'>
+ <div className={'channel-intro ' + centeredIntro}>
<FormattedHTMLMessage
id='intro_messages.default'
defaultMessage="<h4 class='channel-intro__title'>Beginning of {display_name}</h4><p class='channel-intro__content'><strong>Welcome to {display_name}!</strong><br/><br/>This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.</p>"
@@ -137,7 +142,7 @@ export function createDefaultIntroMessage(channel) {
);
}
-export function createStandardIntroMessage(channel) {
+export function createStandardIntroMessage(channel, centeredIntro) {
var uiName = channel.display_name;
var creatorName = '';
@@ -189,7 +194,7 @@ export function createStandardIntroMessage(channel) {
values={{
name: (uiName),
type: (uiType),
- date: (date)
+ date
}}
/>
);
@@ -202,7 +207,7 @@ export function createStandardIntroMessage(channel) {
values={{
name: (uiName),
type: (uiType),
- date: (date),
+ date,
creator: creatorName
}}
/>
@@ -211,7 +216,7 @@ export function createStandardIntroMessage(channel) {
}
return (
- <div className='channel-intro'>
+ <div className={'channel-intro ' + centeredIntro}>
<h4 className='channel-intro__title'>
<FormattedMessage
id='intro_messages.beginning'
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 52fb23d51..d780efe30 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -33,100 +33,125 @@ 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,
- CREATE_COMMENT: 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_FOR_DM_LIST: null,
- RECEIVED_PROFILES: null,
- RECEIVED_DIRECT_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_ANALYTICS: null,
-
- RECEIVED_INCOMING_WEBHOOKS: null,
- RECEIVED_INCOMING_WEBHOOK: null,
- REMOVED_INCOMING_WEBHOOK: null,
- RECEIVED_OUTGOING_WEBHOOKS: null,
- RECEIVED_OUTGOING_WEBHOOK: null,
- UPDATED_OUTGOING_WEBHOOK: null,
- REMOVED_OUTGOING_WEBHOOK: null,
- RECEIVED_COMMANDS: null,
- RECEIVED_COMMAND: null,
- UPDATED_COMMAND: null,
- REMOVED_COMMAND: null,
-
- RECEIVED_CUSTOM_EMOJIS: null,
- RECEIVED_CUSTOM_EMOJI: null,
- UPDATED_CUSTOM_EMOJI: null,
- REMOVED_CUSTOM_EMOJI: null,
-
- RECEIVED_MSG: null,
-
- RECEIVED_MY_TEAM: null,
- CREATED_TEAM: null,
-
- RECEIVED_CONFIG: null,
- RECEIVED_LOGS: null,
- RECEIVED_SERVER_AUDITS: null,
- RECEIVED_SERVER_COMPLIANCE_REPORTS: null,
- RECEIVED_ALL_TEAMS: null,
- RECEIVED_ALL_TEAM_LISTINGS: null,
- RECEIVED_TEAM_MEMBERS: null,
- RECEIVED_MEMBERS_FOR_TEAM: null,
-
- RECEIVED_LOCALE: null,
-
- SHOW_SEARCH: null,
-
- USER_TYPING: null,
-
- TOGGLE_IMPORT_THEME_MODAL: null,
- TOGGLE_INVITE_MEMBER_MODAL: null,
- TOGGLE_LEAVE_TEAM_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,
- TOGGLE_GET_PUBLIC_LINK_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
- }),
+export const 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',
+ CHANNEL_DISPLAY_MODE: 'channel_display_mode',
+ CHANNEL_DISPLAY_MODE_CENTERED: 'centered',
+ CHANNEL_DISPLAY_MODE_FULL_SCREEN: 'full',
+ CHANNEL_DISPLAY_MODE_DEFAULT: 'centered',
+ MESSAGE_DISPLAY: 'message_display',
+ MESSAGE_DISPLAY_CLEAN: 'clean',
+ MESSAGE_DISPLAY_COMPACT: 'compact',
+ MESSAGE_DISPLAY_DEFAULT: 'clean',
+ COLLAPSE_DISPLAY: 'collapse_previews',
+ COLLAPSE_DISPLAY_DEFAULT: 'false',
+ USE_MILITARY_TIME: 'use_military_time',
+ CATEGORY_THEME: 'theme'
+};
+
+export const ActionTypes = keyMirror({
+ RECEIVED_ERROR: null,
+
+ CLICK_CHANNEL: null,
+ CREATE_CHANNEL: null,
+ LEAVE_CHANNEL: null,
+ CREATE_POST: null,
+ CREATE_COMMENT: 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_FOR_DM_LIST: null,
+ RECEIVED_PROFILES: null,
+ RECEIVED_DIRECT_PROFILES: null,
+ RECEIVED_ME: null,
+ RECEIVED_SESSIONS: null,
+ RECEIVED_AUDITS: null,
+ RECEIVED_TEAMS: null,
+ RECEIVED_STATUSES: null,
+ RECEIVED_PREFERENCE: null,
+ RECEIVED_PREFERENCES: null,
+ DELETED_PREFERENCES: null,
+ RECEIVED_FILE_INFO: null,
+ RECEIVED_ANALYTICS: null,
+
+ RECEIVED_INCOMING_WEBHOOKS: null,
+ RECEIVED_INCOMING_WEBHOOK: null,
+ REMOVED_INCOMING_WEBHOOK: null,
+ RECEIVED_OUTGOING_WEBHOOKS: null,
+ RECEIVED_OUTGOING_WEBHOOK: null,
+ UPDATED_OUTGOING_WEBHOOK: null,
+ REMOVED_OUTGOING_WEBHOOK: null,
+ RECEIVED_COMMANDS: null,
+ RECEIVED_COMMAND: null,
+ UPDATED_COMMAND: null,
+ REMOVED_COMMAND: null,
+
+ RECEIVED_CUSTOM_EMOJIS: null,
+ RECEIVED_CUSTOM_EMOJI: null,
+ UPDATED_CUSTOM_EMOJI: null,
+ REMOVED_CUSTOM_EMOJI: null,
+
+ RECEIVED_MSG: null,
+
+ RECEIVED_MY_TEAM: null,
+ CREATED_TEAM: null,
+
+ RECEIVED_CONFIG: null,
+ RECEIVED_LOGS: null,
+ RECEIVED_SERVER_AUDITS: null,
+ RECEIVED_SERVER_COMPLIANCE_REPORTS: null,
+ RECEIVED_ALL_TEAMS: null,
+ RECEIVED_ALL_TEAM_LISTINGS: null,
+ RECEIVED_TEAM_MEMBERS: null,
+ RECEIVED_MEMBERS_FOR_TEAM: null,
+
+ RECEIVED_LOCALE: null,
+
+ SHOW_SEARCH: null,
+
+ USER_TYPING: null,
+
+ TOGGLE_IMPORT_THEME_MODAL: null,
+ TOGGLE_INVITE_MEMBER_MODAL: null,
+ TOGGLE_LEAVE_TEAM_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,
+ TOGGLE_GET_PUBLIC_LINK_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
+});
+
+export const Constants = {
+ Preferences,
+ ActionTypes,
PayloadSources: keyMirror({
SERVER_ACTION: null,
@@ -174,7 +199,6 @@ export default {
FULLNAME: 'fullname',
NICKNAME: 'nickname',
EMAIL: 'email',
- THEME: 'theme',
LANGUAGE: 'language'
},
@@ -551,25 +575,6 @@ export default {
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',
- CHANNEL_DISPLAY_MODE: 'channel_display_mode',
- CHANNEL_DISPLAY_MODE_CENTERED: 'centered',
- CHANNEL_DISPLAY_MODE_FULL_SCREEN: 'full',
- CHANNEL_DISPLAY_MODE_DEFAULT: 'full',
- MESSAGE_DISPLAY: 'message_display',
- MESSAGE_DISPLAY_CLEAN: 'clean',
- MESSAGE_DISPLAY_COMPACT: 'compact',
- MESSAGE_DISPLAY_DEFAULT: 'clean',
- COLLAPSE_DISPLAY: 'collapse_previews',
- COLLAPSE_DISPLAY_DEFAULT: 'false',
- USE_MILITARY_TIME: 'use_military_time'
- },
TutorialSteps: {
INTRO_SCREENS: 0,
POST_POPOVER: 1,
@@ -761,6 +766,8 @@ export default {
MAX_PASSWORD_LENGTH: 64,
MIN_TRIGGER_LENGTH: 1,
MAX_TRIGGER_LENGTH: 128,
+ MAX_TEXTSETTING_LENGTH: 1024,
+ MAX_SITENAME_LENGTH: 30,
TIME_SINCE_UPDATE_INTERVAL: 30000,
MIN_HASHTAG_LINK_LENGTH: 3,
CHANNEL_SCROLL_ADJUSTMENT: 100,
@@ -777,3 +784,5 @@ export default {
PERMISSIONS_TEAM_ADMIN: 'team_admin',
PERMISSIONS_SYSTEM_ADMIN: 'system_admin'
};
+
+export default Constants;
diff --git a/webapp/utils/text_formatting.jsx b/webapp/utils/text_formatting.jsx
index 0b46edaeb..5ada7727f 100644
--- a/webapp/utils/text_formatting.jsx
+++ b/webapp/utils/text_formatting.jsx
@@ -263,7 +263,8 @@ function autolinkHashtags(text, tokens) {
newTokens.set(newAlias, {
value: `<a class='mention-link' href='#' data-hashtag='${token.originalText}'>${token.originalText}</a>`,
- originalText: token.originalText
+ originalText: token.originalText,
+ hashtag: token.originalText.substring(1)
});
output = output.replace(alias, newAlias);
@@ -276,19 +277,19 @@ function autolinkHashtags(text, tokens) {
}
// look for hashtags in the text
- function replaceHashtagWithToken(fullMatch, prefix, hashtag) {
+ function replaceHashtagWithToken(fullMatch, prefix, originalText) {
const index = tokens.size;
const alias = `MM_HASHTAG${index}`;
- let value = hashtag;
-
- if (hashtag.length > Constants.MIN_HASHTAG_LINK_LENGTH) {
- value = `<a class='mention-link' href='#' data-hashtag='${hashtag}'>${hashtag}</a>`;
+ if (text.length < Constants.MIN_HASHTAG_LINK_LENGTH + 1) {
+ // too short to be a hashtag
+ return fullMatch;
}
tokens.set(alias, {
- value,
- originalText: hashtag
+ value: `<a class='mention-link' href='#' data-hashtag='${originalText}'>${originalText}</a>`,
+ originalText,
+ hashtag: originalText.substring(1)
});
return prefix + alias;
@@ -393,9 +394,11 @@ export function highlightSearchTerms(text, tokens, searchTerm) {
for (const term of terms) {
// highlight existing tokens matching search terms
+ const trimmedTerm = term.replace(/\*$/, '').toLowerCase();
var newTokens = new Map();
for (const [alias, token] of tokens) {
- if (token.originalText.toLowerCase() === term.replace(/\*$/, '').toLowerCase()) {
+ if (token.originalText.toLowerCase() === trimmedTerm ||
+ (token.hashtag && token.hashtag.toLowerCase() === trimmedTerm)) {
const index = tokens.size + newTokens.size;
const newAlias = `MM_SEARCHTERM${index}`;
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index bb17c2fdc..907c01229 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -139,7 +139,7 @@ export function notifyMe(title, body, channel, teamId) {
Notification.requestPermission((permission) => {
if (permission === 'granted') {
try {
- var notification = new Notification(title, {body: body, tag: body, icon: icon50});
+ var notification = new Notification(title, {body, tag: body, icon: icon50});
notification.onclick = () => {
window.focus();
if (channel) {
@@ -413,7 +413,7 @@ export function insertHtmlEntities(text) {
export function searchForTerm(term) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH_TERM,
- term: term,
+ term,
do_search: true
});
}
@@ -494,7 +494,7 @@ export function splitFileLocation(fileLocation) {
var filePath = fileSplit.join('.');
var filename = filePath.split('/')[filePath.split('/').length - 1];
- return {ext: ext, name: filename, path: filePath};
+ return {ext, name: filename, path: filePath};
}
export function getPreviewImagePath(filename) {
@@ -1079,7 +1079,7 @@ export function generateId() {
if (c === 'x') {
v = r;
} else {
- v = r & 0x3 | 0x8;
+ v = (r & 0x3) | 0x8;
}
return v.toString(16);
@@ -1297,7 +1297,7 @@ export function fillArray(value, length) {
// 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()) {
+ if (isBrowserIE() || isBrowserEdge()) {
return files.types != null && files.types.contains('Files');
}
diff --git a/webapp/utils/websocket_client.jsx b/webapp/utils/websocket_client.jsx
new file mode 100644
index 000000000..135d96466
--- /dev/null
+++ b/webapp/utils/websocket_client.jsx
@@ -0,0 +1,7 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import WebSocketClient from 'mattermost/websocket_client.jsx';
+
+var WebClient = new WebSocketClient();
+export default WebClient;
diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js
index 2911c0c7d..da9ed9600 100644
--- a/webapp/webpack.config.js
+++ b/webapp/webpack.config.js
@@ -4,7 +4,7 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
-const htmlExtract = new ExtractTextPlugin('html', 'root.html');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
const NPM_TARGET = process.env.npm_lifecycle_event; //eslint-disable-line no-process-env
@@ -28,8 +28,8 @@ var config = {
output: {
path: 'dist',
publicPath: '/static/',
- filename: 'bundle.js',
- chunkFilename: '[name].[hash].[chunkhash].js'
+ filename: '[name].[hash].js',
+ chunkFilename: '[name].[chunkhash].js'
},
module: {
loaders: [
@@ -53,6 +53,15 @@ var config = {
}
},
{
+ test: /node_modules\/mattermost\/websocket_client\.jsx?$/,
+ loader: 'babel',
+ query: {
+ presets: ['react', 'es2015-webpack', 'stage-0'],
+ plugins: ['transform-runtime'],
+ cacheDirectory: DEV
+ }
+ },
+ {
test: /\.json$/,
loader: 'json'
},
@@ -81,7 +90,7 @@ var config = {
},
{
test: /\.html$/,
- loader: htmlExtract.extract('html?attrs=link:href')
+ loader: 'html?attrs=link:href'
}
]
},
@@ -92,7 +101,6 @@ var config = {
new webpack.ProvidePlugin({
'window.jQuery': 'jquery'
}),
- htmlExtract,
new CopyWebpackPlugin([
{from: 'images/emoji', to: 'emoji'},
{from: 'images/logo-email.png', to: 'images'},
@@ -159,7 +167,18 @@ if (!DEV) {
// Test mode configuration
if (TEST) {
+ config.entry = ['babel-polyfill', './root.jsx'];
+ config.target = 'node';
config.externals = [nodeExternals()];
+} else {
+ // For some reason this breaks mocha. So it goes here.
+ config.plugins.push(
+ new HtmlWebpackPlugin({
+ filename: 'root.html',
+ inject: 'head',
+ template: 'root.html'
+ })
+ );
}
module.exports = config;