summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/install/Production-Ubuntu.md11
-rw-r--r--doc/install/Upgrade-Guide.md21
-rw-r--r--web/react/components/create_comment.jsx2
-rw-r--r--web/react/components/create_post.jsx2
-rw-r--r--web/react/components/msg_typing.jsx55
-rw-r--r--web/react/components/sidebar.jsx64
-rw-r--r--web/react/utils/constants.jsx1
7 files changed, 117 insertions, 39 deletions
diff --git a/doc/install/Production-Ubuntu.md b/doc/install/Production-Ubuntu.md
index 836af3995..2e02cca38 100644
--- a/doc/install/Production-Ubuntu.md
+++ b/doc/install/Production-Ubuntu.md
@@ -119,7 +119,7 @@ exec bin/platform
## Set up Nginx with SSL (Recommended)
1. You will need a SSL cert from a certificate authority.
-1. For simplicity we will generate a test certificate.
+2. For simplicity we will generate a test certificate.
* ``` mkdir ~/cert```
* ``` cd ~/cert```
* ``` sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout mattermost.key -out mattermost.crt```
@@ -133,8 +133,8 @@ exec bin/platform
Common Name (e.g. server FQDN or YOUR name) []:mattermost.example.com
Email Address []:admin@mattermost.example.com
```
-1. Modify the file at `/etc/nginx/sites-available/mattermost` and add the following lines
- *
+3. Run `openssl dhparam -out dhparam.pem 4096` (it will take some time).
+4. Modify the file at `/etc/nginx/sites-available/mattermost` and add the following lines:
```
server {
listen 80;
@@ -149,9 +149,10 @@ exec bin/platform
ssl on;
ssl_certificate /home/ubuntu/cert/mattermost.crt;
ssl_certificate_key /home/ubuntu/cert/mattermost.key;
+ ssl_dhparam /home/ubuntu/cert/dhparam.pem;
ssl_session_timeout 5m;
- ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
- ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
ssl_prefer_server_ciphers on;
# add to location / above
diff --git a/doc/install/Upgrade-Guide.md b/doc/install/Upgrade-Guide.md
index cecd45353..fc3a0711f 100644
--- a/doc/install/Upgrade-Guide.md
+++ b/doc/install/Upgrade-Guide.md
@@ -1,10 +1,23 @@
# Mattermost Upgrade Guide
-### Upgrading Mattermost v0.7 to v1.1.1
+### Upgrading Mattermost in GitLab 8.0 to GitLab 8.1 with omnibus
-_Note: [Mattermost v1.1.1](https://github.com/mattermost/platform/releases/tag/v1.1.1) is a special release of Mattermost v1.1 that upgrades the database to Mattermost v1.1 from EITHER Mattermost v0.7 or Mattermost v1.0. The following instructions are for upgrading from Mattermost v0.7 to v1.1.1 and skipping the upgrade to Mattermost v1.0._
+Mattermost 0.7.1-beta in GitLab 8.0 was a pre-release of Mattermost and Mattermost v1.1.1 in GitLab 8.1 was [updated significantly](https://github.com/mattermost/platform/blob/master/CHANGELOG.md#configjson-changes-from-v07-to-v10) to get to a stable, forwards-compatible platform for Mattermost.
-If you've manually changed Mattermost v0.7 configuration by updating the `config.json` file, you'll need to port those changes to Mattermost v1.1.1:
+The Mattermost team didn't think it made sense for GitLab omnibus to attempt an automated re-configuration of Mattermost (since 0.7.1-beta was a pre-release) given the scale of change, so we're providing instructions for GitLab users who have customized their Mattermost deployments in 8.0 to move to 8.1:
+
+1. Follow the [Upgrading Mattermost v0.7.1-beta to v1.1.1 instructions](https://github.com/mattermost/platform/blob/master/doc/install/Upgrade-Guide.md#upgrading-mattermost-v071-beta-to-v111) below to identify the settings in Mattermost's `config.json` file that differ from defaults and need to be updated from GitLab 8.0 to 8.1.
+2. Upgrade to GitLab 8.1 using omnibus, and allowing it overwrite `config.json` to the new Mattermost v1.1.1 format
+3. Manually update `config.json` to new settings identified in Step 1.
+
+Optionally, you can use the new [System Console user interface](https://github.com/mattermost/platform/blob/master/doc/install/Configuration-Settings.md) to make changes to your new `config.json` file.
+
+
+### Upgrading Mattermost v0.7.1-beta to v1.1.1
+
+_Note: [Mattermost v1.1.1](https://github.com/mattermost/platform/releases/tag/v1.1.1) is a special release of Mattermost v1.1 that upgrades the database to Mattermost v1.1 from EITHER Mattermost v0.7 or Mattermost v1.0. The following instructions are for upgrading from Mattermost v0.7.1-beta to v1.1.1 and skipping the upgrade to Mattermost v1.0._
+
+If you've manually changed Mattermost v0.7.1-beta configuration by updating the `config.json` file, you'll need to port those changes to Mattermost v1.1.1:
1. Go to the `config.json` file that you manually updated and note any differences from the [default `config.json` file in Mattermost 0.7](https://github.com/mattermost/platform/blob/v0.7.0/config/config.json).
@@ -16,3 +29,5 @@ Optionally, you can use the new [System Console user interface](https://github.c
+
+
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 435c7d542..18936e808 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -147,7 +147,7 @@ export default class CreateComment extends React.Component {
}
const t = Date.now();
- if ((t - this.lastTime) > 5000) {
+ if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}});
this.lastTime = t;
}
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 055be112d..b74f1871c 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -208,7 +208,7 @@ export default class CreatePost extends React.Component {
}
const t = Date.now();
- if ((t - this.lastTime) > 5000) {
+ if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}});
this.lastTime = t;
}
diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx
index 1bd23c55c..ccf8a2445 100644
--- a/web/react/components/msg_typing.jsx
+++ b/web/react/components/msg_typing.jsx
@@ -11,11 +11,11 @@ export default class MsgTyping extends React.Component {
constructor(props) {
super(props);
- this.timer = null;
- this.lastTime = 0;
-
this.onChange = this.onChange.bind(this);
+ this.updateTypingText = this.updateTypingText.bind(this);
+ this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this);
+ this.typingUsers = {};
this.state = {
text: ''
};
@@ -27,7 +27,7 @@ export default class MsgTyping extends React.Component {
componentWillReceiveProps(newProps) {
if (this.props.channelId !== newProps.channelId) {
- this.setState({text: ''});
+ this.updateTypingText();
}
}
@@ -36,28 +36,51 @@ export default class MsgTyping extends React.Component {
}
onChange(msg) {
+ let username = 'Someone';
if (msg.action === SocketEvents.TYPING &&
this.props.channelId === msg.channel_id &&
this.props.parentId === msg.props.parent_id) {
- this.lastTime = new Date().getTime();
-
- var username = 'Someone';
if (UserStore.hasProfile(msg.user_id)) {
username = UserStore.getProfile(msg.user_id).username;
}
- this.setState({text: username + ' is typing...'});
-
- if (!this.timer) {
- this.timer = setInterval(function myTimer() {
- if ((new Date().getTime() - this.lastTime) > 8000) {
- this.setState({text: ''});
- }
- }.bind(this), 3000);
+ if (this.typingUsers[username]) {
+ clearTimeout(this.typingUsers[username]);
}
+
+ this.typingUsers[username] = setTimeout(function myTimer(user) {
+ delete this.typingUsers[user];
+ this.updateTypingText();
+ }.bind(this, username), Constants.UPDATE_TYPING_MS);
+
+ this.updateTypingText();
} else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) {
- this.setState({text: ''});
+ if (UserStore.hasProfile(msg.user_id)) {
+ username = UserStore.getProfile(msg.user_id).username;
+ }
+ clearTimeout(this.typingUsers[username]);
+ delete this.typingUsers[username];
+ this.updateTypingText();
+ }
+ }
+
+ updateTypingText() {
+ const users = Object.keys(this.typingUsers);
+ let text = '';
+ switch (users.length) {
+ case 0:
+ text = '';
+ break;
+ case 1:
+ text = users[0] + ' is typing...';
+ break;
+ default:
+ const last = users.pop();
+ text = users.join(', ') + ' and ' + last + ' are typing...';
+ break;
}
+
+ this.setState({text});
}
render() {
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index ed2c84057..5cb6d168b 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -40,6 +40,9 @@ export default class Sidebar extends React.Component {
this.hideMoreDirectChannelsModal = this.hideMoreDirectChannelsModal.bind(this);
this.createChannelElement = this.createChannelElement.bind(this);
+ this.updateTitle = this.updateTitle.bind(this);
+ this.setUnreadCountPerChannel = this.setUnreadCountPerChannel.bind(this);
+ this.getUnreadCount = this.getUnreadCount.bind(this);
this.isLeaving = new Map();
@@ -48,8 +51,45 @@ export default class Sidebar extends React.Component {
state.showDirectChannelsModal = false;
state.loadingDMChannel = -1;
state.windowWidth = Utils.windowWidth();
-
this.state = state;
+
+ this.unreadCountPerChannel = {};
+ this.setUnreadCountPerChannel();
+ }
+ setUnreadCountPerChannel() {
+ const channels = ChannelStore.getAll();
+ const members = ChannelStore.getAllMembers();
+ const channelUnreadCounts = {};
+
+ channels.forEach((ch) => {
+ const chMember = members[ch.id];
+ let chMentionCount = chMember.mention_count;
+ let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount;
+
+ if (ch.type === 'D') {
+ chMentionCount = chUnreadCount;
+ chUnreadCount = 0;
+ }
+
+ channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount};
+ });
+
+ this.unreadCountPerChannel = channelUnreadCounts;
+ }
+ getUnreadCount(channelId) {
+ let mentions = 0;
+ let msgs = 0;
+
+ if (channelId) {
+ return this.unreadCountPerChannel[channelId] ? this.unreadCountPerChannel[channelId] : {msgs, mentions};
+ }
+
+ Object.keys(this.unreadCountPerChannel).forEach((chId) => {
+ msgs += this.unreadCountPerChannel[chId].msgs;
+ mentions += this.unreadCountPerChannel[chId].mentions;
+ });
+
+ return {msgs, mentions};
}
getStateFromStores() {
const members = ChannelStore.getAllMembers();
@@ -192,7 +232,10 @@ export default class Sidebar extends React.Component {
currentChannelName = Utils.getDirectTeammate(channel.id).username;
}
- document.title = currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName;
+ const unread = this.getUnreadCount();
+ const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : '';
+ const unreadTitle = unread.msgs > 0 ? '* ' : '';
+ document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName;
}
}
onScroll() {
@@ -273,6 +316,7 @@ export default class Sidebar extends React.Component {
var members = this.state.members;
var activeId = this.state.activeId;
var channelMember = members[channel.id];
+ var unreadCount = this.getUnreadCount(channel.id);
var msgCount;
var linkClass = '';
@@ -284,7 +328,7 @@ export default class Sidebar extends React.Component {
var unread = false;
if (channelMember) {
- msgCount = channel.total_msg_count - channelMember.msg_count;
+ msgCount = unreadCount.msgs + unreadCount.mentions;
unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0;
}
@@ -301,16 +345,8 @@ export default class Sidebar extends React.Component {
var badge = null;
if (channelMember) {
- if (channel.type === 'D') {
- // direct message channels show badges for any number of unread posts
- msgCount = channel.total_msg_count - channelMember.msg_count;
- if (msgCount > 0) {
- badge = <span className='badge pull-right small'>{msgCount}</span>;
- this.badgesActive = true;
- }
- } else if (channelMember.mention_count > 0) {
- // public and private channels only show badges for mentions
- badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>;
+ if (unreadCount.mentions) {
+ badge = <span className='badge pull-right small'>{unreadCount.mentions}</span>;
this.badgesActive = true;
}
} else if (this.state.loadingDMChannel === index && channel.type === 'D') {
@@ -434,6 +470,8 @@ export default class Sidebar extends React.Component {
render() {
this.badgesActive = false;
+ this.setUnreadCountPerChannel();
+
// keep track of the first and last unread channels so we can use them to set the unread indicators
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index f31bf6740..0e89b9470 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -133,6 +133,7 @@ module.exports = {
OFFLINE_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 fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>",
MENU_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>",
COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>",
+ UPDATE_TYPING_MS: 5000,
THEMES: {
default: {
type: 'Mattermost',