summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/components/access_history_modal.jsx5
-rw-r--r--web/react/components/admin_console/team_analytics.jsx184
-rw-r--r--web/react/components/channel_header.jsx174
-rw-r--r--web/react/components/create_comment.jsx33
-rw-r--r--web/react/components/create_post.jsx33
-rw-r--r--web/react/components/edit_channel_modal.jsx38
-rw-r--r--web/react/components/edit_channel_purpose_modal.jsx124
-rw-r--r--web/react/components/edit_post_modal.jsx23
-rw-r--r--web/react/components/file_attachment.jsx2
-rw-r--r--web/react/components/get_link_modal.jsx2
-rw-r--r--web/react/components/more_channels.jsx2
-rw-r--r--web/react/components/navbar.jsx66
-rw-r--r--web/react/components/navbar_dropdown.jsx1
-rw-r--r--web/react/components/new_channel_flow.jsx10
-rw-r--r--web/react/components/new_channel_modal.jsx14
-rw-r--r--web/react/components/popover_list_members.jsx32
-rw-r--r--web/react/components/post_body.jsx2
-rw-r--r--web/react/components/post_info.jsx2
-rw-r--r--web/react/components/post_list.jsx59
-rw-r--r--web/react/components/register_app_modal.jsx135
-rw-r--r--web/react/components/rhs_comment.jsx1
-rw-r--r--web/react/components/rhs_root_post.jsx1
-rw-r--r--web/react/components/search_autocomplete.jsx23
-rw-r--r--web/react/components/setting_item_max.jsx4
-rw-r--r--web/react/components/setting_picture.jsx4
-rw-r--r--web/react/components/sidebar_header.jsx11
-rw-r--r--web/react/components/team_signup_with_email.jsx2
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx5
-rw-r--r--web/react/components/user_settings/manage_outgoing_hooks.jsx1
-rw-r--r--web/react/components/user_settings/user_settings.jsx12
-rw-r--r--web/react/components/user_settings/user_settings_advanced.jsx169
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx4
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx1
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx4
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx1
-rw-r--r--web/react/stores/post_store.jsx9
-rw-r--r--web/react/utils/client.jsx25
-rw-r--r--web/react/utils/constants.jsx5
-rw-r--r--web/react/utils/markdown.jsx100
-rw-r--r--web/react/utils/text_formatting.jsx32
-rw-r--r--web/react/utils/utils.jsx70
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss59
-rw-r--r--web/sass-files/sass/partials/_base.scss6
-rw-r--r--web/sass-files/sass/partials/_headers.scss2
-rw-r--r--web/sass-files/sass/partials/_modal.scss7
-rw-r--r--web/sass-files/sass/partials/_popover.scss70
-rw-r--r--web/sass-files/sass/partials/_post.scss9
-rw-r--r--web/sass-files/sass/partials/_responsive.scss12
-rw-r--r--web/sass-files/sass/partials/_search.scss42
-rw-r--r--web/sass-files/sass/partials/_settings.scss57
-rw-r--r--web/sass-files/sass/partials/_statistics.scss84
-rw-r--r--web/sass-files/sass/styles.scss1
52 files changed, 1207 insertions, 567 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index c8af2553d..f0a31ce90 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -90,8 +90,9 @@ export default class AccessHistoryModal extends React.Component {
case '/channels/update':
currentAuditDesc = 'Updated the ' + channelName + ' channel/group name';
break;
- case '/channels/update_desc':
- currentAuditDesc = 'Updated the ' + channelName + ' channel/group description';
+ case '/channels/update_desc': // support the old path
+ case '/channels/update_header':
+ currentAuditDesc = 'Updated the ' + channelName + ' channel/group header';
break;
default:
let userIdField = [];
diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx
index a945a551c..0c9d1f61b 100644
--- a/web/react/components/admin_console/team_analytics.jsx
+++ b/web/react/components/admin_console/team_analytics.jsx
@@ -210,69 +210,89 @@ export default class TeamAnalytics extends React.Component {
}
var totalCount = (
- <div className='total-count text-center'>
- <div>{'Total Users'}</div>
- <div>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Total Users'}<i className='fa fa-users'/></div>
+ <div className='content'>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
+ </div>
</div>
);
var openChannelCount = (
- <div className='total-count text-center'>
- <div>{'Public Groups'}</div>
- <div>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Public Groups'}<i className='fa fa-unlock-alt'/></div>
+ <div className='content'>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
+ </div>
</div>
);
var openPrivateCount = (
- <div className='total-count text-center'>
- <div>{'Private Groups'}</div>
- <div>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div>
+ <div className='content'>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
+ </div>
</div>
);
var postCount = (
- <div className='total-count text-center'>
- <div>{'Total Posts'}</div>
- <div>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div>
+ <div className='content'>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
+ </div>
</div>
);
var postCountsByDay = (
- <div className='total-count-by-day'>
- <div>{'Total Posts'}</div>
- <div>{'Loading...'}</div>
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div className='content'>{'Loading...'}</div>
+ </div>
</div>
);
if (this.state.post_counts_day != null) {
postCountsByDay = (
- <div className='total-count-by-day'>
- <div>{'Total Posts'}</div>
- <LineChart
- data={this.state.post_counts_day}
- width='740'
- height='225'
- />
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div className='content'>
+ <LineChart
+ data={this.state.post_counts_day}
+ width='740'
+ height='225'
+ />
+ </div>
+ </div>
</div>
);
}
var usersWithPostsByDay = (
- <div className='total-count-by-day'>
- <div>{'Total Posts'}</div>
- <div>{'Loading...'}</div>
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div>{'Loading...'}</div>
+ </div>
</div>
);
if (this.state.user_counts_with_posts_day != null) {
usersWithPostsByDay = (
- <div className='total-count-by-day'>
- <div>{'Active Users With Posts'}</div>
- <LineChart
- data={this.state.user_counts_with_posts_day}
- width='740'
- height='225'
- />
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Active Users With Posts'}</div>
+ <div className='content'>
+ <LineChart
+ data={this.state.user_counts_with_posts_day}
+ width='740'
+ height='225'
+ />
+ </div>
+ </div>
</div>
);
}
@@ -286,22 +306,26 @@ export default class TeamAnalytics extends React.Component {
if (this.state.recent_active_users != null) {
recentActiveUser = (
- <div className='recent-active-users'>
- <div>{'Recent Active Users'}</div>
- <table width='90%'>
- <tbody>
- {
- this.state.recent_active_users.map((user) => {
- return (
- <tr key={user.id}>
- <td className='recent-active-users-td'>{user.email}</td>
- <td className='recent-active-users-td'>{Utils.displayDateTime(user.last_activity_at)}</td>
- </tr>
- );
- })
- }
- </tbody>
- </table>
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>{'Recent Active Users'}</div>
+ <div className='content'>
+ <table>
+ <tbody>
+ {
+ this.state.recent_active_users.map((user) => {
+ return (
+ <tr key={user.id}>
+ <td>{user.email}</td>
+ <td>{Utils.displayDateTime(user.last_activity_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ </div>
+ </div>
</div>
);
}
@@ -315,38 +339,50 @@ export default class TeamAnalytics extends React.Component {
if (this.state.newly_created_users != null) {
newUsers = (
- <div className='recent-active-users'>
- <div>{'Newly Created Users'}</div>
- <table width='90%'>
- <tbody>
- {
- this.state.newly_created_users.map((user) => {
- return (
- <tr key={user.id}>
- <td className='recent-active-users-td'>{user.email}</td>
- <td className='recent-active-users-td'>{Utils.displayDateTime(user.create_at)}</td>
- </tr>
- );
- })
- }
- </tbody>
- </table>
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>{'Newly Created Users'}</div>
+ <div className='content'>
+ <table>
+ <tbody>
+ {
+ this.state.newly_created_users.map((user) => {
+ return (
+ <tr key={user.id}>
+ <td>{user.email}</td>
+ <td>{Utils.displayDateTime(user.create_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ </div>
+ </div>
</div>
);
}
return (
- <div className='wrapper--fixed'>
- <h2>{'Statistics for ' + this.props.team.name}</h2>
+ <div className='wrapper--fixed team_statistics'>
+ <h3>{'Statistics for ' + this.props.team.name}</h3>
{serverError}
- {totalCount}
- {postCount}
- {openChannelCount}
- {openPrivateCount}
- {postCountsByDay}
- {usersWithPostsByDay}
- {recentActiveUser}
- {newUsers}
+ <div className='row'>
+ {totalCount}
+ {postCount}
+ {openChannelCount}
+ {openPrivateCount}
+ </div>
+ <div className='row'>
+ {postCountsByDay}
+ </div>
+ <div className='row'>
+ {usersWithPostsByDay}
+ </div>
+ <div className='row'>
+ {recentActiveUser}
+ {newUsers}
+ </div>
</div>
);
}
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index d66777cc6..101fd85e5 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -11,6 +11,7 @@ const TextFormatting = require('../utils/text_formatting.jsx');
const Utils = require('../utils/utils.jsx');
const MessageWrapper = require('./message_wrapper.jsx');
const PopoverListMembers = require('./popover_list_members.jsx');
+const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const Constants = require('../utils/constants.jsx');
@@ -27,7 +28,9 @@ export default class ChannelHeader extends React.Component {
this.handleLeave = this.handleLeave.bind(this);
this.searchMentions = this.searchMentions.bind(this);
- this.state = this.getStateFromStores();
+ const state = this.getStateFromStores();
+ state.showEditChannelPurposeModal = false;
+ this.state = state;
}
getStateFromStores() {
return {
@@ -110,11 +113,11 @@ export default class ChannelHeader extends React.Component {
bSize='large'
placement='bottom'
className='description'
- onMouseOver={() => this.refs.descriptionOverlay.show()}
- onMouseOut={() => this.refs.descriptionOverlay.hide()}
+ onMouseOver={() => this.refs.headerOverlay.show()}
+ onMouseOut={() => this.refs.headerOverlay.hide()}
>
<MessageWrapper
- message={channel.description}
+ message={channel.header}
/>
</Popover>
);
@@ -144,7 +147,7 @@ export default class ChannelHeader extends React.Component {
if (isDirect) {
dropdownContents.push(
<li
- key='edit_description_direct'
+ key='edit_header_direct'
role='presentation'
>
<a
@@ -152,11 +155,11 @@ export default class ChannelHeader extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- Set Channel Description...
+ Set Channel Header...
</a>
</li>
);
@@ -216,7 +219,7 @@ export default class ChannelHeader extends React.Component {
dropdownContents.push(
<li
- key='set_channel_description'
+ key='set_channel_header'
role='presentation'
>
<a
@@ -224,11 +227,25 @@ export default class ChannelHeader extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- Set {channelTerm} Description...
+ Set {channelTerm} Header...
+ </a>
+ </li>
+ );
+ dropdownContents.push(
+ <li
+ key='set_channel_purpose'
+ role='presentation'
+ >
+ <a
+ role='menuitem'
+ href='#'
+ onClick={() => this.setState({showEditChannelPurposeModal: true})}
+ >
+ Set {channelTerm} Purpose...
</a>
</li>
);
@@ -307,84 +324,91 @@ export default class ChannelHeader extends React.Component {
}
return (
- <table className='channel-header alt'>
- <tbody>
- <tr>
- <th>
- <div className='channel-header__info'>
- <div className='dropdown'>
+ <div>
+ <table className='channel-header alt'>
+ <tbody>
+ <tr>
+ <th>
+ <div className='channel-header__info'>
+ <div className='dropdown'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <strong className='heading'>{channelTitle} </strong>
+ <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' />
+ </a>
+ <ul
+ className='dropdown-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {dropdownContents}
+ </ul>
+ </div>
+ <OverlayTrigger
+ trigger={['hover', 'focus']}
+ placement='bottom'
+ overlay={popoverContent}
+ ref='headerOverlay'
+ >
+ <div
+ onClick={TextFormatting.handleClick}
+ className='description'
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}}
+ />
+ </OverlayTrigger>
+ </div>
+ </th>
+ <th>
+ <PopoverListMembers
+ members={this.state.users}
+ channelId={channel.id}
+ />
+ </th>
+ <th className='search-bar__container'><NavbarSearchBox /></th>
+ <th>
+ <div className='dropdown channel-header__links'>
<a
href='#'
className='dropdown-toggle theme'
type='button'
- id='channel_header_dropdown'
+ id='channel_header_right_dropdown'
data-toggle='dropdown'
aria-expanded='true'
>
- <strong className='heading'>{channelTitle} </strong>
- <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' />
+ <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
</a>
<ul
- className='dropdown-menu'
+ className='dropdown-menu dropdown-menu-right'
role='menu'
- aria-labelledby='channel_header_dropdown'
+ aria-labelledby='channel_header_right_dropdown'
>
- {dropdownContents}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.searchMentions}
+ >
+ Recent Mentions
+ </a>
+ </li>
</ul>
</div>
- <OverlayTrigger
- trigger={['hover', 'focus']}
- placement='bottom'
- overlay={popoverContent}
- ref='descriptionOverlay'
- >
- <div
- onClick={TextFormatting.handleClick}
- className='description'
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {singleline: true, mentionHighlight: false})}}
- />
- </OverlayTrigger>
- </div>
- </th>
- <th>
- <PopoverListMembers
- members={this.state.users}
- channelId={channel.id}
- />
- </th>
- <th className='search-bar__container'><NavbarSearchBox /></th>
- <th>
- <div className='dropdown channel-header__links'>
- <a
- href='#'
- className='dropdown-toggle theme'
- type='button'
- id='channel_header_right_dropdown'
- data-toggle='dropdown'
- aria-expanded='true'
- >
- <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
- </a>
- <ul
- className='dropdown-menu dropdown-menu-right'
- role='menu'
- aria-labelledby='channel_header_right_dropdown'
- >
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- onClick={this.searchMentions}
- >
- Recent Mentions
- </a>
- </li>
- </ul>
- </div>
- </th>
- </tr>
- </tbody>
- </table>
+ </th>
+ </tr>
+ </tbody>
+ </table>
+ <EditChannelPurposeModal
+ show={this.state.showEditChannelPurposeModal}
+ onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
+ channel={channel}
+ />
+ </div>
);
}
}
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 18936e808..058594165 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -8,6 +8,7 @@ const SocketStore = require('../stores/socket_store.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const PostStore = require('../stores/post_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
const Textbox = require('./textbox.jsx');
const MsgTyping = require('./msg_typing.jsx');
const FileUpload = require('./file_upload.jsx');
@@ -27,7 +28,7 @@ export default class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
- this.handleArrowUp = this.handleArrowUp.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -36,6 +37,7 @@ export default class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleResize = this.handleResize.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
PostStore.clearCommentDraftUploads();
@@ -45,15 +47,23 @@ export default class CreateComment extends React.Component {
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
- windowWidth: Utils.windowWidth()
+ windowWidth: Utils.windowWidth(),
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
};
}
componentDidMount() {
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
window.removeEventListener('resize', this.handleResize);
}
+ onPreferenceChange() {
+ this.setState({
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
+ }
handleResize() {
this.setState({windowWidth: Utils.windowWidth()});
}
@@ -140,10 +150,12 @@ export default class CreateComment extends React.Component {
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
}
commentMsgKeyPress(e) {
- if (e.which === 13 && !e.shiftKey && !e.altKey) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.textbox).blur();
- this.handleSubmit(e);
+ if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') {
+ if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
+ e.preventDefault();
+ ReactDOM.findDOMNode(this.refs.textbox).blur();
+ this.handleSubmit(e);
+ }
}
const t = Date.now();
@@ -161,7 +173,12 @@ export default class CreateComment extends React.Component {
$('.post-right__scroll').perfectScrollbar('update');
this.setState({messageText: messageText});
}
- handleArrowUp(e) {
+ handleKeyDown(e) {
+ if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
+ this.commentMsgKeyPress(e);
+ return;
+ }
+
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
@@ -313,7 +330,7 @@ export default class CreateComment extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.commentMsgKeyPress}
- onKeyDown={this.handleArrowUp}
+ onKeyDown={this.handleKeyDown}
messageText={this.state.messageText}
createMessage='Add a comment...'
initialText=''
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 32ee31efe..cdbc3bc6d 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -8,6 +8,7 @@ const ChannelStore = require('../stores/channel_store.jsx');
const PostStore = require('../stores/post_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const SocketStore = require('../stores/socket_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
const MsgTyping = require('./msg_typing.jsx');
const Textbox = require('./textbox.jsx');
const FileUpload = require('./file_upload.jsx');
@@ -36,9 +37,10 @@ export default class CreatePost extends React.Component {
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
- this.handleArrowUp = this.handleArrowUp.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleResize = this.handleResize.bind(this);
this.sendMessage = this.sendMessage.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
PostStore.clearDraftUploads();
@@ -52,8 +54,16 @@ export default class CreatePost extends React.Component {
submitting: false,
initialText: draft.messageText,
windowWidth: Utils.windowWidth(),
- windowHeight: Utils.windowHeight()
+ windowHeight: Utils.windowHeight(),
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
};
+
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
+ }
+ onPreferenceChange() {
+ this.setState({
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
}
handleResize() {
this.setState({
@@ -201,10 +211,12 @@ export default class CreatePost extends React.Component {
);
}
postMsgKeyPress(e) {
- if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.textbox).blur();
- this.handleSubmit(e);
+ if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') {
+ if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
+ e.preventDefault();
+ ReactDOM.findDOMNode(this.refs.textbox).blur();
+ this.handleSubmit(e);
+ }
}
const t = Date.now();
@@ -328,7 +340,12 @@ export default class CreatePost extends React.Component {
const draft = PostStore.getDraft(channelId);
return draft.previews.length + draft.uploadsInProgress.length;
}
- handleArrowUp(e) {
+ handleKeyDown(e) {
+ if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
+ this.postMsgKeyPress(e);
+ return;
+ }
+
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
@@ -393,7 +410,7 @@ export default class CreatePost extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.postMsgKeyPress}
- onKeyDown={this.handleArrowUp}
+ onKeyDown={this.handleKeyDown}
onHeightChange={this.resizePostHolder}
messageText={this.state.messageText}
createMessage='Write a message...'
diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx
index d63a1db30..5b3c74e82 100644
--- a/web/react/components/edit_channel_modal.jsx
+++ b/web/react/components/edit_channel_modal.jsx
@@ -14,7 +14,7 @@ export default class EditChannelModal extends React.Component {
this.onShow = this.onShow.bind(this);
this.state = {
- description: '',
+ header: '',
title: '',
channelId: '',
serverError: ''
@@ -28,32 +28,32 @@ export default class EditChannelModal extends React.Component {
return;
}
- data.channel_description = this.state.description.trim();
+ data.channel_header = this.state.header.trim();
- Client.updateChannelDesc(data,
- function handleUpdateSuccess() {
+ Client.updateChannelHeader(data,
+ () => {
this.setState({serverError: ''});
AsyncClient.getChannel(this.state.channelId);
$(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
- }.bind(this),
- function handleUpdateError(err) {
- if (err.message === 'Invalid channel_description parameter') {
- this.setState({serverError: 'This description is too long, please enter a shorter one'});
+ },
+ (err) => {
+ if (err.message === 'Invalid channel_header parameter') {
+ this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
} else {
this.setState({serverError: err.message});
}
- }.bind(this)
+ }
);
}
handleUserInput(e) {
- this.setState({description: e.target.value});
+ this.setState({header: e.target.value});
}
handleClose() {
- this.setState({description: '', serverError: ''});
+ this.setState({header: '', serverError: ''});
}
onShow(e) {
const button = e.relatedTarget;
- this.setState({description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
+ this.setState({header: $(button).attr('data-header'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
}
componentDidMount() {
$(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
@@ -73,7 +73,7 @@ export default class EditChannelModal extends React.Component {
className='modal-title'
ref='title'
>
- Edit Description
+ {'Edit Header'}
</h4>
);
if (this.state.title) {
@@ -82,7 +82,7 @@ export default class EditChannelModal extends React.Component {
className='modal-title'
ref='title'
>
- Edit Description for <span className='name'>{this.state.title}</span>
+ {'Edit Header for '}<span className='name'>{this.state.title}</span>
</h4>
);
}
@@ -105,17 +105,17 @@ export default class EditChannelModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
{editTitle}
</div>
<div className='modal-body'>
+ <p>{'Edit the text appearing next to the channel name in the channel header.'}</p>
<textarea
className='form-control no-resize'
rows='6'
- ref='channelDesc'
maxLength='1024'
- value={this.state.description}
+ value={this.state.header}
onChange={this.handleUserInput}
/>
{serverError}
@@ -126,14 +126,14 @@ export default class EditChannelModal extends React.Component {
className='btn btn-default'
data-dismiss='modal'
>
- Cancel
+ {'Cancel'}
</button>
<button
type='button'
className='btn btn-primary'
onClick={this.handleEdit}
>
- Save
+ {'Save'}
</button>
</div>
</div>
diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx
new file mode 100644
index 000000000..4d162cfe7
--- /dev/null
+++ b/web/react/components/edit_channel_purpose_modal.jsx
@@ -0,0 +1,124 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const AsyncClient = require('../utils/async_client.jsx');
+const Client = require('../utils/client.jsx');
+const Modal = ReactBootstrap.Modal;
+
+export default class EditChannelPurposeModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleHide = this.handleHide.bind(this);
+ this.handleSave = this.handleSave.bind(this);
+
+ this.state = {serverError: ''};
+ }
+
+ handleHide() {
+ this.setState({serverError: ''});
+
+ if (this.props.onModalDismissed) {
+ this.props.onModalDismissed();
+ }
+ }
+
+ handleSave() {
+ if (!this.props.channel) {
+ return;
+ }
+
+ const data = {
+ channel_id: this.props.channel.id,
+ channel_purpose: ReactDOM.findDOMNode(this.refs.purpose).value.trim()
+ };
+
+ Client.updateChannelPurpose(data,
+ () => {
+ AsyncClient.getChannel(this.props.channel.id);
+
+ this.handleHide();
+ },
+ (err) => {
+ if (err.message === 'Invalid channel_purpose parameter') {
+ this.setState({serverError: 'This channel purpose is too long, please enter a shorter one'});
+ } else {
+ this.setState({serverError: err.message});
+ }
+ }
+ );
+ }
+
+ render() {
+ if (!this.props.show) {
+ return null;
+ }
+
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='form-group has-error'>
+ <br/>
+ <label className='control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ let title = <span>{'Edit Purpose'}</span>;
+ if (this.props.channel.display_name) {
+ title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>;
+ }
+
+ let channelTerm = 'Channel';
+ if (this.props.channel.channelType === 'P') {
+ channelTerm = 'Group';
+ }
+
+ return (
+ <Modal
+ className='modal-edit-channel-purpose'
+ show={this.props.show}
+ onHide={this.handleHide}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>
+ {title}
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <p>{`Describe how this ${channelTerm} should be used.`}</p>
+ <textarea
+ ref='purpose'
+ className='form-control no-resize'
+ rows='6'
+ maxLength='128'
+ defaultValue={this.props.channel.purpose}
+ />
+ {serverError}
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.handleHide}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ type='button'
+ className='btn btn-primary'
+ onClick={this.handleSave}
+ >
+ {'Save'}
+ </button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+EditChannelPurposeModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ channel: React.PropTypes.object,
+ onModalDismissed: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index b259b3c18..2abb3f151 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -6,6 +6,10 @@ var AsyncClient = require('../utils/async_client.jsx');
var Textbox = require('./textbox.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
var PostStore = require('../stores/post_store.jsx');
+var PreferenceStore = require('../stores/preference_store.jsx');
+
+var Constants = require('../utils/constants.jsx');
+var KeyCodes = Constants.KeyCodes;
export default class EditPostModal extends React.Component {
constructor() {
@@ -16,6 +20,8 @@ export default class EditPostModal extends React.Component {
this.handleEditKeyPress = this.handleEditKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleEditPostEvent = this.handleEditPostEvent.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''};
}
@@ -51,7 +57,7 @@ export default class EditPostModal extends React.Component {
this.setState({editText: editMessage});
}
handleEditKeyPress(e) {
- if (e.which === 13 && !e.shiftKey && !e.altKey) {
+ if (this.state.ctrlSend === 'false' && e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
ReactDOM.findDOMNode(this.refs.editbox).blur();
this.handleEdit(e);
@@ -72,6 +78,16 @@ export default class EditPostModal extends React.Component {
$(ReactDOM.findDOMNode(this.refs.modal)).modal('show');
}
+ handleKeyDown(e) {
+ if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
+ this.handleEdit(e);
+ }
+ }
+ onPreferenceChange() {
+ this.setState({
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
+ }
componentDidMount() {
var self = this;
@@ -84,7 +100,7 @@ export default class EditPostModal extends React.Component {
if (!button) {
return;
}
- self.setState({editText: $(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-refoucsid')});
+ self.setState({editText: $(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')});
});
$(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() {
@@ -101,9 +117,11 @@ export default class EditPostModal extends React.Component {
});
PostStore.addEditPostListener(this.handleEditPostEvent);
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
}
componentWillUnmount() {
PostStore.removeEditPostListener(this.handleEditPostEvent);
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
}
render() {
var error = (<div className='form-group'><br /></div>);
@@ -138,6 +156,7 @@ export default class EditPostModal extends React.Component {
<Textbox
onUserInput={this.handleEditInput}
onKeyPress={this.handleEditKeyPress}
+ onKeyDown={this.handleKeyDown}
messageText={this.state.editText}
createMessage='Edit the post...'
id='edit_textbox'
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index 4d4e8390c..e707e32f5 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -270,7 +270,7 @@ export default class FileAttachment extends React.Component {
href={fileUrl}
download={filenameString}
data-toggle='tooltip'
- title={filenameString}
+ title={'Download ' + filenameString}
className='post-image__name'
>
{trimmedFilename}
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 325e86f3d..8839bc3c7 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -98,7 +98,7 @@ export default class GetLinkModal extends React.Component {
<br /><br />
</p>
<textarea
- className='form-control no-resize'
+ className='form-control no-resize min-height'
readOnly='true'
ref='textarea'
value={this.state.value}
diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx
index a0084ad30..c4f831c2e 100644
--- a/web/react/components/more_channels.jsx
+++ b/web/react/components/more_channels.jsx
@@ -109,7 +109,7 @@ export default class MoreChannels extends React.Component {
<tr key={channel.id}>
<td>
<p className='more-name'>{channel.display_name}</p>
- <p className='more-description'>{channel.description}</p>
+ <p className='more-purpose'>{channel.purpose}</p>
</td>
<td className='td--action'>
{joinButton}
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index f9cd525fd..f7778f25f 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -8,6 +8,7 @@ var ChannelStore = require('../stores/channel_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
var MessageWrapper = require('./message_wrapper.jsx');
var NotifyCounts = require('./notify_counts.jsx');
+const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
const Utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
@@ -26,7 +27,9 @@ export default class Navbar extends React.Component {
this.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this);
- this.state = this.getStateFromStores();
+ const state = this.getStateFromStores();
+ state.showEditChannelPurposeModal = false;
+ this.state = state;
}
getStateFromStores() {
return {
@@ -106,22 +109,35 @@ export default class Navbar extends React.Component {
</li>
);
- var setChannelDescriptionOption = (
+ var setChannelHeaderOption = (
<li role='presentation'>
<a
role='menuitem'
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- Set Channel Description...
+ Set Channel Header...
</a>
</li>
);
+ var setChannelPurposeOption = null;
+ if (!isDirect) {
+ setChannelPurposeOption = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={() => this.setState({showEditChannelPurposeModal: true})}
+ />
+ </li>
+ );
+ }
+
var addMembersOption;
var leaveChannelOption;
if (!isDirect && !ChannelStore.isDefault(channel)) {
@@ -249,7 +265,8 @@ export default class Navbar extends React.Component {
{viewInfoOption}
{addMembersOption}
{manageMembersOption}
- {setChannelDescriptionOption}
+ {setChannelHeaderOption}
+ {setChannelPurposeOption}
{notificationPreferenceOption}
{renameChannelOption}
{deleteChannelOption}
@@ -335,10 +352,10 @@ export default class Navbar extends React.Component {
<Popover
bsStyle='info'
placement='bottom'
- id='description-popover'
+ id='header-popover'
>
<MessageWrapper
- message={channel.description}
+ message={channel.header}
options={{singleline: true, mentionHighlight: false}}
/>
</Popover>
@@ -360,20 +377,20 @@ export default class Navbar extends React.Component {
}
}
- if (channel.description.length === 0) {
+ if (channel.header.length === 0) {
popoverContent = (
<Popover
bsStyle='info'
placement='bottom'
- id='description-popover'
+ id='header-popover'
>
<div>
- {'No channel description yet.'}
+ {'No channel header yet.'}
<br/>
<a
href='#'
data-toggle='modal'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
data-target='#edit_channel'
@@ -392,17 +409,24 @@ export default class Navbar extends React.Component {
var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent);
return (
- <nav
- className='navbar navbar-default navbar-fixed-top'
- role='navigation'
- >
- <div className='container-fluid theme'>
- <div className='navbar-header'>
- {collapseButtons}
- {channelMenuDropdown}
+ <div>
+ <nav
+ className='navbar navbar-default navbar-fixed-top'
+ role='navigation'
+ >
+ <div className='container-fluid theme'>
+ <div className='navbar-header'>
+ {collapseButtons}
+ {channelMenuDropdown}
+ </div>
</div>
- </div>
- </nav>
+ </nav>
+ <EditChannelPurposeModal
+ show={this.state.showEditChannelPurposeModal}
+ onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
+ channel={channel}
+ />
+ </div>
);
}
}
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index cc041e094..dc21fad21 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -58,6 +58,7 @@ export default class NavbarDropdown extends React.Component {
TeamStore.addChangeListener(this.onListenerChange);
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
+ $('.sidebar--left .dropdown-menu').scrollTop(0);
this.blockToggle = true;
setTimeout(() => {
this.blockToggle = false;
diff --git a/web/react/components/new_channel_flow.jsx b/web/react/components/new_channel_flow.jsx
index 186cfc2b0..d6280d118 100644
--- a/web/react/components/new_channel_flow.jsx
+++ b/web/react/components/new_channel_flow.jsx
@@ -30,7 +30,7 @@ export default class NewChannelFlow extends React.Component {
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
- channelDescription: '',
+ channelPurpose: '',
nameModified: false
};
}
@@ -43,7 +43,7 @@ export default class NewChannelFlow extends React.Component {
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
- channelDescription: '',
+ channelPurpose: '',
nameModified: false
});
}
@@ -65,7 +65,7 @@ export default class NewChannelFlow extends React.Component {
const cu = UserStore.getCurrentUser();
channel.team_id = cu.team_id;
- channel.description = this.state.channelDescription;
+ channel.purpose = this.state.channelPurpose;
channel.type = this.state.channelType;
Client.createChannel(channel,
@@ -109,7 +109,7 @@ export default class NewChannelFlow extends React.Component {
channelDataChanged(data) {
this.setState({
channelDisplayName: data.displayName,
- channelDescription: data.description
+ channelPurpose: data.purpose
});
if (!this.state.nameModified) {
this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
@@ -119,7 +119,7 @@ export default class NewChannelFlow extends React.Component {
const channelData = {
name: this.state.channelName,
displayName: this.state.channelDisplayName,
- description: this.state.channelDescription
+ purpose: this.state.channelPurpose
};
let showChannelModal = false;
diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
index 4e6280c99..c0cea496f 100644
--- a/web/react/components/new_channel_modal.jsx
+++ b/web/react/components/new_channel_modal.jsx
@@ -36,7 +36,7 @@ export default class NewChannelModal extends React.Component {
handleChange() {
const newData = {
displayName: ReactDOM.findDOMNode(this.refs.display_name).value,
- description: ReactDOM.findDOMNode(this.refs.channel_desc).value
+ purpose: ReactDOM.findDOMNode(this.refs.channel_purpose).value
};
this.props.onDataChanged(newData);
}
@@ -136,22 +136,22 @@ export default class NewChannelModal extends React.Component {
</div>
<div className='form-group less'>
<div className='col-sm-3'>
- <label className='form__label control-label'>{'Description'}</label>
+ <label className='form__label control-label'>{'Purpose'}</label>
<label className='form__label light'>{'(optional)'}</label>
</div>
<div className='col-sm-9'>
<textarea
className='form-control no-resize'
- ref='channel_desc'
+ ref='channel_purpose'
rows='4'
- placeholder='Description'
- maxLength='1024'
- value={this.props.channelData.description}
+ placeholder='Purpose'
+ maxLength='128'
+ value={this.props.channelData.purpose}
onChange={this.handleChange}
tabIndex='2'
/>
<p className='input__help'>
- {'Description helps others decide whether to join this channel.'}
+ {`Describe how this ${channelTerm} should be used.`}
</p>
{serverError}
</div>
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index 9cffa2400..f3c0fa0b4 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -42,6 +42,10 @@ export default class PopoverListMembers extends React.Component {
};
}
+ componentDidUpdate() {
+ $(ReactDOM.findDOMNode(this.refs.memebersPopover)).find('.popover-content').perfectScrollbar();
+ }
+
handleShowDirectChannel(teammate, e) {
e.preventDefault();
@@ -106,27 +110,27 @@ export default class PopoverListMembers extends React.Component {
let button = '';
if (currentUserId !== m.id && ch.type !== 'D') {
button = (
- <button
- type='button'
- className='btn btn-primary btn-message'
+ <a
+ href='#'
+ className='btn-message'
onClick={(e) => this.handleShowDirectChannel(m, e)}
>
{'Message'}
- </button>
+ </a>
);
}
if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
popoverHtml.push(
<div
- className='text--nowrap'
+ className='text-nowrap'
key={'popover-member-' + i}
>
<img
- className='profile-img pull-left'
- width='38'
- height='38'
+ className='profile-img rounded pull-left'
+ width='26px'
+ height='26px'
src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`}
/>
<div className='pull-left'>
@@ -135,14 +139,9 @@ export default class PopoverListMembers extends React.Component {
>
{m.username}
</div>
- <div
- className='more-description'
- >
- {details}
- </div>
</div>
<div
- className='pull-right profile-action'
+ className='pull-right'
>
{button}
</div>
@@ -182,12 +181,11 @@ export default class PopoverListMembers extends React.Component {
placement='bottom'
>
<Popover
+ ref='memebersPopover'
title='Members'
id='member-list-popover'
>
- <div>
- {popoverHtml}
- </div>
+ {popoverHtml}
</Popover>
</Overlay>
</div>
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 45eae8c6a..7138e2cb4 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -297,7 +297,7 @@ export default class PostBody extends React.Component {
}
let embed;
- if (filenames.length === 0 && this.state.links) {
+ if (filenames.length === 0 && this.state.links && this.state.links.length > 0) {
embed = this.createEmbed(this.state.links[0]);
}
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 36260d77c..6937ec216 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -44,7 +44,7 @@ export default class PostInfo extends React.Component {
role='menuitem'
data-toggle='modal'
data-target='#edit_post'
- data-refoucsid='#post_textbox'
+ data-refocusid='#post_textbox'
data-title={type}
data-message={post.message}
data-postid={post.id}
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 3ceef478c..b9741bac4 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -9,6 +9,7 @@ const LoadingScreen = require('./loading_screen.jsx');
const PostStore = require('../stores/post_store.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
+const TeamStore = require('../stores/team_store.jsx');
const SocketStore = require('../stores/socket_store.jsx');
const PreferenceStore = require('../stores/preference_store.jsx');
@@ -358,11 +359,11 @@ export default class PostList extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- <i className='fa fa-pencil'></i>{'Set a description'}
+ <i className='fa fa-pencil'></i>{'Set a header'}
</a>
</div>
);
@@ -386,17 +387,55 @@ export default class PostList extends React.Component {
}
}
createDefaultIntroMessage(channel) {
+ const team = TeamStore.getCurrent();
+ let inviteModalLink;
+ if (team.type === Constants.INVITE_TEAM) {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#invite_member'
+ >
+ <i className='fa fa-user-plus'></i>{'Invite others to this team'}
+ </a>
+ );
+ } else {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#get_link'
+ data-title='Team Invite'
+ data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + team.id}
+ >
+ <i className='fa fa-user-plus'></i>{'Invite others to this team'}
+ </a>
+ );
+ }
+
return (
<div className='channel-intro'>
<h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
<p className='channel-intro__content'>
- {'Welcome to ' + channel.display_name + '!'}
+ <strong>{'Welcome to ' + channel.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.'}
- <br/><br/>
- {'To create a new channel or join an existing one, go to the Left Sidebar under “Channels” and click “More…”.'}
- <br/>
</p>
+ {inviteModalLink}
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-header={channel.header}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>{'Set a header'}
+ </a>
+ <br/>
</div>
);
}
@@ -413,11 +452,11 @@ export default class PostList extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- <i className='fa fa-pencil'></i>{'Set a description'}
+ <i className='fa fa-pencil'></i>{'Set a header'}
</a>
<a
className='intro-links'
@@ -479,11 +518,11 @@ export default class PostList extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- <i className='fa fa-pencil'></i>{'Set a description'}
+ <i className='fa fa-pencil'></i>{'Set a header'}
</a>
<a
className='intro-links'
diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx
index 3d4d9bf45..c40409dcc 100644
--- a/web/react/components/register_app_modal.jsx
+++ b/web/react/components/register_app_modal.jsx
@@ -96,75 +96,74 @@ export default class RegisterAppModal extends React.Component {
var body = '';
if (this.state.clientId === '') {
body = (
- <div className='form-group user-settings'>
- <h3>{'Register a New Application'}</h3>
- <br/>
- <label className='col-sm-4 control-label'>{'Application Name'}</label>
- <div className='col-sm-7'>
- <input
- ref='name'
- className='form-control'
- type='text'
- placeholder='Required'
- />
- {nameError}
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Homepage URL'}</label>
- <div className='col-sm-7'>
- <input
- ref='homepage'
- className='form-control'
- type='text'
- placeholder='Required'
- />
- {homepageError}
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Description'}</label>
- <div className='col-sm-7'>
- <input
- ref='desc'
- className='form-control'
- type='text'
- placeholder='Optional'
- />
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Callback URL'}</label>
- <div className='col-sm-7'>
- <textarea
- ref='callback'
- className='form-control'
- type='text'
- placeholder='Required'
- rows='5'
- />
- {callbackError}
+ <div className='settings-modal'>
+ <div className='form-horizontal user-settings'>
+ <h4 className='padding-bottom x3'>{'Register a New Application'}</h4>
+ <div className='row'>
+ <label className='col-sm-4 control-label'>{'Application Name'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='name'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ />
+ {nameError}
+ </div>
+ </div>
+ <div className='row padding-top x2'>
+ <label className='col-sm-4 control-label'>{'Homepage URL'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='homepage'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ />
+ {homepageError}
+ </div>
+ </div>
+ <div className='row padding-top x2'>
+ <label className='col-sm-4 control-label'>{'Description'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='desc'
+ className='form-control'
+ type='text'
+ placeholder='Optional'
+ />
+ </div>
+ </div>
+ <div className='row padding-top padding-bottom x2'>
+ <label className='col-sm-4 control-label'>{'Callback URL'}</label>
+ <div className='col-sm-7'>
+ <textarea
+ ref='callback'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ rows='5'
+ />
+ {callbackError}
+ </div>
+ </div>
+ {serverError}
+ <hr />
+ <a
+ className='btn btn-sm theme pull-right'
+ href='#'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ {'Cancel'}
+ </a>
+ <a
+ className='btn btn-sm btn-primary pull-right'
+ onClick={this.register}
+ >
+ {'Register'}
+ </a>
</div>
- <br/>
- <br/>
- <br/>
- <br/>
- <br/>
- {serverError}
- <a
- className='btn btn-sm theme pull-right'
- href='#'
- data-dismiss='modal'
- aria-label='Close'
- >
- {'Cancel'}
- </a>
- <a
- className='btn btn-sm btn-primary pull-right'
- onClick={this.register}
- >
- {'Register'}
- </a>
</div>
);
} else {
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index cfff04fa2..8c6324c72 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -93,6 +93,7 @@ export default class RhsComment extends React.Component {
role='menuitem'
data-toggle='modal'
data-target='#edit_post'
+ data-refocusid='#reply_textbox'
data-title='Comment'
data-message={post.message}
data-postid={post.id}
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index deef389e2..21e52b438 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -79,6 +79,7 @@ export default class RhsRootPost extends React.Component {
role='menuitem'
data-toggle='modal'
data-target='#edit_post'
+ data-refocusid='#reply_textbox'
data-title={type}
data-message={post.message}
data-postid={post.id}
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
index 03c7b894c..f7d772677 100644
--- a/web/react/components/search_autocomplete.jsx
+++ b/web/react/components/search_autocomplete.jsx
@@ -10,6 +10,7 @@ const patterns = new Map([
['channels', /\b(?:in|channel):\s*(\S*)$/i],
['users', /\bfrom:\s*(\S*)$/i]
]);
+const Popover = ReactBootstrap.Popover;
export default class SearchAutocomplete extends React.Component {
constructor(props) {
@@ -36,6 +37,11 @@ export default class SearchAutocomplete extends React.Component {
$(document).on('click', this.handleDocumentClick);
}
+ componentDidUpdate() {
+ $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').perfectScrollbar();
+ $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').css('max-height', $(window).height() - 200);
+ }
+
componentWillUnmount() {
$(document).off('click', this.handleDocumentClick);
}
@@ -193,7 +199,7 @@ export default class SearchAutocomplete extends React.Component {
if (this.state.mode === 'channels') {
suggestions = this.state.suggestions.map((channel, index) => {
- let className = 'search-autocomplete__channel';
+ let className = 'search-autocomplete__item';
if (this.state.selection === index) {
className += ' selected';
}
@@ -211,7 +217,7 @@ export default class SearchAutocomplete extends React.Component {
});
} else if (this.state.mode === 'users') {
suggestions = this.state.suggestions.map((user, index) => {
- let className = 'search-autocomplete__user';
+ let className = 'search-autocomplete__item';
if (this.state.selection === index) {
className += ' selected';
}
@@ -224,7 +230,7 @@ export default class SearchAutocomplete extends React.Component {
className={className}
>
<img
- className='profile-img'
+ className='profile-img rounded'
src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
/>
{user.username}
@@ -234,12 +240,15 @@ export default class SearchAutocomplete extends React.Component {
}
return (
- <div
- ref='container'
- className='search-autocomplete'
+ <Popover
+ ref='searchPopover'
+ onShow={this.componentDidMount}
+ id='search-autocomplete__popover'
+ className='search-help-popover autocomplete visible'
+ placement='bottom'
>
{suggestions}
- </div>
+ </Popover>
);
}
}
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx
index 774f98a43..d6c4b0d4b 100644
--- a/web/react/components/setting_item_max.jsx
+++ b/web/react/components/setting_item_max.jsx
@@ -35,8 +35,10 @@ export default class SettingItemMax extends React.Component {
var widthClass;
if (this.props.width === 'full') {
widthClass = 'col-sm-12';
- } else {
+ } else if (this.props.width === 'medium') {
widthClass = 'col-sm-10 col-sm-offset-2';
+ } else {
+ widthClass = 'col-sm-9 col-sm-offset-3';
}
return (
diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx
index b6bcb13a6..e69412cca 100644
--- a/web/react/components/setting_picture.jsx
+++ b/web/react/components/setting_picture.jsx
@@ -42,7 +42,7 @@ export default class SettingPicture extends React.Component {
img = (
<img
ref='image'
- className='profile-img'
+ className='profile-img rounded'
src=''
/>
);
@@ -50,7 +50,7 @@ export default class SettingPicture extends React.Component {
img = (
<img
ref='image'
- className='profile-img'
+ className='profile-img rounded'
src={this.props.src}
/>
);
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index de28a8374..65e4c6d7e 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -5,6 +5,9 @@ var NavbarDropdown = require('./navbar_dropdown.jsx');
var UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
+const Tooltip = ReactBootstrap.Tooltip;
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
export default class SidebarHeader extends React.Component {
constructor(props) {
super(props);
@@ -47,7 +50,15 @@ export default class SidebarHeader extends React.Component {
{profilePicture}
<div className='header__info'>
<div className='user__name'>{'@' + me.username}</div>
+ <OverlayTrigger
+ trigger={['hover', 'focus']}
+ delayShow={1000}
+ placement='bottom'
+ overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDisplayName}</Tooltip>}
+ ref='descriptionOverlay'
+ >
<div className='team__name'>{this.props.teamDisplayName}</div>
+ </OverlayTrigger>
</div>
</a>
<NavbarDropdown
diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx
index ff4ccd4d8..021713f04 100644
--- a/web/react/components/team_signup_with_email.jsx
+++ b/web/react/components/team_signup_with_email.jsx
@@ -71,7 +71,7 @@ export default class EmailSignUpPage extends React.Component {
className='btn btn-md btn-primary'
type='submit'
>
- {'Sign up'}
+ {'Create Team'}
</button>
{serverError}
</div>
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
index 44b3f4544..095e5b622 100644
--- a/web/react/components/user_settings/custom_theme_chooser.jsx
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -40,11 +40,12 @@ export default class CustomThemeChooser extends React.Component {
const theme = {type: 'custom'};
let index = 0;
Constants.THEME_ELEMENTS.forEach((element) => {
- if (index < colors.length) {
+ if (index < colors.length - 1) {
theme[element.id] = colors[index];
}
index++;
});
+ theme.codeTheme = colors[colors.length - 1];
this.props.updateTheme(theme);
}
@@ -78,6 +79,8 @@ export default class CustomThemeChooser extends React.Component {
colors += theme[element.id] + ',';
});
+ colors += theme.codeTheme;
+
const pasteBox = (
<div className='col-sm-12'>
<label className='custom-label'>
diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx
index 6e9b2205d..4c56db0a1 100644
--- a/web/react/components/user_settings/manage_outgoing_hooks.jsx
+++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx
@@ -236,6 +236,7 @@ export default class ManageOutgoingHooks extends React.Component {
return (
<div key='addOutgoingHook'>
<label className='control-label'>{'Add a new outgoing webhook'}</label>
+ <div className='padding-top divider-light'></div>
<div className='padding-top'>
<div>
<label className='control-label'>{'Channel'}</label>
diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx
index 15bf961d6..546e26ca3 100644
--- a/web/react/components/user_settings/user_settings.jsx
+++ b/web/react/components/user_settings/user_settings.jsx
@@ -10,6 +10,7 @@ var AppearanceTab = require('./user_settings_appearance.jsx');
var DeveloperTab = require('./user_settings_developer.jsx');
var IntegrationsTab = require('./user_settings_integrations.jsx');
var DisplayTab = require('./user_settings_display.jsx');
+var AdvancedTab = require('./user_settings_advanced.jsx');
export default class UserSettings extends React.Component {
constructor(props) {
@@ -110,6 +111,17 @@ export default class UserSettings extends React.Component {
/>
</div>
);
+ } else if (this.props.activeTab === 'advanced') {
+ return (
+ <div>
+ <AdvancedTab
+ user={this.state.user}
+ activeSection={this.props.activeSection}
+ updateSection={this.props.updateSection}
+ updateTab={this.props.updateTab}
+ />
+ </div>
+ );
}
return <div/>;
diff --git a/web/react/components/user_settings/user_settings_advanced.jsx b/web/react/components/user_settings/user_settings_advanced.jsx
new file mode 100644
index 000000000..910444735
--- /dev/null
+++ b/web/react/components/user_settings/user_settings_advanced.jsx
@@ -0,0 +1,169 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const Client = require('../../utils/client.jsx');
+const SettingItemMin = require('../setting_item_min.jsx');
+const SettingItemMax = require('../setting_item_max.jsx');
+const Constants = require('../../utils/constants.jsx');
+const PreferenceStore = require('../../stores/preference_store.jsx');
+
+export default class AdvancedSettingsDisplay extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateSection = this.updateSection.bind(this);
+ this.updateSetting = this.updateSetting.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.setupInitialState = this.setupInitialState.bind(this);
+
+ this.state = this.setupInitialState();
+ }
+
+ setupInitialState() {
+ const sendOnCtrlEnter = PreferenceStore.getPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ 'send_on_ctrl_enter',
+ {value: 'false'}
+ ).value;
+
+ return {
+ settings: {send_on_ctrl_enter: sendOnCtrlEnter}
+ };
+ }
+
+ updateSetting(setting, value) {
+ const settings = this.state.settings;
+ settings[setting] = value;
+ this.setState(settings);
+ }
+
+ handleSubmit(setting) {
+ const preference = PreferenceStore.setPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ setting,
+ this.state.settings[setting]
+ );
+
+ Client.savePreferences([preference],
+ () => {
+ PreferenceStore.emitChange();
+ this.updateSection('');
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ updateSection(section) {
+ this.props.updateSection(section);
+ }
+
+ handleClose() {
+ this.updateSection('');
+ }
+
+ componentDidMount() {
+ $('#user_settings').on('hidden.bs.modal', this.handleClose);
+ }
+
+ componentWillUnmount() {
+ $('#user_settings').off('hidden.bs.modal', this.handleClose);
+ }
+
+ render() {
+ const serverError = this.state.serverError || null;
+ let ctrlSendSection;
+
+ if (this.props.activeSection === 'advancedCtrlSend') {
+ const ctrlSendActive = [
+ this.state.settings.send_on_ctrl_enter === 'true',
+ this.state.settings.send_on_ctrl_enter === 'false'
+ ];
+
+ const inputs = [
+ <div key='ctrlSendSetting'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={ctrlSendActive[0]}
+ onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'true')}
+ />
+ {'On'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={ctrlSendActive[1]}
+ onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'false')}
+ />
+ {'Off'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'If enabled \'Enter\' inserts a new line and \'Ctrl + Enter\' submits the message.'}</div>
+ </div>
+ ];
+
+ ctrlSendSection = (
+ <SettingItemMax
+ title='Send messages on Ctrl + Enter'
+ inputs={inputs}
+ submit={() => this.handleSubmit('send_on_ctrl_enter')}
+ server_error={serverError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ ctrlSendSection = (
+ <SettingItemMin
+ title='Send messages on Ctrl + Enter'
+ describe={this.state.settings.send_on_ctrl_enter === 'true' ? 'On' : 'Off'}
+ updateSection={() => this.props.updateSection('advancedCtrlSend')}
+ />
+ );
+ }
+
+ return (
+ <div>
+ <div className='modal-header'>
+ <button
+ type='button'
+ className='close'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </button>
+ <h4
+ className='modal-title'
+ ref='title'
+ >
+ <i className='modal-back'></i>
+ {'Advanced Settings'}
+ </h4>
+ </div>
+ <div className='user-settings'>
+ <h3 className='tab-header'>{'Advanced Settings'}</h3>
+ <div className='divider-dark first'/>
+ {ctrlSendSection}
+ <div className='divider-dark'/>
+ </div>
+ </div>
+ );
+ }
+}
+
+AdvancedSettingsDisplay.propTypes = {
+ user: React.PropTypes.object,
+ updateSection: React.PropTypes.func,
+ updateTab: React.PropTypes.func,
+ activeSection: React.PropTypes.string
+};
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index e94894a1d..7b4b54e27 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -100,7 +100,9 @@ export default class UserSettingsAppearance extends React.Component {
);
}
updateTheme(theme) {
- theme.codeTheme = this.state.theme.codeTheme;
+ if (!theme.codeTheme) {
+ theme.codeTheme = this.state.theme.codeTheme;
+ }
this.setState({theme});
Utils.applyTheme(theme);
}
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index 1c8ce3c79..3adac197a 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -570,6 +570,7 @@ export default class UserSettingsGeneralTab extends React.Component {
/>
);
}
+
return (
<div>
<div className='modal-header'>
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index 4b1e5e532..9bee74343 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -43,6 +43,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMax
title='Incoming Webhooks'
+ width='medium'
inputs={inputs}
updateSection={(e) => {
this.updateSection('');
@@ -54,6 +55,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMin
title='Incoming Webhooks'
+ width='medium'
describe='Manage your incoming webhooks (Developer feature)'
updateSection={() => {
this.updateSection('incoming-hooks');
@@ -72,6 +74,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
outgoingHooksSection = (
<SettingItemMax
title='Outgoing Webhooks'
+ width='medium'
inputs={inputs}
updateSection={(e) => {
this.updateSection('');
@@ -83,6 +86,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
outgoingHooksSection = (
<SettingItemMin
title='Outgoing Webhooks'
+ width='medium'
describe='Manage your outgoing webhooks'
updateSection={() => {
this.updateSection('outgoing-hooks');
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 5449ae91e..18dd490e7 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -43,6 +43,7 @@ export default class UserSettingsModal extends React.Component {
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
}
tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'});
+ tabs.push({name: 'advanced', uiName: 'Advanced', icon: 'glyphicon glyphicon-list-alt'});
return (
<div
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index a58fdde3a..8f4e30e7c 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -172,14 +172,15 @@ class PostStoreClass extends EventEmitter {
var lastPost = null;
for (i; i < len; i++) {
- if (postList.posts[postList.order[i]].user_id === userId) {
+ let post = postList.posts[postList.order[i]];
+ if (post.user_id === userId && (post.props && !post.props.from_webhook || !post.props)) {
if (rootId) {
- if (postList.posts[postList.order[i]].root_id === rootId || postList.posts[postList.order[i]].id === rootId) {
- lastPost = postList.posts[postList.order[i]];
+ if (post.root_id === rootId || post.id === rootId) {
+ lastPost = post;
break;
}
} else {
- lastPost = postList.posts[postList.order[i]];
+ lastPost = post;
break;
}
}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index e1dee9c65..7ce1346f9 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -589,21 +589,38 @@ export function updateChannel(channel, success, error) {
track('api', 'api_channels_update');
}
-export function updateChannelDesc(data, success, error) {
+export function updateChannelHeader(data, success, error) {
$.ajax({
- url: '/api/v1/channels/update_desc',
+ url: '/api/v1/channels/update_header',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success,
error: function onError(xhr, status, err) {
- var e = handleError('updateChannelDesc', xhr, status, err);
+ var e = handleError('updateChannelHeader', xhr, status, err);
error(e);
}
});
- track('api', 'api_channels_desc');
+ track('api', 'api_channels_header');
+}
+
+export function updateChannelPurpose(data, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/update_purpose',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updateChannelPurpose', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_purpose');
}
export function updateNotifyProps(data, success, error) {
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 0e89b9470..1593f6706 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -127,6 +127,8 @@ module.exports = {
MAX_DMS: 20,
DM_CHANNEL: 'D',
OPEN_CHANNEL: 'O',
+ INVITE_TEAM: 'I',
+ OPEN_TEAM: 'O',
MAX_POST_LEN: 4000,
EMOJI_SIZE: 16,
ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>",
@@ -311,7 +313,8 @@ module.exports = {
DEFAULT_CODE_THEME: 'github',
Preferences: {
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
- CATEGORY_DISPLAY_SETTINGS: 'display_settings'
+ CATEGORY_DISPLAY_SETTINGS: 'display_settings',
+ CATEGORY_ADVANCED_SETTINGS: 'advanced_settings'
},
KeyCodes: {
UP: 38,
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index ad11a95ac..179416ea0 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -34,7 +34,43 @@ const highlightJsIni = require('highlight.js/lib/languages/ini.js');
const Constants = require('../utils/constants.jsx');
const HighlightedLanguages = Constants.HighlightedLanguages;
-export class MattermostMarkdownRenderer extends marked.Renderer {
+class MattermostInlineLexer extends marked.InlineLexer {
+ constructor(links, options) {
+ super(links, options);
+
+ this.rules = Object.assign({}, this.rules);
+
+ // modified version of the regex that doesn't break up words in snake_case,
+ // allows for links starting with www, and allows links succounded by parentheses
+ // the original is /^[\s\S]+?(?=[\\<!\[_*`~]|https?:\/\/| {2,}\n|$)/
+ this.rules.text = /^[\s\S]+?(?=[^\w\/]_|[\\<!\[*`~]|https?:\/\/|www\.|\(| {2,}\n|$)/;
+
+ // modified version of the regex that allows links starting with www and those surrounded
+ // by parentheses
+ // the original is /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/
+ this.rules.url = /^(\(?(?:https?:\/\/|www\.)[^\s<.][^\s<]*[^<.,:;"'\]\s])/;
+
+ // modified version of the regex that allows <links> starting with www.
+ // the original is /^<([^ >]+(@|:\/)[^ >]+)>/
+ this.rules.autolink = /^<((?:[^ >]+(@|:\/)|www\.)[^ >]+)>/;
+ }
+}
+
+class MattermostParser extends marked.Parser {
+ parse(src) {
+ this.inline = new MattermostInlineLexer(src.links, this.options, this.renderer);
+ this.tokens = src.reverse();
+
+ var out = '';
+ while (this.next()) {
+ out += this.tok();
+ }
+
+ return out;
+ }
+}
+
+class MattermostMarkdownRenderer extends marked.Renderer {
constructor(options, formattingOptions = {}) {
super(options);
@@ -70,15 +106,21 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
}
code(code, language) {
- if (!language || highlightJs.listLanguages().indexOf(language) < 0) {
- let parsed = super.code(code, language);
- return '<code class="hljs">' + $(parsed).text() + '</code>';
+ let usedLanguage = language;
+
+ if (String(usedLanguage).toLocaleLowerCase() === 'html') {
+ usedLanguage = 'xml';
}
- let parsed = highlightJs.highlight(language, code);
+ if (!usedLanguage || highlightJs.listLanguages().indexOf(usedLanguage) < 0) {
+ let parsed = super.code(code, usedLanguage);
+ return '<div class="post-body--code"><code class="hljs">' + TextFormatting.sanitizeHtml($(parsed).text()) + '</code></div>';
+ }
+
+ let parsed = highlightJs.highlight(usedLanguage, code);
return '<div class="post-body--code">' +
- '<span class="post-body--code__language">' + HighlightedLanguages[language] + '</span>' +
- '<code style="white-space: pre;" class="hljs">' + parsed.value + '</code>' +
+ '<span class="post-body--code__language">' + HighlightedLanguages[usedLanguage] + '</span>' +
+ '<code class="hljs">' + parsed.value + '</code>' +
'</div>';
}
@@ -97,8 +139,20 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
link(href, title, text) {
let outHref = href;
+ let outText = text;
+ let prefix = '';
+ let suffix = '';
+
+ // some links like https://en.wikipedia.org/wiki/Rendering_(computer_graphics) contain brackets
+ // and we try our best to differentiate those from ones just wrapped in brackets when autolinking
+ if (outHref.startsWith('(') && outHref.endsWith(')') && text === outHref) {
+ prefix = '(';
+ suffix = ')';
+ outText = text.substring(1, text.length - 1);
+ outHref = outHref.substring(1, outHref.length - 1);
+ }
- if (!(/^(mailto|https?|ftp)/.test(outHref))) {
+ if (!(/[a-z+.-]+:/i).test(outHref)) {
outHref = `http://${outHref}`;
}
@@ -113,26 +167,17 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
output += ' target="_blank">';
}
- output += text + '</a>';
+ output += outText + '</a>';
- return output;
+ return prefix + output + suffix;
}
paragraph(text) {
- let outText = text;
-
- // required so markdown does not strip '_' from @user_names
- outText = TextFormatting.doFormatMentions(text);
-
- if (!('emoticons' in this.options) || this.options.emoticon) {
- outText = TextFormatting.doFormatEmoticons(outText);
- }
-
if (this.formattingOptions.singleline) {
- return `<p class="markdown__paragraph-inline">${outText}</p>`;
+ return `<p class="markdown__paragraph-inline">${text}</p>`;
}
- return super.paragraph(outText);
+ return super.paragraph(text);
}
table(header, body) {
@@ -143,3 +188,16 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
return TextFormatting.doFormatText(txt, this.formattingOptions);
}
}
+
+export function format(text, options) {
+ const markdownOptions = {
+ renderer: new MattermostMarkdownRenderer(null, options),
+ sanitize: true,
+ gfm: true
+ };
+
+ const tokens = marked.lexer(text, markdownOptions);
+
+ return new MattermostParser(markdownOptions).parse(tokens);
+}
+
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 9f1a5a53f..2de858a17 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -8,8 +8,6 @@ const Markdown = require('./markdown.jsx');
const UserStore = require('../stores/user_store.jsx');
const Utils = require('./utils.jsx');
-const marked = require('marked');
-
// Performs formatting of user posts including highlighting mentions and search terms and converting urls, hashtags, and
// @mentions to links by taking a user's message and returning a string of formatted html. Also takes a number of options
// as part of the second parameter:
@@ -22,11 +20,8 @@ export function formatText(text, options = {}) {
let output;
if (!('markdown' in options) || options.markdown) {
- // the markdown renderer will call doFormatText as necessary so just call marked
- output = marked(text, {
- renderer: new Markdown.MattermostMarkdownRenderer(null, options),
- sanitize: true
- });
+ // the markdown renderer will call doFormatText as necessary
+ output = Markdown.format(text, options);
} else {
output = sanitizeHtml(text);
output = doFormatText(output, options);
@@ -48,7 +43,7 @@ export function doFormatText(text, options) {
// replace important words and phrases with tokens
output = autolinkAtMentions(output, tokens);
- output = autolinkUrls(output, tokens);
+ output = autolinkEmails(output, tokens);
output = autolinkHashtags(output, tokens);
if (!('emoticons' in options) || options.emoticon) {
@@ -98,28 +93,21 @@ export function sanitizeHtml(text) {
return output;
}
-// Convert URLs into tokens
-function autolinkUrls(text, tokens) {
- function replaceUrlWithToken(autolinker, match) {
+// Convert emails into tokens
+function autolinkEmails(text, tokens) {
+ function replaceEmailWithToken(autolinker, match) {
const linkText = match.getMatchedText();
let url = linkText;
if (match.getType() === 'email') {
url = `mailto:${url}`;
- } else if (!(/^(mailto|https?|ftp)/.test(url))) {
- url = `http://${url}`;
}
const index = tokens.size;
- const alias = `MM_LINK${index}`;
-
- var target = 'target="_blank"';
- if (url.lastIndexOf(Utils.getTeamURLFromAddressBar(), 0) === 0) {
- target = '';
- }
+ const alias = `MM_EMAIL${index}`;
tokens.set(alias, {
- value: `<a class="theme" ${target} href="${url}">${linkText}</a>`,
+ value: `<a class="theme" href="${url}">${linkText}</a>`,
originalText: linkText
});
@@ -128,12 +116,12 @@ function autolinkUrls(text, tokens) {
// we can't just use a static autolinker because we need to set replaceFn
const autolinker = new Autolinker({
- urls: true,
+ urls: false,
email: true,
phone: false,
twitter: false,
hashtag: false,
- replaceFn: replaceUrlWithToken
+ replaceFn: replaceEmailWithToken
});
return autolinker.link(text);
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 3140a5d77..c7c8549b9 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -231,46 +231,62 @@ export function getTimestamp() {
return Date.now();
}
-function testUrlMatch(text) {
- var urlMatcher = new Autolinker.matchParser.MatchParser({
+// extracts links not styled by Markdown
+export function extractLinks(text) {
+ const urlMatcher = new Autolinker.matchParser.MatchParser({
urls: true,
emails: false,
twitter: false,
phone: false,
hashtag: false
});
- var result = [];
+ const links = [];
+ let replaceText = text;
+
+ // pull out the Markdown code blocks
+ const codeBlocks = [];
+ const splitText = replaceText.split('`'); // also handles ```
+ for (let i = 1; i < splitText.length; i += 2) {
+ if (splitText[i].trim() !== '') {
+ codeBlocks.push(splitText[i]);
+ }
+ }
+
function replaceFn(match) {
- var linkData = {};
- var matchText = match.getMatchedText();
+ let link = '';
+ const matchText = match.getMatchedText();
+ const tempText = replaceText;
+
+ const start = replaceText.indexOf(matchText);
+ const end = start + matchText.length;
+
+ replaceText = replaceText.substring(0, start) + replaceText.substring(end);
+
+ // if it's a Markdown link, just skip it
+ if (start > 1) {
+ if (tempText.charAt(start - 2) === ']' && tempText.charAt(start - 1) === '(' && tempText.charAt(end) === ')') {
+ return;
+ }
+ }
+
+ // if it's in a Markdown code block, skip it
+ for (const i in codeBlocks) {
+ if (codeBlocks[i].indexOf(matchText) === 0) {
+ codeBlocks[i] = codeBlocks[i].replace(matchText, '');
+ return;
+ }
+ }
- linkData.text = matchText;
if (matchText.trim().indexOf('http') === 0) {
- linkData.link = matchText;
+ link = matchText;
} else {
- linkData.link = 'http://' + matchText;
+ link = 'http://' + matchText;
}
- result.push(linkData);
+ links.push(link);
}
urlMatcher.replace(text, replaceFn, this);
- return result;
-}
-
-export function extractLinks(text) {
- var repRegex = new RegExp('<br>', 'g');
- var matches = testUrlMatch(text.replace(repRegex, '\n'));
-
- if (!matches.length) {
- return {links: null, text: text};
- }
-
- var links = [];
- for (var i = 0; i < matches.length; i++) {
- links.push(matches[i].link);
- }
-
- return {links: links, text: text};
+ return {links, text};
}
export function escapeRegExp(string) {
@@ -443,6 +459,8 @@ export function applyTheme(theme) {
if (theme.sidebarTextActiveColor) {
changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2);
changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.1), 1);
+ changeCss('.search-help-popover .search-autocomplete__item:hover', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.05), 1);
+ changeCss('.search-help-popover .search-autocomplete__item.selected', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.15), 1);
}
if (theme.sidebarHeaderBg) {
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index 9c65e2d1e..206d5bfca 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -2,63 +2,16 @@
> div {
height: 100%;
}
- .table {
- background: #fff;
- }
-
- .total-count {
- width: 175px;
- height: 100px;
- border: 1px solid #ddd;
- padding: 22px 10px 10px 10px;
- margin: 10px 10px 10px 10px;
- background: #fff;
- float: left;
-
- > div {
- font-size: 18px;
- font-weight: 300;
- }
- }
-
- .total-count-by-day {
- width: 760px;
- height: 275px;
- border: 1px solid #ddd;
- padding: 5px 10px 10px 10px;
- margin: 10px 10px 10px 10px;
- background: #fff;
- clear: both;
- > div {
- font-size: 18px;
- font-weight: 300;
- }
+ h3 {
+ font-weight: 600;
+ border-bottom: 1px solid #ddd;
+ padding-bottom: 0.5em;
+ margin: 1em 0;
}
- .recent-active-users {
- width: 365px;
- border: 1px solid #ddd;
- padding: 5px 10px 10px 10px;
- margin: 10px 10px 10px 10px;
+ .table {
background: #fff;
- float: left;
-
- > div {
- font-size: 18px;
- font-weight: 300;
- }
-
- > table {
- margin: 10px 10px 10px 10px;
- }
-
- .recent-active-users-td {
- font-size: 14px;
- font-weight: 300;
- border: 1px solid #ddd;
- padding: 3px 3px 3px 5px;
- }
}
.sidebar--left {
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 6399b8fd8..635928fe3 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -37,6 +37,9 @@ body {
img {
max-width: 100%;
height: auto;
+ &.rounded {
+ @include border-radius(100%);
+ }
}
.popover {
@@ -122,6 +125,9 @@ a:focus, a:hover {
&.no-resize {
resize: none;
}
+ &.min-height {
+ min-height: 100px;
+ }
}
.form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control {
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index 8bf163f90..7e776bf76 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -215,7 +215,7 @@
color: #999;
cursor: pointer;
.fa {
- margin-left: 3px;
+ margin-left: 4px;
font-size: 16px;
}
}
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index 1dcdbf348..9314b4980 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -113,11 +113,10 @@
text-align: center;
padding: 2em 1em;
.primary-message {
- font-size: 1.2em;
+ font-size: 1.25em;
}
.secondary-message {
- font-size: 1.25em;
- color: #888;
+ @include opacity(0.8);
margin: 1em 0 0;
}
}
@@ -377,7 +376,7 @@
@include opacity(0.8);
}
- .more-description {
+ .more-purpose {
@include opacity(0.7);
}
diff --git a/web/sass-files/sass/partials/_popover.scss b/web/sass-files/sass/partials/_popover.scss
index 4a2ad2748..430813e63 100644
--- a/web/sass-files/sass/partials/_popover.scss
+++ b/web/sass-files/sass/partials/_popover.scss
@@ -28,6 +28,39 @@
@include single-transition(opacity, 0.3s, ease-in);
font-size: em(13px);
+ &.autocomplete {
+ display: block;
+ .popover-content {
+ padding: 10px;
+ position: relative;
+ }
+ }
+
+ .search-autocomplete__item {
+ cursor: pointer;
+ padding: 6px 8px;
+ margin: 3px 0;
+ @include border-radius(2px);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:hover {
+ background: rgba(black, 0.1);
+ }
+
+ &.selected {
+ background: rgba(black, 0.2);
+ }
+
+ .profile-img {
+ margin-top: -1px;
+ height: 16px;
+ margin-right: 6px;
+ width: 16px;
+ }
+ }
+
&.bottom > .arrow {
top: -18px;
border-width: 9px;
@@ -64,33 +97,20 @@
#member-list-popover {
max-width: initial;
- .popover-content > div {
+ .popover-content {
+ position: relative;
+ padding: 0;
+ width: 260px;
max-height: 350px;
- overflow-y: auto;
- overflow-x: hidden;
- > div {
- border-bottom: 1px solid rgba(51,51,51,0.1);
- padding: 8px 8px 8px 15px;
+ .text-nowrap {
+ padding: 6px 10px;
width: 100%;
- box-sizing: content-box;
- @include clearfix;
- .profile-img {
- border-radius: 50px;
- margin-right: 8px;
- }
- .more-name {
- font-weight: 600;
- font-size: 0.95em;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .more-description {
- @include opacity(0.7);
- }
- .profile-action {
- margin-left: 8px;
- margin-right: 18px;
- }
+ overflow: hidden;
+ line-height: 26px;
+ font-size: 13px;
+ }
+ .more-name {
+ margin-left: 6px;
}
}
}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 3fac1fed9..7709e17f3 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -397,11 +397,18 @@ body.ios {
padding: 0;
}
p {
- margin: 0 0 5px;
+ margin: 0 0 1em;
+ line-height: 1.6em;
font-size: 0.97em;
white-space: pre-wrap;
}
+ span {
+ p:last-child {
+ margin-bottom: 0.5em;
+ }
+ }
+
.post-loading-gif {
height:10px;
width:10px;
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 2cd5560ef..b85fa638a 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -379,7 +379,9 @@
}
.btn {
&.btn-primary {
- margin: 8px 0 0 -10px;
+ display: block;
+ margin: 10px 0 6px;
+ width: 100%;
float: none;
}
}
@@ -401,7 +403,13 @@
&.minimize-settings {
display: block;
.section-edit {
- text-align: left;
+ position: absolute;
+ top: 7px;
+ right: 0;
+ width: 50px;
+ .fa {
+ display: inline-block;
+ }
}
}
.no-padding--left {
diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss
index ce3563885..e50dc398a 100644
--- a/web/sass-files/sass/partials/_search.scss
+++ b/web/sass-files/sass/partials/_search.scss
@@ -108,44 +108,4 @@
.search-highlight {
background-color: #FFF2BB;
-}
-
-.search-autocomplete {
- background-color: #fff;
- border: $border-gray;
- line-height: 36px;
- overflow-x: hidden;
- overflow-y: scroll;
- position: absolute;
- text-align: left;
- width: 100%;
- z-index: 100;
- @extend %popover-box-shadow;
-}
-
-.search-autocomplete__channel {
- cursor: pointer;
- height: 36px;
- padding: 0px 6px;
-
- &.selected {
- background-color:rgba(51, 51, 51, 0.15);
- }
-}
-
-.search-autocomplete__user {
- cursor: pointer;
- height: 36px;
- padding: 0px;
-
- .profile-img {
- height: 32px;
- margin-right: 6px;
- width: 32px;
- @include border-radius(16px);
- }
-
- &.selected {
- background-color:rgba(51, 51, 51, 0.15);
- }
-}
+} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index c881f9073..fbbd07485 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -33,6 +33,33 @@
label {
font-weight: 600;
}
+ .no-padding--left {
+ padding-left: 0;
+ }
+ .padding-top {
+ padding-top: 7px;
+ &.x2 {
+ padding-top: 14px;
+ }
+ &.x3 {
+ padding-top: 21px;
+ }
+ }
+ .padding-bottom {
+ padding-bottom: 7px;
+ &.x2 {
+ padding-bottom: 14px;
+ }
+ &.x3 {
+ padding-bottom: 21px;
+ }
+ .control-label {
+ font-weight: 600;
+ &.text-left {
+ text-align: left;
+ }
+ }
+ }
.settings-table {
display: table;
table-layout: fixed;
@@ -44,7 +71,7 @@
.nav {
position: fixed;
top: 57px;
- width: 180px;
+ width: 179px;
}
.security-links {
margin-right: 20px;
@@ -66,6 +93,7 @@
padding: 1em 0;
margin-bottom: 0;
cursor: pointer;
+ position: relative;
@include clearfix;
&:hover {
background: #f9f9f9;
@@ -170,33 +198,6 @@
.has-error {
color: #a94442;
}
- .no-padding--left {
- padding-left: 0;
- }
- .padding-top {
- padding-top: 7px;
- &.x2 {
- padding-top: 14px;
- }
- &.x3 {
- padding-top: 21px;
- }
- }
- .padding-bottom {
- padding-bottom: 7px;
- &.x2 {
- padding-bottom: 14px;
- }
- &.x3 {
- padding-bottom: 21px;
- }
- .control-label {
- font-weight: 600;
- &.text-left {
- text-align: left;
- }
- }
- }
.file-status {
font-size: 13px;
diff --git a/web/sass-files/sass/partials/_statistics.scss b/web/sass-files/sass/partials/_statistics.scss
new file mode 100644
index 000000000..a2401a70f
--- /dev/null
+++ b/web/sass-files/sass/partials/_statistics.scss
@@ -0,0 +1,84 @@
+.team_statistics {
+ .total-count {
+ margin: 1em 0;
+ text-align: center;
+ background: #fff;
+ border: 1px solid #ddd;
+ width: 100%;
+ @include border-radius(3px);
+
+ .title {
+ font-weight: 400;
+ padding: 7px 10px;
+ border-bottom: 1px solid #ddd;
+ text-align: left;
+
+ .fa {
+ float: right;
+ margin: 3px 0 0;
+ color: #555;
+ font-size: 16px;
+ }
+
+ }
+
+ .content {
+ font-size: 30px;
+ font-weight: 600;
+ color: #555;
+ padding: 0.3em 0 0.35em;
+ overflow: auto;
+ }
+
+ }
+
+ .total-count--day {
+ width: 760px;
+ height: 275px;
+ border: 1px solid #ddd;
+ padding: 5px 10px 10px 10px;
+ margin: 10px 10px 10px 10px;
+ background: #fff;
+ clear: both;
+
+ > div {
+ font-size: 18px;
+ font-weight: 300;
+ }
+ }
+
+ .recent-active-users {
+
+ table {
+ width: 100%;
+ table-layout: fixed;
+ }
+
+ .content {
+ max-height: 400px;
+ overflow: auto;
+ padding: 0;
+ }
+
+ tr {
+ &:first-child {
+ td {
+ border-top: none;
+ }
+ }
+ td {
+ font-weight: 400;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ font-size: 13px;
+ border-left: 1px solid #ddd;
+ border-top: 1px solid #ddd;
+ padding: 5px 5px 6px;
+ @include clearfix;
+ &:first-child {
+ border-left: none;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss
index c614052da..ad2cae194 100644
--- a/web/sass-files/sass/styles.scss
+++ b/web/sass-files/sass/styles.scss
@@ -38,6 +38,7 @@
@import "partials/loading";
@import "partials/get-link";
@import "partials/markdown";
+@import "partials/statistics";
// Responsive Css
@import "partials/responsive";