diff --git a/app/components/selected_users/index.tsx b/app/components/selected_users/index.tsx
new file mode 100644
index 000000000..64472dab8
--- /dev/null
+++ b/app/components/selected_users/index.tsx
@@ -0,0 +1,265 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import {LayoutChangeEvent, Platform, ScrollView, useWindowDimensions, View} from 'react-native';
+import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated';
+import {useSafeAreaInsets} from 'react-native-safe-area-context';
+
+import Toast from '@components/toast';
+import {General} from '@constants';
+import {useTheme} from '@context/theme';
+import {useIsTablet, useKeyboardHeightWithDuration} from '@hooks/device';
+import Button from '@screens/bottom_sheet/button';
+import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+
+import SelectedUser, {USER_CHIP_BOTTOM_MARGIN, USER_CHIP_HEIGHT} from './selected_user';
+
+type Props = {
+
+ /**
+ * Name of the button Icon
+ */
+ buttonIcon: string;
+
+ /*
+ * Text displayed on the action button
+ */
+ buttonText: string;
+
+ /**
+ * the height of the parent container
+ */
+ containerHeight?: number;
+
+ /**
+ * the Y position of the first view in the parent container
+ */
+ modalPosition?: number;
+
+ /**
+ * A handler function that will select or deselect a user when clicked on.
+ */
+ onPress: (selectedId?: {[id: string]: boolean}) => void;
+
+ /**
+ * A handler function that will deselect a user when clicked on.
+ */
+ onRemove: (id: string) => void;
+
+ /**
+ * An object mapping user ids to a falsey value indicating whether or not they have been selected.
+ */
+ selectedIds: {[id: string]: UserProfile};
+
+ /**
+ * callback to set the value of showToast
+ */
+ setShowToast: (show: boolean) => void;
+
+ /**
+ * show the toast
+ */
+ showToast: boolean;
+
+ /**
+ * How to display the names of users.
+ */
+ teammateNameDisplay: string;
+
+ /**
+ * toast Icon
+ */
+ toastIcon?: string;
+
+ /**
+ * toast Message
+ */
+ toastMessage: string;
+}
+
+const BUTTON_HEIGHT = 48;
+const CHIP_HEIGHT_WITH_MARGIN = USER_CHIP_HEIGHT + USER_CHIP_BOTTOM_MARGIN;
+const EXPOSED_CHIP_HEIGHT = 0.33 * USER_CHIP_HEIGHT;
+const MAX_CHIP_ROWS = 2;
+const SCROLL_PADDING_TOP = 20;
+const PANEL_MAX_HEIGHT = SCROLL_PADDING_TOP + (CHIP_HEIGHT_WITH_MARGIN * MAX_CHIP_ROWS) + EXPOSED_CHIP_HEIGHT;
+const TABLET_MARGIN_BOTTOM = 20;
+const TOAST_BOTTOM_MARGIN = 24;
+
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ borderBottomWidth: 0,
+ borderColor: changeOpacity(theme.centerChannelColor, 0.16),
+ borderTopLeftRadius: 12,
+ borderTopRightRadius: 12,
+ borderWidth: 1,
+ maxHeight: PANEL_MAX_HEIGHT + BUTTON_HEIGHT,
+ overflow: 'hidden',
+ paddingHorizontal: 20,
+ shadowColor: theme.centerChannelColor,
+ shadowOffset: {
+ width: 0,
+ height: 8,
+ },
+ shadowOpacity: 0.16,
+ shadowRadius: 24,
+ },
+ toast: {
+ backgroundColor: theme.centerChannelColor,
+ },
+ users: {
+ paddingTop: SCROLL_PADDING_TOP,
+ paddingBottom: 12,
+ flexDirection: 'row',
+ flexGrow: 1,
+ flexWrap: 'wrap',
+ },
+ message: {
+ color: changeOpacity(theme.centerChannelColor, 0.6),
+ fontSize: 12,
+ marginRight: 5,
+ marginTop: 10,
+ marginBottom: 2,
+ },
+ };
+});
+
+export default function SelectedUsers({
+ buttonIcon, buttonText, containerHeight = 0,
+ modalPosition = 0, onPress, onRemove,
+ selectedIds, setShowToast, showToast = false,
+ teammateNameDisplay, toastIcon, toastMessage,
+}: Props) {
+ const theme = useTheme();
+ const style = getStyleFromTheme(theme);
+ const isTablet = useIsTablet();
+ const keyboard = useKeyboardHeightWithDuration();
+ const insets = useSafeAreaInsets();
+ const dimensions = useWindowDimensions();
+
+ const panelHeight = useSharedValue(0);
+ const [isVisible, setIsVisible] = useState(false);
+ const numberSelectedIds = Object.keys(selectedIds).length;
+ const bottomSpace = (dimensions.height - containerHeight - modalPosition);
+
+ const users = useMemo(() => {
+ const u = [];
+ for (const id of Object.keys(selectedIds)) {
+ if (!selectedIds[id]) {
+ continue;
+ }
+
+ u.push(
+ ,
+ );
+ }
+ return u;
+ }, [selectedIds, teammateNameDisplay, onRemove]);
+
+ const totalPanelHeight = useDerivedValue(() => (
+ isVisible ? panelHeight.value + BUTTON_HEIGHT : 0
+ ), [isVisible, isTablet]);
+
+ const marginBottom = useMemo(() => {
+ let margin = keyboard.height && Platform.OS === 'ios' ? keyboard.height - insets.bottom : 0;
+ if (isTablet) {
+ margin = keyboard.height ? (keyboard.height - bottomSpace - insets.bottom) : 0;
+ }
+ return margin;
+ }, [keyboard, isTablet, insets.bottom, bottomSpace]);
+
+ const handlePress = useCallback(() => {
+ onPress();
+ }, [onPress]);
+
+ const onLayout = useCallback((e: LayoutChangeEvent) => {
+ panelHeight.value = Math.min(PANEL_MAX_HEIGHT, e.nativeEvent.layout.height);
+ }, []);
+
+ const androidMaxHeight = Platform.select({
+ android: {
+ maxHeight: isVisible ? undefined : 0,
+ },
+ });
+
+ const animatedContainerStyle = useAnimatedStyle(() => ({
+ marginBottom: withTiming(marginBottom, {duration: keyboard.duration}),
+ paddingBottom: (isVisible || Platform.OS === 'android') ? TABLET_MARGIN_BOTTOM + insets.bottom : 0,
+ backgroundColor: isVisible ? theme.centerChannelBg : 'transparent',
+ ...androidMaxHeight,
+ }), [marginBottom, keyboard.duration, isVisible, theme.centerChannelBg]);
+
+ const animatedToastStyle = useAnimatedStyle(() => {
+ return {
+ bottom: TOAST_BOTTOM_MARGIN + totalPanelHeight.value,
+ opacity: withTiming(showToast ? 1 : 0, {duration: 250}),
+ position: 'absolute',
+ };
+ }, [showToast, keyboard]);
+
+ const animatedViewStyle = useAnimatedStyle(() => ({
+ height: withTiming(totalPanelHeight.value, {duration: 250}),
+ borderWidth: isVisible ? 1 : 0,
+ maxHeight: isVisible ? PANEL_MAX_HEIGHT + BUTTON_HEIGHT : 0,
+ }), [totalPanelHeight.value, isVisible]);
+
+ const animatedButtonStyle = useAnimatedStyle(() => ({
+ opacity: withTiming(isVisible ? 1 : 0, {duration: isVisible ? 500 : 100}),
+ }), [isVisible]);
+
+ useEffect(() => {
+ setIsVisible(numberSelectedIds > 0);
+ }, [numberSelectedIds > 0]);
+
+ // This effect hides the toast after 4 seconds
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+ if (showToast) {
+ timer = setTimeout(() => {
+ setShowToast(false);
+ }, 4000);
+ }
+
+ return () => clearTimeout(timer);
+ }, [showToast]);
+
+ return (
+
+ {showToast &&
+
+ }
+
+
+
+ {users}
+
+
+
+
+
+
+ );
+}
+
diff --git a/app/screens/create_direct_message/selected_users/selected_user.tsx b/app/components/selected_users/selected_user.tsx
similarity index 68%
rename from app/screens/create_direct_message/selected_users/selected_user.tsx
rename to app/components/selected_users/selected_user.tsx
index 8f32d13db..03ecd506c 100644
--- a/app/screens/create_direct_message/selected_users/selected_user.tsx
+++ b/app/components/selected_users/selected_user.tsx
@@ -8,8 +8,10 @@ import {
TouchableOpacity,
View,
} from 'react-native';
+import Animated, {FadeIn, FadeOut} from 'react-native-reanimated';
import CompassIcon from '@components/compass_icon';
+import ProfilePicture from '@components/profile_picture';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@@ -38,6 +40,10 @@ type Props = {
testID?: string;
}
+export const USER_CHIP_HEIGHT = 32;
+export const USER_CHIP_BOTTOM_MARGIN = 8;
+const FADE_DURATION = 100;
+
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
container: {
@@ -45,16 +51,22 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
justifyContent: 'center',
flexDirection: 'row',
borderRadius: 16,
+ height: USER_CHIP_HEIGHT,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
- marginBottom: 8,
- marginRight: 10,
- paddingLeft: 12,
- paddingVertical: 8,
- paddingRight: 7,
+ marginBottom: USER_CHIP_BOTTOM_MARGIN,
+ marginRight: 8,
+ paddingHorizontal: 7,
},
remove: {
+ justifyContent: 'center',
marginLeft: 7,
},
+ profileContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginRight: 8,
+ color: theme.centerChannelColor,
+ },
text: {
color: theme.centerChannelColor,
...typography('Body', 100, 'SemiBold'),
@@ -76,11 +88,22 @@ export default function SelectedUser({
onRemove(user.id);
}, [onRemove, user.id]);
+ const userItemTestID = `${testID}.${user.id}`;
return (
-
+
+
+
-
+
);
}
diff --git a/app/components/user_list/__snapshots__/index.test.tsx.snap b/app/components/user_list/__snapshots__/index.test.tsx.snap
index e71b66fab..7ed2953d8 100644
--- a/app/components/user_list/__snapshots__/index.test.tsx.snap
+++ b/app/components/user_list/__snapshots__/index.test.tsx.snap
@@ -40,11 +40,11 @@ exports[`components/channel_list_row should show no results 1`] = `
"username": "johndoe",
},
],
+ "first": true,
"id": "J",
},
]
}
- extraData={false}
getItem={[Function]}
getItemCount={[Function]}
initialNumToRender={15}
@@ -237,7 +237,7 @@ exports[`components/channel_list_row should show no results 1`] = `
}
>
@@ -312,11 +312,11 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
"username": "johndoe",
},
],
+ "first": true,
"id": "J",
},
]
}
- extraData={false}
getItem={[Function]}
getItemCount={[Function]}
initialNumToRender={15}
@@ -509,7 +509,7 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
}
>
@@ -584,11 +584,11 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
"username": "johndoe",
},
],
+ "first": true,
"id": "J",
},
]
}
- extraData={false}
getItem={[Function]}
getItemCount={[Function]}
initialNumToRender={15}
@@ -781,7 +781,7 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
}
>
diff --git a/app/components/user_list/index.tsx b/app/components/user_list/index.tsx
index 7459d775c..4a5313dc6 100644
--- a/app/components/user_list/index.tsx
+++ b/app/components/user_list/index.tsx
@@ -57,9 +57,10 @@ export function createProfilesSections(profiles: UserProfile[]) {
sectionKeys.sort();
- return sectionKeys.map((sectionKey) => {
+ return sectionKeys.map((sectionKey, index) => {
return {
id: sectionKey,
+ first: index === 0,
data: sections[sectionKey],
};
});
@@ -70,11 +71,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
list: {
backgroundColor: theme.centerChannelBg,
flex: 1,
- ...Platform.select({
- android: {
- marginBottom: 20,
- },
- }),
},
container: {
flexGrow: 1,
@@ -174,12 +170,13 @@ export default function UserList({
return (
);
- }, [selectedIds, currentUserId, handleSelectProfile, teammateNameDisplay, tutorialWatched, data]);
+ }, [selectedIds, currentUserId, handleSelectProfile, teammateNameDisplay, tutorialWatched]);
const renderLoading = useCallback(() => {
if (!loading) {
@@ -230,7 +227,6 @@ export default function UserList({
{
const DISABLED_OPACITY = 0.32;
const DEFAULT_ICON_OPACITY = 0.32;
-export default function UserListRow({
+function UserListRow({
id,
isMyUser,
highlight,
@@ -124,7 +124,7 @@ export default function UserListRow({
const bounds: TutorialItemBounds = {
startX: x - 20,
startY: y,
- endX: x + w + 20,
+ endX: x + w,
endY: y + h,
};
if (viewRef.current) {
@@ -160,7 +160,7 @@ export default function UserListRow({
return null;
}
- const iconOpacity = DEFAULT_ICON_OPACITY * (disabled ? 1 : DISABLED_OPACITY);
+ const iconOpacity = DEFAULT_ICON_OPACITY * (disabled ? DISABLED_OPACITY : 1);
const color = selected ? theme.buttonBg : changeOpacity(theme.centerChannelColor, iconOpacity);
return (
@@ -267,3 +267,4 @@ export default function UserListRow({
);
}
+export default React.memo(UserListRow);
diff --git a/app/hooks/device.ts b/app/hooks/device.ts
index e6db62d43..c29a91ec5 100644
--- a/app/hooks/device.ts
+++ b/app/hooks/device.ts
@@ -45,21 +45,21 @@ export function useIsTablet() {
return Device.IS_TABLET && !isSplitView;
}
-export function useKeyboardHeight(keyboardTracker?: React.RefObject) {
- const [keyboardHeight, setKeyboardHeight] = useState(0);
+export function useKeyboardHeightWithDuration(keyboardTracker?: React.RefObject) {
+ const [keyboardHeight, setKeyboardHeight] = useState({height: 0, duration: 0});
const updateTimeout = useRef(null);
const insets = useSafeAreaInsets();
// This is a magic number. With tracking view, to properly get the final position, this had to be added.
const KEYBOARD_TRACKINGVIEW_SEPARATION = 4;
- const updateValue = (v: number) => {
+ const updateValue = (height: number, duration: number) => {
if (updateTimeout.current != null) {
clearTimeout(updateTimeout.current);
updateTimeout.current = null;
}
updateTimeout.current = setTimeout(() => {
- setKeyboardHeight(v);
+ setKeyboardHeight({height, duration});
updateTimeout.current = null;
}, 200);
};
@@ -69,21 +69,21 @@ export function useKeyboardHeight(keyboardTracker?: React.RefObject {
+ const hide = Keyboard.addListener(Platform.select({ios: 'keyboardWillHide', default: 'keyboardDidHide'}), (event) => {
if (updateTimeout.current != null) {
clearTimeout(updateTimeout.current);
updateTimeout.current = null;
}
- setKeyboardHeight(0);
+ setKeyboardHeight({height: 0, duration: event.duration});
});
return () => {
@@ -95,9 +95,15 @@ export function useKeyboardHeight(keyboardTracker?: React.RefObject) {
+ const {height} = useKeyboardHeightWithDuration(keyboardTracker);
+ return height;
+}
+
export function useModalPosition(viewRef: RefObject, deps?: React.DependencyList) {
const [modalPosition, setModalPosition] = useState(0);
const isTablet = useIsTablet();
+ const height = useKeyboardHeight();
useEffect(() => {
if (Platform.OS === 'ios' && isTablet) {
@@ -107,7 +113,7 @@ export function useModalPosition(viewRef: RefObject, deps?: React.Dependen
}
});
}
- }, deps);
+ }, [...(deps || []), isTablet, height]);
return modalPosition;
}
diff --git a/app/screens/bottom_sheet/button.tsx b/app/screens/bottom_sheet/button.tsx
index 8bff6d715..a05c54b28 100644
--- a/app/screens/bottom_sheet/button.tsx
+++ b/app/screens/bottom_sheet/button.tsx
@@ -27,6 +27,7 @@ const styles = StyleSheet.create({
width: 24,
height: 24,
marginTop: 2,
+ marginRight: 8,
},
});
diff --git a/app/screens/create_direct_message/create_direct_message.tsx b/app/screens/create_direct_message/create_direct_message.tsx
index 297f7ba40..53d5b002d 100644
--- a/app/screens/create_direct_message/create_direct_message.tsx
+++ b/app/screens/create_direct_message/create_direct_message.tsx
@@ -2,8 +2,8 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
-import {useIntl} from 'react-intl';
-import {Keyboard, Platform, View} from 'react-native';
+import {defineMessages, useIntl} from 'react-intl';
+import {Keyboard, LayoutChangeEvent, Platform, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {makeDirectChannel, makeGroupChannel} from '@actions/remote/channel';
@@ -11,11 +11,13 @@ import {fetchProfiles, fetchProfilesInTeam, searchProfiles} from '@actions/remot
import CompassIcon from '@components/compass_icon';
import Loading from '@components/loading';
import Search from '@components/search';
+import SelectedUsers from '@components/selected_users';
import UserList from '@components/user_list';
import {General} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {debounce} from '@helpers/api/general';
+import {useModalPosition} from '@hooks/device';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {t} from '@i18n';
import {dismissModal, setButtons} from '@screens/navigation';
@@ -24,9 +26,25 @@ import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme}
import {typography} from '@utils/typography';
import {displayUsername, filterProfilesMatchingTerm} from '@utils/user';
-import SelectedUsers from './selected_users';
+const messages = defineMessages({
+ dm: {
+ id: t('mobile.open_dm.error'),
+ defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
+ },
+ gm: {
+ id: t('mobile.open_gm.error'),
+ defaultMessage: "We couldn't open a group message with those users. Please check your connection and try again.",
+ },
+ buttonText: {
+ id: t('mobile.create_direct_message.start'),
+ defaultMessage: 'Start Conversation',
+ },
+ toastMessage: {
+ id: t('mobile.create_direct_message.max_limit_reached'),
+ defaultMessage: 'Group messages are limited to {maxCount} members',
+ },
+});
-const START_BUTTON = 'start-conversation';
const CLOSE_BUTTON = 'close-dms';
type Props = {
@@ -43,7 +61,7 @@ const close = () => {
dismissModal();
};
-const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
return {
container: {
flex: 1,
@@ -82,6 +100,13 @@ function reduceProfiles(state: UserProfile[], action: {type: 'add'; values?: Use
return state;
}
+function removeProfileFromList(list: {[id: string]: UserProfile}, id: string) {
+ const newSelectedIds = Object.assign({}, list);
+
+ Reflect.deleteProperty(newSelectedIds, id);
+ return newSelectedIds;
+}
+
export default function CreateDirectMessage({
componentId,
currentTeamId,
@@ -100,6 +125,8 @@ export default function CreateDirectMessage({
const next = useRef(true);
const page = useRef(-1);
const mounted = useRef(false);
+ const mainView = useRef(null);
+ const modalPosition = useModalPosition(mainView);
const [profiles, dispatchProfiles] = useReducer(reduceProfiles, []);
const [searchResults, setSearchResults] = useState([]);
@@ -107,6 +134,8 @@ export default function CreateDirectMessage({
const [term, setTerm] = useState('');
const [startingConversation, setStartingConversation] = useState(false);
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
+ const [showToast, setShowToast] = useState(false);
+ const [containerHeight, setContainerHeight] = useState(0);
const selectedCount = Object.keys(selectedIds).length;
const isSearch = Boolean(term);
@@ -123,6 +152,28 @@ export default function CreateDirectMessage({
}
};
+ const data = useMemo(() => {
+ if (term) {
+ const exactMatches: UserProfile[] = [];
+ const filterByTerm = (p: UserProfile) => {
+ if (selectedCount > 0 && p.id === currentUserId) {
+ return false;
+ }
+
+ if (p.username === term || p.username.startsWith(term)) {
+ exactMatches.push(p);
+ return false;
+ }
+
+ return true;
+ };
+
+ const results = filterProfilesMatchingTerm(searchResults, term).filter(filterByTerm);
+ return [...exactMatches, ...results];
+ }
+ return profiles;
+ }, [term, isSearch && selectedCount, isSearch && searchResults, profiles]);
+
const clearSearch = useCallback(() => {
setTerm('');
setSearchResults([]);
@@ -140,32 +191,16 @@ export default function CreateDirectMessage({
}, 100), [loading, isSearch, restrictDirectMessage, serverUrl, currentTeamId]);
const handleRemoveProfile = useCallback((id: string) => {
- const newSelectedIds = Object.assign({}, selectedIds);
-
- Reflect.deleteProperty(newSelectedIds, id);
-
- setSelectedIds(newSelectedIds);
- }, [selectedIds]);
+ setSelectedIds((current) => removeProfileFromList(current, id));
+ }, []);
const createDirectChannel = useCallback(async (id: string): Promise => {
const user = selectedIds[id];
-
const displayName = displayUsername(user, intl.locale, teammateNameDisplay);
-
const result = await makeDirectChannel(serverUrl, id, displayName);
if (result.error) {
- alertErrorWithFallback(
- intl,
- result.error,
- {
- id: 'mobile.open_dm.error',
- defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
- },
- {
- displayName,
- },
- );
+ alertErrorWithFallback(intl, result.error, messages.dm);
}
return !result.error;
@@ -175,14 +210,7 @@ export default function CreateDirectMessage({
const result = await makeGroupChannel(serverUrl, ids);
if (result.error) {
- alertErrorWithFallback(
- intl,
- result.error,
- {
- id: t('mobile.open_gm.error'),
- defaultMessage: "We couldn't open a group message with those users. Please check your connection and try again.",
- },
- );
+ alertErrorWithFallback(intl, result.error, messages.gm);
}
return !result.error;
@@ -213,11 +241,6 @@ export default function CreateDirectMessage({
}, [startingConversation, selectedIds, createGroupChannel, createDirectChannel]);
const handleSelectProfile = useCallback((user: UserProfile) => {
- if (selectedIds[user.id]) {
- handleRemoveProfile(user.id);
- return;
- }
-
if (user.id === currentUserId) {
const selectedId = {
[currentUserId]: true,
@@ -225,22 +248,28 @@ export default function CreateDirectMessage({
startConversation(selectedId);
} else {
- const wasSelected = selectedIds[user.id];
-
- if (!wasSelected && selectedCount >= General.MAX_USERS_IN_GM) {
- return;
- }
-
- const newSelectedIds = Object.assign({}, selectedIds);
- if (!wasSelected) {
- newSelectedIds[user.id] = user;
- }
-
- setSelectedIds(newSelectedIds);
-
clearSearch();
+ setSelectedIds((current) => {
+ if (current[user.id]) {
+ return removeProfileFromList(current, user.id);
+ }
+
+ const wasSelected = current[user.id];
+
+ if (!wasSelected && selectedCount >= General.MAX_USERS_IN_GM) {
+ setShowToast(true);
+ return current;
+ }
+
+ const newSelectedIds = Object.assign({}, current);
+ if (!wasSelected) {
+ newSelectedIds[user.id] = user;
+ }
+
+ return newSelectedIds;
+ });
}
- }, [selectedIds, currentUserId, handleRemoveProfile, startConversation, clearSearch]);
+ }, [currentUserId, clearSearch]);
const searchUsers = useCallback(async (searchTerm: string) => {
const lowerCasedTerm = searchTerm.toLowerCase();
@@ -253,12 +282,12 @@ export default function CreateDirectMessage({
results = await searchProfiles(serverUrl, lowerCasedTerm, {allow_inactive: true});
}
- let data: UserProfile[] = [];
+ let searchData: UserProfile[] = [];
if (results.data) {
- data = results.data;
+ searchData = results.data;
}
- setSearchResults(data);
+ setSearchResults(searchData);
setLoading(false);
}, [restrictDirectMessage, serverUrl, currentTeamId]);
@@ -266,6 +295,10 @@ export default function CreateDirectMessage({
searchUsers(term);
}, [searchUsers, term]);
+ const onLayout = useCallback((e: LayoutChangeEvent) => {
+ setContainerHeight(e.nativeEvent.layout.height);
+ }, []);
+
const onSearch = useCallback((text: string) => {
if (text) {
setTerm(text);
@@ -281,7 +314,7 @@ export default function CreateDirectMessage({
}
}, [searchUsers, clearSearch]);
- const updateNavigationButtons = useCallback(async (startEnabled: boolean) => {
+ const updateNavigationButtons = useCallback(async () => {
const closeIcon = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
setButtons(componentId, {
leftButtons: [{
@@ -289,23 +322,14 @@ export default function CreateDirectMessage({
icon: closeIcon,
testID: 'close.create_direct_message.button',
}],
- rightButtons: [{
- color: theme.sidebarHeaderTextColor,
- id: START_BUTTON,
- text: formatMessage({id: 'mobile.create_direct_message.start', defaultMessage: 'Start'}),
- showAsAction: 'always',
- enabled: startEnabled,
- testID: 'create_direct_message.start.button',
- }],
});
}, [intl.locale, theme]);
- useNavButtonPressed(START_BUTTON, componentId, startConversation, [startConversation]);
useNavButtonPressed(CLOSE_BUTTON, componentId, close, [close]);
useEffect(() => {
mounted.current = true;
- updateNavigationButtons(false);
+ updateNavigationButtons();
getProfiles();
return () => {
mounted.current = false;
@@ -313,31 +337,8 @@ export default function CreateDirectMessage({
}, []);
useEffect(() => {
- const canStart = selectedCount > 0 && !startingConversation;
- updateNavigationButtons(canStart);
- }, [selectedCount > 0, startingConversation, updateNavigationButtons]);
-
- const data = useMemo(() => {
- if (term) {
- const exactMatches: UserProfile[] = [];
- const filterByTerm = (p: UserProfile) => {
- if (selectedCount > 0 && p.id === currentUserId) {
- return false;
- }
-
- if (p.username === term || p.username.startsWith(term)) {
- exactMatches.push(p);
- return false;
- }
-
- return true;
- };
-
- const results = filterProfilesMatchingTerm(searchResults, term).filter(filterByTerm);
- return [...exactMatches, ...results];
- }
- return profiles;
- }, [term, isSearch && selectedCount, isSearch && searchResults, profiles]);
+ setShowToast(selectedCount >= General.MAX_USERS_IN_GM);
+ }, [selectedCount >= General.MAX_USERS_IN_GM]);
if (startingConversation) {
return (
@@ -351,12 +352,15 @@ export default function CreateDirectMessage({
- {selectedCount > 0 &&
-
- }
+
);
}
diff --git a/app/screens/create_direct_message/selected_users/index.tsx b/app/screens/create_direct_message/selected_users/index.tsx
deleted file mode 100644
index 2eba3bce8..000000000
--- a/app/screens/create_direct_message/selected_users/index.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import React, {useMemo} from 'react';
-import {View} from 'react-native';
-
-import FormattedText from '@components/formatted_text';
-import {useTheme} from '@context/theme';
-import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
-
-import SelectedUser from './selected_user';
-
-type Props = {
-
- /*
- * An object mapping user ids to a falsey value indicating whether or not they've been selected.
- */
- selectedIds: {[id: string]: UserProfile};
-
- /*
- * How to display the names of users.
- */
- teammateNameDisplay: string;
-
- /*
- * The number of users that will be selected when we start to display a message indicating
- * the remaining number of users that can be selected.
- */
- warnCount: number;
-
- /*
- * The maximum number of users that can be selected.
- */
- maxCount: number;
-
- /*
- * A handler function that will deselect a user when clicked on.
- */
- onRemove: (id: string) => void;
-}
-
-const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
- return {
- container: {
- marginHorizontal: 12,
- },
- users: {
- alignItems: 'flex-start',
- flexDirection: 'row',
- flexWrap: 'wrap',
- },
- message: {
- color: changeOpacity(theme.centerChannelColor, 0.6),
- fontSize: 12,
- marginRight: 5,
- marginTop: 10,
- marginBottom: 2,
- },
- };
-});
-
-export default function SelectedUsers({
- selectedIds,
- teammateNameDisplay,
- warnCount,
- maxCount,
- onRemove,
-}: Props) {
- const theme = useTheme();
- const style = getStyleFromTheme(theme);
-
- const users = useMemo(() => {
- const u = [];
- for (const id of Object.keys(selectedIds)) {
- if (!selectedIds[id]) {
- continue;
- }
-
- u.push(
- ,
- );
- }
- return u;
- }, [selectedIds, teammateNameDisplay, onRemove]);
-
- const showWarn = users.length >= warnCount && users.length < maxCount;
-
- const message = useMemo(() => {
- if (users.length >= maxCount) {
- return (
-
- );
- } else if (users.length >= warnCount) {
- const remaining = maxCount - users.length;
- if (remaining === 1) {
- return (
-
- );
- }
- return (
-
- );
- }
-
- return null;
- }, [users.length >= maxCount, showWarn && users.length, theme, maxCount]);
-
- return (
-
-
- {users}
-
- {message}
-
- );
-}
-
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 8eba07a3e..160617d12 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -296,6 +296,7 @@
"general_settings.display": "Display",
"general_settings.help": "Help",
"general_settings.notifications": "Notifications",
+ "general_settings.report_problem": "Report a Problem",
"get_post_link_modal.title": "Copy Link",
"global_threads.allThreads": "All Your Threads",
"global_threads.emptyThreads.message": "Any threads you are mentioned in or have participated in will show here along with any threads you have followed.",
@@ -437,10 +438,8 @@
"mobile.components.select_server_view.proceed": "Proceed",
"mobile.create_channel": "Create",
"mobile.create_channel.title": "New channel",
- "mobile.create_direct_message.add_more": "You can add {remaining, number} more users",
- "mobile.create_direct_message.cannot_add_more": "You cannot add more users",
- "mobile.create_direct_message.one_more": "You can add 1 more user",
- "mobile.create_direct_message.start": "Start",
+ "mobile.create_direct_message.max_limit_reached": "Group messages are limited to {maxCount} members",
+ "mobile.create_direct_message.start": "Start Conversation",
"mobile.create_direct_message.you": "@{username} - you",
"mobile.create_post.read_only": "This channel is read-only.",
"mobile.custom_list.no_results": "No Results",
@@ -474,7 +473,6 @@
"mobile.gallery.title": "{index} of {total}",
"mobile.integration_selector.loading_channels": "Loading channels...",
"mobile.integration_selector.loading_options": "Loading options...",
- "mobile.integration_selector.loading_users": "Loading users...",
"mobile.ios.photos_permission_denied_description": "Upload photos and videos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo and video library.",
"mobile.ios.photos_permission_denied_title": "{applicationName} would like to access your photos",
"mobile.join_channel.error": "We couldn't join the channel {displayName}.",
@@ -527,6 +525,7 @@
"mobile.onboarding.next": "Next",
"mobile.onboarding.sign_in": "Sign in",
"mobile.onboarding.sign_in_to_get_started": "Sign in to get started",
+ "mobile.open_dm.error": "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
"mobile.open_gm.error": "We couldn't open a group message with those users. Please check your connection and try again.",
"mobile.participants.header": "Thread Participants",
"mobile.permission_denied_dismiss": "Don't Allow",