Merge branch 'master' into PLT-25

This commit is contained in:
=Corey Hulen
2015-10-25 22:42:38 -07:00
30 changed files with 583 additions and 146 deletions

5
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,5 @@
# Contributing
## Contributing Code
Please see [Mattermost Code Contribution Guidelines](https://github.com/mattermost/platform/blob/master/doc/developer/Code-Contribution-Guidelines.md)

View File

@@ -18,7 +18,7 @@
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">You updated your password</h2>
<p>You updated your password for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} by {{.Props.Method}}.<br> If this change wasn't initiated by you, please reply to this email and let us know.</p>
<p>You updated your password for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} by {{.Props.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator.</p>
</td>
</tr>
<tr>

View File

@@ -8,4 +8,8 @@ Some things to know about search:
- You can use quotes to return search results for exact terms, like `"Mattermost website"` will only return messages containing the entire phrase `"Mattermost website"` and not return messages with only `Mattermost` or `website`
- You can use the `*` character for wildcard searches that match within words. For example: Searching for `rea*` brings back messages containing `reach`, `reason` and other words starting with `rea`.
Search in Mattermost uses the full text search features in MySQL and Postgres databases. Special cases that are not supported in default full text search, such as searching for IP addresses like `10.100.200.101`, can be added in future as the search feature evolves.
#### Limitations
- Search in Mattermost uses the full text search features included in either a MySQL or Postgres database, which has some limitations
- Special cases that are not supported in default full text search, such as searching for IP addresses like `10.100.200.101`, can be added in future as the search feature evolves
- Searches with fewer than three characters will return no results, so for searching in Chinese try adding * to the end of queries

View File

@@ -12,7 +12,8 @@ To enable email, configure an SMTP email service as follows:
2. If you don't have an SMTP service, here are simple instructions to set one up with [Amazon Simple Email Service (SES)](https://aws.amazon.com/ses/):
2. Go to [Amazon SES console](https://console.aws.amazon.com/ses) then `SMTP Settings > Create My SMTP Credentials`
3. Copy the `Server Name`, `Port`, `SMTP Username`, and `SMTP Password` for Step 2 below.
4. From the `Domains` menu set up and verify a new domain, then enable `Generate DKIM Settings` for the domain.
4. From the `Domains` menu set up and verify a new domain, then enable `Generate DKIM Settings` for the domain.
1. We recommend you set up _[Sender Policy Framework](https://en.wikipedia.org/wiki/Sender_Policy_Framework) (SPF)_ and/or _[Domain Keys Identified Mail](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) (DKIM)_ for your email domain.
5. Choose an sender address like `mattermost@example.com` and click `Send a Test Email` to verify setup is working correctly.
2. **Configure SMTP settings**
@@ -57,7 +58,11 @@ To enable email, configure an SMTP email service as follows:
* Information needed
##### Hotmail
* Information needed
* Set **SMTP Username** to **your_email@hotmail.com**
* Set **SMTP Password** to **your_password**
* Set **SMTP Server** to **smtp-mail.outlook.com**
* Set **SMTP Port** to **587**
* Set **Connection Security** to **STARTTLS**
### Troubleshooting SMTP
@@ -91,4 +96,4 @@ Connected to mail.example.com.
250-STARTTLS
250-PIPELINING
250 8BITMIME
```
```

View File

@@ -136,16 +136,15 @@ export default class ChannelNotifications extends React.Component {
var inputs = [];
inputs.push(
<div>
<div key='channel-notification-level-radio'>
<div className='radio'>
<label>
<input
type='radio'
checked={notifyActive[0]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
>
/>
{`Global default (${globalNotifyLevelName})`}
</input>
</label>
<br/>
</div>
@@ -155,9 +154,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={notifyActive[1]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
>
/>
{'For all activity'}
</input>
</label>
<br/>
</div>
@@ -167,9 +165,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={notifyActive[2]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
>
/>
{'Only for mentions'}
</input>
</label>
<br/>
</div>
@@ -179,9 +176,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={notifyActive[3]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
>
/>
{'Never'}
</input>
</label>
</div>
</div>
@@ -274,16 +270,15 @@ export default class ChannelNotifications extends React.Component {
if (this.state.activeSection === 'markUnreadLevel') {
const inputs = [(
<div>
<div key='channel-notification-unread-radio'>
<div className='radio'>
<label>
<input
type='radio'
checked={this.state.markUnreadLevel === 'all'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
>
/>
{'For all unread messages'}
</input>
</label>
<br />
</div>
@@ -293,9 +288,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={this.state.markUnreadLevel === 'mention'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
>
/>
{'Only for mentions'}
</input>
</label>
<br />
</div>
@@ -370,7 +364,7 @@ export default class ChannelNotifications extends React.Component {
data-dismiss='modal'
>
<span aria-hidden='true'>&times;</span>
<span className='sr-only'>Close</span>
<span className='sr-only'>{'Close'}</span>
</button>
<h4 className='modal-title'>Notification Preferences for <span className='name'>{this.state.title}</span></h4>
</div>

View File

@@ -260,6 +260,12 @@ export default class InviteMemberModal extends React.Component {
var content = null;
var sendButton = null;
var sendButtonLabel = 'Send Invitation';
if (this.state.inviteIds.length > 1) {
sendButtonLabel = 'Send Invitations';
}
if (this.state.emailEnabled) {
content = (
<div>
@@ -281,7 +287,7 @@ export default class InviteMemberModal extends React.Component {
onClick={this.handleSubmit}
type='button'
className='btn btn-primary'
>Send Invitations</button>
>{sendButtonLabel}</button>
);
} else {
var teamInviteLink = null;

View File

@@ -169,7 +169,7 @@ export default class MoreDirectChannels extends React.Component {
}
return (
<tr>
<tr key={'direct-channel-row-user' + user.id}>
<td
key={user.id}
className='direct-channel'

View File

@@ -0,0 +1,249 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
const ChannelStore = require('../stores/channel_store.jsx');
const KeyCodes = require('../utils/constants.jsx').KeyCodes;
const UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
const patterns = new Map([
['channels', /\b(?:in|channel):\s*(\S*)$/i],
['users', /\bfrom:\s*(\S*)$/i]
]);
export default class SearchAutocomplete extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleDocumentClick = this.handleDocumentClick.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.completeWord = this.completeWord.bind(this);
this.updateSuggestions = this.updateSuggestions.bind(this);
this.state = {
show: false,
mode: '',
filter: '',
selection: 0,
suggestions: new Map()
};
}
componentDidMount() {
$(document).on('click', this.handleDocumentClick);
}
componentWillUnmount() {
$(document).off('click', this.handleDocumentClick);
}
handleClick(value) {
this.completeWord(value);
}
handleDocumentClick(e) {
const container = $(ReactDOM.findDOMNode(this.refs.container));
if (!(container.is(e.target) || container.has(e.target).length > 0)) {
this.setState({
show: false
});
}
}
handleInputChange(textbox, text) {
const caret = Utils.getCaretPosition(textbox);
const preText = text.substring(0, caret);
let mode = '';
let filter = '';
for (const [modeForPattern, pattern] of patterns) {
const result = pattern.exec(preText);
if (result) {
mode = modeForPattern;
filter = result[1];
break;
}
}
if (mode !== this.state.mode || filter !== this.state.filter) {
this.updateSuggestions(mode, filter);
}
this.setState({
mode,
filter,
show: mode || filter
});
}
handleKeyDown(e) {
if (!this.state.show || this.state.suggestions.length === 0) {
return;
}
if (e.which === KeyCodes.UP || e.which === KeyCodes.DOWN) {
e.preventDefault();
let selection = this.state.selection;
if (e.which === KeyCodes.UP) {
selection -= 1;
} else {
selection += 1;
}
if (selection >= 0 && selection < this.state.suggestions.length) {
this.setState({
selection
});
}
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) {
e.preventDefault();
this.completeSelectedWord();
}
}
completeSelectedWord() {
if (this.state.mode === 'channels') {
this.completeWord(this.state.suggestions[this.state.selection].name);
} else if (this.state.mode === 'users') {
this.completeWord(this.state.suggestions[this.state.selection].username);
}
}
completeWord(value) {
// add a space so that anything else typed doesn't interfere with the search flag
this.props.completeWord(this.state.filter, value + ' ');
this.setState({
show: false,
mode: '',
filter: '',
selection: 0
});
}
updateSuggestions(mode, filter) {
let suggestions = [];
if (mode === 'channels') {
let channels = ChannelStore.getAll();
if (filter) {
channels = channels.filter((channel) => channel.name.startsWith(filter));
}
channels.sort((a, b) => a.name.localeCompare(b.name));
suggestions = channels;
} else if (mode === 'users') {
let users = UserStore.getActiveOnlyProfileList();
if (filter) {
users = users.filter((user) => user.username.startsWith(filter));
}
users.sort((a, b) => a.username.localeCompare(b.username));
suggestions = users;
}
let selection = this.state.selection;
// keep the same user/channel selected if it's still visible as a suggestion
if (selection > 0 && this.state.suggestions.length > 0) {
// we can't just use indexOf to find if the selection is still in the list since they are different javascript objects
const currentSelectionId = this.state.suggestions[selection].id;
let found = false;
for (let i = 0; i < suggestions.length; i++) {
if (suggestions[i].id === currentSelectionId) {
selection = i;
found = true;
break;
}
}
if (!found) {
selection = 0;
}
} else {
selection = 0;
}
this.setState({
suggestions,
selection
});
}
render() {
if (!this.state.show || this.state.suggestions.length === 0) {
return null;
}
let suggestions = [];
if (this.state.mode === 'channels') {
suggestions = this.state.suggestions.map((channel, index) => {
let className = 'search-autocomplete__channel';
if (this.state.selection === index) {
className += ' selected';
}
return (
<div
key={channel.name}
ref={channel.name}
onClick={this.handleClick.bind(this, channel.name)}
className={className}
>
{channel.name}
</div>
);
});
} else if (this.state.mode === 'users') {
suggestions = this.state.suggestions.map((user, index) => {
let className = 'search-autocomplete__user';
if (this.state.selection === index) {
className += ' selected';
}
return (
<div
key={user.username}
ref={user.username}
onClick={this.handleClick.bind(this, user.username)}
className={className}
>
<img
className='profile-img'
src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
/>
{user.username}
</div>
);
});
}
return (
<div
ref='container'
className='search-autocomplete'
>
{suggestions}
</div>
);
}
}
SearchAutocomplete.propTypes = {
completeWord: React.PropTypes.func.isRequired
};

View File

@@ -9,6 +9,7 @@ var utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var Popover = ReactBootstrap.Popover;
var SearchAutocomplete = require('./search_autocomplete.jsx');
export default class SearchBar extends React.Component {
constructor() {
@@ -16,11 +17,13 @@ export default class SearchBar extends React.Component {
this.mounted = false;
this.onListenerChange = this.onListenerChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleUserFocus = this.handleUserFocus.bind(this);
this.handleUserBlur = this.handleUserBlur.bind(this);
this.performSearch = this.performSearch.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.completeWord = this.completeWord.bind(this);
const state = this.getSearchTermStateFromStores();
state.focused = false;
@@ -74,11 +77,18 @@ export default class SearchBar extends React.Component {
results: null
});
}
handleKeyDown(e) {
if (this.refs.autocomplete) {
this.refs.autocomplete.handleKeyDown(e);
}
}
handleUserInput(e) {
var term = e.target.value;
PostStore.storeSearchTerm(term);
PostStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
this.refs.autocomplete.handleInputChange(e.target, term);
}
handleMouseInput(e) {
e.preventDefault();
@@ -97,7 +107,7 @@ export default class SearchBar extends React.Component {
this.setState({isSearching: true});
client.search(
terms,
function success(data) {
(data) => {
this.setState({isSearching: false});
if (utils.isMobile()) {
ReactDOM.findDOMNode(this.refs.search).value = '';
@@ -108,11 +118,11 @@ export default class SearchBar extends React.Component {
results: data,
is_mention_search: isMentionSearch
});
}.bind(this),
function error(err) {
},
(err) => {
this.setState({isSearching: false});
AsyncClient.dispatchError(err, 'search');
}.bind(this)
}
);
}
}
@@ -120,6 +130,24 @@ export default class SearchBar extends React.Component {
e.preventDefault();
this.performSearch(this.state.searchTerm.trim());
}
completeWord(partialWord, word) {
const textbox = ReactDOM.findDOMNode(this.refs.search);
let text = textbox.value;
const caret = utils.getCaretPosition(textbox);
const preText = text.substring(0, caret - partialWord.length);
const postText = text.substring(caret);
text = preText + word + postText;
textbox.value = text;
utils.setCaretPosition(textbox, preText.length + word.length);
PostStore.storeSearchTerm(text);
PostStore.emitSearchTermChange(false);
this.setState({searchTerm: text});
}
render() {
var isSearching = null;
if (this.state.isSearching) {
@@ -143,12 +171,13 @@ export default class SearchBar extends React.Component {
className='search__clear'
onClick={this.clearFocus}
>
Cancel
{'Cancel'}
</span>
<form
role='form'
className='search__form relative-div'
onSubmit={this.handleSubmit}
style={{overflow: 'visible'}}
>
<span className='glyphicon glyphicon-search sidebar__search-icon' />
<input
@@ -160,10 +189,16 @@ export default class SearchBar extends React.Component {
onFocus={this.handleUserFocus}
onBlur={this.handleUserBlur}
onChange={this.handleUserInput}
onKeyDown={this.handleKeyDown}
onMouseUp={this.handleMouseInput}
/>
{isSearching}
<SearchAutocomplete
ref='autocomplete'
completeWord={this.completeWord}
/>
<Popover
id='searchbar-help-popup'
placement='bottom'
className={helpClass}
>

View File

@@ -36,7 +36,7 @@ export default class SettingItemMax extends React.Component {
if (this.props.width === 'full') {
widthClass = 'col-sm-12';
} else {
widthClass = 'col-sm-9 col-sm-offset-3';
widthClass = 'col-sm-10 col-sm-offset-2';
}
return (

View File

@@ -2,6 +2,10 @@
// See License.txt for license information.
export default class SettingsSidebar extends React.Component {
componentDidUpdate() {
$('.settings-modal').find('.modal-body').scrollTop(0);
$('.settings-modal').find('.modal-body').perfectScrollbar('update');
}
constructor(props) {
super(props);

View File

@@ -19,6 +19,7 @@ export default class TeamSettingsModal extends React.Component {
componentDidMount() {
$('body').on('click', '.modal-back', function handleBackClick() {
$(this).closest('.modal-dialog').removeClass('display--content');
$(this).closest('.modal-dialog').find('.settings-table .nav li.active').removeClass('active');
});
$('body').on('click', '.modal-header .close', () => {
setTimeout(() => {

View File

@@ -15,11 +15,6 @@ export default class TeamSignupSendInvitesPage extends React.Component {
this.state = {
emailEnabled: global.window.mm_config.SendEmailNotifications === 'true'
};
if (!this.state.emailEnabled) {
this.props.state.wizard = 'username';
this.props.updateParent(this.props.state);
}
}
submitBack(e) {
e.preventDefault();

View File

@@ -54,7 +54,11 @@ export default class TeamSignupUrlPage extends React.Component {
if (data) {
this.setState({nameError: 'This URL is unavailable. Please try another.'});
} else {
this.props.state.wizard = 'send_invites';
if (global.window.mm_config.SendEmailNotifications === 'true') {
this.props.state.wizard = 'send_invites';
} else {
this.props.state.wizard = 'username';
}
this.props.state.team.type = 'O';
this.props.state.team.name = name;

View File

@@ -104,21 +104,19 @@ export default class TeamSignupWelcomePage extends React.Component {
return (
<div>
<p>
<img
className='signup-team-logo'
src='/static/images/logo.png'
/>
<h3 className='sub-heading'>Welcome to:</h3>
<h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1>
</p>
<img
className='signup-team-logo'
src='/static/images/logo.png'
/>
<h3 className='sub-heading'>Welcome to:</h3>
<h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1>
<p className='margin--less'>Let's set up your new team</p>
<p>
<div>
Please confirm your email address:<br />
<div className='inner__content'>
<div className='block--gray'>{this.props.state.team.email}</div>
</div>
</p>
</div>
<p className='margin--extra color--light'>
Your account will administer the new team site. <br />
You can add other administrators later.

View File

@@ -119,24 +119,23 @@ export default class ManageIncomingHooks extends React.Component {
hooks.push(
<div
key={hook.id}
className='font--small'
className='webhook__item'
>
<div className='padding-top x2 divider-light'></div>
<div className='padding-top x2'>
<strong>{'URL: '}</strong><span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
<div className='padding-top x2 webhook__url'>
<strong>{'URL: '}</strong>
<span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
</div>
<div className='padding-top'>
<strong>{'Channel: '}</strong>{c.display_name}
</div>
<div className='padding-top'>
<a
className={'text-danger'}
href='#'
onClick={this.removeHook.bind(this, hook.id)}
>
{'Remove'}
</a>
</div>
<a
className={'webhook__remove'}
href='#'
onClick={this.removeHook.bind(this, hook.id)}
>
<span aria-hidden='true'>{'×'}</span>
</a>
<div className='padding-top x2 divider-light'></div>
</div>
);
}
@@ -148,35 +147,38 @@ export default class ManageIncomingHooks extends React.Component {
} else if (hooks.length > 0) {
displayHooks = hooks;
} else {
displayHooks = <label>{': None'}</label>;
displayHooks = <div className='padding-top x2'>{'None'}</div>;
}
const existingHooks = (
<div className='padding-top x2'>
<div className='webhooks__container'>
<label className='control-label padding-top x2'>{'Existing incoming webhooks'}</label>
{displayHooks}
<div className='padding-top divider-light'></div>
<div className='webhooks__list'>
{displayHooks}
</div>
</div>
);
return (
<div key='addIncomingHook'>
{'Create webhook URLs for use in external integrations. Please see '}<a href='http://mattermost.org/webhooks'>{'http://mattermost.org/webhooks'}</a> {' to learn more.'}
<br/>
<br/>
<label className='control-label'>{'Add a new incoming webhook'}</label>
<div className='padding-top'>
<select
ref='channelName'
className='form-control'
value={this.state.channelId}
onChange={this.updateChannelId}
>
{options}
</select>
{serverError}
<div className='padding-top'>
<label className='control-label padding-top x2'>{'Add a new incoming webhook'}</label>
<div className='row padding-top'>
<div className='col-sm-10 padding-bottom'>
<select
ref='channelName'
className='form-control'
value={this.state.channelId}
onChange={this.updateChannelId}
>
{options}
</select>
{serverError}
</div>
<div className='col-sm-2 col-xs-4 no-padding--left padding-bottom'>
<a
className={'btn btn-sm btn-primary' + disableButton}
className={'btn form-control no-padding btn-sm btn-primary' + disableButton}
href='#'
onClick={this.addNewHook}
>

View File

@@ -6,6 +6,7 @@ var Constants = require('../../utils/constants.jsx');
var ChannelStore = require('../../stores/channel_store.jsx');
var LoadingScreen = require('../loading_screen.jsx');
export default class ManageOutgoingHooks extends React.Component {
constructor() {
super();
@@ -180,9 +181,8 @@ export default class ManageOutgoingHooks extends React.Component {
hooks.push(
<div
key={hook.id}
className='font--small'
className='webhook__item'
>
<div className='padding-top x2 divider-light'></div>
<div className='padding-top x2'>
<strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span>
</div>
@@ -199,15 +199,15 @@ export default class ManageOutgoingHooks extends React.Component {
>
{'Regen Token'}
</a>
<span>{' - '}</span>
<a
className='text-danger'
className='webhook__remove'
href='#'
onClick={this.removeHook.bind(this, hook.id)}
>
{'Remove'}
<span aria-hidden='true'>{'×'}</span>
</a>
</div>
<div className='padding-top x2 divider-light'></div>
</div>
);
});
@@ -218,13 +218,16 @@ export default class ManageOutgoingHooks extends React.Component {
} else if (hooks.length > 0) {
displayHooks = hooks;
} else {
displayHooks = <label>{': None'}</label>;
displayHooks = <div className='padding-top x2'>{'None'}</div>;
}
const existingHooks = (
<div className='padding-top x2'>
<div className='webhooks__container'>
<label className='control-label padding-top x2'>{'Existing outgoing webhooks'}</label>
{displayHooks}
<div className='padding-top divider-light'></div>
<div className='webhooks__list'>
{displayHooks}
</div>
</div>
);
@@ -234,41 +237,49 @@ export default class ManageOutgoingHooks extends React.Component {
<div key='addOutgoingHook'>
<label className='control-label'>{'Add a new outgoing webhook'}</label>
<div className='padding-top'>
<strong>{'Channel:'}</strong>
<select
ref='channelName'
className='form-control'
value={this.state.channelId}
onChange={this.updateChannelId}
>
{options}
</select>
<span>{'Only public channels can be used'}</span>
<br/>
<br/>
<strong>{'Trigger Words:'}</strong>
<input
ref='triggerWords'
className='form-control'
value={this.state.triggerWords}
onChange={this.updateTriggerWords}
placeholder='Optional if channel selected'
/>
<span>{'Comma separated words to trigger on'}</span>
<br/>
<br/>
<strong>{'Callback URLs:'}</strong>
<textarea
ref='callbackURLs'
className='form-control no-resize'
value={this.state.callbackURLs}
resize={false}
rows={3}
onChange={this.updateCallbackURLs}
/>
<span>{'New line separated URLs that will receive the HTTP POST event'}</span>
{serverError}
<div className='padding-top'>
<div>
<label className='control-label'>{'Channel'}</label>
<div className='padding-top'>
<select
ref='channelName'
className='form-control'
value={this.state.channelId}
onChange={this.updateChannelId}
>
{options}
</select>
</div>
<div className='padding-top'>{'Only public channels can be used'}</div>
</div>
<div className='padding-top x2'>
<label className='control-label'>{'Trigger Words:'}</label>
<div className='padding-top'>
<input
ref='triggerWords'
className='form-control'
value={this.state.triggerWords}
onChange={this.updateTriggerWords}
placeholder='Optional if channel selected'
/>
</div>
<div className='padding-top'>{'Comma separated words to trigger on'}</div>
</div>
<div className='padding-top x2'>
<label className='control-label'>{'Callback URLs:'}</label>
<div className='padding-top'>
<textarea
ref='callbackURLs'
className='form-control no-resize'
value={this.state.callbackURLs}
resize={false}
rows={3}
onChange={this.updateCallbackURLs}
/>
</div>
<div className='padding-top'>{'New line separated URLs that will receive the HTTP POST event'}</div>
{serverError}
</div>
<div className='padding-top padding-bottom'>
<a
className={'btn btn-sm btn-primary'}
href='#'

View File

@@ -43,7 +43,6 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMax
title='Incoming Webhooks'
width = 'full'
inputs={inputs}
updateSection={(e) => {
this.updateSection('');
@@ -55,7 +54,6 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMin
title='Incoming Webhooks'
width = 'full'
describe='Manage your incoming webhooks (Developer feature)'
updateSection={() => {
this.updateSection('incoming-hooks');

View File

@@ -74,9 +74,9 @@ class BrowserStoreClass {
var result = null;
try {
if (this.isLocalStorageSupported()) {
result = JSON.parse(getPrefix() + localStorage.getItem(name));
result = JSON.parse(localStorage.getItem(getPrefix() + name));
} else {
result = JSON.parse(getPrefix() + sessionStorage.getItem(name));
result = JSON.parse(sessionStorage.getItem(getPrefix() + name));
}
} catch (err) {
result = null;

View File

@@ -158,7 +158,7 @@ function handleNewPostEvent(msg) {
// Update channel state
if (ChannelStore.getCurrentId() === msg.channel_id) {
if (window.isActive) {
AsyncClient.updateLastViewedAt();
AsyncClient.updateLastViewedAt(true);
}
} else {
AsyncClient.getChannel(msg.channel_id);

View File

@@ -48,6 +48,7 @@ class UserStoreClass extends EventEmitter {
this.getProfilesUsernameMap = this.getProfilesUsernameMap.bind(this);
this.getProfiles = this.getProfiles.bind(this);
this.getActiveOnlyProfiles = this.getActiveOnlyProfiles.bind(this);
this.getActiveOnlyProfileList = this.getActiveOnlyProfileList.bind(this);
this.saveProfile = this.saveProfile.bind(this);
this.setSessions = this.setSessions.bind(this);
this.getSessions = this.getSessions.bind(this);
@@ -215,6 +216,19 @@ class UserStoreClass extends EventEmitter {
return active;
}
getActiveOnlyProfileList() {
const profileMap = this.getActiveOnlyProfiles();
const profiles = [];
for (const id in profileMap) {
if (profileMap.hasOwnProperty(id)) {
profiles.push(profileMap[id]);
}
}
return profiles;
}
saveProfile(profile) {
var ps = this.getProfiles();
ps[profile.id] = profile;

View File

@@ -152,14 +152,14 @@ export function getChannel(id) {
);
}
export function updateLastViewedAt() {
export function updateLastViewedAt(force) {
const channelId = ChannelStore.getCurrentId();
if (channelId === null) {
return;
}
if (isCallInProgress(`updateLastViewed${channelId}`)) {
if (isCallInProgress(`updateLastViewed${channelId}`) && !force) {
return;
}

View File

@@ -311,6 +311,7 @@ module.exports = {
RIGHT: 39,
BACKSPACE: 8,
ENTER: 13,
ESCAPE: 27
ESCAPE: 27,
SPACE: 32
}
};

View File

@@ -431,6 +431,7 @@ 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);
}
if (theme.sidebarHeaderBg) {
@@ -494,7 +495,7 @@ export function applyTheme(theme) {
changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .webhooks__container, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
changeCss('.popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
changeCss('.popover.left>.arrow', 'border-left-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
@@ -509,7 +510,7 @@ export function applyTheme(theme) {
changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
changeCss('.channel-intro .channel-intro__content, .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
@@ -521,7 +522,7 @@ export function applyTheme(theme) {
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
changeCss('.post.current--user:hover .post-body ', 'background: none;', 1);
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);

View File

@@ -94,8 +94,11 @@ a:focus, a:hover {
margin: 0;
}
.text-danger {
.text-danger, a.text-danger {
color: #E05F5D;
&:hover, &:focus {
color: #E05F5D;
}
}
.btn {
@@ -112,6 +115,10 @@ a:focus, a:hover {
&:focus {
@include box-shadow(none);
}
&.no-padding {
line-height: 32px;
padding: 0;
}
&.no-resize {
resize: none;
}

View File

@@ -569,9 +569,10 @@ body.ios {
}
.bot-indicator {
background-color: lightgrey;
border-radius:2px;
padding-left:2px;
padding-right:2px;
font-family:"Courier New"
font-family: inherit;
font-size: 11px;
padding: 2px 4px;
border-radius: 2px;
font-weight: 600;
margin: 0 0 0 -4px;
}

View File

@@ -284,6 +284,9 @@
height: auto;
}
}
.search-help-popover.visible {
visibility: hidden;
}
.modal-direct-channels {
.member-count {
float: none;
@@ -401,6 +404,9 @@
text-align: left;
}
}
.no-padding--left {
padding-left: 15px;
}
}
.settings-links {
display: none;
@@ -425,6 +431,11 @@
}
}
.settings-table {
.nav {
position: relative;
top: auto;
width: 100%;
}
.settings-content {
&.minimize-settings {
padding: 0;

View File

@@ -109,3 +109,43 @@
.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);
}
}

View File

@@ -1,5 +1,6 @@
@import "access-history";
@import "activity-log";
@import "webhooks";
.user-settings {
min-height:300px;
@@ -29,6 +30,9 @@
li {
list-style: none;
}
label {
font-weight: 600;
}
.settings-table {
display: table;
table-layout: fixed;
@@ -37,6 +41,11 @@
display: table-cell;
vertical-align: top;
}
.nav {
position: fixed;
top: 57px;
width: 180px;
}
.security-links {
margin-right: 20px;
.fa {
@@ -129,10 +138,6 @@
}
}
.font--small {
font-size: 13px;
}
.section-describe {
@include opacity(0.7);
white-space:pre;
@@ -161,14 +166,29 @@
.setting-list-item {
margin-top:7px;
.has-error {
color: #a94442;
}
.has-error {
color: #a94442;
}
.no-padding--left {
padding-left: 0;
}
.padding-top {
padding-top: 7px;
&.x2 {
padding-top: 14px;
}
.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;

View File

@@ -0,0 +1,31 @@
.webhooks__container {
background: rgba(black, 0.1);
border: 1px solid;
@include border-radius(3px);
padding: 0 13px 15px;
margin-top: 10px;
}
.webhook__item {
font-size: 13px;
position: relative;
&:last-child {
.divider-light:last-child {
display: none;
}
}
.webhook__remove {
position: absolute;
right: -7px;
top: 8px;
width: 30px;
height: 30px;
font-size: 22px;
font-weight: bold;
text-align: center;
text-decoration: none;
color: #E05F5D;
}
.webhook__url {
padding-right: 20px;
}
}