summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/post.go16
-rw-r--r--config/config.json5
-rw-r--r--doc/developer/Setup.md26
-rw-r--r--doc/process/overview.md2
-rw-r--r--docker/dev/config_docker.json5
-rw-r--r--docker/local/config_docker.json5
-rw-r--r--mattermost.go26
-rw-r--r--model/config.go18
-rw-r--r--model/push_notification.go45
-rw-r--r--model/push_notification_test.go19
-rw-r--r--utils/apns.go37
-rw-r--r--utils/config.go1
-rw-r--r--web/react/components/admin_console/email_settings.jsx67
-rw-r--r--web/react/components/get_link_modal.jsx2
-rw-r--r--web/react/components/member_list_item.jsx2
-rw-r--r--web/react/components/member_list_team_item.jsx2
-rw-r--r--web/react/components/popover_list_members.jsx46
-rw-r--r--web/react/components/post.jsx7
-rw-r--r--web/react/components/posts_view.jsx16
-rw-r--r--web/react/components/register_app_modal.jsx192
-rw-r--r--web/react/components/sidebar.jsx2
-rw-r--r--web/react/components/user_profile.jsx6
-rw-r--r--web/react/components/user_settings/user_settings_developer.jsx12
-rw-r--r--web/react/components/view_image.jsx3
-rw-r--r--web/react/dispatcher/event_helpers.jsx7
-rw-r--r--web/react/stores/modal_store.jsx1
-rw-r--r--web/react/utils/constants.jsx4
-rw-r--r--web/react/utils/text_formatting.jsx2
-rw-r--r--web/react/utils/utils.jsx21
-rw-r--r--web/sass-files/sass/partials/_responsive.scss9
-rw-r--r--web/sass-files/sass/partials/_search.scss1
31 files changed, 400 insertions, 207 deletions
diff --git a/api/post.go b/api/post.go
index 88d0127d3..81cc9a1c6 100644
--- a/api/post.go
+++ b/api/post.go
@@ -536,7 +536,7 @@ func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team,
l4g.Error("Failed to send mention email successfully email=%v err=%v", profileMap[id].Email, err)
}
- if len(utils.Cfg.EmailSettings.ApplePushServer) > 0 {
+ if *utils.Cfg.EmailSettings.SendPushNotifications {
sessionChan := Srv.Store.Session().GetSessions(id)
if result := <-sessionChan; result.Err != nil {
l4g.Error("Failed to retrieve sessions in notifications id=%v, err=%v", id, result.Err)
@@ -548,7 +548,19 @@ func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team,
if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" && strings.HasPrefix(session.DeviceId, "apple:") {
alreadySeen[session.DeviceId] = session.DeviceId
- utils.SendAppleNotifyAndForget(strings.TrimPrefix(session.DeviceId, "apple:"), subjectPage.Render(), 1)
+ msg := model.PushNotification{}
+ msg.Platform = model.PUSH_NOTIFY_APPLE
+ msg.Message = subjectPage.Render()
+ msg.Badge = 1
+ msg.DeviceId = strings.TrimPrefix(session.DeviceId, "apple:")
+ msg.ServerId = utils.CfgDiagnosticId
+
+ httpClient := http.Client{}
+ request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson()))
+
+ if _, err := httpClient.Do(request); err != nil {
+ l4g.Error("Failed to send push notificationid=%v, err=%v", id, err)
+ }
}
}
}
diff --git a/config/config.json b/config/config.json
index 932bed8a2..999ea8a83 100644
--- a/config/config.json
+++ b/config/config.json
@@ -68,9 +68,8 @@
"ConnectionSecurity": "",
"InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
- "ApplePushServer": "",
- "ApplePushCertPublic": "",
- "ApplePushCertPrivate": ""
+ "SendPushNotifications": true,
+ "PushNotificationServer": "https://push.mattermost.com"
},
"RateLimitSettings": {
"EnableRateLimiter": true,
diff --git a/doc/developer/Setup.md b/doc/developer/Setup.md
index 882aac530..24e7d6a90 100644
--- a/doc/developer/Setup.md
+++ b/doc/developer/Setup.md
@@ -11,7 +11,10 @@ Developer Machine Setup
`docker-machine ip dev`
3. Add a line to your /etc/hosts that goes `<Docker IP> dockerhost`
4. Run `docker-machine env dev` and copy the export statements to your ~/.bash_profile
-2. Download Go 1.5.1 from http://golang.org/dl/
+2. Download Go 1.5.1 and Node.js using Homebrew
+ 1. Download Homebrew from http://brew.sh/
+ 2. `brew install go`
+ 3. `brew install node`
3. Set up your Go workspace
1. `mkdir ~/go`
2. Add the following to your ~/.bash_profile
@@ -21,20 +24,17 @@ Developer Machine Setup
If you don't increase the file handle limit you may see some weird build issues with browserify or npm.
3. Reload your bash profile
`source ~/.bash_profile`
-4. Install Node.js using Homebrew
- 1. Download Homebrew from http://brew.sh/
- 2. `brew install node`
-5. Install Compass
+4. Install Compass
1. Run `ruby -v` and check the ruby version is 1.8.7 or higher
2. `sudo gem install compass`
-6. Download Mattermost
+5. Download Mattermost
`cd ~/go`
`mkdir -p src/github.com/mattermost`
`cd src/github.com/mattermost`
`git clone https://github.com/mattermost/platform.git`
`cd platform`
-7. Run unit tests on Mattermost using `make test` to make sure the installation was successful
-8. If tests passed, you can now run Mattermost using `make run`
+6. Run unit tests on Mattermost using `make test` to make sure the installation was successful
+7. If tests passed, you can now run Mattermost using `make run`
Any issues? Please let us know on our forums at: http://forum.mattermost.org
@@ -64,13 +64,9 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
If you don't increase the file handle limit you may see some weird build issues with browserify or npm.
3. Reload your bashrc
`source ~/.bashrc`
-6. Install Node.js
- 1. Download the newest version of the Node.js sources from https://nodejs.org/en/download/
- 2. Extract the contents of the package and cd into the extracted files
- 3. Compile and install Node.js
- `./configure`
- `make`
- `make install`
+6. Install Node.js
+ `curl -sL https://deb.nodesource.com/setup_5.x | sudo -E bash -`
+ `sudo apt-get install -y nodejs`
7. Install Ruby and Compass
`apt-get install ruby`
`apt-get install ruby-dev`
diff --git a/doc/process/overview.md b/doc/process/overview.md
index af632e393..b34908782 100644
--- a/doc/process/overview.md
+++ b/doc/process/overview.md
@@ -64,7 +64,7 @@ Mattermost priorities are managed in Jira tickets, which are created by the core
On non-holiday weekdays new tickets are reviewed in a process called "triage", and assigned a Fix Version of "backlog", indicating the ticket has enough specificity that it can be assigned to a developer to be completed.
-By default, all tickets are created as internal-only, and the triage process reviews them for sufficient specifity and abscense of sensitive information before switching their visibility to public as part of the triage process.
+By default, all tickets are created as public unless they contain sensitive information. The triage process reviews them for sufficient specifity. If the ticket is unclear, triage may reassign the ticket back to the original reporter to add more details.
View [current issues scheduled for the next triage meeting](https://mattermost.atlassian.net/browse/PLT-1203?filter=10105).
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index c23a72cd1..4c5502ddd 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -68,9 +68,8 @@
"ConnectionSecurity": "",
"InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
- "ApplePushServer": "",
- "ApplePushCertPublic": "",
- "ApplePushCertPrivate": ""
+ "SendPushNotifications": true,
+ "PushNotificationServer": "https://push.mattermost.com"
},
"RateLimitSettings": {
"EnableRateLimiter": true,
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index c23a72cd1..4c5502ddd 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -68,9 +68,8 @@
"ConnectionSecurity": "",
"InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
- "ApplePushServer": "",
- "ApplePushCertPublic": "",
- "ApplePushCertPrivate": ""
+ "SendPushNotifications": true,
+ "PushNotificationServer": "https://push.mattermost.com"
},
"RateLimitSettings": {
"EnableRateLimiter": true,
diff --git a/mattermost.go b/mattermost.go
index 2d5727400..eaab1de88 100644
--- a/mattermost.go
+++ b/mattermost.go
@@ -68,6 +68,7 @@ func main() {
manualtesting.InitManualTesting()
}
+ setDiagnosticId()
runSecurityAndDiagnosticsJobAndForget()
// wait for kill signal before attempting to gracefully shutdown
@@ -80,6 +81,21 @@ func main() {
}
}
+func setDiagnosticId() {
+ if result := <-api.Srv.Store.System().Get(); result.Err == nil {
+ props := result.Data.(model.StringMap)
+
+ id := props[model.SYSTEM_DIAGNOSTIC_ID]
+ if len(id) == 0 {
+ id = model.NewId()
+ systemId := &model.System{Name: model.SYSTEM_DIAGNOSTIC_ID, Value: id}
+ <-api.Srv.Store.System().Save(systemId)
+ }
+
+ utils.CfgDiagnosticId = id
+ }
+}
+
func runSecurityAndDiagnosticsJobAndForget() {
go func() {
for {
@@ -92,15 +108,9 @@ func runSecurityAndDiagnosticsJobAndForget() {
if (currentTime - lastSecurityTime) > 1000*60*60*24*1 {
l4g.Debug("Checking for security update from Mattermost")
- id := props[model.SYSTEM_DIAGNOSTIC_ID]
- if len(id) == 0 {
- id = model.NewId()
- systemId := &model.System{Name: model.SYSTEM_DIAGNOSTIC_ID, Value: id}
- <-api.Srv.Store.System().Save(systemId)
- }
-
v := url.Values{}
- v.Set(utils.PROP_DIAGNOSTIC_ID, id)
+
+ v.Set(utils.PROP_DIAGNOSTIC_ID, utils.CfgDiagnosticId)
v.Set(utils.PROP_DIAGNOSTIC_BUILD, model.CurrentVersion+"."+model.BuildNumber)
v.Set(utils.PROP_DIAGNOSTIC_DATABASE, utils.Cfg.SqlSettings.DriverName)
v.Set(utils.PROP_DIAGNOSTIC_OS, runtime.GOOS)
diff --git a/model/config.go b/model/config.go
index 50a8dc133..195cefae8 100644
--- a/model/config.go
+++ b/model/config.go
@@ -96,11 +96,8 @@ type EmailSettings struct {
ConnectionSecurity string
InviteSalt string
PasswordResetSalt string
-
- // For Future Use
- ApplePushServer string
- ApplePushCertPublic string
- ApplePushCertPrivate string
+ SendPushNotifications *bool
+ PushNotificationServer *string
}
type RateLimitSettings struct {
@@ -181,6 +178,17 @@ func (o *Config) SetDefaults() {
o.TeamSettings.EnableTeamListing = new(bool)
*o.TeamSettings.EnableTeamListing = false
}
+
+ if o.EmailSettings.SendPushNotifications == nil {
+ o.EmailSettings.SendPushNotifications = new(bool)
+ *o.EmailSettings.SendPushNotifications = true
+ }
+
+ if o.EmailSettings.PushNotificationServer == nil {
+ o.EmailSettings.PushNotificationServer = new(string)
+ *o.EmailSettings.PushNotificationServer = "https://push.mattermost.com"
+ }
+
}
func (o *Config) IsValid() *AppError {
diff --git a/model/push_notification.go b/model/push_notification.go
new file mode 100644
index 000000000..76f5bd125
--- /dev/null
+++ b/model/push_notification.go
@@ -0,0 +1,45 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ PUSH_NOTIFY_APPLE = "apple"
+ PUSH_NOTIFY_ANDROID = "android"
+)
+
+type PushNotification struct {
+ Platform string `json:"platform"`
+ ServerId string `json:"server_id"`
+ DeviceId string `json:"device_id"`
+ Category string `json:"category"`
+ Sound string `json:"sound"`
+ Message string `json:"message"`
+ Badge int `json:"badge"`
+ ContentAvailable int `json:"cont_ava"`
+}
+
+func (me *PushNotification) ToJson() string {
+ b, err := json.Marshal(me)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func PushNotificationFromJson(data io.Reader) *PushNotification {
+ decoder := json.NewDecoder(data)
+ var me PushNotification
+ err := decoder.Decode(&me)
+ if err == nil {
+ return &me
+ } else {
+ return nil
+ }
+}
diff --git a/model/push_notification_test.go b/model/push_notification_test.go
new file mode 100644
index 000000000..94329f389
--- /dev/null
+++ b/model/push_notification_test.go
@@ -0,0 +1,19 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestPushNotification(t *testing.T) {
+ msg := PushNotification{Platform: "test"}
+ json := msg.ToJson()
+ result := PushNotificationFromJson(strings.NewReader(json))
+
+ if msg.Platform != result.Platform {
+ t.Fatal("Ids do not match")
+ }
+}
diff --git a/utils/apns.go b/utils/apns.go
deleted file mode 100644
index 06e8ce6ef..000000000
--- a/utils/apns.go
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package utils
-
-import (
- l4g "code.google.com/p/log4go"
- "fmt"
- "github.com/anachronistic/apns"
- "github.com/mattermost/platform/model"
-)
-
-func SendAppleNotifyAndForget(deviceId string, message string, badge int) {
- go func() {
- if err := SendAppleNotify(deviceId, message, badge); err != nil {
- l4g.Error(fmt.Sprintf("%v %v", err.Message, err.DetailedError))
- }
- }()
-}
-
-func SendAppleNotify(deviceId string, message string, badge int) *model.AppError {
- payload := apns.NewPayload()
- payload.Alert = message
- payload.Badge = 1
-
- pn := apns.NewPushNotification()
- pn.DeviceToken = deviceId
- pn.AddPayload(payload)
- client := apns.BareClient(Cfg.EmailSettings.ApplePushServer, Cfg.EmailSettings.ApplePushCertPublic, Cfg.EmailSettings.ApplePushCertPrivate)
- resp := client.Send(pn)
-
- if resp.Error != nil {
- return model.NewAppError("", "Could not send apple push notification", fmt.Sprintf("id=%v err=%v", deviceId, resp.Error))
- } else {
- return nil
- }
-}
diff --git a/utils/config.go b/utils/config.go
index 2fd799cd1..0b292a2ca 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -24,6 +24,7 @@ const (
)
var Cfg *model.Config = &model.Config{}
+var CfgDiagnosticId = ""
var CfgLastModified int64 = 0
var CfgFileName string = ""
var ClientCfg map[string]string = map[string]string{}
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index d0565a0e0..238ace3da 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -18,6 +18,7 @@ export default class EmailSettings extends React.Component {
this.state = {
sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications,
+ sendPushNotifications: this.props.config.EmailSettings.SendPushNotifications,
saveNeeded: false,
serverError: null,
emailSuccess: null,
@@ -36,6 +37,14 @@ export default class EmailSettings extends React.Component {
s.sendEmailNotifications = false;
}
+ if (action === 'sendPushNotifications_true') {
+ s.sendPushNotifications = true;
+ }
+
+ if (action === 'sendPushNotifications_false') {
+ s.sendPushNotifications = false;
+ }
+
this.setState(s);
}
@@ -43,11 +52,12 @@ export default class EmailSettings extends React.Component {
var config = this.props.config;
config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked;
config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
+ config.EmailSettings.SendPushlNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).checked;
config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked;
- config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
config.EmailSettings.FeedbackName = ReactDOM.findDOMNode(this.refs.feedbackName).value.trim();
config.EmailSettings.FeedbackEmail = ReactDOM.findDOMNode(this.refs.feedbackEmail).value.trim();
config.EmailSettings.SMTPServer = ReactDOM.findDOMNode(this.refs.SMTPServer).value.trim();
+ config.EmailSettings.PushNotificationServer = ReactDOM.findDOMNode(this.refs.PushNotificationServer).value.trim();
config.EmailSettings.SMTPPort = ReactDOM.findDOMNode(this.refs.SMTPPort).value.trim();
config.EmailSettings.SMTPUsername = ReactDOM.findDOMNode(this.refs.SMTPUsername).value.trim();
config.EmailSettings.SMTPPassword = ReactDOM.findDOMNode(this.refs.SMTPPassword).value.trim();
@@ -526,6 +536,61 @@ export default class EmailSettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='sendPushNotifications'
+ >
+ {'Send Push Notifications: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendPushNotifications'
+ value='true'
+ ref='sendPushNotifications'
+ defaultChecked={this.props.config.EmailSettings.SendPushNotifications}
+ onChange={this.handleChange.bind(this, 'sendPushNotifications_true')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendPushNotifications'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.SendPushNotifications}
+ onChange={this.handleChange.bind(this, 'sendPushNotifications_false')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send iOS and Android push notifications through the push notification server.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PushNotificationServer'
+ >
+ {'Push Notification Server:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PushNotificationServer'
+ ref='PushNotificationServer'
+ placeholder='E.g.: "https://push.mattermost.com"'
+ defaultValue={this.props.config.EmailSettings.PushNotificationServer}
+ onChange={this.handleChange}
+ disabled={!this.state.sendPushNotifications}
+ />
+ <p className='help-text'>{'Location of the push notification server.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index df5d6b8e1..fd20834f4 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -75,7 +75,7 @@ export default class GetLinkModal extends React.Component {
onHide={this.onHide}
>
<Modal.Header closeButton={true}>
- {this.props.title}
+ <h4 className='modal-title'>{this.props.title}</h4>
</Modal.Header>
<Modal.Body>
{helpText}
diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx
index 390d25f2e..f5d5ab28b 100644
--- a/web/react/components/member_list_item.jsx
+++ b/web/react/components/member_list_item.jsx
@@ -110,7 +110,7 @@ export default class MemberListItem extends React.Component {
height='36'
width='36'
/>
- <div className='member-name'>{member.username}</div>
+ <div className='member-name'>{Utils.displayUsername(member.id)}</div>
<div className='member-description'>{member.email}</div>
</td>
<td className='td--action lg'>{invite}</td>
diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx
index 27fb6a4c1..316fad01a 100644
--- a/web/react/components/member_list_team_item.jsx
+++ b/web/react/components/member_list_team_item.jsx
@@ -174,7 +174,7 @@ export default class MemberListTeamItem extends React.Component {
height='36'
width='36'
/>
- <span className='member-name'>{Utils.getDisplayName(user)}</span>
+ <span className='member-name'>{Utils.displayUsername(user.id)}</span>
<span className='member-email'>{email}</span>
<div className='dropdown member-drop'>
<a
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index b5000141a..f4cb542e4 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -5,6 +5,7 @@ import UserStore from '../stores/user_store.jsx';
var Popover = ReactBootstrap.Popover;
var Overlay = ReactBootstrap.Overlay;
import * as Utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
import ChannelStore from '../stores/channel_store.jsx';
@@ -68,7 +69,7 @@ export default class PopoverListMembers extends React.Component {
}
render() {
- let popoverHtml = [];
+ const popoverHtml = [];
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
const currentUserId = UserStore.getCurrentId();
@@ -76,35 +77,13 @@ export default class PopoverListMembers extends React.Component {
if (members && teamMembers) {
members.sort((a, b) => {
- return a.username.localeCompare(b.username);
+ const aName = Utils.displayUsername(a.id);
+ const bName = Utils.displayUsername(b.id);
+
+ return aName.localeCompare(bName);
});
members.forEach((m, i) => {
- const details = [];
-
- const fullName = Utils.getFullName(m);
- if (fullName) {
- details.push(
- <span
- key={`${m.id}__full-name`}
- className='full-name'
- >
- {fullName}
- </span>
- );
- }
-
- if (m.nickname) {
- const separator = fullName ? ' - ' : '';
- details.push(
- <span
- key={`${m.nickname}__nickname`}
- >
- {separator + m.nickname}
- </span>
- );
- }
-
let button = '';
if (currentUserId !== m.id && ch.type !== 'D') {
button = (
@@ -118,7 +97,12 @@ export default class PopoverListMembers extends React.Component {
);
}
- if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
+ let name = '';
+ if (teamMembers[m.username]) {
+ name = Utils.displayUsername(teamMembers[m.username].id);
+ }
+
+ if (name && teamMembers[m.username].delete_at <= 0) {
popoverHtml.push(
<div
className='text-nowrap'
@@ -135,7 +119,7 @@ export default class PopoverListMembers extends React.Component {
<div
className='more-name'
>
- {m.username}
+ {name}
</div>
</div>
<div
@@ -157,8 +141,8 @@ export default class PopoverListMembers extends React.Component {
count = members.length;
}
- if (count > 20) {
- countText = '20+';
+ if (count > Constants.MAX_CHANNEL_POPOVER_COUNT) {
+ countText = Constants.MAX_CHANNEL_POPOVER_COUNT + '+';
} else if (count > 0) {
countText = count.toString();
}
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 278261e22..66d8c507a 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -87,6 +87,10 @@ export default class Post extends React.Component {
return true;
}
+ if (nextProps.displayNameType !== this.props.displayNameType) {
+ return true;
+ }
+
if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) {
return true;
}
@@ -224,5 +228,6 @@ Post.propTypes = {
sameRoot: React.PropTypes.bool,
hideProfilePic: React.PropTypes.bool,
isLastComment: React.PropTypes.bool,
- shouldHighlight: React.PropTypes.bool
+ shouldHighlight: React.PropTypes.bool,
+ displayNameType: React.PropTypes.string
};
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index 6b61d465c..242b26b91 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -2,15 +2,18 @@
// See License.txt for license information.
import UserStore from '../stores/user_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as Utils from '../utils/utils.jsx';
import Post from './post.jsx';
import Constants from '../utils/constants.jsx';
+const Preferences = Constants.Preferences;
export default class PostsView extends React.Component {
constructor(props) {
super(props);
+ this.updateState = this.updateState.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.isAtBottom = this.isAtBottom.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
@@ -22,6 +25,8 @@ export default class PostsView extends React.Component {
this.jumpToPostNode = null;
this.wasAtBottom = true;
this.scrollHeight = 0;
+
+ this.state = {displayNameType: PreferenceStore.getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value};
}
static get SCROLL_TYPE_FREE() {
return 1;
@@ -38,6 +43,9 @@ export default class PostsView extends React.Component {
static get SCROLL_TYPE_POST() {
return 5;
}
+ updateState() {
+ this.setState({displayNameType: PreferenceStore.getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value});
+ }
isAtBottom() {
return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight);
}
@@ -166,6 +174,7 @@ export default class PostsView extends React.Component {
isLastComment={isLastComment}
shouldHighlight={shouldHighlight}
onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func
+ displayNameType={this.state.displayNameType}
/>
);
@@ -272,9 +281,11 @@ export default class PostsView extends React.Component {
}
window.addEventListener('resize', this.handleResize);
$(this.refs.postlist).perfectScrollbar();
+ PreferenceStore.addChangeListener(this.updateState);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
+ PreferenceStore.removeChangeListener(this.updateState);
}
componentDidUpdate() {
if (this.props.postList != null) {
@@ -282,7 +293,7 @@ export default class PostsView extends React.Component {
}
$(this.refs.postlist).perfectScrollbar('update');
}
- shouldComponentUpdate(nextProps) {
+ shouldComponentUpdate(nextProps, nextState) {
if (this.props.isActive !== nextProps.isActive) {
return true;
}
@@ -301,6 +312,9 @@ export default class PostsView extends React.Component {
if (!Utils.areObjectsEqual(this.props.postList, nextProps.postList)) {
return true;
}
+ if (nextState.displayNameType !== this.state.displayNameType) {
+ return true;
+ }
return false;
}
diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx
index 100600c4b..f49b33f73 100644
--- a/web/react/components/register_app_modal.jsx
+++ b/web/react/components/register_app_modal.jsx
@@ -2,21 +2,57 @@
// See License.txt for license information.
import * as Client from '../utils/client.jsx';
+import ModalStore from '../stores/modal_store.jsx';
+
+const Modal = ReactBootstrap.Modal;
+
+import Constants from '../utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
export default class RegisterAppModal extends React.Component {
constructor() {
super();
- this.register = this.register.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
this.onHide = this.onHide.bind(this);
this.save = this.save.bind(this);
+ this.updateShow = this.updateShow.bind(this);
- this.state = {clientId: '', clientSecret: '', saved: false};
+ this.state = {
+ clientId: '',
+ clientSecret: '',
+ saved: false,
+ show: false
+ };
}
componentDidMount() {
- $(ReactDOM.findDOMNode(this)).on('hide.bs.modal', this.onHide);
+ ModalStore.addModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow);
+ }
+ componentWillUnmount() {
+ ModalStore.removeModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow);
+ }
+ updateShow(show) {
+ if (!show) {
+ if (this.state.clientId !== '' && !this.state.saved) {
+ return;
+ }
+
+ this.setState({
+ clientId: '',
+ clientSecret: '',
+ saved: false,
+ homepageError: null,
+ callbackError: null,
+ serverError: null,
+ nameError: null
+ });
+ }
+
+ this.setState({show});
}
- register() {
+ handleSubmit(e) {
+ e.preventDefault();
+
var state = this.state;
state.serverError = null;
@@ -94,6 +130,7 @@ export default class RegisterAppModal extends React.Component {
}
var body = '';
+ var footer = '';
if (this.state.clientId === '') {
body = (
<div className='settings-modal'>
@@ -148,24 +185,29 @@ export default class RegisterAppModal extends React.Component {
</div>
</div>
{serverError}
- <hr />
- <a
- className='btn btn-sm theme pull-right'
- href='#'
- data-dismiss='modal'
- aria-label='Close'
- >
- {'Cancel'}
- </a>
- <a
- className='btn btn-sm btn-primary pull-right'
- onClick={this.register}
- >
- {'Register'}
- </a>
</div>
</div>
);
+
+ footer = (
+ <div>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={() => this.updateShow(false)}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ onClick={this.handleSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='3'
+ >
+ {'Register'}
+ </button>
+ </div>
+ );
} else {
var btnClass = ' disabled';
if (this.state.saved) {
@@ -173,17 +215,35 @@ export default class RegisterAppModal extends React.Component {
}
body = (
- <div className='form-group user-settings'>
- <h3>{'Your Application Credentials'}</h3>
- <br/>
- <br/>
- <label className='col-sm-12 control-label'>{'Client ID: '}{this.state.clientId}</label>
- <label className='col-sm-12 control-label'>{'Client Secret: '}{this.state.clientSecret}</label>
+ <div className='form-horizontal user-settings'>
+ <h4 className='padding-bottom x3'>{'Your Application Credentials'}</h4>
<br/>
+ <div className='row'>
+ <label className='col-sm-4 control-label'>{'Client ID'}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ value={this.state.clientId}
+ readOnly='true'
+ />
+ </div>
+ </div>
<br/>
+ <div className='row padding-top x2'>
+ <label className='col-sm-4 control-label'>{'Client Secret'}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ value={this.state.clientSecret}
+ readOnly='true'
+ />
+ </div>
+ </div>
<br/>
<br/>
- <strong>{'Save these somewhere SAFE and SECURE. We can retrieve your Client Id if you lose it, but your Client Secret will be lost forever if you were to lose it.'}</strong>
+ <strong>{'Save these somewhere SAFE and SECURE. Treat your Client ID as your app\'s username and your Client Secret as the app\'s password.'}</strong>
<br/>
<br/>
<div className='checkbox'>
@@ -192,56 +252,50 @@ export default class RegisterAppModal extends React.Component {
ref='save'
type='checkbox'
checked={this.state.saved}
- onClick={this.save}
- >
- {'I have saved both my Client Id and Client Secret somewhere safe'}
- </input>
+ onChange={this.save}
+ />
+ {'I have saved both my Client Id and Client Secret somewhere safe'}
</label>
</div>
- <a
- className={'btn btn-sm btn-primary pull-right' + btnClass}
- href='#'
- data-dismiss='modal'
- aria-label='Close'
- >
- {'Close'}
- </a>
</div>
);
+
+ footer = (
+ <a
+ className={'btn btn-sm btn-primary pull-right' + btnClass}
+ href='#'
+ onClick={(e) => {
+ e.preventDefault();
+ this.updateShow(false);
+ }}
+ >
+ {'Close'}
+ </a>
+ );
}
return (
- <div
- className='modal fade'
- ref='modal'
- id='register_app'
- role='dialog'
- aria-hidden='true'
- >
- <div className='modal-dialog'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>{'×'}</span>
- </button>
- <h4
- className='modal-title'
- ref='title'
- >
- {'Developer Applications'}
- </h4>
- </div>
- <div className='modal-body'>
- {body}
- </div>
- </div>
- </div>
- </div>
+ <span>
+ <Modal
+ show={this.state.show}
+ onHide={() => this.updateShow(false)}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Developer Applications'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ {body}
+ </Modal.Body>
+ <Modal.Footer>
+ {footer}
+ </Modal.Footer>
+ </form>
+ </Modal>
+ </span>
);
}
}
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index b4c037183..5c8e73874 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -3,7 +3,6 @@
import NewChannelFlow from './new_channel_flow.jsx';
import MoreDirectChannels from './more_direct_channels.jsx';
-import SearchBox from './search_bar.jsx';
import SidebarHeader from './sidebar_header.jsx';
import UnreadChannelIndicator from './unread_channel_indicator.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
@@ -586,7 +585,6 @@ export default class Sidebar extends React.Component {
teamName={TeamStore.getCurrent().name}
teamType={TeamStore.getCurrent().type}
/>
- <SearchBox />
<UnreadChannelIndicator
show={this.state.showTopUnread}
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 438c0bc82..ea104fedb 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -54,9 +54,11 @@ export default class UserProfile extends React.Component {
}
}
render() {
- var name = this.state.profile.username;
+ var name = Utils.displayUsername(this.state.profile.id);
if (this.props.overwriteName) {
name = this.props.overwriteName;
+ } else if (!name) {
+ name = '...';
}
if (this.props.disablePopover) {
@@ -107,7 +109,7 @@ export default class UserProfile extends React.Component {
rootClose={true}
overlay={
<Popover
- title={this.state.profile.username}
+ title={name}
id='user-profile-popover'
>
{dataContent}
diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx
index 2d02c255a..01e13be57 100644
--- a/web/react/components/user_settings/user_settings_developer.jsx
+++ b/web/react/components/user_settings/user_settings_developer.jsx
@@ -3,16 +3,19 @@
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
+import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
export default class DeveloperTab extends React.Component {
constructor(props) {
super(props);
+ this.register = this.register.bind(this);
+
this.state = {};
}
register() {
- $('#user_settings1').modal('hide');
- $('#register_app').modal('show');
+ this.props.closeModal();
+ EventHelpers.showRegisterAppModal();
}
render() {
var appSection;
@@ -21,7 +24,10 @@ export default class DeveloperTab extends React.Component {
var inputs = [];
inputs.push(
- <div className='form-group'>
+ <div
+ key='registerbtn'
+ className='form-group'
+ >
<div className='col-sm-7'>
<a
className='btn btn-sm btn-primary'
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index 2b505607e..820f8fd8e 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -423,10 +423,11 @@ export default class ViewImageModal extends React.Component {
onClick={this.props.onModalDismissed}
>
<div
- className={'image-wrapper ' + bgClass}
+ className={'image-wrapper'}
onClick={this.props.onModalDismissed}
>
<div
+ className={bgClass}
onMouseEnter={this.onMouseEnterImage}
onMouseLeave={this.onMouseLeaveImage}
onClick={(e) => e.stopPropagation()}
diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx
index d7f255aaa..856eec2f1 100644
--- a/web/react/dispatcher/event_helpers.jsx
+++ b/web/react/dispatcher/event_helpers.jsx
@@ -104,3 +104,10 @@ export function showInviteMemberModal() {
value: true
});
}
+
+export function showRegisterAppModal() {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_REGISTER_APP_MODAL,
+ value: true
+ });
+}
diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx
index a26a97f53..9f33cf022 100644
--- a/web/react/stores/modal_store.jsx
+++ b/web/react/stores/modal_store.jsx
@@ -35,6 +35,7 @@ class ModalStoreClass extends EventEmitter {
case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL:
case ActionTypes.TOGGLE_DELETE_POST_MODAL:
case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL:
+ case ActionTypes.TOGGLE_REGISTER_APP_MODAL:
this.emit(type, value, args);
break;
}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 2009e07dd..99bd2453c 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -49,7 +49,8 @@ export default {
TOGGLE_IMPORT_THEME_MODAL: null,
TOGGLE_INVITE_MEMBER_MODAL: null,
TOGGLE_DELETE_POST_MODAL: null,
- TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null
+ TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
+ TOGGLE_REGISTER_APP_MODAL: null
}),
PayloadSources: keyMirror({
@@ -137,6 +138,7 @@ export default {
],
MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
MAX_DMS: 20,
+ MAX_CHANNEL_POPOVER_COUNT: 20,
DM_CHANNEL: 'D',
OPEN_CHANNEL: 'O',
PRIVATE_CHANNEL: 'P',
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 3a912fd75..f0bd46f9d 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -188,7 +188,7 @@ function highlightCurrentMentions(text, tokens) {
const newAlias = `MM_SELFMENTION${index}`;
newTokens.set(newAlias, {
- value: `<span class='mention-highlight'>${alias}</span>` + token.extraText,
+ value: `<span class='mention-highlight'>${alias}</span>` + (token.extraText || ''),
originalText: token.originalText
});
output = output.replace(alias, newAlias);
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 0c08c2df6..d6ed34e70 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -607,7 +607,7 @@ export function applyTheme(theme) {
changeCss('.popover.right>.arrow:after, .tip-overlay.tip-overlay--sidebar .arrow, .tip-overlay.tip-overlay--header .arrow', 'border-right-color:' + theme.centerChannelBg, 1);
changeCss('.popover.left>.arrow:after', 'border-left-color:' + theme.centerChannelBg, 1);
changeCss('.popover.top>.arrow:after, .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg, 1);
- changeCss('.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
+ changeCss('@media(min-width: 960px){.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
changeCss('.attachment__content', 'background:' + theme.centerChannelBg, 1);
}
@@ -638,8 +638,7 @@ export function applyTheme(theme) {
changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2);
changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2);
changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1);
- changeCss('.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2);
- changeCss('@media(max-width: 960px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
+ changeCss('@media(min-width: 960px){.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2);
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('.attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
@@ -981,13 +980,15 @@ export function displayUsername(userId) {
const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value;
let username = '';
- if (nameFormat === 'nickname_full_name') {
- username = user.nickname || getFullName(user);
- } else if (nameFormat === 'full_name') {
- username = getFullName(user);
- }
- if (!username.trim().length) {
- username = user.username;
+ if (user) {
+ if (nameFormat === 'nickname_full_name') {
+ username = user.nickname || getFullName(user);
+ } else if (nameFormat === 'full_name') {
+ username = getFullName(user);
+ }
+ if (!username.trim().length) {
+ username = user.username;
+ }
}
return username;
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 5f5cca89b..a56c4bb17 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -484,7 +484,8 @@
padding: 0 10px 0 31px;
background: rgba(black, 0.2);
@include border-radius(3px);
- color: inherit;
+ color: #444;
+ background: #fff;
}
}
}
@@ -509,15 +510,15 @@
&.move--right {
@include translate3d(0, 0, 0);
}
+ > div {
+ padding-bottom: 0;
+ }
.nav-pills__unread-indicator-bottom {
bottom: 10px;
}
.badge {
top: 13px;
}
- > div {
- padding-bottom: 65px;
- }
.team__header {
display: none;
@include clearfix;
diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss
index 27b55f214..b7f658114 100644
--- a/web/sass-files/sass/partials/_search.scss
+++ b/web/sass-files/sass/partials/_search.scss
@@ -38,6 +38,7 @@
font-size: 14px;
@include opacity(0.5);
display: none;
+ color: #777;
}
.search__form {
.search-bar__container & {