Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
[ICU-621] Unreads sidebar section (#632)
Browse files Browse the repository at this point in the history
* Add Unread Section Option to the sidebar user settings

* Add unread section to the channel sidebar

* handle unread mentions with redux

* Fix window title to include mentions

* Fix and add some unit tests

* Keep selected unread channel in the unreads section

* Update snapshots

* Specify commit for mattermost-redux in package.json

* Make unreads section follow mentions when sorting

* Update mattermost-redux
  • Loading branch information
enahum committed Jan 31, 2018
1 parent 743d11c commit 8baa8d5
Show file tree
Hide file tree
Showing 20 changed files with 919 additions and 95 deletions.
17 changes: 17 additions & 0 deletions actions/views/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,20 @@ export function checkAndSetMobileView() {
});
};
}

export function keepChanneIdAsUnread(channelId, hadMentions = false) {
return (dispatch) => {
let data = null;
if (channelId) {
data = {
id: channelId,
hadMentions
};
}

dispatch({
type: ActionTypes.KEEP_CHANNEL_AS_UNREAD,
data
});
};
}
45 changes: 39 additions & 6 deletions components/sidebar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// See License.txt for license information.

import {connect} from 'react-redux';

import {Preferences} from 'mattermost-redux/constants/index';
import {
getSortedPublicChannelWithUnreadsIds,
getSortedPrivateChannelWithUnreadsIds,
Expand All @@ -10,9 +12,14 @@ import {
getCurrentChannel,
getMyChannelMemberships,
getUnreads,
getUnreadChannelIds
getSortedUnreadChannelIds,
getSortedDirectChannelIds,
getSortedFavoriteChannelIds,
getSortedPublicChannelIds,
getSortedPrivateChannelIds
} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getBool as getBoolPreference} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeam, isCurrentUserCurrentTeamAdmin} from 'mattermost-redux/selectors/entities/teams';

