summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexander Smaga <smagaan@gmail.com>2016-10-26 15:20:45 +0300
committerChristopher Speller <crspeller@gmail.com>2016-10-26 08:20:45 -0400
commitb354d25d3731b53613489d95cfa4c946cf8e0888 (patch)
treeb1e11d3c9eef60cd9d8bd7a51b16dc49cff01af7
parent66ed155a58b6f0f365db26fa9e4e605d3d4a61a2 (diff)
downloadchat-b354d25d3731b53613489d95cfa4c946cf8e0888.tar.gz
chat-b354d25d3731b53613489d95cfa4c946cf8e0888.tar.bz2
chat-b354d25d3731b53613489d95cfa4c946cf8e0888.zip
GH-4095 Favorite/Starred Channels (#4222)
-rw-r--r--webapp/actions/channel_actions.jsx14
-rw-r--r--webapp/components/channel_header.jsx63
-rw-r--r--webapp/components/navbar.jsx58
-rw-r--r--webapp/components/sidebar.jsx97
-rw-r--r--webapp/i18n/en.json2
-rw-r--r--webapp/sass/layout/_headers.scss5
-rw-r--r--webapp/utils/channel_utils.jsx135
-rw-r--r--webapp/utils/constants.jsx1
8 files changed, 297 insertions, 78 deletions
diff --git a/webapp/actions/channel_actions.jsx b/webapp/actions/channel_actions.jsx
index 61c839652..c9c4e6883 100644
--- a/webapp/actions/channel_actions.jsx
+++ b/webapp/actions/channel_actions.jsx
@@ -172,3 +172,17 @@ export function openDirectChannelToUser(user, success, error) {
}
);
}
+
+export function markFavorite(channelId) {
+ AsyncClient.savePreference(Preferences.CATEGORY_FAVORITE_CHANNEL, channelId, 'true');
+}
+
+export function unmarkFavorite(channelId) {
+ const pref = {
+ user_id: UserStore.getCurrentId(),
+ category: Preferences.CATEGORY_FAVORITE_CHANNEL,
+ name: channelId
+ };
+
+ AsyncClient.deletePreferences([pref]);
+}
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 1a8625cd2..2d3de5998 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -26,7 +26,9 @@ import WebrtcStore from 'stores/webrtc_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import * as WebrtcActions from 'actions/webrtc_actions.jsx';
+import * as ChannelActions from 'actions/channel_actions.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
@@ -63,18 +65,19 @@ export default class ChannelHeader extends React.Component {
}
getStateFromStores() {
+ const channel = ChannelStore.get(this.props.channelId);
const stats = ChannelStore.getStats(this.props.channelId);
-
const users = UserStore.getProfileListInChannel(this.props.channelId);
return {
- channel: ChannelStore.get(this.props.channelId),
+ channel,
memberChannel: ChannelStore.getMyMember(this.props.channelId),
users,
userCount: stats.member_count,
currentUser: UserStore.getCurrentUser(),
enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
- isBusy: WebrtcStore.isBusy()
+ isBusy: WebrtcStore.isBusy(),
+ isFavorite: channel && ChannelUtils.isFavoriteChannel(channel)
};
}
@@ -125,11 +128,17 @@ export default class ChannelHeader extends React.Component {
handleLeave() {
Client.leaveChannel(this.state.channel.id,
() => {
+ const channelId = this.state.channel.id;
+
AppDispatcher.handleViewAction({
type: ActionTypes.LEAVE_CHANNEL,
- id: this.state.channel.id
+ id: channelId
});
+ if (this.state.isFavorite) {
+ ChannelActions.unmarkFavorite(channelId);
+ }
+
const townsquare = ChannelStore.getByName('town-square');
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
},
@@ -139,6 +148,16 @@ export default class ChannelHeader extends React.Component {
);
}
+ toggleFavorite = (e) => {
+ e.preventDefault();
+
+ if (this.state.isFavorite) {
+ ChannelActions.unmarkFavorite(this.state.channel.id);
+ } else {
+ ChannelActions.markFavorite(this.state.channel.id);
+ }
+ };
+
searchMentions(e) {
e.preventDefault();
const user = this.state.currentUser;
@@ -272,9 +291,9 @@ export default class ChannelHeader extends React.Component {
if (isDirect) {
const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
const contact = this.state.users[0];
- if (contact) {
- channelTitle = Utils.displayUsername(contact.id);
- }
+
+ const teammateId = Utils.getUserIdFromChannelName(channel);
+ channelTitle = Utils.displayUsername(teammateId);
const webrtcEnabled = global.mm_config.EnableWebrtc === 'true' && global.mm_license.Webrtc === 'true' &&
global.mm_config.EnableDeveloper === 'true' && userMedia && Utils.isFeatureEnabled(PreReleaseFeatures.WEBRTC_PREVIEW);
@@ -607,6 +626,35 @@ export default class ChannelHeader extends React.Component {
headerText = channel.header;
}
+ const toggleFavoriteTooltip = (
+ <Tooltip id='favoriteTooltip'>
+ {this.state.isFavorite ?
+ <FormattedMessage
+ id='channelHeader.removeFromFavorites'
+ defaultMessage='Remove from Favorites'
+ /> :
+ <FormattedMessage
+ id='channelHeader.addToFavorites'
+ defaultMessage='Add to Favorites'
+ />}
+ </Tooltip>
+ );
+ const toggleFavorite = (
+ <OverlayTrigger
+ delayShow={Constants.OVERLAY_TIME_DELAY}
+ placement='bottom'
+ overlay={toggleFavoriteTooltip}
+ >
+ <a
+ href='#'
+ onClick={this.toggleFavorite}
+ className='channel-header__favorites'
+ >
+ <i className={'icon fa ' + (this.state.isFavorite ? 'fa-star' : 'fa-star-o')}/>
+ </a>
+ </OverlayTrigger>
+ );
+
return (
<div
id='channel-header'
@@ -618,6 +666,7 @@ export default class ChannelHeader extends React.Component {
<th>
<div className='channel-header__info'>
{webrtc}
+ {toggleFavorite}
<div className='dropdown'>
<a
href='#'
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index 865e2ac78..18ea84376 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -18,15 +18,19 @@ import StatusIcon from './status_icon.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
import ChannelSwitchModal from './channel_switch_modal.jsx';
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as ChannelUtils from 'utils/channel_utils.jsx';
+import * as ChannelActions from 'actions/channel_actions.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
+
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import {FormattedMessage} from 'react-intl';
@@ -66,12 +70,15 @@ export default class Navbar extends React.Component {
}
getStateFromStores() {
+ const channel = ChannelStore.getCurrent();
+
return {
- channel: ChannelStore.getCurrent(),
+ channel,
member: ChannelStore.getCurrentMember(),
users: [],
userCount: ChannelStore.getCurrentStats().member_count,
- currentUser: UserStore.getCurrentUser()
+ currentUser: UserStore.getCurrentUser(),
+ isFavorite: channel && ChannelUtils.isFavoriteChannel(channel)
};
}
@@ -83,6 +90,7 @@ export default class Navbar extends React.Component {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addStatsChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
+ PreferenceStore.addChangeListener(this.onChange);
$('.inner-wrap').click(this.hideSidebars);
document.addEventListener('keydown', this.showChannelSwitchModal);
}
@@ -91,6 +99,7 @@ export default class Navbar extends React.Component {
ChannelStore.removeChangeListener(this.onChange);
ChannelStore.removeStatsChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
+ PreferenceStore.removeChangeListener(this.onChange);
document.removeEventListener('keydown', this.showChannelSwitchModal);
}
@@ -99,10 +108,17 @@ export default class Navbar extends React.Component {
}
handleLeave() {
- Client.leaveChannel(this.state.channel.id,
+ var channelId = this.state.channel.id;
+
+ Client.leaveChannel(channelId,
() => {
AsyncClient.getChannels(true);
- browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square');
+ if (this.state.isFavorite) {
+ ChannelActions.unmarkFavorite(channelId);
+ }
+
+ const townsquare = ChannelStore.getByName('town-square');
+ browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
},
(err) => {
AsyncClient.dispatchError(err, 'handleLeave');
@@ -214,6 +230,16 @@ export default class Navbar extends React.Component {
return true;
}
+ toggleFavorite = (e) => {
+ e.preventDefault();
+
+ if (this.state.isFavorite) {
+ ChannelActions.unmarkFavorite(this.state.channel.id);
+ } else {
+ ChannelActions.markFavorite(this.state.channel.id);
+ }
+ };
+
createDropdown(channel, channelTitle, isAdmin, isSystemAdmin, isDirect, popoverContent) {
if (channel) {
let channelTerm = (
@@ -425,6 +451,29 @@ export default class Navbar extends React.Component {
}
}
+ const toggleFavoriteOption = (
+ <li
+ key='toggle_favorite'
+ role='presentation'
+ >
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.toggleFavorite}
+ >
+ {this.state.isFavorite ?
+ <FormattedMessage
+ id='channelHeader.removeFromFavorites'
+ defaultMessage='Remove from Favorites'
+ /> :
+ <FormattedMessage
+ id='channelHeader.addToFavorites'
+ defaultMessage='Add to Favorites'
+ />}
+ </a>
+ </li>
+ );
+
return (
<div className='navbar-brand'>
<div className='dropdown'>
@@ -461,6 +510,7 @@ export default class Navbar extends React.Component {
{renameChannelOption}
{deleteChannelOption}
{leaveChannelOption}
+ {toggleFavoriteOption}
</ul>
</div>
</div>
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index c8a7e1eb9..2a589b996 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -14,10 +14,10 @@ import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
-import LocalizationStore from 'stores/localization_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
@@ -96,63 +96,13 @@ export default class Sidebar extends React.Component {
getStateFromStores() {
const members = ChannelStore.getMyMembers();
const currentChannelId = ChannelStore.getCurrentId();
- const currentUserId = UserStore.getCurrentId();
-
- const channels = Object.assign([], ChannelStore.getAll());
- channels.sort(this.sortChannelsByDisplayName);
-
- const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
- const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
-
- const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
-
- const directChannels = [];
- const directNonTeamChannels = [];
- for (const [name, value] of preferences) {
- if (value !== 'true') {
- continue;
- }
-
- const teammateId = name;
-
- let directChannel = channels.find(Utils.isDirectChannelForUser.bind(null, teammateId));
-
- // a direct channel doesn't exist yet so create a fake one
- if (directChannel == null) {
- directChannel = {
- name: Utils.getDirectChannelName(currentUserId, teammateId),
- last_post_at: 0,
- total_msg_count: 0,
- type: Constants.DM_CHANNEL,
- fake: true
- };
- } else {
- directChannel = JSON.parse(JSON.stringify(directChannel));
- }
-
- directChannel.display_name = Utils.displayUsername(teammateId);
- directChannel.teammate_id = teammateId;
- directChannel.status = UserStore.getStatus(teammateId) || 'offline';
-
- if (TeamStore.hasActiveMemberInTeam(TeamStore.getCurrentId(), teammateId)) {
- directChannels.push(directChannel);
- } else if (TeamStore.hasMemberNotInTeam(TeamStore.getCurrentId(), teammateId)) {
- directNonTeamChannels.push(directChannel);
- }
- }
-
- directChannels.sort(this.sortChannelsByDisplayName);
- directNonTeamChannels.sort(this.sortChannelsByDisplayName);
-
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
+ const channelList = ChannelUtils.buildDisplayableChannelList(Object.assign([], ChannelStore.getAll()));
return {
activeId: currentChannelId,
members,
- publicChannels,
- privateChannels,
- directChannels,
- directNonTeamChannels,
+ ...channelList,
unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER,
currentTeam: TeamStore.getCurrent(),
@@ -379,6 +329,10 @@ export default class Sidebar extends React.Component {
}
);
+ if (ChannelUtils.isFavoriteChannel(channel)) {
+ ChannelActions.unmarkFavorite(channel.id);
+ }
+
this.setState(this.getStateFromStores());
}
@@ -387,16 +341,6 @@ export default class Sidebar extends React.Component {
}
}
- sortChannelsByDisplayName(a, b) {
- const locale = LocalizationStore.getLocale();
-
- if (a.display_name === b.display_name) {
- return a.name.localeCompare(b.name, locale, {numeric: true});
- }
-
- return a.display_name.localeCompare(b.display_name, locale, {numeric: true});
- }
-
showMoreChannelsModal() {
// manually show the modal because using data-toggle messes with keyboard focus when the modal is dismissed
$('#more_channels').modal({'data-channeltype': 'O'}).modal('show');
@@ -522,7 +466,7 @@ export default class Sidebar extends React.Component {
badge = <span className='badge pull-right small'>{unreadCount.mentions}</span>;
this.badgesActive = true;
}
- } else if (this.state.loadingDMChannel === index && channel.type === 'D') {
+ } else if (this.state.loadingDMChannel === index && channel.type === Constants.DM_CHANNEL) {
badge = (
<img
className='channel-loading-gif pull-right'
@@ -536,9 +480,9 @@ export default class Sidebar extends React.Component {
}
var icon = null;
- if (channel.type === 'O') {
+ if (channel.type === Constants.OPEN_CHANNEL) {
icon = <div className='status'><i className='fa fa-globe'/></div>;
- } else if (channel.type === 'P') {
+ } else if (channel.type === Constants.PRIVATE_CHANNEL) {
icon = <div className='status'><i className='fa fa-lock'/></div>;
} else {
// set up status icon for direct message channels (status is null for other channel types)
@@ -618,7 +562,15 @@ export default class Sidebar extends React.Component {
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;
- // create elements for all 3 types of channels
+ // create elements for all 4 types of channels
+ const favoriteItems = this.state.favoriteChannels.map((channel, index, arr) => {
+ if (channel.type === Constants.DM_CHANNEL) {
+ return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
+ }
+
+ return this.createChannelElement(channel);
+ });
+
const publicChannelItems = this.state.publicChannels.map(this.createChannelElement);
const privateChannelItems = this.state.privateChannels.map(this.createChannelElement);
@@ -801,6 +753,17 @@ export default class Sidebar extends React.Component {
className='nav-pills__container'
onScroll={this.onScroll}
>
+ {favoriteItems.length !== 0 && <ul className='nav nav-pills nav-stacked'>
+ <li>
+ <h4>
+ <FormattedMessage
+ id='sidebar.favorite'
+ defaultMessage='Favorites'
+ />
+ </h4>
+ </li>
+ {favoriteItems}
+ </ul>}
<ul className='nav nav-pills nav-stacked'>
<li>
<h4>
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 9b78b2803..24d61e5f3 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -986,6 +986,8 @@
"channel_header.flagged": "Flagged Posts",
"channel_header.group": "Group",
"channel_header.leave": "Leave {term}",
+ "channel_header.addToFavorites": "Add to Favorites",
+ "channel_header.removeFromFavorites": "Remove from Favorites",
"channel_header.manageMembers": "Manage Members",
"channel_header.notificationPreferences": "Notification Preferences",
"channel_header.recentMentions": "Recent Mentions",
diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss
index 58ca512b6..579875b47 100644
--- a/webapp/sass/layout/_headers.scss
+++ b/webapp/sass/layout/_headers.scss
@@ -378,6 +378,11 @@
}
}
+.channel-header__favorites {
+ float: left;
+ margin: 1px 10px 0 0;
+}
+
.app__body {
.channel-header__links {
diff --git a/webapp/utils/channel_utils.jsx b/webapp/utils/channel_utils.jsx
new file mode 100644
index 000000000..119021fce
--- /dev/null
+++ b/webapp/utils/channel_utils.jsx
@@ -0,0 +1,135 @@
+
+import Constants from 'utils/constants.jsx';
+const Preferences = Constants.Preferences;
+
+import * as Utils from 'utils/utils.jsx';
+
+import UserStore from 'stores/user_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
+import LocalizationStore from 'stores/localization_store.jsx';
+
+/**
+ * Returns list of sorted channels grouped by type. Favorites here is considered as separated type.
+ *
+ * Example: {
+ * publicChannels: [...],
+ * privateChannels: [...],
+ * directChannels: [...],
+ * directNonTeamChannels: [...],
+ * favoriteChannels: [...]
+ * }
+ */
+export function buildDisplayableChannelList(persistentChannels) {
+ const missingDMChannels = createMissingDirectChannels(persistentChannels);
+
+ const channels = persistentChannels.concat(missingDMChannels).map(completeDirectChannelInfo);
+ channels.sort(sortChannelsByDisplayName);
+
+ const favoriteChannels = channels.filter(isFavoriteChannel);
+ const notFavoriteChannels = channels.filter(not(isFavoriteChannel));
+ const directChannels = notFavoriteChannels.filter(andX(isDirectChannel, isDirectChannelVisible));
+
+ return {
+ favoriteChannels,
+ publicChannels: notFavoriteChannels.filter(isOpenChannel),
+ privateChannels: notFavoriteChannels.filter(isPrivateChannel),
+ directChannels: directChannels.filter(isConnectedToTeamMember),
+ directNonTeamChannels: directChannels.filter(not(isConnectedToTeamMember))
+ };
+}
+
+export function isFavoriteChannel(channel) {
+ return PreferenceStore.getBool(Preferences.CATEGORY_FAVORITE_CHANNEL, channel.id);
+}
+
+export function isOpenChannel(channel) {
+ return channel.type === Constants.OPEN_CHANNEL;
+}
+
+export function isPrivateChannel(channel) {
+ return channel.type === Constants.PRIVATE_CHANNEL;
+}
+
+export function isDirectChannel(channel) {
+ return channel.type === Constants.DM_CHANNEL;
+}
+
+export function isDirectChannelVisible(channel) {
+ const channelId = Utils.getUserIdFromChannelName(channel);
+
+ return PreferenceStore.getBool(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channelId);
+}
+
+export function completeDirectChannelInfo(channel) {
+ if (!isDirectChannel(channel)) {
+ return channel;
+ }
+
+ const dmChannelClone = JSON.parse(JSON.stringify(channel));
+ const teammateId = Utils.getUserIdFromChannelName(channel);
+
+ return Object.assign(dmChannelClone, {
+ display_name: Utils.displayUsername(teammateId),
+ teammate_id: teammateId,
+ status: UserStore.getStatus(teammateId) || 'offline'
+ });
+}
+
+export function sortChannelsByDisplayName(a, b) {
+ const locale = LocalizationStore.getLocale();
+
+ return buildDisplayNameAndTypeComparable(a).localeCompare(buildDisplayNameAndTypeComparable(b), locale, {numeric: true});
+}
+
+/*
+ * not exported helpers
+ */
+
+function createMissingDirectChannels(channels) {
+ const directChannelsDisplayPreferences = PreferenceStore.getCategory(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
+
+ return Array.
+ from(directChannelsDisplayPreferences).
+ filter((entry) => entry[1] === 'true').
+ map((entry) => entry[0]).
+ filter((teammateId) => !channels.some(Utils.isDirectChannelForUser.bind(null, teammateId))).
+ map(createFakeChannelCurried(UserStore.getCurrentId()));
+}
+
+function createFakeChannel(userId, otherUserId) {
+ return {
+ name: Utils.getDirectChannelName(userId, otherUserId),
+ last_post_at: 0,
+ total_msg_count: 0,
+ type: Constants.DM_CHANNEL,
+ fake: true
+ };
+}
+
+function createFakeChannelCurried(userId) {
+ return (otherUserId) => createFakeChannel(userId, otherUserId);
+}
+
+function isConnectedToTeamMember(channel) {
+ return isTeamMember(channel.teammate_id);
+}
+
+function isTeamMember(userId) {
+ return TeamStore.hasActiveMemberInTeam(TeamStore.getCurrentId(), userId);
+}
+
+function not(f) {
+ return (...args) => !f(...args);
+}
+
+function andX(...fns) {
+ return (...args) => fns.every((f) => f(...args));
+}
+
+const defaultPrefix = 'D'; // fallback for future types
+const typeToPrefixMap = {[Constants.OPEN_CHANNEL]: 'A', [Constants.PRIVATE_CHANNEL]: 'B', [Constants.DM_CHANNEL]: 'C'};
+
+function buildDisplayNameAndTypeComparable(channel) {
+ return (typeToPrefixMap[channel.type] || defaultPrefix) + channel.display_name + channel.name;
+}
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 2dae06282..f87b36fc8 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -54,6 +54,7 @@ export const Preferences = {
CATEGORY_THEME: 'theme',
CATEGORY_FLAGGED_POST: 'flagged_post',
CATEGORY_NOTIFICATIONS: 'notifications',
+ CATEGORY_FAVORITE_CHANNEL: 'favorite_channel',
EMAIL_INTERVAL: 'email_interval'
};