diff --git a/app/components/channel_list/channel_list.js b/app/components/channel_list/channel_list.js index d97eb16e5..76a124fa5 100644 --- a/app/components/channel_list/channel_list.js +++ b/app/components/channel_list/channel_list.js @@ -78,6 +78,7 @@ class ChannelList extends Component { onSelectChannel: PropTypes.func.isRequired, actions: PropTypes.shape({ viewChannel: PropTypes.func.isRequired, + markChannelAsRead: PropTypes.func.isRequired, closeDMChannel: PropTypes.func.isRequired, leaveChannel: PropTypes.func.isRequired, markFavorite: PropTypes.func.isRequired, @@ -162,7 +163,8 @@ class ChannelList extends Component { } = this.props; this.props.onSelectChannel(channel.id); - this.props.actions.viewChannel(currentTeam.id, channel.id, currentChannel.id); + this.props.actions.viewChannel(currentTeam.id, channel.id); + this.props.actions.markChannelAsRead(channel.id, currentChannel.id); }; handleClose = (channel) => { @@ -297,7 +299,7 @@ class ChannelList extends Component { mentions = member.mention_count; unreadCount = channel.total_msg_count - member.msg_count; - if (member.notify_props && member.notify_props.mark_unread === 'mention') { + if (member.notify_props && member.notify_props.mark_unread === Constants.MENTION) { unreadCount = 0; } } diff --git a/app/components/channel_list/channel_list_container.js b/app/components/channel_list/channel_list_container.js index e2abf5b02..51497c2e8 100644 --- a/app/components/channel_list/channel_list_container.js +++ b/app/components/channel_list/channel_list_container.js @@ -7,7 +7,7 @@ import {connect} from 'react-redux'; import {showDirectMessagesModal, showOptionsModal, closeModal} from 'app/actions/navigation'; import {closeDMChannel, leaveChannel, markFavorite, unmarkFavorite} from 'app/actions/views/channel'; -import {viewChannel} from 'service/actions/channels'; +import {viewChannel, markChannelAsRead} from 'service/actions/channels'; import ChannelList from './channel_list'; function mapStateToProps(state, ownProps) { @@ -20,6 +20,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ viewChannel, + markChannelAsRead, closeDMChannel, leaveChannel, markFavorite, diff --git a/app/scenes/channel_drawer/channel_drawer_container.js b/app/scenes/channel_drawer/channel_drawer_container.js index 5d60b30da..13a6ae2a8 100644 --- a/app/scenes/channel_drawer/channel_drawer_container.js +++ b/app/scenes/channel_drawer/channel_drawer_container.js @@ -5,9 +5,8 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {closeDrawers} from 'app/actions/navigation'; -import {closeDMChannel, handleSelectChannel, leaveChannel, markFavorite, unmarkFavorite} from 'app/actions/views/channel'; +import {handleSelectChannel} from 'app/actions/views/channel'; -import {viewChannel} from 'service/actions/channels'; import {getChannelsByCategory, getCurrentChannel} from 'service/selectors/entities/channels'; import {getTheme} from 'service/selectors/entities/preferences'; import {getCurrentTeam} from 'service/selectors/entities/teams'; @@ -27,12 +26,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - viewChannel, - closeDMChannel, closeDrawers, - leaveChannel, - markFavorite, - unmarkFavorite, handleSelectChannel }, dispatch) }; diff --git a/service/actions/channels.js b/service/actions/channels.js index cd7e951b0..b862922cc 100644 --- a/service/actions/channels.js +++ b/service/actions/channels.js @@ -407,8 +407,16 @@ export function deleteChannel(teamId, channelId) { }; } -export function viewChannel(teamId, channelId, prevChannelId = '') { +export function viewChannel(teamId, channelId) { return async (dispatch, getState) => { + const state = getState(); + const {currentId} = state.entities.channels; + let prevChannelId = ''; + + if (channelId !== currentId) { + prevChannelId = currentId; + } + dispatch({type: ChannelTypes.UPDATE_LAST_VIEWED_REQUEST}, getState); try { @@ -420,41 +428,7 @@ export function viewChannel(teamId, channelId, prevChannelId = '') { return; } - const {channels} = getState().entities.channels; - let totalMsgCount = 0; - if (channels[channelId]) { - totalMsgCount = channels[channelId].total_msg_count; - } - const actions = [{ - type: ChannelTypes.RECEIVED_LAST_VIEWED, - data: { - channel_id: channelId, - last_viewed_at: new Date().getTime(), - total_msg_count: totalMsgCount - } - }]; - - if (prevChannelId) { - let prevTotalMsgCount = 0; - if (channels[channelId]) { - prevTotalMsgCount = channels[channelId].total_msg_count; - } - actions.push({ - type: ChannelTypes.RECEIVED_LAST_VIEWED, - data: { - channel_id: prevChannelId, - last_viewed_at: new Date().getTime(), - total_msg_count: prevTotalMsgCount - } - }); - } - - dispatch(batchActions([ - ...actions, - { - type: ChannelTypes.UPDATE_LAST_VIEWED_SUCCESS - } - ]), getState); + dispatch({type: ChannelTypes.UPDATE_LAST_VIEWED_SUCCESS}, getState); }; } @@ -582,6 +556,76 @@ export function updateChannelPurpose(channelId, purpose) { }; } +export function markChannelAsRead(channelId, prevChannelId) { + return async (dispatch, getState) => { + const state = getState(); + + const {channels} = state.entities.channels; + let totalMsgCount = 0; + if (channels[channelId]) { + totalMsgCount = channels[channelId].total_msg_count; + } + const actions = [{ + type: ChannelTypes.RECEIVED_LAST_VIEWED, + data: { + channel_id: channelId, + last_viewed_at: new Date().getTime(), + total_msg_count: totalMsgCount + } + }]; + + if (prevChannelId) { + let prevTotalMsgCount = 0; + if (channels[prevChannelId]) { + prevTotalMsgCount = channels[prevChannelId].total_msg_count; + } + actions.push({ + type: ChannelTypes.RECEIVED_LAST_VIEWED, + data: { + channel_id: prevChannelId, + last_viewed_at: new Date().getTime(), + total_msg_count: prevTotalMsgCount + } + }); + } + + dispatch(batchActions([...actions]), getState); + }; +} + +export function markChannelAsUnread(channelId, mentionsArray) { + return async (dispatch, getState) => { + const state = getState(); + const {channels, myMembers} = state.entities.channels; + const currentUserId = state.entities.users.currentId; + const channel = {...channels[channelId]}; + const member = {...myMembers[channelId]}; + + if (channel && member) { + channel.total_msg_count++; + if (member.notify_props && member.notify_props.mark_unread === Constants.MENTION) { + member.msg_count++; + } + + let mentions = []; + if (mentionsArray) { + mentions = JSON.parse(mentionsArray); + if (mentions.indexOf(currentUserId) !== -1) { + member.mention_count++; + } + } + + dispatch(batchActions([{ + type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, + data: member + }, { + type: ChannelTypes.RECEIVED_CHANNEL, + data: channel + }]), getState); + } + }; +} + export default { selectChannel, createChannel, @@ -600,5 +644,7 @@ export default { addChannelMember, removeChannelMember, updateChannelHeader, - updateChannelPurpose + updateChannelPurpose, + markChannelAsRead, + markChannelAsUnread }; diff --git a/service/actions/general.js b/service/actions/general.js index 0b2459c8d..2d0ef4a4a 100644 --- a/service/actions/general.js +++ b/service/actions/general.js @@ -4,6 +4,7 @@ import Client from 'service/client'; import {bindClientFunc} from './helpers.js'; import {GeneralTypes} from 'service/constants'; +import {getMyChannelMembers} from './channels'; export function getPing() { return bindClientFunc( @@ -46,6 +47,13 @@ export function logClientError(message, level = 'ERROR') { export function setAppState(state) { return async (dispatch, getState) => { dispatch({type: GeneralTypes.RECEIVED_APP_STATE, data: state}, getState); + + if (state) { + const teamId = getState().entities.teams.currentId; + if (teamId) { + getMyChannelMembers(teamId)(dispatch, getState); + } + } }; } diff --git a/service/actions/websocket.js b/service/actions/websocket.js index 3be7a1cd3..2a9d28033 100644 --- a/service/actions/websocket.js +++ b/service/actions/websocket.js @@ -19,9 +19,10 @@ import { fetchMyChannelsAndMembers, getChannel, getChannelStats, - viewChannel, updateChannelHeader, - updateChannelPurpose + updateChannelPurpose, + markChannelAsUnread, + markChannelAsRead } from 'service/actions/channels'; import { @@ -124,9 +125,6 @@ function handleEvent(msg, dispatch, getState) { case WebsocketEvents.USER_UPDATED: handleUserUpdatedEvent(msg, dispatch, getState); break; - case WebsocketEvents.CHANNEL_VIEWED: - handleChannelViewedEvent(msg, dispatch, getState); - break; case WebsocketEvents.CHANNEL_DELETED: handleChannelDeletedEvent(msg, dispatch, getState); break; @@ -142,13 +140,11 @@ function handleEvent(msg, dispatch, getState) { } } -function handleNewPostEvent(msg, dispatch, getState) { +async function handleNewPostEvent(msg, dispatch, getState) { const state = getState(); + const currentChannelId = state.entities.channels.currentId; const users = state.entities.users; - const channels = state.entities.channels; - const teams = state.entities.teams; const {posts} = state.entities.posts; - const isActive = state.entities.general.appState; const post = JSON.parse(msg.data.post); const userId = post.user_id; const teamId = msg.data.team_id; @@ -162,26 +158,17 @@ function handleNewPostEvent(msg, dispatch, getState) { getStatusesByIds([userId])(dispatch, getState); } - if (post.channel_id === channels.currentId) { - if (isActive) { - viewChannel(teamId, post.channel_id)(dispatch, getState); - switch (post.type) { - case Constants.POST_HEADER_CHANGE: - updateChannelHeader(post.channel_id, post.props.new_header)(dispatch, getState); - break; - case Constants.POST_PURPOSE_CHANGE: - updateChannelPurpose(post.channel_id, post.props.new_purpose)(dispatch, getState); - break; - } - } else { - getChannel(teamId, post.channel_id)(dispatch, getState); - } - } else if (teamId === teams.currentId || msg.data.channel_type === Constants.DM_CHANNEL) { - getChannel(teamId, post.channel_id)(dispatch, getState); + switch (post.type) { + case Constants.POST_HEADER_CHANGE: + updateChannelHeader(post.channel_id, post.props.new_header)(dispatch, getState); + break; + case Constants.POST_PURPOSE_CHANGE: + updateChannelPurpose(post.channel_id, post.props.new_purpose)(dispatch, getState); + break; } if (post.root_id && !posts[post.root_id]) { - Client.getPost(teamId, post.channel_id, post.root_id).then((data) => { + await Client.getPost(teamId, post.channel_id, post.root_id).then((data) => { const rootUserId = data.posts[post.root_id].user_id; const rootStatus = users.statuses[rootUserId]; if (!users.profiles[rootUserId]) { @@ -210,20 +197,18 @@ function handleNewPostEvent(msg, dispatch, getState) { }, channelId: post.channel_id }, getState); + + if (userId === users.currentId || post.channel_id === currentChannelId) { + markChannelAsRead(post.channel_id); + } else { + markChannelAsUnread(post.channel_id, msg.data.mentions)(dispatch, getState); + } } function handlePostEdited(msg, dispatch, getState) { - const state = getState(); - const channels = state.entities.channels; - const isActive = state.entities.general.appState; const data = JSON.parse(msg.data.post); dispatch({type: PostsTypes.RECEIVED_POST, data}, getState); - - if (msg.broadcast.channel_id === channels.currentId && isActive) { - // FIXME: Update post should include team_id in the message cause its not always the current team - // viewChannel(msg.data.team_id, data.channel_id)(dispatch, getState); - } } function handlePostDeleted(msg, dispatch, getState) { @@ -295,18 +280,6 @@ function handleUserUpdatedEvent(msg, dispatch, getState) { } } -function handleChannelViewedEvent(msg, dispatch, getState) { - const state = getState(); - const channels = state.entities.channels; - const teams = state.entities.teams; - const users = state.entities.users; - - if (teams.currentId === msg.broadcast.team_id && channels.currentId !== msg.data.channel_id && - users.currentId === msg.broadcast.user_id) { - getChannel(teams.currentId, msg.data.channel_id)(dispatch, getState); - } -} - function handleChannelDeletedEvent(msg, dispatch, getState) { const entities = getState().entities; const {channels, currentId} = entities.channels; diff --git a/service/constants/constants.js b/service/constants/constants.js index 0af9ef1e9..f376bff73 100644 --- a/service/constants/constants.js +++ b/service/constants/constants.js @@ -7,6 +7,8 @@ const Constants = { CHANNELS_CHUNK_SIZE: 50, SEARCH_TIMEOUT_MILLISECONDS: 100, + MENTION: 'mention', + OFFLINE: 'offline', AWAY: 'away', ONLINE: 'online', diff --git a/service/constants/websocket.js b/service/constants/websocket.js index e0887e0ce..98d874370 100644 --- a/service/constants/websocket.js +++ b/service/constants/websocket.js @@ -6,7 +6,6 @@ const WebsocketEvents = { POST_EDITED: 'post_edited', POST_DELETED: 'post_deleted', CHANNEL_DELETED: 'channel_deleted', - CHANNEL_VIEWED: 'channel_viewed', DIRECT_ADDED: 'direct_added', LEAVE_TEAM: 'leave_team', USER_ADDED: 'user_added', diff --git a/test/service/actions/channels.test.js b/test/service/actions/channels.test.js index 6d09d862c..42d7346dd 100644 --- a/test/service/actions/channels.test.js +++ b/test/service/actions/channels.test.js @@ -264,12 +264,11 @@ describe('Actions.Channels', () => { TestHelper.fakeChannel(TestHelper.basicTeam.id) ); await Actions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)(store.dispatch, store.getState); - let members = store.getState().entities.channels.myMembers; - let member = members[TestHelper.basicChannel.id]; - let otherMember = members[userChannel.id]; + const members = store.getState().entities.channels.myMembers; + const member = members[TestHelper.basicChannel.id]; + const otherMember = members[userChannel.id]; assert.ok(member); assert.ok(otherMember); - const lastViewed = member.last_viewed_at; await Actions.viewChannel( TestHelper.basicTeam.id, @@ -281,12 +280,6 @@ describe('Actions.Channels', () => { if (updateRequest.status === RequestStatus.FAILURE) { throw new Error(JSON.stringify(updateRequest.error)); } - - members = store.getState().entities.channels.myMembers; - member = members[TestHelper.basicChannel.id]; - otherMember = members[userChannel.id]; - assert.ok(member.last_viewed_at > lastViewed); - assert.ok(otherMember.last_viewed_at > lastViewed); }); it('getMoreChannels', async () => { diff --git a/test/service/actions/websocket.test.js b/test/service/actions/websocket.test.js index b2cf07261..bdf06427e 100644 --- a/test/service/actions/websocket.test.js +++ b/test/service/actions/websocket.test.js @@ -210,27 +210,6 @@ describe('Actions.Websocket', () => { }); }); - it('Websocket Handle Channel Viewed', (done) => { - async function test() { - await TeamActions.selectTeam(TestHelper.basicTeam)(store.dispatch, store.getState); - - await Client.viewChannel( - TestHelper.basicTeam.id, - TestHelper.basicChannel.id - ); - - setTimeout(() => { - const state = store.getState(); - const entities = state.entities; - const {channels} = entities.channels; - assert.ok(channels[TestHelper.basicChannel.id]); - done(); - }, 1500); - } - - test(); - }); - it('Websocket Handle Channel Deleted', (done) => { async function test() { await ChannelActions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)(store.dispatch, store.getState); @@ -255,30 +234,29 @@ describe('Actions.Websocket', () => { }); it('Websocket Handle Direct Channel', (done) => { - // TODO: Uncomment once fixed on platform - // const test = async () => { - // const client = TestHelper.createClient(); - // const user = await client.createUserWithInvite( - // TestHelper.fakeUser(), - // null, - // null, - // TestHelper.basicTeam.invite_id - // ); - // - // await client.login(user.email, 'password1'); - // await TeamActions.selectTeam(TestHelper.basicTeam)(store.dispatch, store.getState); - // - // store.subscribe(() => { - // const entities = store.getState().entities; - // const {channels} = entities.channels; - // assert.ok(Object.keys(channels).length); - // done(); - // }); - // - // await client.createDirectChannel(TestHelper.basicTeam.id, TestHelper.basicUser.id); - // }; - // test(); - done(); + async function test() { + const client = TestHelper.createClient(); + const user = await client.createUserWithInvite( + TestHelper.fakeUser(), + null, + null, + TestHelper.basicTeam.invite_id + ); + + await client.login(user.email, 'password1'); + await TeamActions.selectTeam(TestHelper.basicTeam)(store.dispatch, store.getState); + + setTimeout(() => { + const entities = store.getState().entities; + const {channels} = entities.channels; + assert.ok(Object.keys(channels).length); + done(); + }, 500); + + await client.createDirectChannel(TestHelper.basicTeam.id, TestHelper.basicUser.id); + } + + test(); }); it('Websocket Handle Preferences Changed', (done) => {