summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
Diffstat (limited to 'webapp')
-rw-r--r--webapp/actions/user_actions.jsx15
-rw-r--r--webapp/components/admin_console/admin_sidebar_header.jsx2
-rw-r--r--webapp/components/admin_console/logs.jsx3
-rw-r--r--webapp/components/logged_in.jsx25
-rw-r--r--webapp/components/popover_list_members.jsx2
-rw-r--r--webapp/components/post_view/components/post.jsx6
-rw-r--r--webapp/components/post_view/components/post_message_container.jsx2
-rw-r--r--webapp/components/post_view/components/post_message_view.jsx38
-rw-r--r--webapp/components/profile_popover.jsx35
-rw-r--r--webapp/components/rhs_comment.jsx2
-rw-r--r--webapp/components/rhs_root_post.jsx2
-rw-r--r--webapp/components/root.jsx27
-rw-r--r--webapp/components/search_results.jsx6
-rw-r--r--webapp/components/search_results_item.jsx4
-rw-r--r--webapp/components/setting_item_max.jsx4
-rw-r--r--webapp/components/setting_picture.jsx4
-rw-r--r--webapp/components/sidebar_header.jsx13
-rw-r--r--webapp/components/suggestion/at_mention_provider.jsx2
-rw-r--r--webapp/components/suggestion/search_user_provider.jsx2
-rw-r--r--webapp/components/suggestion/switch_channel_provider.jsx2
-rw-r--r--webapp/components/user_list_row.jsx2
-rw-r--r--webapp/components/user_profile.jsx2
-rw-r--r--webapp/components/user_settings/user_settings_security.jsx46
-rw-r--r--webapp/components/webrtc/components/webrtc_notification.jsx2
-rw-r--r--webapp/components/webrtc/webrtc_controller.jsx6
-rw-r--r--webapp/i18n/en.json35
-rw-r--r--webapp/sass/base/_typography.scss5
-rw-r--r--webapp/sass/layout/_post.scss39
-rw-r--r--webapp/sass/responsive/_mobile.scss5
-rw-r--r--webapp/sass/responsive/_tablet.scss2
-rw-r--r--webapp/stores/notification_store.jsx2
-rw-r--r--webapp/utils/channel_intro_messages.jsx2
-rw-r--r--webapp/utils/post_utils.jsx6
-rw-r--r--webapp/utils/text_formatting.jsx13
34 files changed, 258 insertions, 105 deletions
diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx
index 0f5fb0731..a5cef65d8 100644
--- a/webapp/actions/user_actions.jsx
+++ b/webapp/actions/user_actions.jsx
@@ -449,3 +449,18 @@ export function updateActive(userId, active, success, error) {
}
);
}
+
+export function updatePassword(userId, currentPassword, newPassword, success, error) {
+ Client.updatePassword(userId, currentPassword, newPassword,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
diff --git a/webapp/components/admin_console/admin_sidebar_header.jsx b/webapp/components/admin_console/admin_sidebar_header.jsx
index 86c2c6b0f..5725551bf 100644
--- a/webapp/components/admin_console/admin_sidebar_header.jsx
+++ b/webapp/components/admin_console/admin_sidebar_header.jsx
@@ -42,7 +42,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
- src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.update_at}
+ src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.last_picture_update}
/>
);
}
diff --git a/webapp/components/admin_console/logs.jsx b/webapp/components/admin_console/logs.jsx
index 8dc0c1e2e..5846c91db 100644
--- a/webapp/components/admin_console/logs.jsx
+++ b/webapp/components/admin_console/logs.jsx
@@ -24,12 +24,14 @@ export default class Logs extends React.Component {
componentDidMount() {
AdminStore.addLogChangeListener(this.onLogListenerChange);
AsyncClient.getLogs();
+ this.refs.logPanel.focus();
}
componentDidUpdate() {
// Scroll Down to get the latest logs
var node = this.refs.logPanel;
node.scrollTop = node.scrollHeight;
+ node.focus();
}
componentWillUnmount() {
@@ -100,6 +102,7 @@ export default class Logs extends React.Component {
/>
</button>
<div
+ tabIndex='-1'
ref='logPanel'
className='log__panel'
>
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index 841061d48..9282e74ca 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -4,7 +4,6 @@
import LoadingScreen from 'components/loading_screen.jsx';
import UserStore from 'stores/user_store.jsx';
-import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
@@ -29,30 +28,6 @@ export default class LoggedIn extends React.Component {
this.onUserChanged = this.onUserChanged.bind(this);
this.setupUser = this.setupUser.bind(this);
- // Force logout of all tabs if one tab is logged out
- $(window).bind('storage', (e) => {
- // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
- if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
- // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
- return;
- }
-
- console.log('detected logout from a different tab'); //eslint-disable-line no-console
- GlobalActions.emitUserLoggedOutEvent('/', false);
- }
-
- if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
- // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
- return;
- }
-
- console.log('detected login from a different tab'); //eslint-disable-line no-console
- location.reload();
- }
- });
-
// Because current CSS requires the root tag to have specific stuff
$('#root').attr('class', 'channel-view');
diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx
index 9cea3922a..a0b36cdc3 100644
--- a/webapp/components/popover_list_members.jsx
+++ b/webapp/components/popover_list_members.jsx
@@ -96,7 +96,7 @@ export default class PopoverListMembers extends React.Component {
key={'popover-member-' + i}
>
<ProfilePicture
- src={`${Client.getUsersRoute()}/${m.id}/image?time=${m.update_at}`}
+ src={`${Client.getUsersRoute()}/${m.id}/image?time=${m.last_picture_update}`}
width='26'
height='26'
/>
diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx
index f052ac4ae..03075a3fb 100644
--- a/webapp/components/post_view/components/post.jsx
+++ b/webapp/components/post_view/components/post.jsx
@@ -150,10 +150,10 @@ export default class Post extends React.Component {
}
let timestamp = 0;
- if (!this.props.user || this.props.user.update_at == null) {
- timestamp = this.props.currentUser.update_at;
+ if (!this.props.user || this.props.user.last_picture_update == null) {
+ timestamp = this.props.currentUser.last_picture_update;
} else {
- timestamp = this.props.user.update_at;
+ timestamp = this.props.user.last_picture_update;
}
let sameUserClass = '';
diff --git a/webapp/components/post_view/components/post_message_container.jsx b/webapp/components/post_view/components/post_message_container.jsx
index 2d17e74c4..4e27cd29a 100644
--- a/webapp/components/post_view/components/post_message_container.jsx
+++ b/webapp/components/post_view/components/post_message_container.jsx
@@ -89,7 +89,7 @@ export default class PostMessageContainer extends React.Component {
return (
<PostMessageView
options={this.props.options}
- message={this.props.post.message}
+ post={this.props.post}
emojis={this.state.emojis}
enableFormatting={this.state.enableFormatting}
mentionKeys={this.state.mentionKeys}
diff --git a/webapp/components/post_view/components/post_message_view.jsx b/webapp/components/post_view/components/post_message_view.jsx
index 24f96a8d9..eff791aec 100644
--- a/webapp/components/post_view/components/post_message_view.jsx
+++ b/webapp/components/post_view/components/post_message_view.jsx
@@ -2,14 +2,16 @@
// See License.txt for license information.
import React from 'react';
+import {FormattedMessage} from 'react-intl';
import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as PostUtils from 'utils/post_utils.jsx';
export default class PostMessageView extends React.Component {
static propTypes = {
options: React.PropTypes.object.isRequired,
- message: React.PropTypes.string.isRequired,
+ post: React.PropTypes.object.isRequired,
emojis: React.PropTypes.object.isRequired,
enableFormatting: React.PropTypes.bool.isRequired,
mentionKeys: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
@@ -23,7 +25,7 @@ export default class PostMessageView extends React.Component {
return true;
}
- if (nextProps.message !== this.props.message) {
+ if (nextProps.post.message !== this.props.post.message) {
return true;
}
@@ -47,9 +49,28 @@ export default class PostMessageView extends React.Component {
return false;
}
+ editedIndicator() {
+ return (
+ PostUtils.isEdited(this.props.post) ?
+ <span className='edited'>
+ <FormattedMessage
+ id='post_message_view.edited'
+ defaultMessage='(edited)'
+ />
+ </span> :
+ ''
+ );
+ }
+
render() {
if (!this.props.enableFormatting) {
- return <span>{this.props.message}</span>;
+ return (
+ <span>
+ {this.props.post.message}
+ &nbsp;
+ {this.editedIndicator()}
+ </span>
+ );
}
const options = Object.assign({}, this.props.options, {
@@ -62,10 +83,13 @@ export default class PostMessageView extends React.Component {
});
return (
- <span
- onClick={Utils.handleFormattedTextClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, options)}}
- />
+ <div>
+ <span
+ onClick={Utils.handleFormattedTextClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, options)}}
+ />
+ {this.editedIndicator()}
+ </div>
);
}
}
diff --git a/webapp/components/profile_popover.jsx b/webapp/components/profile_popover.jsx
index 7cb2f7261..22cf60004 100644
--- a/webapp/components/profile_popover.jsx
+++ b/webapp/components/profile_popover.jsx
@@ -83,6 +83,9 @@ export default class ProfilePopover extends React.Component {
openDirectChannelToUser(
user,
(channel) => {
+ if (Utils.isMobile()) {
+ GlobalActions.emitCloseRightHandSide();
+ }
this.setState({loadingDMChannel: -1});
if (this.props.hide) {
this.props.hide();
@@ -185,34 +188,34 @@ export default class ProfilePopover extends React.Component {
const fullname = Utils.getFullName(this.props.user);
if (fullname) {
dataContent.push(
- <div
- data-toggle='tooltip'
- title={fullname}
- key='user-popover-fullname'
+ <OverlayTrigger
+ delayShow={Constants.WEBRTC_TIME_DELAY}
+ placement='top'
+ overlay={<Tooltip id='fullNameTooltip'>{fullname}</Tooltip>}
>
- <p
- className='text-nowrap'
+ <div
+ className='overflow--ellipsis text-nowrap padding-bottom'
>
{fullname}
- </p>
- </div>
+ </div>
+ </OverlayTrigger>
);
}
if (this.props.user.position) {
const position = this.props.user.position.substring(0, Constants.MAX_POSITION_LENGTH);
dataContent.push(
- <div
- data-toggle='tooltip'
- title={position}
- key='user-popover-position'
+ <OverlayTrigger
+ delayShow={Constants.WEBRTC_TIME_DELAY}
+ placement='top'
+ overlay={<Tooltip id='positionTooltip'>{position}</Tooltip>}
>
- <p
- className='text-nowrap'
+ <div
+ className='overflow--ellipsis text-nowrap padding-bottom'
>
{position}
- </p>
- </div>
+ </div>
+ </OverlayTrigger>
);
}
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index 8b7642fd8..f83574496 100644
--- a/webapp/components/rhs_comment.jsx
+++ b/webapp/components/rhs_comment.jsx
@@ -239,7 +239,7 @@ export default class RhsComment extends React.Component {
currentUserCss = 'current--user';
}
- var timestamp = this.props.currentUser.update_at;
+ var timestamp = this.props.currentUser.last_picture_update;
let status = this.props.status;
if (post.props && post.props.from_webhook === 'true') {
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index 95f5fc1ac..235ed8db7 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -99,7 +99,7 @@ export default class RhsRootPost extends React.Component {
var isOwner = this.props.currentUser.id === post.user_id;
var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
- var timestamp = user ? user.update_at : 0;
+ var timestamp = user ? user.last_picture_update : 0;
var channel = ChannelStore.get(post.channel_id);
const flagIcon = Constants.FLAG_ICON_SVG;
diff --git a/webapp/components/root.jsx b/webapp/components/root.jsx
index be50c7d48..465df5d79 100644
--- a/webapp/components/root.jsx
+++ b/webapp/components/root.jsx
@@ -8,11 +8,12 @@ import Client from 'client/web_client.jsx';
import {IntlProvider} from 'react-intl';
import React from 'react';
-
import FastClick from 'fastclick';
+import $ from 'jquery';
import {browserHistory} from 'react-router/es6';
import UserStore from 'stores/user_store.jsx';
+import BrowserStore from 'stores/browser_store.jsx';
export default class Root extends React.Component {
constructor(props) {
@@ -35,6 +36,30 @@ export default class Root extends React.Component {
}
/*eslint-enable */
+ // Force logout of all tabs if one tab is logged out
+ $(window).bind('storage', (e) => {
+ // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
+ if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
+ // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
+ if (BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
+ return;
+ }
+
+ console.log('detected logout from a different tab'); //eslint-disable-line no-console
+ GlobalActions.emitUserLoggedOutEvent('/', false);
+ }
+
+ if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
+ // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
+ if (BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
+ return;
+ }
+
+ console.log('detected login from a different tab'); //eslint-disable-line no-console
+ location.reload();
+ }
+ });
+
// Fastclick
FastClick.attach(document.body);
}
diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx
index a0245b7e4..86d1bac1d 100644
--- a/webapp/components/search_results.jsx
+++ b/webapp/components/search_results.jsx
@@ -124,6 +124,12 @@ export default class SearchResults extends React.Component {
window.removeEventListener('resize', this.handleResize);
}
+ componentDidUpdate(prevProps, prevState) {
+ if (this.state.searchTerm !== prevState.searchTerm) {
+ this.resize();
+ }
+ }
+
handleResize() {
this.setState({
windowWidth: Utils.windowWidth(),
diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx
index 76681959e..be62653c0 100644
--- a/webapp/components/search_results_item.jsx
+++ b/webapp/components/search_results_item.jsx
@@ -62,7 +62,7 @@ export default class SearchResultsItem extends React.Component {
render() {
let channelName = null;
const channel = this.props.channel;
- const timestamp = UserStore.getCurrentUser().update_at;
+ const timestamp = UserStore.getCurrentUser().last_picture_update;
const user = this.props.user || {};
const post = this.props.post;
const flagIcon = Constants.FLAG_ICON_SVG;
@@ -285,7 +285,7 @@ export default class SearchResultsItem extends React.Component {
</li>
{rhsControls}
</ul>
- <div className='search-item-snippet'>
+ <div className='search-item-snippet post__body'>
{message}
</div>
</div>
diff --git a/webapp/components/setting_item_max.jsx b/webapp/components/setting_item_max.jsx
index 904e6c8d1..5971ce584 100644
--- a/webapp/components/setting_item_max.jsx
+++ b/webapp/components/setting_item_max.jsx
@@ -49,7 +49,7 @@ export default class SettingItemMax extends React.Component {
submit = (
<input
type='submit'
- className='btn btn-sm btn-primary'
+ className='btn btn-sm btn-primary pull-right'
href='#'
onClick={this.props.submit}
value={Utils.localizeMessage('setting_item_max.save', 'Save')}
@@ -88,7 +88,7 @@ export default class SettingItemMax extends React.Component {
{clientError}
{submit}
<a
- className='btn btn-sm theme'
+ className='btn btn-sm pull-right'
href='#'
onClick={this.props.updateSection}
>
diff --git a/webapp/components/setting_picture.jsx b/webapp/components/setting_picture.jsx
index b74ee8eb7..d1ff60c6a 100644
--- a/webapp/components/setting_picture.jsx
+++ b/webapp/components/setting_picture.jsx
@@ -73,7 +73,7 @@ export default class SettingPicture extends React.Component {
/>
);
} else {
- var confirmButtonClass = 'btn btn-sm';
+ var confirmButtonClass = 'btn btn-sm pull-right';
if (this.props.submitActive) {
confirmButtonClass += ' btn-primary';
} else {
@@ -132,7 +132,7 @@ export default class SettingPicture extends React.Component {
</span>
{confirmButton}
<a
- className='btn btn-sm theme'
+ className='btn btn-sm theme pull-right'
href='#'
onClick={self.props.updateSection}
>
diff --git a/webapp/components/sidebar_header.jsx b/webapp/components/sidebar_header.jsx
index a5fbd2659..ded3b5d1a 100644
--- a/webapp/components/sidebar_header.jsx
+++ b/webapp/components/sidebar_header.jsx
@@ -10,7 +10,7 @@ import * as Utils from 'utils/utils.jsx';
import SidebarHeaderDropdown from './sidebar_header_dropdown.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
-import {Preferences, TutorialSteps} from 'utils/constants.jsx';
+import {Preferences, TutorialSteps, OVERLAY_TIME_DELAY} from 'utils/constants.jsx';
import {createMenuTip} from 'components/tutorial/tutorial_tip.jsx';
export default class SidebarHeader extends React.Component {
@@ -59,7 +59,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
- src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.update_at}
+ src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.last_picture_update}
/>
);
}
@@ -78,7 +78,7 @@ export default class SidebarHeader extends React.Component {
teamNameWithToolTip = (
<OverlayTrigger
trigger={['hover', 'focus']}
- delayShow={1000}
+ delayShow={OVERLAY_TIME_DELAY}
placement='bottom'
overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDescription}</Tooltip>}
ref='descriptionOverlay'
@@ -91,16 +91,13 @@ export default class SidebarHeader extends React.Component {
return (
<div className='team__header theme'>
{tutorialTip}
- <a
- href='#'
- onClick={this.toggleDropdown}
- >
+ <div>
{profilePicture}
<div className='header__info'>
<div className='user__name'>{'@' + me.username}</div>
{teamNameWithToolTip}
</div>
- </a>
+ </div>
<SidebarHeaderDropdown
ref='dropdown'
teamType={this.props.teamType}
diff --git a/webapp/components/suggestion/at_mention_provider.jsx b/webapp/components/suggestion/at_mention_provider.jsx
index 9263c6e50..f90266d95 100644
--- a/webapp/components/suggestion/at_mention_provider.jsx
+++ b/webapp/components/suggestion/at_mention_provider.jsx
@@ -70,7 +70,7 @@ class AtMentionSuggestion extends Suggestion {
icon = (
<img
className='mention__image'
- src={Client.getUsersRoute() + '/' + user.id + '/image?time=' + user.update_at}
+ src={Client.getUsersRoute() + '/' + user.id + '/image?time=' + user.last_picture_update}
/>
);
}
diff --git a/webapp/components/suggestion/search_user_provider.jsx b/webapp/components/suggestion/search_user_provider.jsx
index bff59ace8..70808ca26 100644
--- a/webapp/components/suggestion/search_user_provider.jsx
+++ b/webapp/components/suggestion/search_user_provider.jsx
@@ -41,7 +41,7 @@ class SearchUserSuggestion extends Suggestion {
<i className='fa fa fa-plus-square'/>
<img
className='profile-img rounded'
- src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.update_at}
+ src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.last_picture_update}
/>
<div className='mention--align'>
<span>
diff --git a/webapp/components/suggestion/switch_channel_provider.jsx b/webapp/components/suggestion/switch_channel_provider.jsx
index 301974b9a..0bc30a79f 100644
--- a/webapp/components/suggestion/switch_channel_provider.jsx
+++ b/webapp/components/suggestion/switch_channel_provider.jsx
@@ -35,7 +35,7 @@ class SwitchChannelSuggestion extends Suggestion {
<div className='pull-left'>
<img
className='mention__image'
- src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.update_at}
+ src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.last_picture_update}
/>
</div>
);
diff --git a/webapp/components/user_list_row.jsx b/webapp/components/user_list_row.jsx
index ff381a30b..3a13ccb66 100644
--- a/webapp/components/user_list_row.jsx
+++ b/webapp/components/user_list_row.jsx
@@ -64,7 +64,7 @@ export default function UserListRow({user, extraInfo, actions, actionProps, acti
className='more-modal__row'
>
<ProfilePicture
- src={`${Client.getUsersRoute()}/${user.id}/image?time=${user.update_at}`}
+ src={`${Client.getUsersRoute()}/${user.id}/image?time=${user.last_picture_update}`}
status={status}
width='32'
height='32'
diff --git a/webapp/components/user_profile.jsx b/webapp/components/user_profile.jsx
index d0267c0d8..d9bd5c378 100644
--- a/webapp/components/user_profile.jsx
+++ b/webapp/components/user_profile.jsx
@@ -56,7 +56,7 @@ export default class UserProfile extends React.Component {
let profileImg = '';
if (this.props.user) {
name = Utils.displayUsername(this.props.user.id);
- profileImg = Client.getUsersRoute() + '/' + this.props.user.id + '/image?time=' + this.props.user.update_at;
+ profileImg = Client.getUsersRoute() + '/' + this.props.user.id + '/image?time=' + this.props.user.last_picture_update;
}
if (this.props.overwriteName) {
diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx
index 3484b8183..e936f5b96 100644
--- a/webapp/components/user_settings/user_settings_security.jsx
+++ b/webapp/components/user_settings/user_settings_security.jsx
@@ -14,6 +14,8 @@ import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
+import {updatePassword} from 'actions/user_actions.jsx';
+
import $ from 'jquery';
import React from 'react';
import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl';
@@ -91,7 +93,7 @@ export default class SecurityTab extends React.Component {
return;
}
- Client.updatePassword(
+ updatePassword(
user.id,
currentPassword,
newPassword,
@@ -425,6 +427,34 @@ export default class SecurityTab extends React.Component {
</div>
</div>
);
+ } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.security.passwordGoogleCantUpdate'
+ defaultMessage='Login occurs through Google Apps. Password cannot be updated.'
+ />
+ </div>
+ </div>
+ );
+ } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.security.passwordOffice365CantUpdate'
+ defaultMessage='Login occurs through Office 365. Password cannot be updated.'
+ />
+ </div>
+ </div>
+ );
}
updateSectionStatus = function resetSection(e) {
@@ -502,6 +532,20 @@ export default class SecurityTab extends React.Component {
defaultMessage='Login done through SAML'
/>
);
+ } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.security.loginGoogle'
+ defaultMessage='Login done through Google Apps'
+ />
+ );
+ } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.security.loginOffice365'
+ defaultMessage='Login done through Office 365'
+ />
+ );
}
updateSectionStatus = function updateSection() {
diff --git a/webapp/components/webrtc/components/webrtc_notification.jsx b/webapp/components/webrtc/components/webrtc_notification.jsx
index 5456d6cb8..f69e731f8 100644
--- a/webapp/components/webrtc/components/webrtc_notification.jsx
+++ b/webapp/components/webrtc/components/webrtc_notification.jsx
@@ -197,7 +197,7 @@ export default class WebrtcNotification extends React.Component {
const user = this.state.userCalling;
if (user) {
const username = Utils.displayUsername(user.id);
- const profileImgSrc = Client.getUsersRoute() + '/' + user.id + '/image?time=' + (user.update_at || new Date().getTime());
+ const profileImgSrc = Client.getUsersRoute() + '/' + user.id + '/image?time=' + (user.last_picture_update || new Date().getTime());
const profileImg = (
<img
className='user-popover__image'
diff --git a/webapp/components/webrtc/webrtc_controller.jsx b/webapp/components/webrtc/webrtc_controller.jsx
index 94e5b3475..03953a19a 100644
--- a/webapp/components/webrtc/webrtc_controller.jsx
+++ b/webapp/components/webrtc/webrtc_controller.jsx
@@ -81,14 +81,14 @@ export default class WebrtcController extends React.Component {
const currentUser = UserStore.getCurrentUser();
const remoteUser = UserStore.getProfile(props.userId);
- const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.update_at;
+ const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.last_picture_update;
this.state = {
windowWidth: Utils.windowWidth(),
windowHeight: Utils.windowHeight(),
channelId: ChannelStore.getCurrentId(),
currentUser,
- currentUserImage: Client.getUsersRoute() + '/' + currentUser.id + '/image?time=' + currentUser.update_at,
+ currentUserImage: Client.getUsersRoute() + '/' + currentUser.id + '/image?time=' + currentUser.last_picture_update,
remoteUserImage,
localMediaLoaded: false,
isPaused: false,
@@ -130,7 +130,7 @@ export default class WebrtcController extends React.Component {
(nextProps.userId !== this.props.userId) ||
(nextProps.isCaller !== this.props.isCaller)) {
const remoteUser = UserStore.getProfile(nextProps.userId);
- const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.update_at;
+ const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.last_picture_update;
this.setState({
error: null,
remoteUserImage
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index c2ec7a4e9..37e2a645e 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -251,15 +251,16 @@
"admin.email.mhpnsHelp": "Download <a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target='_blank'>Mattermost iOS app</a> from iTunes. Download <a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target='_blank'>Mattermost Android app</a> from Google Play. Learn more about <a href=\"http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns\" target='_blank'>HPNS</a>.",
"admin.email.mtpns": "Use iOS and Android apps on iTunes and Google Play with TPNS",
"admin.email.mtpnsHelp": "Download <a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target='_blank'>Mattermost iOS app</a> from iTunes. Download <a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target='_blank'>Mattermost Android app</a> from Google Play. Learn more about <a href=\"http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns\" target='_blank'>TPNS</a>.",
- "admin.email.nofificationOrganizationExample": "Ex. \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
+ "admin.email.nofificationOrganizationExample": "E.g.: \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationDisplayDescription": "Display name on email account used when sending notification emails from Mattermost.",
- "admin.email.notificationDisplayExample": "Ex: \"Mattermost Notification\", \"System\", \"No-Reply\"",
+ "admin.email.notificationDisplayExample": "E.g.: \"Mattermost Notification\", \"System\", \"No-Reply\"",
"admin.email.notificationDisplayTitle": "Notification Display Name:",
"admin.email.notificationEmailDescription": "Email address displayed on email account used when sending notification emails from Mattermost.",
- "admin.email.notificationEmailExample": "Ex: \"mattermost@yourcompany.com\", \"admin@yourcompany.com\"",
+ "admin.email.notificationEmailExample": "E.g.: \"mattermost@yourcompany.com\", \"admin@yourcompany.com\"",
"admin.email.notificationEmailTitle": "Notification From Address:",
"admin.email.notificationOrganization": "Notification Footer Mailing Address:",
"admin.email.notificationOrganizationDescription": "Organization name and address displayed on email notifications from Mattermost, such as \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\". If the field is left empty, the organization name and address will not be displayed.",
+ "admin.email.notificationOrganizationExample": "E.g.: \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationsDescription": "Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).",
"admin.email.notificationsTitle": "Enable Email Notifications: ",
"admin.email.passwordSaltDescription": "32-character salt added to signing of password reset emails. Randomly generated on install. Click \"Regenerate\" to create new salt.",
@@ -278,16 +279,16 @@
"admin.email.requireVerificationTitle": "Require Email Verification: ",
"admin.email.selfPush": "Manually enter Push Notification Service location",
"admin.email.smtpPasswordDescription": " Obtain this credential from administrator setting up your email server.",
- "admin.email.smtpPasswordExample": "Ex: \"yourpassword\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
+ "admin.email.smtpPasswordExample": "E.g.: \"yourpassword\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.email.smtpPasswordTitle": "SMTP Server Password:",
"admin.email.smtpPortDescription": "Port of SMTP email server.",
- "admin.email.smtpPortExample": "Ex: \"25\", \"465\", \"587\"",
+ "admin.email.smtpPortExample": "E.g.: \"25\", \"465\", \"587\"",
"admin.email.smtpPortTitle": "SMTP Server Port:",
"admin.email.smtpServerDescription": "Location of SMTP email server.",
- "admin.email.smtpServerExample": "Ex: \"smtp.yourcompany.com\", \"email-smtp.us-east-1.amazonaws.com\"",
+ "admin.email.smtpServerExample": "E.g.: \"smtp.yourcompany.com\", \"email-smtp.us-east-1.amazonaws.com\"",
"admin.email.smtpServerTitle": "SMTP Server:",
"admin.email.smtpUsernameDescription": " Obtain this credential from administrator setting up your email server.",
- "admin.email.smtpUsernameExample": "Ex: \"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
+ "admin.email.smtpUsernameExample": "E.g.: \"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
"admin.email.smtpUsernameTitle": "SMTP Server Username:",
"admin.email.testing": "Testing...",
"admin.false": "false",
@@ -363,6 +364,7 @@
"admin.image.amazonS3BucketExample": "E.g.: \"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Amazon S3 Bucket:",
"admin.image.amazonS3EndpointDescription": "Hostname of your S3 Compatible Storage provider. Defaults to `s3.amazonaws.com`.",
+ "admin.image.amazonS3EndpointExample": "E.g.: \"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "Amazon S3 Endpoint:",
"admin.image.amazonS3IdDescription": "Obtain this credential from your Amazon EC2 administrator.",
"admin.image.amazonS3IdExample": "E.g.: \"AKIADTOVBGERKLCBV\"",
@@ -467,7 +469,7 @@
"admin.ldap.testSuccess": "AD/LDAP Test Successful",
"admin.ldap.uernameAttrDesc": "The attribute in the AD/LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.",
"admin.ldap.userFilterDisc": "(Optional) Enter an AD/LDAP Filter to use when searching for user objects. Only the users selected by the query will be able to access Mattermost. For Active Directory, the query to filter out disabled users is (&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).",
- "admin.ldap.userFilterEx": "Ex. \"(objectClass=user)\"",
+ "admin.ldap.userFilterEx": "E.g.: \"(objectClass=user)\"",
"admin.ldap.userFilterTitle": "User Filter:",
"admin.ldap.usernameAttrEx": "E.g.: \"sAMAccountName\"",
"admin.ldap.usernameAttrTitle": "Username Attribute:",
@@ -513,7 +515,7 @@
"admin.metrics.enableDescription": "When true, Mattermost will enable performance monitoring collection and profiling. Please see <a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>documentation</a> to learn more about configuring performance monitoring for Mattermost.",
"admin.metrics.enableTitle": "Enable Performance Monitoring:",
"admin.metrics.listenAddressDesc": "The address the server will listen on to expose performance metrics.",
- "admin.metrics.listenAddressEx": "Ex \":8067\"",
+ "admin.metrics.listenAddressEx": "E.g.: \":8067\"",
"admin.metrics.listenAddressTitle": "Listen Address:",
"admin.mfa.bannerDesc": "Multi-factor authentication is only available for accounts with LDAP and email login methods. If there are users on your system with other login methods, it is recommended you set up multi-factor authentication directly with the SSO or SAML provider.",
"admin.mfa.cluster": "High",
@@ -564,7 +566,7 @@
"admin.rate.enableLimiterTitle": "Enable Rate Limiting: ",
"admin.rate.httpHeaderDescription": "When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring NGINX set to \"X-Real-IP\", when configuring AmazonELB set to \"X-Forwarded-For\").",
"admin.rate.httpHeaderExample": "E.g.: \"X-Real-IP\", \"X-Forwarded-For\"",
- "admin.rate.httpHeaderTitle": "Vary rate limit by HTTP header",
+ "admin.rate.httpHeaderTitle": "Vary rate limit by HTTP header:",
"admin.rate.maxBurst": "Maximum Burst Size:",
"admin.rate.maxBurstDescription": "Maximum number of requests allowed beyond the per second query limit.",
"admin.rate.maxBurstExample": "E.g.: \"100\"",
@@ -831,10 +833,10 @@
"admin.team.dirDesc": "When true, teams that are configured to show in team directory will show on main page inplace of creating a new team.",
"admin.team.dirTitle": "Enable Team Directory: ",
"admin.team.maxChannelsDescription": "Maximum total number of channels per team, including both active and deleted channels.",
- "admin.team.maxChannelsExample": "Ex \"100\"",
+ "admin.team.maxChannelsExample": "E.g.: \"100\"",
"admin.team.maxChannelsTitle": "Max Channels Per Team:",
"admin.team.maxNotificationsPerChannelDescription": "Maximum total number of users in a channel before users typing messages, @all, @here, and @channel no longer send notifications because of performance.",
- "admin.team.maxNotificationsPerChannelExample": "Ex \"1000\"",
+ "admin.team.maxNotificationsPerChannelExample": "E.g.: \"1000\"",
"admin.team.maxNotificationsPerChannelTitle": "Max Notifications Per Channel:",
"admin.team.maxUsersDescription": "Maximum total number of users per team, including both active and inactive users.",
"admin.team.maxUsersExample": "E.g.: \"25\"",
@@ -901,13 +903,13 @@
"admin.webrtc.gatewayWebsocketUrlTitle": "Gateway WebSocket URL:",
"admin.webrtc.stunUriDescription": "Enter your STUN URI as stun:<your-stun-url>:<port>. STUN is a standardized network protocol to allow an end host to assist devices to access its public IP address if it is located behind a NAT.",
"admin.webrtc.stunUriExample": "E.g.: \"stun:webrtc.mattermost.com:5349\"",
- "admin.webrtc.stunUriTitle": "STUN URI",
+ "admin.webrtc.stunUriTitle": "STUN URI:",
"admin.webrtc.turnSharedKeyDescription": "Enter your TURN Server Shared Key. This is used to created dynamic passwords to establish the connection. Each password is valid for a short period of time.",
"admin.webrtc.turnSharedKeyExample": "E.g.: \"bXdkOWQxc3d0Ynk3emY5ZmsxZ3NtazRjaWg=\"",
"admin.webrtc.turnSharedKeyTitle": "TURN Shared Key:",
"admin.webrtc.turnUriDescription": "Enter your TURN URI as turn:<your-turn-url>:<port>. TURN is a standardized network protocol to allow an end host to assist devices to establish a connection by using a relay public IP address if it is located behind a symmetric NAT.",
"admin.webrtc.turnUriExample": "E.g.: \"turn:webrtc.mattermost.com:5349\"",
- "admin.webrtc.turnUriTitle": "TURN URI",
+ "admin.webrtc.turnUriTitle": "TURN URI:",
"admin.webrtc.turnUsernameDescription": "Enter your TURN Server Username.",
"admin.webrtc.turnUsernameExample": "E.g.: \"myusername\"",
"admin.webrtc.turnUsernameTitle": "TURN Username:",
@@ -1665,6 +1667,7 @@
"post_info.mobile.unflag": "Unflag",
"post_info.permalink": "Permalink",
"post_info.reply": "Reply",
+ "post_message_view.edited": "(edited)",
"posts_view.loadMore": "Load more messages",
"posts_view.newMsg": "New Messages",
"posts_view.newMsgBelow": "New {count, plural, one {message} other {messages}} below",
@@ -2130,6 +2133,8 @@
"user.settings.security.lastUpdated": "Last updated {date} at {time}",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "Login done through GitLab",
+ "user.settings.security.loginGoogle": "Login done through Google Apps",
+ "user.settings.security.loginOffice365": "Login done through Office 365",
"user.settings.security.loginLdap": "Login done through AD/LDAP",
"user.settings.security.loginSaml": "Login done through SAML",
"user.settings.security.logoutActiveSessions": "View and Logout of Active Sessions",
@@ -2159,6 +2164,8 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "Your password must contain at least {min} characters made up of at least one uppercase letter, at least one number, and at least one symbol (e.g. \"~!@#$%^&*()\").",
"user.settings.security.passwordErrorUppercaseSymbol": "Your password must contain at least {min} characters made up of at least one uppercase letter and at least one symbol (e.g. \"~!@#$%^&*()\").",
"user.settings.security.passwordGitlabCantUpdate": "Login occurs through GitLab. Password cannot be updated.",
+ "user.settings.security.passwordGoogleCantUpdate": "Login occurs through Google Apps. Password cannot be updated.",
+ "user.settings.security.passwordOffice365CantUpdate": "Login occurs through Office 365. Password cannot be updated.",
"user.settings.security.passwordLdapCantUpdate": "Login occurs through AD/LDAP. Password cannot be updated.",
"user.settings.security.passwordMatchError": "The new passwords you entered do not match.",
"user.settings.security.passwordMinLength": "Invalid minimum length, cannot show preview.",
diff --git a/webapp/sass/base/_typography.scss b/webapp/sass/base/_typography.scss
index f595e0ed9..1d3f1d052 100644
--- a/webapp/sass/base/_typography.scss
+++ b/webapp/sass/base/_typography.scss
@@ -26,6 +26,11 @@ body {
word-break: break-all;
}
+.overflow--ellipsis {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
.fa {
&.fa-margin--left {
margin-left: 2px;
diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss
index cd32ba55e..a04865bd8 100644
--- a/webapp/sass/layout/_post.scss
+++ b/webapp/sass/layout/_post.scss
@@ -560,9 +560,9 @@
}
blockquote {
- display: inline-block;
font-size: 1em;
margin-left: 0;
+ margin-top: 1.3em;
padding: 3px 0 0 25px;
vertical-align: top;
@@ -572,6 +572,11 @@
top: 2px;
}
}
+ .search-item-snippet {
+ blockquote {
+ margin-top: 0;
+ }
+ }
.markdown__heading {
clear: both;
@@ -598,7 +603,15 @@
}
p + p {
- margin-top: 1em;
+ margin: 1em 0;
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+ }
+ span {
+ > p:first-child {
+ margin-bottom: 1em;
+ }
}
ol,
@@ -978,12 +991,24 @@
width: 100%;
word-wrap: break-word;
- p {
+ div {
margin: 0 0 .4em;
}
p + p {
- margin-top: 1.4em;
+ margin: 1.4em 0;
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+ }
+
+ span {
+ > p:last-child {
+ display: inline;
+ }
+ > p:first-child {
+ margin-bottom: 1.4em;
+ }
}
li {
@@ -1063,6 +1088,12 @@
color: $white;
}
}
+
+ span.edited {
+ color: #A3A3A3;
+ font-size: 0.87em;
+ opacity: 0.6;
+ }
}
.post__link {
diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss
index 11b6312c9..5ade6046e 100644
--- a/webapp/sass/responsive/_mobile.scss
+++ b/webapp/sass/responsive/_mobile.scss
@@ -219,6 +219,9 @@
}
}
}
+ blockquote {
+ margin-top: 0;
+ }
}
&.same--root {
@@ -1077,7 +1080,7 @@
}
.post-create-footer {
- padding: 1em 0;
+ padding: 0 45px 0 45px;
.control-label {
margin: .5em 0;
diff --git a/webapp/sass/responsive/_tablet.scss b/webapp/sass/responsive/_tablet.scss
index c9bc1b06b..ac8b50961 100644
--- a/webapp/sass/responsive/_tablet.scss
+++ b/webapp/sass/responsive/_tablet.scss
@@ -26,7 +26,7 @@
}
.post-create-footer {
- padding: 0 1em;
+ padding: 0 45px 0 45px;
.msg-typing {
display: none;
diff --git a/webapp/stores/notification_store.jsx b/webapp/stores/notification_store.jsx
index 878ac3c9d..4c89fe480 100644
--- a/webapp/stores/notification_store.jsx
+++ b/webapp/stores/notification_store.jsx
@@ -111,7 +111,7 @@ class NotificationStoreClass extends EventEmitter {
// the window itself is not active
const activeChannel = ChannelStore.getCurrent();
const channelId = channel ? channel.id : null;
- const notify = activeChannel.id !== channelId || !this.inFocus;
+ const notify = (activeChannel && activeChannel.id !== channelId) || !this.inFocus;
if (notify) {
Utils.notifyMe(title, body, channel, teamId, duration, !sound);
diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx
index 0c011734a..991bf54e8 100644
--- a/webapp/utils/channel_intro_messages.jsx
+++ b/webapp/utils/channel_intro_messages.jsx
@@ -48,7 +48,7 @@ export function createDMIntroMessage(channel, centeredIntro) {
<div className={'channel-intro ' + centeredIntro}>
<div className='post-profile-img__container channel-intro-img'>
<ProfilePicture
- src={Client.getUsersRoute() + '/' + teammate.id + '/image?time=' + teammate.update_at}
+ src={Client.getUsersRoute() + '/' + teammate.id + '/image?time=' + teammate.last_picture_update}
width='50'
height='50'
user={teammate}
diff --git a/webapp/utils/post_utils.jsx b/webapp/utils/post_utils.jsx
index 4bba784cb..88021c2a5 100644
--- a/webapp/utils/post_utils.jsx
+++ b/webapp/utils/post_utils.jsx
@@ -15,6 +15,10 @@ export function isComment(post) {
return false;
}
+export function isEdited(post) {
+ return post.edit_at > 0;
+}
+
export function getProfilePicSrcForPost(post, timestamp) {
let src = Client.getUsersRoute() + '/' + post.user_id + '/image?time=' + timestamp;
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
@@ -28,4 +32,4 @@ export function getProfilePicSrcForPost(post, timestamp) {
}
return src;
-} \ No newline at end of file
+}
diff --git a/webapp/utils/text_formatting.jsx b/webapp/utils/text_formatting.jsx
index 9f983a1ee..171226558 100644
--- a/webapp/utils/text_formatting.jsx
+++ b/webapp/utils/text_formatting.jsx
@@ -395,7 +395,13 @@ function parseSearchTerms(searchTerm) {
}
// remove punctuation from each term
- terms = terms.map((term) => term.replace(puncStart, '').replace(puncEnd, ''));
+ terms = terms.map((term) => {
+ term.replace(puncStart, '');
+ if (term.charAt(term.length - 1) !== '*') {
+ term.replace(puncEnd, '');
+ }
+ return term;
+ });
return terms;
}
@@ -452,6 +458,11 @@ export function highlightSearchTerms(text, tokens, searchPatterns) {
output = output.replace(alias, newAlias);
}
+
+ // The pattern regexes are global, so calling pattern.test() above alters their
+ // state. Reset lastIndex to 0 between calls to test() to ensure it returns the
+ // same result every time it is called with the same value of token.originalText.
+ pattern.lastIndex = 0;
}
// the new tokens are stashed in a separate map since we can't add objects to a map during iteration