From 3a91d4e5e419a43ff19a0736ce697f8d611d36e3 Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Thu, 2 Mar 2017 17:48:56 -0500 Subject: PLT-3077 Add group messaging (#5489) * Implement server changes for group messaging * Majority of client-side implementation * Some server updates * Added new React multiselect component * Fix style issues * Add custom renderer for options * Fix model test * Update ENTER functionality for multiselect control * Remove buttons from multiselect UI control * Updating group messaging UI (#5524) * Move filter controls up a component level * Scroll with arrow keys * Updating mobile layout for multiselect (#5534) * Fix race condition when backspacing quickly * Hidden or new GMs show up for regular messages * Add overriding of number remaining text * Add UI filtering for team if config setting set * Add icon to channel switcher and class prop to status icon * Minor updates per feedback * Improving group messaging UI (#5563) * UX changes per feedback * Update email for group messages * UI fixes for group messaging (#5587) * Fix missing localization string * Add maximum users message when adding members to GM * Fix input clearing on Android * Updating group messaging UI (#5603) * Updating UI for group messaging (#5604) --- webapp/utils/channel_intro_messages.jsx | 63 ++++++++++++++++++++++++++++++++- webapp/utils/channel_utils.jsx | 54 ++++++++++++++++------------ webapp/utils/constants.jsx | 5 +++ webapp/utils/utils.jsx | 4 ++- 4 files changed, 102 insertions(+), 24 deletions(-) (limited to 'webapp/utils') diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx index 991bf54e8..390ce6d28 100644 --- a/webapp/utils/channel_intro_messages.jsx +++ b/webapp/utils/channel_intro_messages.jsx @@ -23,8 +23,10 @@ export function createChannelIntroMessage(channel, fullWidthIntro) { centeredIntro = 'channel-intro--centered'; } - if (channel.type === 'D') { + if (channel.type === Constants.DM_CHANNEL) { return createDMIntroMessage(channel, centeredIntro); + } else if (channel.type === Constants.GM_CHANNEL) { + return createGMIntroMessage(channel, centeredIntro); } else if (ChannelStore.isDefault(channel)) { return createDefaultIntroMessage(channel, centeredIntro); } else if (channel.name === Constants.OFFTOPIC_CHANNEL) { @@ -35,6 +37,65 @@ export function createChannelIntroMessage(channel, fullWidthIntro) { return null; } +export function createGMIntroMessage(channel, centeredIntro) { + const profiles = UserStore.getProfileListInChannel(channel.id, true); + + if (profiles.length > 0) { + const pictures = []; + let names = ''; + for (let i = 0; i < profiles.length; i++) { + const profile = profiles[i]; + + pictures.push( + + ); + + if (i === profiles.length - 1) { + names += Utils.displayUsernameForUser(profile); + } else if (i === profiles.length - 2) { + names += Utils.displayUsernameForUser(profile) + ' and '; + } else { + names += Utils.displayUsernameForUser(profile) + ', '; + } + } + + return ( +
+
+ {pictures} +
+

+ +

+ {createSetHeaderButton(channel)} +
+ ); + } + + return ( +
+

+ +

+
+ ); +} + export function createDMIntroMessage(channel, centeredIntro) { var teammate = Utils.getDirectTeammate(channel.id); diff --git a/webapp/utils/channel_utils.jsx b/webapp/utils/channel_utils.jsx index 22c428cb8..2bb30af5c 100644 --- a/webapp/utils/channel_utils.jsx +++ b/webapp/utils/channel_utils.jsx @@ -5,7 +5,6 @@ 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'; @@ -15,30 +14,28 @@ import LocalizationStore from 'stores/localization_store.jsx'; * Example: { * publicChannels: [...], * privateChannels: [...], - * directChannels: [...], - * directNonTeamChannels: [...], + * directAndGroupChannels: [...], * favoriteChannels: [...] * } */ export function buildDisplayableChannelList(persistentChannels) { - const missingDMChannels = createMissingDirectChannels(persistentChannels); + const missingDirectChannels = createMissingDirectChannels(persistentChannels); const channels = persistentChannels. - concat(missingDMChannels). + concat(missingDirectChannels). map(completeDirectChannelInfo). filter(isNotDeletedChannel). sort(sortChannelsByDisplayName); const favoriteChannels = channels.filter(isFavoriteChannel); const notFavoriteChannels = channels.filter(not(isFavoriteChannel)); - const directChannels = notFavoriteChannels.filter(andX(isDirectChannel, isDirectChannelVisible)); + const directAndGroupChannels = notFavoriteChannels.filter(orX(andX(isGroupChannel, isGroupChannelVisible), andX(isDirectChannel, isDirectChannelVisible))); return { favoriteChannels, publicChannels: notFavoriteChannels.filter(isOpenChannel), privateChannels: notFavoriteChannels.filter(isPrivateChannel), - directChannels: directChannels.filter(isConnectedToTeamMember), - directNonTeamChannels: directChannels.filter(isNotConnectedToTeamMember) + directAndGroupChannels }; } @@ -62,6 +59,14 @@ export function isPrivateChannel(channel) { return channel.type === Constants.PRIVATE_CHANNEL; } +export function isGroupChannel(channel) { + return channel.type === Constants.GM_CHANNEL; +} + +export function isGroupChannelVisible(channel) { + return PreferenceStore.getBool(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel.id); +} + export function isDirectChannel(channel) { return channel.type === Constants.DM_CHANNEL; } @@ -88,12 +93,12 @@ export function completeDirectChannelInfo(channel) { } const defaultPrefix = 'D'; // fallback for future types -const typeToPrefixMap = {[Constants.OPEN_CHANNEL]: 'A', [Constants.PRIVATE_CHANNEL]: 'B', [Constants.DM_CHANNEL]: 'C'}; +const typeToPrefixMap = {[Constants.OPEN_CHANNEL]: 'A', [Constants.PRIVATE_CHANNEL]: 'B', [Constants.DM_CHANNEL]: 'C', [Constants.GM_CHANNEL]: 'C'}; export function sortChannelsByDisplayName(a, b) { const locale = LocalizationStore.getLocale(); - if (a.type !== b.type) { + if (a.type !== b.type && typeToPrefixMap[a.type] !== typeToPrefixMap[b.type]) { return (typeToPrefixMap[a.type] || defaultPrefix).localeCompare((typeToPrefixMap[b.type] || defaultPrefix), locale); } @@ -186,6 +191,19 @@ export function showDeleteOption(channel, isAdmin, isSystemAdmin, isChannelAdmin return true; } +export function buildGroupChannelName(channelId) { + const profiles = UserStore.getProfileListInChannel(channelId, true); + let displayName = ''; + for (let i = 0; i < profiles.length; i++) { + displayName += Utils.displayUsernameForUser(profiles[i]); + if (i !== profiles.length - 1) { + displayName += ', '; + } + } + + return displayName; +} + /* * not exported helpers */ @@ -215,22 +233,14 @@ 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 isNotConnectedToTeamMember(channel) { - return TeamStore.hasMemberNotInTeam(TeamStore.getCurrentId(), channel.teammate_id); -} - function not(f) { return (...args) => !f(...args); } +function orX(...fns) { + return (...args) => fns.some((f) => f(...args)); +} + function andX(...fns) { return (...args) => fns.every((f) => f(...args)); } diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 0f3e217b9..fafad9f44 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -35,6 +35,7 @@ import windows10ThemeImage from 'images/themes/windows_dark.png'; export const Preferences = { CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', + CATEGORY_GROUP_CHANNEL_SHOW: 'group_channel_show', CATEGORY_DISPLAY_SETTINGS: 'display_settings', DISPLAY_PREFER_NICKNAME: 'nickname_full_name', DISPLAY_PREFER_FULL_NAME: 'full_name', @@ -164,6 +165,7 @@ export const ActionTypes = keyMirror({ TOGGLE_GET_POST_LINK_MODAL: null, TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null, TOGGLE_GET_PUBLIC_LINK_MODAL: null, + TOGGLE_DM_MODAL: null, SUGGESTION_PRETEXT_CHANGED: null, SUGGESTION_RECEIVED_SUGGESTIONS: null, @@ -390,8 +392,11 @@ export const Constants = { ], MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], MAX_DMS: 20, + MAX_USERS_IN_GM: 8, + MIN_USERS_IN_GM: 3, MAX_CHANNEL_POPOVER_COUNT: 100, DM_CHANNEL: 'D', + GM_CHANNEL: 'G', OPEN_CHANNEL: 'O', PRIVATE_CHANNEL: 'P', INVITE_TEAM: 'I', diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 3d7941158..7573eb887 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -477,6 +477,7 @@ export function applyTheme(theme) { changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.6)); changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText); changeCss('.sidebar--left .status .offline--icon', 'fill:' + theme.sidebarText); + changeCss('.sidebar--left .status.status--group', 'background:' + changeOpacity(theme.sidebarText, 0.3)); changeCss('@media(max-width: 768px){.app__body .modal .settings-modal .settings-table .nav>li>a, .app__body .sidebar--menu .divider', 'border-color:' + changeOpacity(theme.sidebarText, 0.2)); changeCss('@media(max-width: 768px){.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + changeOpacity(theme.sidebarText, 0.6)); } @@ -569,6 +570,7 @@ export function applyTheme(theme) { } if (theme.centerChannelColor) { + changeCss('.app__body .mentions__name .status.status--group, .app__body .multi-select__note', 'background:' + changeOpacity(theme.centerChannelColor, 0.12)); changeCss('.app__body .post-list__arrows, .app__body .post .flag-icon__container', 'fill:' + changeOpacity(theme.centerChannelColor, 0.3)); changeCss('.app__body .modal .status .offline--icon, .app__body .channel-header__links .icon, .app__body .sidebar--right .sidebar--right__subheader .usage__icon', 'fill:' + theme.centerChannelColor); changeCss('@media(min-width: 768px){.app__body .post:hover .post__header .col__reply, .app__body .post.post--hovered .post__header .col__reply', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2)); @@ -618,7 +620,7 @@ export function applyTheme(theme) { changeCss('@media(max-width: 1800px){.app__body .inner-wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07)); changeCss('.app__body .post.post--hovered', 'background:' + changeOpacity(theme.centerChannelColor, 0.08)); changeCss('@media(min-width: 768px){.app__body .post:hover, .app__body .more-modal__list .more-modal__row:hover, .app__body .modal .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.08)); - changeCss('.app__body .date-separator.hovered--before:after, .app__body .date-separator.hovered--after:before, .app__body .new-separator.hovered--after:before, .app__body .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07)); + changeCss('.app__body .more-modal__row.more-modal__row--selected, .app__body .date-separator.hovered--before:after, .app__body .date-separator.hovered--after:before, .app__body .new-separator.hovered--after:before, .app__body .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07)); changeCss('@media(min-width: 768px){.app__body .suggestion-list__content .command:hover, .app__body .mentions__name:hover, .app__body .dropdown-menu>li>a:focus, .app__body .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15)); changeCss('.app__body .suggestion--selected, .app__body .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); changeCss('code, .app__body .form-control[disabled], .app__body .form-control[readonly], .app__body fieldset[disabled] .form-control', 'background:' + changeOpacity(theme.centerChannelColor, 0.1)); -- cgit v1.2.3-1-g7c22