Expand All @@ -24,14 +31,40 @@ function mapStateToProps(state) {
const config = getConfig(state);
const currentChannel = getCurrentChannel(state);
const currentTeammate = currentChannel && currentChannel.teammate_id && getCurrentChannel(state, currentChannel.teammate_id);
let publicChannelIds;
let privateChannelIds;
let favoriteChannelIds;
let directAndGroupChannelIds;

const showUnreadSection = config.ExperimentalGroupUnreadChannels === 'true' && getBoolPreference(
state,
Preferences.CATEGORY_SIDEBAR_SETTINGS,
'show_unread_section',
true
);

const keepChannelIdAsUnread = state.views.channel.keepChannelIdAsUnread;

if (showUnreadSection) {
publicChannelIds = getSortedPublicChannelIds(state, keepChannelIdAsUnread);
privateChannelIds = getSortedPrivateChannelIds(state, keepChannelIdAsUnread);
favoriteChannelIds = getSortedFavoriteChannelIds(state, keepChannelIdAsUnread);
directAndGroupChannelIds = getSortedDirectChannelIds(state, keepChannelIdAsUnread);
} else {
publicChannelIds = getSortedPublicChannelWithUnreadsIds(state);
privateChannelIds = getSortedPrivateChannelWithUnreadsIds(state);
favoriteChannelIds = getSortedFavoriteChannelWithUnreadsIds(state);
directAndGroupChannelIds = getSortedDirectChannelWithUnreadsIds(state);
}

return {
config,
publicChannelIds: getSortedPublicChannelWithUnreadsIds(state),
privateChannelIds: getSortedPrivateChannelWithUnreadsIds(state),
favoriteChannelIds: getSortedFavoriteChannelWithUnreadsIds(state),
directAndGroupChannelIds: getSortedDirectChannelWithUnreadsIds(state),
unreadChannelIds: getUnreadChannelIds(state),
showUnreadSection,
publicChannelIds,
privateChannelIds,
favoriteChannelIds,
directAndGroupChannelIds,
unreadChannelIds: getSortedUnreadChannelIds(state, keepChannelIdAsUnread),
currentChannel,
currentTeammate,
currentTeam: getCurrentTeam(state),
Expand Down
88 changes: 63 additions & 25 deletions components/sidebar/sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,15 @@ export default class Sidebar extends React.PureComponent {
*/
isTeamAdmin: PropTypes.bool.isRequired,

/**
* Flag to display the Unread channels section
*/
showUnreadSection: PropTypes.bool.isRequired,

actions: PropTypes.shape({
goToChannelById: PropTypes.func.isRequired
}).isRequired
}
};

static defaultProps = {
currentChannel: {}
Expand Down Expand Up @@ -234,23 +239,29 @@ export default class Sidebar extends React.PureComponent {
}

updateTitle = () => {
const channel = this.props.currentChannel;
if (channel && this.props.currentTeam) {
const {
config,
currentChannel,
currentTeam,
currentTeammate,
unreads
} = this.props;

if (currentChannel && currentTeam) {
let currentSiteName = '';
if (this.props.config.SiteName != null) {
currentSiteName = this.props.config.SiteName;
if (config.SiteName != null) {
currentSiteName = config.SiteName;
}

let currentChannelName = channel.display_name;
if (channel.type === Constants.DM_CHANNEL) {
if (this.props.currentTeammate != null) {
currentChannelName = this.props.currentTeammate.display_name;
let currentChannelName = currentChannel.display_name;
if (currentChannel.type === Constants.DM_CHANNEL) {
if (currentTeammate != null) {
currentChannelName = currentTeammate.display_name;
}
}

const unread = this.props.unreads;
const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : '';
const unreadTitle = unread.messageCount > 0 ? '* ' : '';
const mentionTitle = unreads.mentionCount > 0 ? '(' + unreads.mentionCount + ') ' : '';
const unreadTitle = unreads.messageCount > 0 ? '* ' : '';
document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.props.currentTeam.display_name + ' ' + currentSiteName;
}
}
Expand Down Expand Up @@ -283,24 +294,26 @@ export default class Sidebar extends React.PureComponent {
updateUnreadIndicators = () => {
const container = $(ReactDOM.findDOMNode(this.refs.container));

var showTopUnread = false;
var showBottomUnread = false;
let showTopUnread = false;
let showBottomUnread = false;

// Consider partially obscured channels as above/below
const unreadMargin = 15;

if (this.firstUnreadChannel) {
var firstUnreadElement = $(ReactDOM.findDOMNode(this.refs[this.firstUnreadChannel]));
const firstUnreadElement = $(ReactDOM.findDOMNode(this.refs[this.firstUnreadChannel]));
const fistUnreadPosition = firstUnreadElement ? firstUnreadElement.position() : null;

if (firstUnreadElement.position().top + firstUnreadElement.height() < unreadMargin) {
if (fistUnreadPosition && fistUnreadPosition.top + firstUnreadElement.height() < unreadMargin) {
showTopUnread = true;
}
}

if (this.lastUnreadChannel) {
var lastUnreadElement = $(ReactDOM.findDOMNode(this.refs[this.lastUnreadChannel]));
const lastUnreadElement = $(ReactDOM.findDOMNode(this.refs[this.lastUnreadChannel]));
const lastUnreadPosition = lastUnreadElement ? lastUnreadElement.position() : null;

if (lastUnreadElement.position().top > container.height() - unreadMargin) {
if (lastUnreadPosition && lastUnreadPosition.top > container.height() - unreadMargin) {
showBottomUnread = true;
}
}
Expand Down Expand Up @@ -391,8 +404,12 @@ export default class Sidebar extends React.PureComponent {
}

getDisplayedChannels = (props = this.props) => {
return props.favoriteChannelIds.concat(props.publicChannelIds).concat(props.privateChannelIds).concat(props.directAndGroupChannelIds);
}
return props.unreadChannelIds.
concat(props.favoriteChannelIds).
concat(props.publicChannelIds).
concat(props.privateChannelIds).
concat(props.directAndGroupChannelIds);
};

channelIdIsDisplayedForProps = (props, id) => {
const allChannels = this.getDisplayedChannels(props);
Expand Down Expand Up @@ -452,6 +469,15 @@ export default class Sidebar extends React.PureComponent {
}

render() {
const {
directAndGroupChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
unreadChannelIds,
showUnreadSection
} = this.props;

// Check if we have all info needed to render
if (this.props.currentTeam == null || this.props.currentUser == null) {
return (<div/>);
Expand All @@ -463,11 +489,12 @@ export default class Sidebar extends React.PureComponent {
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;

// create elements for all 4 types of channels
const favoriteItems = this.props.favoriteChannelIds.map(this.createSidebarChannel);
const publicChannelItems = this.props.publicChannelIds.map(this.createSidebarChannel);
const privateChannelItems = this.props.privateChannelIds.map(this.createSidebarChannel);
const directMessageItems = this.props.directAndGroupChannelIds.map(this.createSidebarChannel);
// create elements for all 5 types of channels
const unreadChannelItems = showUnreadSection ? unreadChannelIds.map(this.createSidebarChannel) : [];
const favoriteItems = favoriteChannelIds.map(this.createSidebarChannel);
const publicChannelItems = publicChannelIds.map(this.createSidebarChannel);
const privateChannelItems = privateChannelIds.map(this.createSidebarChannel);
const directMessageItems = directAndGroupChannelIds.map(this.createSidebarChannel);

var directMessageMore = (
<li key='more'>
Expand Down Expand Up @@ -672,6 +699,17 @@ export default class Sidebar extends React.PureComponent {
className='nav-pills__container'
onScroll={this.onScroll}
>
{unreadChannelItems.length !== 0 && <ul className='nav nav-pills nav-stacked'>
<li>
<h4 id='favoriteChannel'>
<FormattedMessage
id='sidebar.unreadSection'
defaultMessage='UNREADS'
/>
</h4>
</li>
{unreadChannelItems}
</ul>}
{favoriteItems.length !== 0 && <ul className='nav nav-pills nav-stacked'>
<li>
<h4 id='favoriteChannel'>
Expand Down
3 changes: 3 additions & 0 deletions components/sidebar/sidebar_channel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
import {leaveChannel} from 'mattermost-redux/actions/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';

import {keepChanneIdAsUnread} from 'actions/views/channel';

import {Constants} from 'utils/constants.jsx';

import SidebarChannel from './sidebar_channel.jsx';
Expand Down Expand Up @@ -70,6 +72,7 @@ function makeMapStateToProps() {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
keepChanneIdAsUnread,
savePreferences,
leaveChannel
}, dispatch)
Expand Down
26 changes: 19 additions & 7 deletions components/sidebar/sidebar_channel/sidebar_channel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export default class SidebarChannel extends React.PureComponent {
membersCount: PropTypes.number.isRequired,

actions: PropTypes.shape({
keepChanneIdAsUnread: PropTypes.func.isRequired,
savePreferences: PropTypes.func.isRequired,
leaveChannel: PropTypes.func.isRequired
}).isRequired
Expand Down Expand Up @@ -172,6 +173,22 @@ export default class SidebarChannel extends React.PureComponent {
}
}

handleSelectChannel = () => {
const {actions, channelId} = this.props;
actions.keepChanneIdAsUnread(this.isChannelUnread() ? channelId : null, this.props.unreadMentions > 0);
};

isChannelUnread = () => {
const {membership, unreadMsgs, unreadMentions} = this.props;

if (membership) {
const msgCount = unreadMsgs + unreadMentions;
return msgCount > 0 || membership.mention_count > 0;
}

return false;
};

render = () => {
if (!this.props.channelDisplayName || !this.props.channelType) {
return (<div/>);
Expand All @@ -194,13 +211,7 @@ export default class SidebarChannel extends React.PureComponent {

let rowClass = 'sidebar-item';

var unread = false;
if (this.props.membership) {
const msgCount = this.props.unreadMsgs + this.props.unreadMentions;
unread = msgCount > 0 || this.props.membership.mention_count > 0;
}

if (unread) {
if (this.isChannelUnread()) {
rowClass += ' unread-title';
}

Expand Down Expand Up @@ -269,6 +280,7 @@ export default class SidebarChannel extends React.PureComponent {
membersCount={this.props.membersCount}
teammateId={this.props.channelTeammateId}
teammateDeletedAt={this.props.channelTeammateDeletedAt}
onSelectChannel={this.handleSelectChannel}
/>
{tutorialTip}
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ export default class SidebarChannelButtonOrLink extends React.PureComponent {
membersCount: PropTypes.number.isRequired,
unreadMentions: PropTypes.number,
teammateId: PropTypes.string,
teammateDeletedAt: PropTypes.number
teammateDeletedAt: PropTypes.number,
onSelectChannel: PropTypes.func.isRequired
}

trackChannelSelectedEvent = () => {
this.props.onSelectChannel();
mark('SidebarChannelLink#click');
trackEvent('ui', 'ui_channel_selected');
}
Expand Down
3 changes: 1 addition & 2 deletions components/user_settings/user_settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import DisplayTab from './user_settings_display.jsx';
import GeneralTab from './user_settings_general';
import NotificationsTab from './user_settings_notifications.jsx';
import SecurityTab from './user_settings_security';
import SidebarTab from './user_settings_sidebar.jsx';
import SidebarTab from './user_settings_sidebar';

export default class UserSettings extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -98,7 +98,6 @@ export default class UserSettings extends React.Component {
return (
<div>
<SidebarTab
user={this.state.user}
activeSection={this.props.activeSection}
updateSection={this.props.updateSection}
updateTab={this.props.updateTab}
Expand Down
2 changes: 1 addition & 1 deletion components/user_settings/user_settings_modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ class UserSettingsModal extends React.Component {
tabs.push({name: 'security', uiName: formatMessage(holders.security), icon: 'icon fa fa-lock'});
tabs.push({name: 'notifications', uiName: formatMessage(holders.notifications), icon: 'icon fa fa-exclamation-circle'});
tabs.push({name: 'display', uiName: formatMessage(holders.display), icon: 'icon fa fa-eye'});
if (global.mm_config.CloseUnusedDirectMessages === 'true') {
if (global.mm_config.CloseUnusedDirectMessages === 'true' || global.mm_config.ExperimentalGroupUnreadChannels === 'true') {
tabs.push({name: 'sidebar', uiName: formatMessage(holders.sidebar), icon: 'icon fa fa-columns'});
}
tabs.push({name: 'advanced', uiName: formatMessage(holders.advanced), icon: 'icon fa fa-list-alt'});
Expand Down
Loading

0 comments on commit 8baa8d5

Please sign in to comment.