From 830c6b3f1ebdfc38ca8ee0e6c672301fbe884e21 Mon Sep 17 00:00:00 2001 From: Melvi Ts Date: Thu, 15 Oct 2015 21:17:52 +0800 Subject: Fix email encoding issue --- utils/mail.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/utils/mail.go b/utils/mail.go index c91c15a6a..07a79eeb2 100644 --- a/utils/mail.go +++ b/utils/mail.go @@ -6,15 +6,22 @@ package utils import ( l4g "code.google.com/p/log4go" "crypto/tls" + "encoding/base64" "fmt" "github.com/mattermost/platform/model" - "html" "net" "net/mail" "net/smtp" "time" ) +func encodeRFC2047Word(s string) string { + // TODO: use `mime.BEncoding.Encode` instead when `go` >= 1.5 + // return mime.BEncoding.Encode("utf-8", s) + dst := base64.StdEncoding.EncodeToString([]byte(s)) + return "=?utf-8?b?" + dst + "?=" +} + func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) { var conn net.Conn var err error @@ -102,9 +109,10 @@ func SendMailUsingConfig(to, subject, body string, config *model.Config) *model. headers := make(map[string]string) headers["From"] = fromMail.String() headers["To"] = toMail.String() - headers["Subject"] = html.UnescapeString(subject) + headers["Subject"] = encodeRFC2047Word(subject) headers["MIME-version"] = "1.0" - headers["Content-Type"] = "text/html" + headers["Content-Type"] = "text/html; charset=\"utf-8\"" + headers["Content-Transfer-Encoding"] = "8bit" headers["Date"] = time.Now().Format(time.RFC1123Z) message := "" -- cgit v1.2.3-1-g7c22 From b00ffa83e7371fa7dd4570130ee8b506943aee01 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Fri, 16 Oct 2015 18:06:22 +0500 Subject: Multiple UI Improvements --- web/react/components/more_channels.jsx | 6 +-- web/react/components/more_direct_channels.jsx | 72 +++++++++++++------------- web/react/components/popover_list_members.jsx | 2 +- web/react/components/post_info.jsx | 2 +- web/react/components/sidebar.jsx | 6 +-- web/react/components/user_profile.jsx | 2 +- web/sass-files/sass/partials/_modal.scss | 73 +++++++++++++-------------- web/sass-files/sass/partials/_settings.scss | 7 --- 8 files changed, 82 insertions(+), 88 deletions(-) diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx index a20c5cad5..a0084ad30 100644 --- a/web/react/components/more_channels.jsx +++ b/web/react/components/more_channels.jsx @@ -83,7 +83,7 @@ export default class MoreChannels extends React.Component { moreChannels = ; } else if (channels.length) { moreChannels = ( - +
{channels.map(function cMap(channel, index) { var joinButton; @@ -108,8 +108,8 @@ export default class MoreChannels extends React.Component { return ( + + + ); } @@ -213,7 +211,7 @@ export default class MoreDirectChannels extends React.Component { const userEntries = users.map(this.createRowForUser); if (userEntries.length === 0) { - userEntries.push(
  • {'No users found :('}
  • ); + userEntries.push(
    {'No users found :('}
    ); } let memberString = 'Member'; @@ -232,26 +230,32 @@ export default class MoreDirectChannels extends React.Component { - {'More Direct Messages'} + {'Team Directory'} -
    - - {count} +
    +
    + +
    +
    + {count} +
    +
    +
    +
    -

    {channel.display_name}

    -

    {channel.description}

    +

    {channel.display_name}

    +

    {channel.description}

    {joinButton} diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 08b64de8b..a599c1872 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -140,12 +140,12 @@ export default class MoreDirectChannels extends React.Component { if (user.nickname) { const separator = fullName ? ' - ' : ''; details.push( - {separator + user.nickname} - +

    ); } @@ -170,28 +170,26 @@ export default class MoreDirectChannels extends React.Component { } return ( -
  • -
    +
  • - -
    -
    +
    {user.username}
    -
    - {details} -
    -
    -
    + {details} +
    {joinButton} - - +
    + + {userEntries} + +
    -
      - {userEntries} -

    + > {'Display Settings'}

    diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx index 4dbb9b96f..8693af494 100644 --- a/web/react/components/user_settings/user_settings_notifications.jsx +++ b/web/react/components/user_settings/user_settings_notifications.jsx @@ -228,9 +228,8 @@ export default class NotificationsTab extends React.Component { - For all activity - + /> + {'For all activity'}
    @@ -240,9 +239,8 @@ export default class NotificationsTab extends React.Component { type='radio' checked={notifyActive[1]} onChange={this.handleNotifyRadio.bind(this, 'mention')} - > - Only for mentions and direct messages - + /> + {'Only for mentions and direct messages'}
    @@ -252,9 +250,8 @@ export default class NotificationsTab extends React.Component { type='radio' checked={notifyActive[2]} onChange={this.handleNotifyRadio.bind(this, 'none')} - > - Never - + /> + {'Never'} @@ -320,9 +317,8 @@ export default class NotificationsTab extends React.Component { type='radio' checked={soundActive[0]} onChange={this.handleSoundRadio.bind(this, 'true')} - > - On - + /> + {'On'}
    @@ -332,9 +328,8 @@ export default class NotificationsTab extends React.Component { type='radio' checked={soundActive[1]} onChange={this.handleSoundRadio.bind(this, 'false')} - > - Off - + /> + {'Off'}
    @@ -402,9 +397,8 @@ export default class NotificationsTab extends React.Component { type='radio' checked={emailActive[0]} onChange={this.handleEmailRadio.bind(this, 'true')} - > - On - + /> + {'On'}
    @@ -414,9 +408,8 @@ export default class NotificationsTab extends React.Component { type='radio' checked={emailActive[1]} onChange={this.handleEmailRadio.bind(this, 'false')} - > - Off - + /> + {'Off'}
    @@ -482,9 +475,8 @@ export default class NotificationsTab extends React.Component { type='checkbox' checked={this.state.firstNameKey} onChange={handleUpdateFirstNameKey} - > - {'Your case sensitive first name "' + user.first_name + '"'} - + /> + {'Your case sensitive first name "' + user.first_name + '"'} @@ -502,9 +494,8 @@ export default class NotificationsTab extends React.Component { type='checkbox' checked={this.state.usernameKey} onChange={handleUpdateUsernameKey} - > - {'Your non-case sensitive username "' + user.username + '"'} - + /> + {'Your non-case sensitive username "' + user.username + '"'} @@ -521,9 +512,8 @@ export default class NotificationsTab extends React.Component { type='checkbox' checked={this.state.mentionKey} onChange={handleUpdateMentionKey} - > - {'Your username mentioned "@' + user.username + '"'} - + /> + {'Your username mentioned "@' + user.username + '"'} @@ -540,9 +530,8 @@ export default class NotificationsTab extends React.Component { type='checkbox' checked={this.state.allKey} onChange={handleUpdateAllKey} - > - {'Team-wide mentions "@all"'} - + /> + {'Team-wide mentions "@all"'} @@ -559,9 +548,8 @@ export default class NotificationsTab extends React.Component { type='checkbox' checked={this.state.channelKey} onChange={handleUpdateChannelKey} - > - {'Channel-wide mentions "@channel"'} - + /> + {'Channel-wide mentions "@channel"'} @@ -576,9 +564,8 @@ export default class NotificationsTab extends React.Component { type='checkbox' checked={this.state.customKeysChecked} onChange={this.updateCustomMentionKeys} - > - {'Other non-case sensitive words, separated by commas:'} - + /> + {'Other non-case sensitive words, separated by commas:'} Date: Thu, 15 Oct 2015 10:44:04 -0400 Subject: Small refactor of websocket code on client and server --- api/channel.go | 7 +- api/web_conn.go | 25 +---- api/web_team_hub.go | 31 +++++- model/message.go | 16 +-- web/react/components/channel_header.jsx | 16 +-- web/react/components/msg_typing.jsx | 11 +- web/react/components/post_list.jsx | 60 +++++----- web/react/components/sidebar.jsx | 92 ---------------- web/react/stores/socket_store.jsx | 188 ++++++++++++++++++++++++++++++-- web/react/utils/constants.jsx | 12 ++ 10 files changed, 276 insertions(+), 182 deletions(-) diff --git a/api/channel.go b/api/channel.go index adf125378..70f7eba4b 100644 --- a/api/channel.go +++ b/api/channel.go @@ -568,7 +568,7 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { Srv.Store.Channel().UpdateLastViewedAt(id, c.Session.UserId) - message := model.NewMessage(c.Session.TeamId, id, c.Session.UserId, model.ACTION_VIEWED) + message := model.NewMessage(c.Session.TeamId, id, c.Session.UserId, model.ACTION_CHANNEL_VIEWED) message.Add("channel_id", id) PublishAndForget(message) @@ -777,9 +777,8 @@ func RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel UpdateChannelAccessCacheAndForget(channel.TeamId, userIdToRemove, channel.Id) - message := model.NewMessage(channel.TeamId, "", userIdToRemove, model.ACTION_USER_REMOVED) - message.Add("channel_id", channel.Id) - message.Add("remover", removerUserId) + message := model.NewMessage(channel.TeamId, channel.Id, userIdToRemove, model.ACTION_USER_REMOVED) + message.Add("remover_id", removerUserId) PublishAndForget(message) return nil diff --git a/api/web_conn.go b/api/web_conn.go index a5099e520..50a003ace 100644 --- a/api/web_conn.go +++ b/api/web_conn.go @@ -92,24 +92,9 @@ func (c *WebConn) writePump() { return } - if len(msg.ChannelId) > 0 { - allowed, ok := c.ChannelAccessCache[msg.ChannelId] - if !ok { - allowed = hasPermissionsToChannel(Srv.Store.Channel().CheckPermissionsTo(c.TeamId, msg.ChannelId, c.UserId)) - c.ChannelAccessCache[msg.ChannelId] = allowed - } - - if allowed { - c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) - if err := c.WebSocket.WriteJSON(msg); err != nil { - return - } - } - } else { - c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) - if err := c.WebSocket.WriteJSON(msg); err != nil { - return - } + c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) + if err := c.WebSocket.WriteJSON(msg); err != nil { + return } case <-ticker.C: @@ -121,9 +106,11 @@ func (c *WebConn) writePump() { } } -func (c *WebConn) updateChannelAccessCache(channelId string) { +func (c *WebConn) updateChannelAccessCache(channelId string) bool { allowed := hasPermissionsToChannel(Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.UserId)) c.ChannelAccessCache[channelId] = allowed + + return allowed } func hasPermissionsToChannel(sc store.StoreChannel) bool { diff --git a/api/web_team_hub.go b/api/web_team_hub.go index c57de550f..6a25b7d3d 100644 --- a/api/web_team_hub.go +++ b/api/web_team_hub.go @@ -53,7 +53,7 @@ func (h *TeamHub) Start() { } case msg := <-h.broadcast: for webCon := range h.connections { - if !(webCon.UserId == msg.UserId && msg.Action == model.ACTION_TYPING) { + if ShouldSendEvent(webCon, msg) { select { case webCon.Send <- msg: default: @@ -86,3 +86,32 @@ func (h *TeamHub) UpdateChannelAccessCache(userId string, channelId string) { } } } + +func ShouldSendEvent(webCon *WebConn, msg *model.Message) bool { + + if webCon.UserId == msg.UserId { + // Don't need to tell the user they are typing + if msg.Action == model.ACTION_TYPING { + return false + } + } else { + // Don't share a user's view events with other users + if msg.Action == model.ACTION_CHANNEL_VIEWED { + return false + } + + // Only report events to a user who is the subject of the event, or is in the channel of the event + if len(msg.ChannelId) > 0 { + allowed, ok := webCon.ChannelAccessCache[msg.ChannelId] + if !ok { + allowed = webCon.updateChannelAccessCache(msg.ChannelId) + } + + if !allowed { + return false + } + } + } + + return true +} diff --git a/model/message.go b/model/message.go index 122af4d9c..2725353ac 100644 --- a/model/message.go +++ b/model/message.go @@ -9,14 +9,14 @@ import ( ) const ( - ACTION_TYPING = "typing" - ACTION_POSTED = "posted" - ACTION_POST_EDITED = "post_edited" - ACTION_POST_DELETED = "post_deleted" - ACTION_VIEWED = "viewed" - ACTION_NEW_USER = "new_user" - ACTION_USER_ADDED = "user_added" - ACTION_USER_REMOVED = "user_removed" + ACTION_TYPING = "typing" + ACTION_POSTED = "posted" + ACTION_POST_EDITED = "post_edited" + ACTION_POST_DELETED = "post_deleted" + ACTION_CHANNEL_VIEWED = "channel_viewed" + ACTION_NEW_USER = "new_user" + ACTION_USER_ADDED = "user_added" + ACTION_USER_REMOVED = "user_removed" ) type Message struct { diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 7582de6c4..1b709336f 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -4,7 +4,6 @@ const ChannelStore = require('../stores/channel_store.jsx'); const UserStore = require('../stores/user_store.jsx'); const PostStore = require('../stores/post_store.jsx'); -const SocketStore = require('../stores/socket_store.jsx'); const NavbarSearchBox = require('./search_bar.jsx'); const AsyncClient = require('../utils/async_client.jsx'); const Client = require('../utils/client.jsx'); @@ -25,7 +24,6 @@ export default class ChannelHeader extends React.Component { super(props); this.onListenerChange = this.onListenerChange.bind(this); - this.onSocketChange = this.onSocketChange.bind(this); this.handleLeave = this.handleLeave.bind(this); this.searchMentions = this.searchMentions.bind(this); @@ -45,7 +43,6 @@ export default class ChannelHeader extends React.Component { ChannelStore.addExtraInfoChangeListener(this.onListenerChange); PostStore.addSearchChangeListener(this.onListenerChange); UserStore.addChangeListener(this.onListenerChange); - SocketStore.addChangeListener(this.onSocketChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); @@ -60,16 +57,9 @@ export default class ChannelHeader extends React.Component { } $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); } - onSocketChange(msg) { - if (msg.action === 'new_user' || - msg.action === 'user_added' || - (msg.action === 'user_removed' && msg.user_id !== UserStore.getCurrentId())) { - AsyncClient.getChannelExtraInfo(true); - } - } handleLeave() { Client.leaveChannel(this.state.channel.id, - function handleLeaveSuccess() { + () => { AppDispatcher.handleViewAction({ type: ActionTypes.LEAVE_CHANNEL, id: this.state.channel.id @@ -77,8 +67,8 @@ export default class ChannelHeader extends React.Component { const townsquare = ChannelStore.getByName('town-square'); Utils.switchChannel(townsquare); - }.bind(this), - function handleLeaveError(err) { + }, + (err) => { AsyncClient.dispatchError(err, 'handleLeave'); } ); diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx index 569942390..216cbc7f0 100644 --- a/web/react/components/msg_typing.jsx +++ b/web/react/components/msg_typing.jsx @@ -1,8 +1,11 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var SocketStore = require('../stores/socket_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); +const SocketStore = require('../stores/socket_store.jsx'); +const UserStore = require('../stores/user_store.jsx'); + +const Constants = require('../utils/constants.jsx'); +const SocketEvents = Constants.SocketEvents; export default class MsgTyping extends React.Component { constructor(props) { @@ -33,7 +36,7 @@ export default class MsgTyping extends React.Component { } onChange(msg) { - if (msg.action === 'typing' && + if (msg.action === SocketEvents.TYPING && this.props.channelId === msg.channel_id && this.props.parentId === msg.props.parent_id) { this.lastTime = new Date().getTime(); @@ -52,7 +55,7 @@ export default class MsgTyping extends React.Component { } }.bind(this), 3000); } - } else if (msg.action === 'posted' && msg.channel_id === this.props.channelId) { + } else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) { this.setState({text: ''}); } } diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 29728d368..4402745e1 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -1,20 +1,24 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var PostStore = require('../stores/post_store.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var PreferenceStore = require('../stores/preference_store.jsx'); -var UserProfile = require('./user_profile.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var Post = require('./post.jsx'); -var LoadingScreen = require('./loading_screen.jsx'); -var SocketStore = require('../stores/socket_store.jsx'); -var utils = require('../utils/utils.jsx'); -var Client = require('../utils/client.jsx'); -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; +const Post = require('./post.jsx'); +const UserProfile = require('./user_profile.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const LoadingScreen = require('./loading_screen.jsx'); + +const PostStore = require('../stores/post_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const SocketStore = require('../stores/socket_store.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); + +const utils = require('../utils/utils.jsx'); +const Client = require('../utils/client.jsx'); +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; +const SocketEvents = Constants.SocketEvents; + +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); export default class PostList extends React.Component { constructor(props) { @@ -58,7 +62,7 @@ export default class PostList extends React.Component { } } - postList.order.sort(function postSort(a, b) { + postList.order.sort((a, b) => { if (postList.posts[a].create_at > postList.posts[b].create_at) { return -1; } @@ -82,7 +86,7 @@ export default class PostList extends React.Component { } return { - postList: postList + postList }; } componentDidMount() { @@ -263,14 +267,14 @@ export default class PostList extends React.Component { Client.getPosts( id, PostStore.getLatestUpdate(id), - function success() { + () => { this.loadInProgress = false; this.setState({isFirstLoadComplete: true}); - }.bind(this), - function fail() { + }, + () => { this.loadInProgress = false; this.setState({isFirstLoadComplete: true}); - }.bind(this) + } ); } onChange() { @@ -281,28 +285,16 @@ export default class PostList extends React.Component { } } onSocketChange(msg) { - var post; - if (msg.action === 'posted' || msg.action === 'post_edited') { - post = JSON.parse(msg.props.post); - PostStore.storePost(post); - } else if (msg.action === 'post_deleted') { + if (msg.action === SocketEvents.POST_DELETED) { var activeRoot = $(document.activeElement).closest('.comment-create-body')[0]; var activeRootPostId = ''; if (activeRoot && activeRoot.id.length > 0) { activeRootPostId = activeRoot.id; } - post = JSON.parse(msg.props.post); - - PostStore.storeUnseenDeletedPost(post); - PostStore.removePost(post, true); - PostStore.emitChange(); - if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) { $('#post_deleted').modal('show'); } - } else if (msg.action === 'new_user') { - AsyncClient.getProfiles(); } } onTimeChange() { @@ -352,7 +344,7 @@ export default class PostList extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Set a description + {'Set a description'}
    ); diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 89506c028..6b582882e 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. const AsyncClient = require('../utils/async_client.jsx'); -const BrowserStore = require('../stores/browser_store.jsx'); const ChannelStore = require('../stores/channel_store.jsx'); const Client = require('../utils/client.jsx'); const Constants = require('../utils/constants.jsx'); @@ -11,7 +10,6 @@ const NewChannelFlow = require('./new_channel_flow.jsx'); const MoreDirectChannels = require('./more_direct_channels.jsx'); const SearchBox = require('./search_bar.jsx'); const SidebarHeader = require('./sidebar_header.jsx'); -const SocketStore = require('../stores/socket_store.jsx'); const TeamStore = require('../stores/team_store.jsx'); const UnreadChannelIndicator = require('./unread_channel_indicator.jsx'); const UserStore = require('../stores/user_store.jsx'); @@ -129,7 +127,6 @@ export default class Sidebar extends React.Component { UserStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); TeamStore.addChangeListener(this.onChange); - SocketStore.addChangeListener(this.onSocketChange); PreferenceStore.addChangeListener(this.onChange); $('.nav-pills__container').perfectScrollbar(); @@ -160,7 +157,6 @@ export default class Sidebar extends React.Component { UserStore.removeChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange); - SocketStore.removeChangeListener(this.onSocketChange); PreferenceStore.removeChangeListener(this.onChange); } onChange() { @@ -169,94 +165,6 @@ export default class Sidebar extends React.Component { this.setState(newState); } } - onSocketChange(msg) { - if (msg.action === 'posted') { - if (ChannelStore.getCurrentId() === msg.channel_id) { - if (window.isActive) { - AsyncClient.updateLastViewedAt(); - } - } else { - AsyncClient.getChannels(); - } - - if (UserStore.getCurrentId() !== msg.user_id) { - var mentions = []; - if (msg.props.mentions) { - mentions = JSON.parse(msg.props.mentions); - } - var channel = ChannelStore.get(msg.channel_id); - - const user = UserStore.getCurrentUser(); - const member = ChannelStore.getMember(msg.channel_id); - - var notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default'; - if (notifyLevel === 'default') { - notifyLevel = user.notify_props.desktop; - } - - if (notifyLevel === 'none') { - return; - } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') { - return; - } - - var username = 'Someone'; - if (UserStore.hasProfile(msg.user_id)) { - username = UserStore.getProfile(msg.user_id).username; - } - - var title = 'Posted'; - if (channel) { - title = channel.display_name; - } - - var repRegex = new RegExp('
    ', 'g'); - var post = JSON.parse(msg.props.post); - var msgProps = msg.props; - var notifyText = post.message.replace(repRegex, '\n').replace(/\n+/g, ' ').replace('', '').replace('', ''); - - if (notifyText.length > 50) { - notifyText = notifyText.substring(0, 49) + '...'; - } - - if (notifyText.length === 0) { - if (msgProps.image) { - Utils.notifyMe(title, username + ' uploaded an image', channel); - } else if (msgProps.otherFile) { - Utils.notifyMe(title, username + ' uploaded a file', channel); - } else { - Utils.notifyMe(title, username + ' did something new', channel); - } - } else { - Utils.notifyMe(title, username + ' wrote: ' + notifyText, channel); - } - if (!user.notify_props || user.notify_props.desktop_sound === 'true') { - Utils.ding(); - } - } - } else if (msg.action === 'viewed') { - if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) { - AsyncClient.getChannel(msg.channel_id); - } - } else if (msg.action === 'user_added') { - if (UserStore.getCurrentId() === msg.user_id) { - AsyncClient.getChannel(msg.channel_id); - } - } else if (msg.action === 'user_removed') { - if (msg.user_id === UserStore.getCurrentId()) { - AsyncClient.getChannels(true); - - if (msg.props.remover !== msg.user_id && msg.props.channel_id === ChannelStore.getCurrentId() && $('#removed_from_channel').length > 0) { - var sentState = {}; - sentState.channelName = ChannelStore.getCurrent().display_name; - sentState.remover = UserStore.getProfile(msg.props.remover).username; - - BrowserStore.setItem('channel-removed-state', sentState); - $('#removed_from_channel').modal('show'); - } - } - } - } updateTitle() { const channel = ChannelStore.getCurrent(); if (channel) { diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 77e7067ad..8645ef3b4 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -1,15 +1,22 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var UserStore = require('./user_store.jsx'); -var ErrorStore = require('./error_store.jsx'); -var EventEmitter = require('events').EventEmitter; +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const UserStore = require('./user_store.jsx'); +const PostStore = require('./post_store.jsx'); +const ChannelStore = require('./channel_store.jsx'); +const BrowserStore = require('./browser_store.jsx'); +const ErrorStore = require('./error_store.jsx'); +const EventEmitter = require('events').EventEmitter; -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; +const Utils = require('../utils/utils.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); -var CHANGE_EVENT = 'change'; +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; +const SocketEvents = Constants.SocketEvents; + +const CHANGE_EVENT = 'change'; var conn; @@ -21,6 +28,7 @@ class SocketStoreClass extends EventEmitter { this.emitChange = this.emitChange.bind(this); this.addChangeListener = this.addChangeListener.bind(this); this.removeChangeListener = this.removeChangeListener.bind(this); + this.handleMessage = this.handleMessage.bind(this); this.sendMessage = this.sendMessage.bind(this); this.failCount = 0; @@ -94,6 +102,39 @@ class SocketStoreClass extends EventEmitter { removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } + handleMessage(msg) { + switch (msg.action) { + case SocketEvents.POSTED: + handleNewPostEvent(msg); + break; + + case SocketEvents.POST_EDITED: + handlePostEditEvent(msg); + break; + + case SocketEvents.POST_DELETED: + handlePostDeleteEvent(msg); + break; + + case SocketEvents.NEW_USER: + handleNewUserEvent(); + break; + + case SocketEvents.USER_ADDED: + handleUserAddedEvent(msg); + break; + + case SocketEvents.USER_REMOVED: + handleUserRemovedEvent(msg); + break; + + case SocketEvents.CHANNEL_VIEWED: + handleChannelViewedEvent(msg); + break; + + default: + } + } sendMessage(msg) { if (conn && conn.readyState === WebSocket.OPEN) { conn.send(JSON.stringify(msg)); @@ -104,6 +145,138 @@ class SocketStoreClass extends EventEmitter { } } +function handleNewPostEvent(msg) { + // Store post + const post = JSON.parse(msg.props.post); + PostStore.storePost(post); + + // Update channel state + if (ChannelStore.getCurrentId() === msg.channel_id) { + if (window.isActive) { + AsyncClient.updateLastViewedAt(); + } + } else { + AsyncClient.getChannel(msg.channel_id); + } + + // Send desktop notification + if (UserStore.getCurrentId() !== msg.user_id) { + const msgProps = msg.props; + + let mentions = []; + if (msgProps.mentions) { + mentions = JSON.parse(msg.props.mentions); + } + + const channel = ChannelStore.get(msg.channel_id); + const user = UserStore.getCurrentUser(); + const member = ChannelStore.getMember(msg.channel_id); + + let notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default'; + if (notifyLevel === 'default') { + notifyLevel = user.notify_props.desktop; + } + + if (notifyLevel === 'none') { + return; + } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') { + return; + } + + let username = 'Someone'; + if (UserStore.hasProfile(msg.user_id)) { + username = UserStore.getProfile(msg.user_id).username; + } + + let title = 'Posted'; + if (channel) { + title = channel.display_name; + } + + let notifyText = post.message.replace(/\n+/g, ' '); + if (notifyText.length > 50) { + notifyText = notifyText.substring(0, 49) + '...'; + } + + if (notifyText.length === 0) { + if (msgProps.image) { + Utils.notifyMe(title, username + ' uploaded an image', channel); + } else if (msgProps.otherFile) { + Utils.notifyMe(title, username + ' uploaded a file', channel); + } else { + Utils.notifyMe(title, username + ' did something new', channel); + } + } else { + Utils.notifyMe(title, username + ' wrote: ' + notifyText, channel); + } + if (!user.notify_props || user.notify_props.desktop_sound === 'true') { + Utils.ding(); + } + } +} + +function handlePostEditEvent(msg) { + // Store post + const post = JSON.parse(msg.props.post); + PostStore.storePost(post); + + // Update channel state + if (ChannelStore.getCurrentId() === msg.channel_id) { + if (window.isActive) { + AsyncClient.updateLastViewedAt(); + } + } +} + +function handlePostDeleteEvent(msg) { + const post = JSON.parse(msg.props.post); + + PostStore.storeUnseenDeletedPost(post); + PostStore.removePost(post, true); + PostStore.emitChange(); +} + +function handleNewUserEvent() { + AsyncClient.getProfiles(); + AsyncClient.getChannelExtraInfo(true); +} + +function handleUserAddedEvent(msg) { + if (ChannelStore.getCurrentId() === msg.channel_id) { + AsyncClient.getChannelExtraInfo(true); + } + + if (UserStore.getCurrentId() === msg.user_id) { + AsyncClient.getChannel(msg.channel_id); + } +} + +function handleUserRemovedEvent(msg) { + if (UserStore.getCurrentId() === msg.user_id) { + AsyncClient.getChannels(); + + if (msg.props.remover_id !== msg.user_id && + msg.channel_id === ChannelStore.getCurrentId() && + $('#removed_from_channel').length > 0) { + var sentState = {}; + sentState.channelName = ChannelStore.getCurrent().display_name; + sentState.remover = UserStore.getProfile(msg.props.remover_id).username; + + BrowserStore.setItem('channel-removed-state', sentState); + $('#removed_from_channel').modal('show'); + } + } else if (ChannelStore.getCurrentId() === msg.channel_id) { + AsyncClient.getChannelExtraInfo(true); + } +} + +function handleChannelViewedEvent(msg) { + // Useful for when multiple devices have the app open to different channels + if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) { + AsyncClient.getChannel(msg.channel_id); + } +} + var SocketStore = new SocketStoreClass(); SocketStore.dispatchToken = AppDispatcher.register((payload) => { @@ -111,6 +284,7 @@ SocketStore.dispatchToken = AppDispatcher.register((payload) => { switch (action.type) { case ActionTypes.RECIEVED_MSG: + SocketStore.handleMessage(action.msg); SocketStore.emitChange(action.msg); break; diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index b7b8d3c60..b2e9bc6a7 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -47,6 +47,18 @@ module.exports = { SERVER_ACTION: null, VIEW_ACTION: null }), + + SocketEvents: { + POSTED: 'posted', + POST_EDITED: 'post_edited', + POST_DELETED: 'post_deleted', + CHANNEL_VIEWED: 'channel_viewed', + NEW_USER: 'new_user', + USER_ADDED: 'user_added', + USER_REMOVED: 'user_removed', + TYPING: 'user_typing' + }, + SPECIAL_MENTIONS: ['all', 'channel'], CHARACTER_LIMIT: 4000, IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'], -- cgit v1.2.3-1-g7c22 From 20723437aa607abdfa0e9353927e8bf8694a6b2c Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Thu, 15 Oct 2015 10:57:09 -0400 Subject: Fix typing socket constant --- web/react/components/msg_typing.jsx | 4 ++-- web/react/utils/constants.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx index 216cbc7f0..1bd23c55c 100644 --- a/web/react/components/msg_typing.jsx +++ b/web/react/components/msg_typing.jsx @@ -37,8 +37,8 @@ export default class MsgTyping extends React.Component { onChange(msg) { if (msg.action === SocketEvents.TYPING && - this.props.channelId === msg.channel_id && - this.props.parentId === msg.props.parent_id) { + this.props.channelId === msg.channel_id && + this.props.parentId === msg.props.parent_id) { this.lastTime = new Date().getTime(); var username = 'Someone'; diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index b2e9bc6a7..5eb8378ca 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -56,7 +56,7 @@ module.exports = { NEW_USER: 'new_user', USER_ADDED: 'user_added', USER_REMOVED: 'user_removed', - TYPING: 'user_typing' + TYPING: 'typing' }, SPECIAL_MENTIONS: ['all', 'channel'], -- cgit v1.2.3-1-g7c22 From 93eaa80a3bfacbb0c4a350474ca3d81b28b5d7ab Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Thu, 15 Oct 2015 11:12:09 -0400 Subject: Remove unnecessary bind --- web/react/stores/socket_store.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 8645ef3b4..77951f214 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -28,7 +28,6 @@ class SocketStoreClass extends EventEmitter { this.emitChange = this.emitChange.bind(this); this.addChangeListener = this.addChangeListener.bind(this); this.removeChangeListener = this.removeChangeListener.bind(this); - this.handleMessage = this.handleMessage.bind(this); this.sendMessage = this.sendMessage.bind(this); this.failCount = 0; -- cgit v1.2.3-1-g7c22 From 6e350cb9b9f5641fc553c55d43c07e1bcd9f2756 Mon Sep 17 00:00:00 2001 From: Joel Vasallo Date: Fri, 16 Oct 2015 11:26:28 -0500 Subject: Updated Email Check Regex to RFC5322 Standard --- web/react/utils/utils.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 561c2c4c4..38ac68d58 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -13,7 +13,8 @@ var client = require('./client.jsx'); var Autolinker = require('autolinker'); export function isEmail(email) { - var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/; + //var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/; + var regex = /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i; return regex.test(email); } -- cgit v1.2.3-1-g7c22 From 6658d61e43e8781141319cc2e2cc02736cc0db33 Mon Sep 17 00:00:00 2001 From: Pat Lathem Date: Fri, 16 Oct 2015 12:54:55 -0500 Subject: Add z-index to command-box so that elements behind it do not show through autocomplete popup --- web/sass-files/sass/partials/_command-box.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/web/sass-files/sass/partials/_command-box.scss b/web/sass-files/sass/partials/_command-box.scss index f1aa4dca2..41e1631b5 100644 --- a/web/sass-files/sass/partials/_command-box.scss +++ b/web/sass-files/sass/partials/_command-box.scss @@ -5,6 +5,7 @@ border: $border-gray; bottom: 38px; overflow: auto; + z-index: 100 @extend %popover-box-shadow; .sidebar--right & { bottom: 100px; -- cgit v1.2.3-1-g7c22 From 4e1e6b6354450cc17e0fe2e0425c245b16a10c6f Mon Sep 17 00:00:00 2001 From: Pat Lathem Date: Fri, 16 Oct 2015 13:04:01 -0500 Subject: Add semi-colon --- web/sass-files/sass/partials/_command-box.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/sass-files/sass/partials/_command-box.scss b/web/sass-files/sass/partials/_command-box.scss index 41e1631b5..184fb55eb 100644 --- a/web/sass-files/sass/partials/_command-box.scss +++ b/web/sass-files/sass/partials/_command-box.scss @@ -5,7 +5,7 @@ border: $border-gray; bottom: 38px; overflow: auto; - z-index: 100 + z-index: 100; @extend %popover-box-shadow; .sidebar--right & { bottom: 100px; -- cgit v1.2.3-1-g7c22 From 09514fb9f5dd2bfcdfaad6c9ff324298e983d1ee Mon Sep 17 00:00:00 2001 From: Reed Garmsen Date: Fri, 16 Oct 2015 14:21:46 -0700 Subject: Added proper recognition of android devices in the Active Sessions UI --- web/react/components/activity_log_modal.jsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 74d6c64e3..2c944913f 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -81,6 +81,7 @@ export default class ActivityLogModal extends React.Component { const currentSession = this.state.sessions[i]; const lastAccessTime = new Date(currentSession.last_activity_at); const firstAccessTime = new Date(currentSession.create_at); + let devicePlatform = currentSession.props.platform; let devicePicture = ''; if (currentSession.props.platform === 'Windows') { @@ -88,7 +89,12 @@ export default class ActivityLogModal extends React.Component { } else if (currentSession.props.platform === 'Macintosh' || currentSession.props.platform === 'iPhone') { devicePicture = 'fa fa-apple'; } else if (currentSession.props.platform === 'Linux') { - devicePicture = 'fa fa-linux'; + if (currentSession.props.os.indexOf('Android') >= 0) { + devicePlatform = 'Android'; + devicePicture = 'fa fa-android'; + } else { + devicePicture = 'fa fa-linux'; + } } let moreInfo; @@ -119,7 +125,7 @@ export default class ActivityLogModal extends React.Component { className='activity-log__table' >
    -
    {currentSession.props.platform}
    +
    {devicePlatform}
    {`Last activity: ${lastAccessTime.toDateString()}, ${lastAccessTime.toLocaleTimeString()}`}
    {moreInfo} -- cgit v1.2.3-1-g7c22 From 5f45556b433b5787b79fd02dd4b16181a6b25805 Mon Sep 17 00:00:00 2001 From: Reed Garmsen Date: Fri, 16 Oct 2015 14:50:45 -0700 Subject: Fixes back button on username page during team signup when email is turned off --- web/react/components/team_signup_username_page.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx index fa8a031a0..21e76e2b8 100644 --- a/web/react/components/team_signup_username_page.jsx +++ b/web/react/components/team_signup_username_page.jsx @@ -15,7 +15,12 @@ export default class TeamSignupUsernamePage extends React.Component { } submitBack(e) { e.preventDefault(); - this.props.state.wizard = 'send_invites'; + if (global.window.config.SendEmailNotifications === 'true') { + this.props.state.wizard = 'send_invites'; + } else { + this.props.state.wizard = 'team_url'; + } + this.props.updateParent(this.props.state); } submitNext(e) { -- cgit v1.2.3-1-g7c22 From aa40f88bbbca68089e8fad144f590f27e3afa3da Mon Sep 17 00:00:00 2001 From: Florian Orben Date: Sat, 17 Oct 2015 02:52:46 +0200 Subject: PLT-463: Remove AUTHOR from YouTube preview not helpful and takes up too much room --- web/react/components/post_body.jsx | 8 +------- web/sass-files/sass/partials/_videos.scss | 7 +------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 1db0b12e7..fb838b736 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -116,7 +116,7 @@ export default class PostBody extends React.Component { } var metadata = data.items[0].snippet; this.receivedYoutubeData = true; - this.setState({youtubeUploader: metadata.channelTitle, youtubeTitle: metadata.title}); + this.setState({youtubeTitle: metadata.title}); } if (global.window.config.GoogleDeveloperKey && !this.receivedYoutubeData) { @@ -134,18 +134,12 @@ export default class PostBody extends React.Component { header = header + ' - '; } - let uploader = this.state.youtubeUploader; - if (!uploader) { - uploader = 'unknown'; - } - return (

    {header} {this.state.youtubeTitle}

    -

    {uploader}

    Date: Sun, 18 Oct 2015 20:03:34 +0500 Subject: Updating userEntries div to tr td --- web/react/components/more_direct_channels.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index e99cdc257..105199035 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -220,7 +220,7 @@ export default class MoreDirectChannels extends React.Component { const userEntries = users.map(this.createRowForUser); if (userEntries.length === 0) { - userEntries.push(
    {'No users found :('}
    ); + userEntries.push({'No users found :('}); } let memberString = 'Member'; -- cgit v1.2.3-1-g7c22 From 9fa3c996d522589139f56964087022bd0942e5e4 Mon Sep 17 00:00:00 2001 From: Stas Vovk Date: Sun, 18 Oct 2015 18:55:04 +0300 Subject: change url when a user navigates between tabs [#962] --- web/react/components/admin_console/admin_controller.jsx | 12 +++++++++--- web/react/components/admin_console/admin_sidebar.jsx | 1 + web/react/pages/admin_console.jsx | 7 +++++-- web/templates/admin_console.html | 2 +- web/web.go | 10 ++++++++++ 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index f2fb8ac78..f770d166c 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -40,9 +40,13 @@ export default class AdminController extends React.Component { config: AdminStore.getConfig(), teams: AdminStore.getAllTeams(), selectedTeams, - selected: 'service_settings', - selectedTeam: null + selected: props.tab || 'service_settings', + selectedTeam: props.teamId || null }; + + if (!props.tab) { + history.replaceState(null, null, `/admin_console/${this.state.selected}`); + } } componentDidMount() { @@ -142,7 +146,9 @@ export default class AdminController extends React.Component { } else if (this.state.selected === 'service_settings') { tab = ; } else if (this.state.selected === 'team_users') { - tab = ; + if (this.state.teams) { + tab = ; + } } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 4c2a473b6..c7faa83fe 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -24,6 +24,7 @@ export default class AdminSidebar extends React.Component { handleClick(name, teamId, e) { e.preventDefault(); this.props.selectTab(name, teamId); + history.pushState({name: name, teamId: teamId}, null, `/admin_console/${name}/${teamId || ''}`); } isSelected(name, teamId) { diff --git a/web/react/pages/admin_console.jsx b/web/react/pages/admin_console.jsx index c89cb4edc..ea9ae06f4 100644 --- a/web/react/pages/admin_console.jsx +++ b/web/react/pages/admin_console.jsx @@ -5,9 +5,12 @@ var ErrorBar = require('../components/error_bar.jsx'); var SelectTeamModal = require('../components/admin_console/select_team_modal.jsx'); var AdminController = require('../components/admin_console/admin_controller.jsx'); -export function setupAdminConsolePage() { +export function setupAdminConsolePage(props) { ReactDOM.render( - , + , document.getElementById('admin_controller') ); diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html index a046478f6..574caf730 100644 --- a/web/templates/admin_console.html +++ b/web/templates/admin_console.html @@ -12,7 +12,7 @@