diff --git a/api/channel.go b/api/channel.go index 0cc7aae0c97..37c0d4941c6 100644 --- a/api/channel.go +++ b/api/channel.go @@ -1257,12 +1257,11 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - if len(view.ChannelId) == 0 { - ReturnStatusOK(w) - return - } + channelIds := []string{} - channelIds := []string{view.ChannelId} + if len(view.ChannelId) > 0 { + channelIds = append(channelIds, view.ChannelId) + } var pchan store.StoreChannel if len(view.PrevChannelId) > 0 { @@ -1273,6 +1272,11 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) { } } + if len(channelIds) == 0 { + ReturnStatusOK(w) + return + } + uchan := Srv.Store.Channel().UpdateLastViewedAt(channelIds, c.Session.UserId) if pchan != nil { @@ -1291,10 +1295,6 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, c.TeamId, "", c.Session.UserId, nil) - message.Add("channel_id", view.ChannelId) - go Publish(message) - ReturnStatusOK(w) } diff --git a/api/deprecated.go b/api/deprecated.go index 4865ab5e015..bad4d49bfb8 100644 --- a/api/deprecated.go +++ b/api/deprecated.go @@ -99,11 +99,6 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref}) - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, c.TeamId, "", c.Session.UserId, nil) - message.Add("channel_id", id) - - go Publish(message) - result := make(map[string]string) result["id"] = id w.Write([]byte(model.MapToJson(result))) @@ -134,11 +129,6 @@ func setLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref}) - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, c.TeamId, "", c.Session.UserId, nil) - message.Add("channel_id", id) - - go Publish(message) - result := make(map[string]string) result["id"] = id w.Write([]byte(model.MapToJson(result))) diff --git a/model/websocket_message.go b/model/websocket_message.go index 5c956d5763d..cfbc51ed9a6 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -14,7 +14,6 @@ const ( WEBSOCKET_EVENT_POST_EDITED = "post_edited" WEBSOCKET_EVENT_POST_DELETED = "post_deleted" WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted" - WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed" WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added" WEBSOCKET_EVENT_NEW_USER = "new_user" WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team" diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx index 0222d426b14..1d745959c6e 100644 --- a/webapp/actions/global_actions.jsx +++ b/webapp/actions/global_actions.jsx @@ -47,16 +47,17 @@ export function emitChannelClickEvent(channel) { function switchToChannel(chan) { const channelMember = ChannelStore.getMyMember(chan.id); const getMyChannelMembersPromise = AsyncClient.getChannelMember(chan.id, UserStore.getCurrentId()); + const oldChannelId = ChannelStore.getCurrentId(); getMyChannelMembersPromise.then(() => { AsyncClient.getChannelStats(chan.id, true); - AsyncClient.viewChannel(chan.id, ChannelStore.getCurrentId()); + AsyncClient.viewChannel(chan.id, oldChannelId); loadPosts(chan.id); trackPage(); }); // Mark previous and next channel as read - ChannelStore.resetCounts(ChannelStore.getCurrentId()); + ChannelStore.resetCounts(oldChannelId); ChannelStore.resetCounts(chan.id); BrowserStore.setGlobalItem(chan.team_id, chan.id); @@ -68,7 +69,7 @@ export function emitChannelClickEvent(channel) { team_id: chan.team_id, total_msg_count: chan.total_msg_count, channelMember, - prev: ChannelStore.getCurrentId() + prev: oldChannelId }); } diff --git a/webapp/actions/post_actions.jsx b/webapp/actions/post_actions.jsx index 2bce13ce005..a0ef58f4f67 100644 --- a/webapp/actions/post_actions.jsx +++ b/webapp/actions/post_actions.jsx @@ -5,7 +5,6 @@ import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import PostStore from 'stores/post_store.jsx'; -import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; import {loadStatusesForChannel} from 'actions/status_actions.jsx'; @@ -19,30 +18,11 @@ const ActionTypes = Constants.ActionTypes; const Preferences = Constants.Preferences; export function handleNewPost(post, msg) { - const teamId = TeamStore.getCurrentId(); - - if (ChannelStore.getCurrentId() === post.channel_id) { - if (window.isActive) { - AsyncClient.viewChannel(); - } else { - AsyncClient.getChannel(post.channel_id); - } - } else if (msg && (teamId === msg.data.team_id || msg.data.channel_type === Constants.DM_CHANNEL)) { - if (Client.teamId) { - AsyncClient.getChannel(post.channel_id); - } - } - let websocketMessageProps = null; if (msg) { websocketMessageProps = msg.data; } - const myTeams = TeamStore.getMyTeamMembers(); - if (msg.data.team_id !== teamId && myTeams.filter((m) => m.team_id === msg.data.team_id).length) { - AsyncClient.getMyTeamsUnread(teamId); - } - if (msg && msg.data && msg.data.channel_type === Constants.DM_CHANNEL) { loadNewDMIfNeeded(post.user_id); } diff --git a/webapp/actions/team_actions.jsx b/webapp/actions/team_actions.jsx index 3a86bada9f1..3f43f060dc0 100644 --- a/webapp/actions/team_actions.jsx +++ b/webapp/actions/team_actions.jsx @@ -92,3 +92,8 @@ export function updateTeamMemberRoles(teamId, userId, newRoles, success, error) } ); } + +export function switchTeams(url) { + AsyncClient.viewChannel(); + browserHistory.push(url); +} diff --git a/webapp/actions/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx index 1a0ddda6304..714c44c2caa 100644 --- a/webapp/actions/websocket_actions.jsx +++ b/webapp/actions/websocket_actions.jsx @@ -136,10 +136,6 @@ function handleEvent(msg) { handleUserUpdatedEvent(msg); break; - case SocketEvents.CHANNEL_VIEWED: - handleChannelViewedEvent(msg); - break; - case SocketEvents.CHANNEL_DELETED: handleChannelDeletedEvent(msg); break; @@ -281,15 +277,6 @@ function handleUserUpdatedEvent(msg) { } } -function handleChannelViewedEvent(msg) { - // Useful for when multiple devices have the app open to different channels - if (TeamStore.getCurrentId() === msg.broadcast.team_id && - ChannelStore.getCurrentId() !== msg.data.channel_id && - UserStore.getCurrentId() === msg.broadcast.user_id) { - AsyncClient.getChannel(msg.data.channel_id); - } -} - function handleChannelDeletedEvent(msg) { if (ChannelStore.getCurrentId() === msg.data.channel_id) { const teamUrl = TeamStore.getCurrentTeamRelativeUrl(); diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index ba42d7ae8fd..d51afa4ba3b 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -1469,6 +1469,15 @@ export default class Client { end(this.handleResponse.bind(this, 'getMyChannelMembers', success, error)); } + getMyChannelMembersForTeam(teamId, success, error) { + request. + get(`${this.getTeamsRoute()}/${teamId}/channels/members`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getMyChannelMembersForTeam', success, error)); + } + getChannelByName(channelName, success, error) { request. get(`${this.getChannelsRoute()}/name/${channelName}`). diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx index 0b91814c3b3..fb6029c2b38 100644 --- a/webapp/components/needs_team.jsx +++ b/webapp/components/needs_team.jsx @@ -42,6 +42,8 @@ import SelectTeamModal from 'components/admin_console/select_team_modal.jsx'; import iNoBounce from 'inobounce'; import * as UserAgent from 'utils/user_agent.jsx'; +const UNREAD_CHECK_TIME_MILLISECONDS = 10000; + export default class NeedsTeam extends React.Component { constructor(params) { super(params); @@ -49,6 +51,8 @@ export default class NeedsTeam extends React.Component { this.onTeamChanged = this.onTeamChanged.bind(this); this.onPreferencesChanged = this.onPreferencesChanged.bind(this); + this.blurTime = new Date().getTime(); + const team = TeamStore.getCurrent(); this.state = { @@ -97,11 +101,16 @@ export default class NeedsTeam extends React.Component { AsyncClient.viewChannel(); ChannelStore.resetCounts(ChannelStore.getCurrentId()); ChannelStore.emitChange(); + window.isActive = true; + if (new Date().getTime() - this.blurTime > UNREAD_CHECK_TIME_MILLISECONDS) { + AsyncClient.getMyChannelMembers(); + } }); $(window).on('blur', () => { window.isActive = false; + this.blurTime = new Date().getTime(); if (UserStore.getCurrentUser()) { AsyncClient.viewChannel(''); } diff --git a/webapp/components/post_view/post_view_cache.jsx b/webapp/components/post_view/post_view_cache.jsx index 7de11d6673f..5cf5b3094ca 100644 --- a/webapp/components/post_view/post_view_cache.jsx +++ b/webapp/components/post_view/post_view_cache.jsx @@ -32,7 +32,7 @@ export default class PostViewCache extends React.Component { componentWillUnmount() { if (UserStore.getCurrentUser()) { - AsyncClient.viewChannel(''); + AsyncClient.viewChannel('', this.state.currentChannelId || ''); } ChannelStore.removeChangeListener(this.onChannelChange); } diff --git a/webapp/components/team_sidebar/components/team_button.jsx b/webapp/components/team_sidebar/components/team_button.jsx index 2df21b20b75..6fbf8aef944 100644 --- a/webapp/components/team_sidebar/components/team_button.jsx +++ b/webapp/components/team_sidebar/components/team_button.jsx @@ -3,6 +3,8 @@ import Constants from 'utils/constants.jsx'; +import {switchTeams} from 'actions/team_actions.jsx'; + import React from 'react'; import {Link} from 'react-router/es6'; import {Tooltip, OverlayTrigger} from 'react-bootstrap'; @@ -11,9 +13,15 @@ export default class TeamButton extends React.Component { constructor(props) { super(props); + this.handleSwitch = this.handleSwitch.bind(this); this.handleDisabled = this.handleDisabled.bind(this); } + handleSwitch(e) { + e.preventDefault(); + switchTeams(this.props.url); + } + handleDisabled(e) { e.preventDefault(); } @@ -22,7 +30,7 @@ export default class TeamButton extends React.Component { let teamClass = this.props.active ? 'active' : ''; const btnClass = this.props.btnClass; const disabled = this.props.disabled ? 'team-disabled' : ''; - const handleClick = (this.props.active || this.props.disabled) ? this.handleDisabled : null; + const handleClick = (this.props.active || this.props.disabled) ? this.handleDisabled : this.handleSwitch; let badge; if (!teamClass) { diff --git a/webapp/root.jsx b/webapp/root.jsx index b8fa4e6a2f2..98c7444772a 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -11,7 +11,10 @@ import PDFJS from 'pdfjs-dist'; import * as GlobalActions from 'actions/global_actions.jsx'; import * as Websockets from 'actions/websocket_actions.jsx'; import BrowserStore from 'stores/browser_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import UserStore from 'stores/user_store.jsx'; import * as I18n from 'i18n/i18n.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; // Import our styles import 'bootstrap-colorpicker/dist/css/bootstrap-colorpicker.css'; @@ -58,6 +61,9 @@ function preRenderSetup(callwhendone) { $(window).on('beforeunload', () => { BrowserStore.setLastServerVersion(''); + if (UserStore.getCurrentUser()) { + AsyncClient.viewChannel('', ChannelStore.getCurrentId() || ''); + } Websockets.close(); } ); diff --git a/webapp/routes/route_team.jsx b/webapp/routes/route_team.jsx index 41b3372437f..b4d9e068a2b 100644 --- a/webapp/routes/route_team.jsx +++ b/webapp/routes/route_team.jsx @@ -34,7 +34,7 @@ function doChannelChange(state, replace, callback) { } else { channel = ChannelStore.getByName(state.params.channel); - if (channel.type === Constants.DM_CHANNEL) { + if (channel && channel.type === Constants.DM_CHANNEL) { loadNewDMIfNeeded(Utils.getUserIdFromChannelName(channel)); } @@ -106,7 +106,12 @@ function preNeedsTeam(nextState, replace, callback) { if (nextState.location.pathname.indexOf('/channels/') > -1 || nextState.location.pathname.indexOf('/pl/') > -1) { AsyncClient.getMyTeamsUnread(); - AsyncClient.getMyChannelMembers(); + const teams = TeamStore.getAll(); + for (const id in teams) { + if (teams.hasOwnProperty(id)) { + AsyncClient.getMyChannelMembersForTeam(id); + } + } } const d1 = $.Deferred(); //eslint-disable-line new-cap diff --git a/webapp/stores/channel_store.jsx b/webapp/stores/channel_store.jsx index c93edf7f4f0..492ec1fcae2 100644 --- a/webapp/stores/channel_store.jsx +++ b/webapp/stores/channel_store.jsx @@ -5,9 +5,11 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import EventEmitter from 'events'; import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; var Utils; import {ActionTypes, Constants} from 'utils/constants.jsx'; +import {isSystemMessage} from 'utils/post_utils.jsx'; const NotificationPrefs = Constants.NotificationPrefs; const CHANGE_EVENT = 'change'; @@ -343,6 +345,41 @@ class ChannelStoreClass extends EventEmitter { return channelNamesMap; } + + incrementMessages(id, markRead = false) { + if (!this.unreadCounts[id]) { + return; + } + + const member = this.getMyMember(id); + if (member && member.notify_props && member.notify_props.mark_unread === NotificationPrefs.MENTION) { + return; + } + + this.get(id).total_msg_count++; + + if (markRead) { + this.resetCounts(id); + } else { + this.unreadCounts[id].msgs++; + } + } + + incrementMentionsIfNeeded(id, msgProps) { + let mentions = []; + if (msgProps && msgProps.mentions) { + mentions = JSON.parse(msgProps.mentions); + } + + if (!this.unreadCounts[id]) { + return; + } + + if (mentions.indexOf(UserStore.getCurrentId()) !== -1) { + this.unreadCounts[id].mentions++; + this.getMyMember(id).mention_count++; + } + } } var ChannelStore = new ChannelStoreClass(); @@ -417,6 +454,36 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { ChannelStore.emitStatsChange(); break; + case ActionTypes.RECEIVED_POST: + if (action.post.type === Constants.POST_TYPE_JOIN_LEAVE) { + return; + } + + if (action.post.user_id === UserStore.getCurrentId() && !isSystemMessage(action.post)) { + return; + } + + var id = action.post.channel_id; + var teamId = action.websocketMessageProps ? action.websocketMessageProps.team_id : null; + var markRead = id === ChannelStore.getCurrentId() && window.isActive; + + if (TeamStore.getCurrentId() === teamId || teamId === '') { + ChannelStore.incrementMentionsIfNeeded(id, action.websocketMessageProps); + ChannelStore.incrementMessages(id, markRead); + ChannelStore.emitChange(); + } + break; + + case ActionTypes.CREATE_POST: + ChannelStore.incrementMessages(action.post.channel_id, true); + ChannelStore.emitChange(); + break; + + case ActionTypes.CREATE_COMMENT: + ChannelStore.incrementMessages(action.post.channel_id, true); + ChannelStore.emitChange(); + break; + default: break; } diff --git a/webapp/stores/team_store.jsx b/webapp/stores/team_store.jsx index b2cb3ad260c..c08a3c2d495 100644 --- a/webapp/stores/team_store.jsx +++ b/webapp/stores/team_store.jsx @@ -4,8 +4,10 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import EventEmitter from 'events'; import UserStore from 'stores/user_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; import Constants from 'utils/constants.jsx'; +const NotificationPrefs = Constants.NotificationPrefs; const ActionTypes = Constants.ActionTypes; const CHANGE_EVENT = 'change'; @@ -321,6 +323,28 @@ class TeamStoreClass extends EventEmitter { member.mention_count -= channelMember.mention_count; } } + + incrementMessages(id, channelId) { + const channelMember = ChannelStore.getMyMember(channelId); + if (channelMember && channelMember.notify_props && channelMember.notify_props.mark_unread === NotificationPrefs.MENTION) { + return; + } + + const member = this.my_team_members.filter((m) => m.team_id === id)[0]; + member.msg_count++; + } + + incrementMentionsIfNeeded(id, msgProps) { + let mentions = []; + if (msgProps && msgProps.mentions) { + mentions = JSON.parse(msgProps.mentions); + } + + if (mentions.indexOf(UserStore.getCurrentId()) !== -1) { + const member = this.my_team_members.filter((m) => m.team_id === id)[0]; + member.mention_count++; + } + } } var TeamStore = new TeamStoreClass(); @@ -375,6 +399,18 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => { TeamStore.emitUnreadChange(); } break; + case ActionTypes.RECEIVED_POST: + if (action.post.type === Constants.POST_TYPE_JOIN_LEAVE) { + return; + } + + var id = action.websocketMessageProps ? action.websocketMessageProps.team_id : null; + if (id && TeamStore.getCurrentId() !== id) { + TeamStore.incrementMessages(id, action.post.channel_id); + TeamStore.incrementMentionsIfNeeded(id, action.websocketMessageProps); + TeamStore.emitChange(); + } + break; default: } }); diff --git a/webapp/tests/client_channel.test.jsx b/webapp/tests/client_channel.test.jsx index 02d014a1fc1..154f70fef1e 100644 --- a/webapp/tests/client_channel.test.jsx +++ b/webapp/tests/client_channel.test.jsx @@ -352,6 +352,21 @@ describe('Client.Channels', function() { }); }); + it('getMyChannelMembersForTeam', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().getMyChannelMembersForTeam( + TestHelper.basicTeam().id, + function(data) { + assert.equal(data.length > 0, true); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); + it('getChannelStats', function(done) { TestHelper.initBasic(() => { TestHelper.basicClient().getChannelStats( diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index 25724ec5e8b..55377866b64 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -138,6 +138,35 @@ export function getMyChannelMembers() { }); } +export function getMyChannelMembersForTeam(teamId) { + return new Promise((resolve, reject) => { + if (isCallInProgress(`getMyChannelMembers${teamId}`)) { + resolve(); + return; + } + + callTracker[`getMyChannelMembers${teamId}`] = utils.getTimestamp(); + + Client.getMyChannelMembersForTeam( + teamId, + (data) => { + callTracker[`getMyChannelMembers${teamId}`] = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_MY_CHANNEL_MEMBERS, + members: data + }); + resolve(); + }, + (err) => { + callTracker[`getMyChannelMembers${teamId}`] = 0; + dispatchError(err, 'getMyChannelMembersForTeam'); + reject(); + } + ); + }); +} + export function viewChannel(channelId = ChannelStore.getCurrentId(), prevChannelId = '', time = 0) { if (channelId == null || !Client.teamId) { return;