Add members (#7220)
* Add "Add members" modal * Refactor into server user list * Renaming and fixes * Address feedback * Add missing change * Styling fixes for iOS
This commit is contained in:
parent
05469207d7
commit
77095b1034
36 changed files with 826 additions and 277 deletions
|
|
@ -156,6 +156,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
|
|||
noBorder: true,
|
||||
scrollEdgeAppearance: {
|
||||
noBorder: true,
|
||||
active: true,
|
||||
},
|
||||
rightButtons,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -164,10 +164,12 @@ export async function addMembersToChannel(serverUrl: string, channelId: string,
|
|||
|
||||
if (!fetchOnly) {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
modelPromises.push(operator.handleUsers({
|
||||
users,
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
if (users?.length) {
|
||||
modelPromises.push(operator.handleUsers({
|
||||
users,
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
}
|
||||
modelPromises.push(operator.handleChannelMembership({
|
||||
channelMemberships,
|
||||
prepareRecordsOnly: true,
|
||||
|
|
|
|||
|
|
@ -485,10 +485,12 @@ export const fetchProfiles = async (serverUrl: string, page = 0, perPage: number
|
|||
if (!fetchOnly) {
|
||||
const currentUserId = await getCurrentUserId(operator.database);
|
||||
const toStore = removeUserFromList(currentUserId, users);
|
||||
await operator.handleUsers({
|
||||
users: toStore,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
if (toStore.length) {
|
||||
await operator.handleUsers({
|
||||
users: toStore,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {users};
|
||||
|
|
@ -517,7 +519,38 @@ export const fetchProfilesInTeam = async (serverUrl: string, teamId: string, pag
|
|||
if (!fetchOnly) {
|
||||
const currentUserId = await getCurrentUserId(operator.database);
|
||||
const toStore = removeUserFromList(currentUserId, users);
|
||||
if (toStore.length) {
|
||||
await operator.handleUsers({
|
||||
users: toStore,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {users};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchProfilesNotInChannel = async (
|
||||
serverUrl: string,
|
||||
teamId: string,
|
||||
channelId: string,
|
||||
groupConstrained = false,
|
||||
page = 0,
|
||||
perPage: number = General.PROFILE_CHUNK_SIZE,
|
||||
fetchOnly = false,
|
||||
) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const users = await client.getProfilesNotInChannel(teamId, channelId, groupConstrained, page, perPage);
|
||||
|
||||
if (!fetchOnly && users.length) {
|
||||
const currentUserId = await getCurrentUserId(operator.database);
|
||||
const toStore = removeUserFromList(currentUserId, users);
|
||||
await operator.handleUsers({
|
||||
users: toStore,
|
||||
prepareRecordsOnly: false,
|
||||
|
|
|
|||
|
|
@ -52,13 +52,22 @@ const Button = ({
|
|||
textStyle,
|
||||
], [theme, textStyle, size, emphasis, buttonType]);
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() =>
|
||||
(iconSize ? [
|
||||
styles.container,
|
||||
{minHeight: iconSize},
|
||||
] : styles.container),
|
||||
[iconSize],
|
||||
);
|
||||
|
||||
return (
|
||||
<RNButton
|
||||
containerStyle={bgStyle}
|
||||
onPress={onPress}
|
||||
testID={testID}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={containerStyle}>
|
||||
{Boolean(iconName) &&
|
||||
<CompassIcon
|
||||
name={iconName!}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import OptionBox from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {getHeaderOptions} from '@screens/channel_add_members/channel_add_members';
|
||||
import {dismissBottomSheet, goToScreen, showModal} from '@screens/navigation';
|
||||
|
||||
import type {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
displayName: string;
|
||||
inModal?: boolean;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
testID: string;
|
||||
}
|
||||
|
||||
const AddMembersBox = ({
|
||||
channelId,
|
||||
displayName,
|
||||
inModal,
|
||||
containerStyle,
|
||||
testID,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
|
||||
const onAddMembers = useCallback(async () => {
|
||||
const title = intl.formatMessage({id: 'intro.add_members', defaultMessage: 'Add members'});
|
||||
const options = await getHeaderOptions(theme, displayName, inModal);
|
||||
if (inModal) {
|
||||
goToScreen(Screens.CHANNEL_ADD_MEMBERS, title, {channelId, inModal}, options);
|
||||
return;
|
||||
}
|
||||
|
||||
await dismissBottomSheet();
|
||||
showModal(Screens.CHANNEL_ADD_MEMBERS, title, {channelId, inModal}, options);
|
||||
}, [intl, channelId, inModal, testID, displayName]);
|
||||
|
||||
return (
|
||||
<OptionBox
|
||||
containerStyle={containerStyle}
|
||||
iconName='account-plus-outline'
|
||||
onPress={onAddMembers}
|
||||
testID={testID}
|
||||
text={intl.formatMessage({id: 'intro.add_members', defaultMessage: 'Add members'})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddMembersBox;
|
||||
28
app/components/channel_actions/add_members_box/index.ts
Normal file
28
app/components/channel_actions/add_members_box/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
|
||||
import AddMembersBox from './add_members_box';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const channel = observeChannel(database, channelId);
|
||||
const displayName = channel.pipe(switchMap((c) => of$(c?.displayName)));
|
||||
|
||||
return {
|
||||
displayName,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(AddMembersBox));
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import OptionBox from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {dismissBottomSheet, goToScreen, showModal} from '@screens/navigation';
|
||||
|
||||
import type {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
inModal?: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const AddPeopleBox = ({channelId, containerStyle, inModal, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const onAddPeople = useCallback(async () => {
|
||||
const title = intl.formatMessage({id: 'intro.add_people', defaultMessage: 'Add People'});
|
||||
if (inModal) {
|
||||
goToScreen(Screens.CHANNEL_ADD_PEOPLE, title, {channelId});
|
||||
return;
|
||||
}
|
||||
await dismissBottomSheet();
|
||||
showModal(Screens.CHANNEL_ADD_PEOPLE, title, {channelId});
|
||||
}, [intl, channelId, inModal]);
|
||||
|
||||
return (
|
||||
<OptionBox
|
||||
containerStyle={containerStyle}
|
||||
iconName='account-plus-outline'
|
||||
onPress={onAddPeople}
|
||||
testID={testID}
|
||||
text={intl.formatMessage({id: 'intro.add_people', defaultMessage: 'Add People'})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPeopleBox;
|
||||
|
|
@ -5,6 +5,7 @@ import React, {useCallback} from 'react';
|
|||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import ChannelInfoStartButton from '@calls/components/channel_info_start';
|
||||
import AddMembersBox from '@components/channel_actions/add_members_box';
|
||||
import CopyChannelLinkBox from '@components/channel_actions/copy_channel_link_box';
|
||||
import FavoriteBox from '@components/channel_actions/favorite_box';
|
||||
import MutedBox from '@components/channel_actions/mute_box';
|
||||
|
|
@ -13,8 +14,6 @@ import {useServerUrl} from '@context/server';
|
|||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {isTypeDMorGM} from '@utils/channel';
|
||||
|
||||
// import AddPeopleBox from '@components/channel_actions/add_people_box';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
channelType?: ChannelType;
|
||||
|
|
@ -22,6 +21,7 @@ type Props = {
|
|||
dismissChannelInfo: () => void;
|
||||
callsEnabled: boolean;
|
||||
testID?: string;
|
||||
canManageMembers: boolean;
|
||||
}
|
||||
|
||||
export const CHANNEL_ACTIONS_OPTIONS_HEIGHT = 62;
|
||||
|
|
@ -36,7 +36,15 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const ChannelActions = ({channelId, channelType, inModal = false, dismissChannelInfo, callsEnabled, testID}: Props) => {
|
||||
const ChannelActions = ({
|
||||
channelId,
|
||||
channelType,
|
||||
inModal = false,
|
||||
dismissChannelInfo,
|
||||
callsEnabled,
|
||||
canManageMembers,
|
||||
testID,
|
||||
}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const onCopyLinkAnimationEnd = useCallback(() => {
|
||||
|
|
@ -70,15 +78,13 @@ const ChannelActions = ({channelId, channelType, inModal = false, dismissChannel
|
|||
testID={`${testID}.set_header.action`}
|
||||
/>
|
||||
}
|
||||
{/* Add back in after MM-47655 is resolved. https://mattermost.atlassian.net/browse/MM-47655
|
||||
{!isDM &&
|
||||
<AddPeopleBox
|
||||
{canManageMembers &&
|
||||
<AddMembersBox
|
||||
channelId={channelId}
|
||||
inModal={inModal}
|
||||
testID={`${testID}.add_people.action`}
|
||||
testID={`${testID}.add_members.action`}
|
||||
/>
|
||||
}
|
||||
*/}
|
||||
{!isDM && !callsEnabled &&
|
||||
<>
|
||||
<View style={styles.separator}/>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import {of as of$} from 'rxjs';
|
|||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeCanManageChannelMembers} from '@queries/servers/role';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import ChannelActions from './channel_actions';
|
||||
|
||||
|
|
@ -21,8 +23,12 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps
|
|||
switchMap((c) => of$(c?.type)),
|
||||
);
|
||||
|
||||
const canManageMembers = observeCurrentUser(database).pipe(
|
||||
switchMap((u) => (u ? observeCanManageChannelMembers(database, channelId, u) : of$(false))),
|
||||
);
|
||||
return {
|
||||
channelType,
|
||||
canManageMembers,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
text: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
paddingHorizontal: 5,
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
...typography('Body', 50, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -6,12 +6,11 @@ import {LayoutChangeEvent, Platform, ScrollView, useWindowDimensions, View} from
|
|||
import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import Button from '@components/button';
|
||||
import {USER_CHIP_BOTTOM_MARGIN, USER_CHIP_HEIGHT} from '@components/selected_chip';
|
||||
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 from './selected_user';
|
||||
|
|
@ -56,12 +55,12 @@ type Props = {
|
|||
/**
|
||||
* callback to set the value of showToast
|
||||
*/
|
||||
setShowToast: (show: boolean) => void;
|
||||
setShowToast?: (show: boolean) => void;
|
||||
|
||||
/**
|
||||
* show the toast
|
||||
*/
|
||||
showToast: boolean;
|
||||
showToast?: boolean;
|
||||
|
||||
/**
|
||||
* How to display the names of users.
|
||||
|
|
@ -81,15 +80,23 @@ type Props = {
|
|||
/**
|
||||
* toast Message
|
||||
*/
|
||||
toastMessage: string;
|
||||
toastMessage?: string;
|
||||
|
||||
/**
|
||||
* Max number of users in the list
|
||||
*/
|
||||
maxUsers?: number;
|
||||
}
|
||||
|
||||
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 SCROLL_MARGIN_TOP = 20;
|
||||
const SCROLL_MARGIN_BOTTOM = 12;
|
||||
const USERS_CHIPS_MAX_HEIGHT = (CHIP_HEIGHT_WITH_MARGIN * MAX_CHIP_ROWS) + EXPOSED_CHIP_HEIGHT;
|
||||
const SCROLL_MAX_HEIGHT = USERS_CHIPS_MAX_HEIGHT + SCROLL_MARGIN_TOP + SCROLL_MARGIN_BOTTOM;
|
||||
const PANEL_MAX_HEIGHT = SCROLL_MAX_HEIGHT + BUTTON_HEIGHT;
|
||||
const TABLET_MARGIN_BOTTOM = 20;
|
||||
const TOAST_BOTTOM_MARGIN = 24;
|
||||
|
||||
|
|
@ -102,7 +109,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
|||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
borderWidth: 1,
|
||||
maxHeight: PANEL_MAX_HEIGHT + BUTTON_HEIGHT,
|
||||
maxHeight: PANEL_MAX_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
paddingHorizontal: 20,
|
||||
shadowColor: theme.centerChannelColor,
|
||||
|
|
@ -116,9 +123,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
|||
toast: {
|
||||
backgroundColor: theme.centerChannelColor,
|
||||
},
|
||||
usersScroll: {
|
||||
marginTop: SCROLL_MARGIN_TOP,
|
||||
marginBottom: SCROLL_MARGIN_BOTTOM,
|
||||
},
|
||||
users: {
|
||||
paddingTop: SCROLL_PADDING_TOP,
|
||||
paddingBottom: 12,
|
||||
flexDirection: 'row',
|
||||
flexGrow: 1,
|
||||
flexWrap: 'wrap',
|
||||
|
|
@ -134,10 +143,20 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
|||
});
|
||||
|
||||
export default function SelectedUsers({
|
||||
buttonIcon, buttonText, containerHeight = 0,
|
||||
modalPosition = 0, onPress, onRemove,
|
||||
selectedIds, setShowToast, showToast = false,
|
||||
teammateNameDisplay, testID, toastIcon, toastMessage,
|
||||
buttonIcon,
|
||||
buttonText,
|
||||
containerHeight = 0,
|
||||
modalPosition = 0,
|
||||
onPress,
|
||||
onRemove,
|
||||
selectedIds,
|
||||
setShowToast,
|
||||
showToast = false,
|
||||
teammateNameDisplay,
|
||||
testID,
|
||||
toastIcon,
|
||||
toastMessage,
|
||||
maxUsers,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
|
@ -146,11 +165,10 @@ export default function SelectedUsers({
|
|||
const insets = useSafeAreaInsets();
|
||||
const dimensions = useWindowDimensions();
|
||||
|
||||
const panelHeight = useSharedValue(0);
|
||||
const usersChipsHeight = useSharedValue(0);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const numberSelectedIds = Object.keys(selectedIds).length;
|
||||
const bottomSpace = (dimensions.height - containerHeight - modalPosition);
|
||||
const bottomPaddingBottom = isTablet ? CHIP_HEIGHT_WITH_MARGIN : 0;
|
||||
|
||||
const users = useMemo(() => {
|
||||
const u = [];
|
||||
|
|
@ -173,13 +191,15 @@ export default function SelectedUsers({
|
|||
}, [selectedIds, teammateNameDisplay, onRemove]);
|
||||
|
||||
const totalPanelHeight = useDerivedValue(() => (
|
||||
isVisible ? panelHeight.value + BUTTON_HEIGHT + bottomPaddingBottom : 0
|
||||
), [isVisible, isTablet, bottomPaddingBottom]);
|
||||
isVisible ?
|
||||
usersChipsHeight.value + SCROLL_MARGIN_BOTTOM + SCROLL_MARGIN_TOP + BUTTON_HEIGHT :
|
||||
0
|
||||
), [isVisible]);
|
||||
|
||||
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;
|
||||
margin = keyboard.height ? Math.max((keyboard.height - bottomSpace - insets.bottom), 0) : 0;
|
||||
}
|
||||
return margin;
|
||||
}, [keyboard, isTablet, insets.bottom, bottomSpace]);
|
||||
|
|
@ -209,7 +229,10 @@ export default function SelectedUsers({
|
|||
}, [onPress]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
panelHeight.value = Math.min(PANEL_MAX_HEIGHT + bottomPaddingBottom, e.nativeEvent.layout.height);
|
||||
usersChipsHeight.value = Math.min(
|
||||
USERS_CHIPS_MAX_HEIGHT,
|
||||
e.nativeEvent.layout.height,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const androidMaxHeight = Platform.select({
|
||||
|
|
@ -234,10 +257,10 @@ export default function SelectedUsers({
|
|||
}, [showToast, keyboard]);
|
||||
|
||||
const animatedViewStyle = useAnimatedStyle(() => ({
|
||||
height: withTiming(totalPanelHeight.value + insets.bottom, {duration: 250}),
|
||||
height: withTiming(totalPanelHeight.value, {duration: 250}),
|
||||
borderWidth: isVisible ? 1 : 0,
|
||||
maxHeight: isVisible ? PANEL_MAX_HEIGHT + BUTTON_HEIGHT + bottomPaddingBottom + insets.bottom : 0,
|
||||
}), [isVisible, insets, bottomPaddingBottom]);
|
||||
maxHeight: isVisible ? PANEL_MAX_HEIGHT + BUTTON_HEIGHT : 0,
|
||||
}), [isVisible]);
|
||||
|
||||
const animatedButtonStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(isVisible ? 1 : 0, {duration: isVisible ? 500 : 100}),
|
||||
|
|
@ -252,13 +275,14 @@ export default function SelectedUsers({
|
|||
let timer: NodeJS.Timeout;
|
||||
if (showToast) {
|
||||
timer = setTimeout(() => {
|
||||
setShowToast(false);
|
||||
setShowToast?.(false);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [showToast]);
|
||||
|
||||
const isDisabled = Boolean(maxUsers && (numberSelectedIds > maxUsers));
|
||||
return (
|
||||
<Animated.View style={animatedContainerStyle}>
|
||||
{showToast &&
|
||||
|
|
@ -270,7 +294,7 @@ export default function SelectedUsers({
|
|||
/>
|
||||
}
|
||||
<Animated.View style={[style.container, animatedViewStyle]}>
|
||||
<ScrollView>
|
||||
<ScrollView style={style.usersScroll}>
|
||||
<View
|
||||
style={style.users}
|
||||
onLayout={onLayout}
|
||||
|
|
@ -281,9 +305,13 @@ export default function SelectedUsers({
|
|||
<Animated.View style={animatedButtonStyle}>
|
||||
<Button
|
||||
onPress={handlePress}
|
||||
icon={buttonIcon}
|
||||
iconName={buttonIcon}
|
||||
text={buttonText}
|
||||
disabled={numberSelectedIds > General.MAX_USERS_IN_GM}
|
||||
iconSize={20}
|
||||
theme={theme}
|
||||
buttonType={isDisabled ? 'disabled' : 'default'}
|
||||
emphasis={'primary'}
|
||||
size={'lg'}
|
||||
testID={`${testID}.start.button`}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
|
||||
import {fetchProfiles, searchProfiles} from '@actions/remote/user';
|
||||
import UserList from '@components/user_list';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
|
@ -11,26 +10,33 @@ import {debounce} from '@helpers/api/general';
|
|||
import {filterProfilesMatchingTerm} from '@utils/user';
|
||||
|
||||
type Props = {
|
||||
currentTeamId: string;
|
||||
currentUserId: string;
|
||||
teammateNameDisplay: string;
|
||||
tutorialWatched: boolean;
|
||||
handleSelectProfile: (user: UserProfile) => void;
|
||||
term: string;
|
||||
selectedIds: {[id: string]: UserProfile};
|
||||
fetchFunction: (page: number) => Promise<UserProfile[]>;
|
||||
searchFunction: (term: string) => Promise<UserProfile[]>;
|
||||
createFilter: (exactMatches: UserProfile[], term: string) => ((p: UserProfile) => boolean);
|
||||
testID: string;
|
||||
}
|
||||
|
||||
export default function ServerUserList({
|
||||
currentTeamId,
|
||||
currentUserId,
|
||||
teammateNameDisplay,
|
||||
tutorialWatched,
|
||||
handleSelectProfile,
|
||||
term,
|
||||
selectedIds,
|
||||
fetchFunction,
|
||||
searchFunction,
|
||||
createFilter,
|
||||
testID,
|
||||
}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||
const next = useRef(true);
|
||||
const page = useRef(-1);
|
||||
const mounted = useRef(false);
|
||||
|
|
@ -38,13 +44,12 @@ export default function ServerUserList({
|
|||
const [profiles, setProfiles] = useState<UserProfile[]>([]);
|
||||
const [searchResults, setSearchResults] = useState<UserProfile[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const selectedCount = Object.keys(selectedIds).length;
|
||||
|
||||
const isSearch = Boolean(term);
|
||||
|
||||
const loadedProfiles = ({users}: {users?: UserProfile[]}) => {
|
||||
const loadedProfiles = (users: UserProfile[]) => {
|
||||
if (mounted.current) {
|
||||
if (users && !users.length) {
|
||||
if (!users.length) {
|
||||
next.current = false;
|
||||
}
|
||||
|
||||
|
|
@ -63,30 +68,29 @@ export default function ServerUserList({
|
|||
const getProfiles = useCallback(debounce(() => {
|
||||
if (next.current && !loading && !term && mounted.current) {
|
||||
setLoading(true);
|
||||
fetchProfiles(serverUrl, page.current + 1, General.PROFILE_CHUNK_SIZE).then(loadedProfiles);
|
||||
fetchFunction(page.current + 1).then(loadedProfiles);
|
||||
}
|
||||
}, 100), [loading, isSearch, serverUrl, currentTeamId]);
|
||||
|
||||
const onHandleSelectProfile = useCallback((user: UserProfile) => {
|
||||
handleSelectProfile(user);
|
||||
}, [handleSelectProfile]);
|
||||
}, 100), [loading, isSearch, serverUrl]);
|
||||
|
||||
const searchUsers = useCallback(async (searchTerm: string) => {
|
||||
const lowerCasedTerm = searchTerm.toLowerCase();
|
||||
setLoading(true);
|
||||
const results = await searchProfiles(serverUrl, lowerCasedTerm, {allow_inactive: true});
|
||||
|
||||
let data: UserProfile[] = [];
|
||||
if (results.data) {
|
||||
data = results.data;
|
||||
}
|
||||
|
||||
const data = await searchFunction(searchTerm);
|
||||
setSearchResults(data);
|
||||
setLoading(false);
|
||||
}, [serverUrl, currentTeamId]);
|
||||
}, [serverUrl, searchFunction]);
|
||||
|
||||
useEffect(() => {
|
||||
searchUsers(term);
|
||||
if (term) {
|
||||
if (searchTimeoutId.current) {
|
||||
clearTimeout(searchTimeoutId.current);
|
||||
}
|
||||
|
||||
searchTimeoutId.current = setTimeout(() => {
|
||||
searchUsers(term);
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
} else {
|
||||
setSearchResults([]);
|
||||
}
|
||||
}, [term]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -98,31 +102,21 @@ export default function ServerUserList({
|
|||
}, []);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (term) {
|
||||
if (isSearch) {
|
||||
const exactMatches: UserProfile[] = [];
|
||||
const filterByTerm = (p: UserProfile) => {
|
||||
if (selectedCount > 0 && p.id === currentUserId) {
|
||||
return false;
|
||||
}
|
||||
const filterByTerm = createFilter(exactMatches, term);
|
||||
|
||||
if (p.username === term || p.username.startsWith(term)) {
|
||||
exactMatches.push(p);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const results = filterProfilesMatchingTerm(searchResults, term).filter(filterByTerm);
|
||||
const profilesToFilter = searchResults.length ? searchResults : profiles;
|
||||
const results = filterProfilesMatchingTerm(profilesToFilter, term).filter(filterByTerm);
|
||||
return [...exactMatches, ...results];
|
||||
}
|
||||
return profiles;
|
||||
}, [term, isSearch && selectedCount, isSearch && searchResults, profiles]);
|
||||
}, [term, isSearch, isSearch && searchResults, profiles]);
|
||||
|
||||
return (
|
||||
<UserList
|
||||
currentUserId={currentUserId}
|
||||
handleSelectProfile={onHandleSelectProfile}
|
||||
handleSelectProfile={handleSelectProfile}
|
||||
loading={loading}
|
||||
profiles={data}
|
||||
selectedIds={selectedIds}
|
||||
|
|
@ -130,7 +124,7 @@ export default function ServerUserList({
|
|||
teammateNameDisplay={teammateNameDisplay}
|
||||
fetchMore={getProfiles}
|
||||
term={term}
|
||||
testID='create_direct_message.user_list'
|
||||
testID={testID}
|
||||
tutorialWatched={tutorialWatched}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export const BOTTOM_SHEET = 'BottomSheet';
|
|||
export const BROWSE_CHANNELS = 'BrowseChannels';
|
||||
export const CALL = 'Call';
|
||||
export const CHANNEL = 'Channel';
|
||||
export const CHANNEL_ADD_PEOPLE = 'ChannelAddPeople';
|
||||
export const CHANNEL_ADD_MEMBERS = 'ChannelAddMembers';
|
||||
export const CHANNEL_INFO = 'ChannelInfo';
|
||||
export const CHANNEL_NOTIFICATION_PREFERENCES = 'ChannelNotificationPreferences';
|
||||
export const CODE = 'Code';
|
||||
|
|
@ -77,7 +77,7 @@ export default {
|
|||
BROWSE_CHANNELS,
|
||||
CALL,
|
||||
CHANNEL,
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_ADD_MEMBERS,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_NOTIFICATION_PREFERENCES,
|
||||
CODE,
|
||||
|
|
@ -142,6 +142,7 @@ export default {
|
|||
export const MODAL_SCREENS_WITHOUT_BACK = new Set<string>([
|
||||
BROWSE_CHANNELS,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_ADD_MEMBERS,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
CREATE_TEAM,
|
||||
CUSTOM_STATUS,
|
||||
|
|
@ -171,6 +172,5 @@ export const SCREENS_AS_BOTTOM_SHEET = new Set<string>([
|
|||
]);
|
||||
|
||||
export const NOT_READY = [
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CREATE_TEAM,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {t} from '@i18n';
|
|||
import keyMirror from '@utils/key_mirror';
|
||||
|
||||
export const SNACK_BAR_TYPE = keyMirror({
|
||||
ADD_CHANNEL_MEMBERS: null,
|
||||
FAVORITE_CHANNEL: null,
|
||||
LINK_COPIED: null,
|
||||
MESSAGE_COPIED: null,
|
||||
|
|
@ -22,6 +23,12 @@ type SnackBarConfig = {
|
|||
};
|
||||
|
||||
export const SNACK_BAR_CONFIG: Record<string, SnackBarConfig> = {
|
||||
ADD_CHANNEL_MEMBERS: {
|
||||
id: t('snack.bar.channel.members.added'),
|
||||
defaultMessage: '{numMembers, number} {numMembers, plural, one {member} other {members}} added',
|
||||
iconName: 'check',
|
||||
canUndo: false,
|
||||
},
|
||||
FAVORITE_CHANNEL: {
|
||||
id: t('snack.bar.favorited.channel'),
|
||||
defaultMessage: 'This channel was favorited',
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export function useKeyboardHeight(keyboardTracker?: React.RefObject<KeyboardTrac
|
|||
return height;
|
||||
}
|
||||
|
||||
export function useModalPosition(viewRef: RefObject<View>, deps?: React.DependencyList) {
|
||||
export function useModalPosition(viewRef: RefObject<View>, deps: React.DependencyList = []) {
|
||||
const [modalPosition, setModalPosition] = useState(0);
|
||||
const isTablet = useIsTablet();
|
||||
const height = useKeyboardHeight();
|
||||
|
|
@ -123,7 +123,7 @@ export function useModalPosition(viewRef: RefObject<View>, deps?: React.Dependen
|
|||
}
|
||||
});
|
||||
}
|
||||
}, [...(deps || []), isTablet, height]);
|
||||
}, [...deps, isTablet, height]);
|
||||
|
||||
return modalPosition;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ const DirectChannel = ({channel, currentUserId, isBot, members, theme}: Props) =
|
|||
channelId={channel.id}
|
||||
header={true}
|
||||
favorite={true}
|
||||
people={false}
|
||||
canAddMembers={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
// import AddPeopleBox from '@components/channel_actions/add_people_box';
|
||||
import AddMembersBox from '@components/channel_actions/add_members_box';
|
||||
import FavoriteBox from '@components/channel_actions/favorite_box';
|
||||
import InfoBox from '@components/channel_actions/info_box';
|
||||
import SetHeaderBox from '@components/channel_actions/set_header_box';
|
||||
|
|
@ -13,7 +13,7 @@ type Props = {
|
|||
channelId: string;
|
||||
header?: boolean;
|
||||
favorite?: boolean;
|
||||
people?: boolean;
|
||||
canAddMembers?: boolean;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
@ -39,18 +39,18 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const IntroOptions = ({channelId, header, favorite}: Props) => {
|
||||
const IntroOptions = ({channelId, header, favorite, canAddMembers}: Props) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Add back in after MM-47655 is resolved. https://mattermost.atlassian.net/browse/MM-47655
|
||||
{people &&
|
||||
<AddPeopleBox
|
||||
{canAddMembers &&
|
||||
<AddMembersBox
|
||||
channelId={channelId}
|
||||
containerStyle={[styles.item, styles.margin]}
|
||||
testID='channel_post_list.intro_options.add_people.action'
|
||||
testID='channel_post_list.intro_options.add_members.action'
|
||||
inModal={false}
|
||||
/>
|
||||
}
|
||||
*/}
|
||||
|
||||
{header &&
|
||||
<SetHeaderBox
|
||||
channelId={channelId}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ const PublicOrPrivateChannel = ({channel, creator, roles, theme}: Props) => {
|
|||
<IntroOptions
|
||||
channelId={channel.id}
|
||||
header={canSetHeader}
|
||||
people={canManagePeople}
|
||||
canAddMembers={canManagePeople}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ const TownSquare = ({channelId, displayName, roles, theme}: Props) => {
|
|||
<IntroOptions
|
||||
channelId={channelId}
|
||||
header={hasPermission(roles, Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES)}
|
||||
canAddMembers={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
298
app/screens/channel_add_members/channel_add_members.tsx
Normal file
298
app/screens/channel_add_members/channel_add_members.tsx
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, LayoutChangeEvent, Platform, View} from 'react-native';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {addMembersToChannel} from '@actions/remote/channel';
|
||||
import {fetchProfilesNotInChannel, searchProfiles} from '@actions/remote/user';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Loading from '@components/loading';
|
||||
import Search from '@components/search';
|
||||
import SelectedUsers from '@components/selected_users';
|
||||
import ServerUserList from '@components/server_user_list';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useModalPosition} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {t} from '@i18n';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {alertErrorWithFallback} from '@utils/draft';
|
||||
import {mergeNavigationOptions} from '@utils/navigation';
|
||||
import {showAddChannelMembersSnackbar} from '@utils/snack_bar';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
const CLOSE_BUTTON_ID = 'close-add-member';
|
||||
const TEST_ID = 'add_members';
|
||||
const CLOSE_BUTTON_TEST_ID = 'close.button';
|
||||
|
||||
export const getHeaderOptions = async (theme: Theme, displayName: string, inModal = false) => {
|
||||
let leftButtons;
|
||||
if (!inModal) {
|
||||
const closeButton = await CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
leftButtons = [{
|
||||
id: CLOSE_BUTTON_ID,
|
||||
icon: closeButton,
|
||||
testID: `${TEST_ID}.${CLOSE_BUTTON_TEST_ID}`,
|
||||
}];
|
||||
}
|
||||
return {
|
||||
topBar: {
|
||||
subtitle: {
|
||||
color: changeOpacity(theme.sidebarHeaderTextColor, 0.72),
|
||||
text: displayName,
|
||||
},
|
||||
leftButtons,
|
||||
backButton: inModal ? {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
} : undefined,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type Props = {
|
||||
componentId: AvailableScreens;
|
||||
channel?: ChannelModel;
|
||||
currentUserId: string;
|
||||
teammateNameDisplay: string;
|
||||
tutorialWatched: boolean;
|
||||
inModal?: boolean;
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
Keyboard.dismiss();
|
||||
dismissModal();
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
searchBar: {
|
||||
marginLeft: 12,
|
||||
marginRight: Platform.select({ios: 4, default: 12}),
|
||||
marginVertical: 12,
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: 70,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
},
|
||||
noResultContainer: {
|
||||
flexGrow: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
noResultText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
...typography('Body', 600, 'Regular'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function removeProfileFromList(list: {[id: string]: UserProfile}, id: string) {
|
||||
const newSelectedIds = Object.assign({}, list);
|
||||
|
||||
Reflect.deleteProperty(newSelectedIds, id);
|
||||
return newSelectedIds;
|
||||
}
|
||||
|
||||
export default function ChannelAddMembers({
|
||||
componentId,
|
||||
channel,
|
||||
currentUserId,
|
||||
teammateNameDisplay,
|
||||
tutorialWatched,
|
||||
inModal,
|
||||
}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
|
||||
const mainView = useRef<View>(null);
|
||||
const modalPosition = useModalPosition(mainView);
|
||||
|
||||
const [term, setTerm] = useState('');
|
||||
const [addingMembers, setAddingMembers] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setTerm('');
|
||||
}, []);
|
||||
|
||||
const handleRemoveProfile = useCallback((id: string) => {
|
||||
setSelectedIds((current) => removeProfileFromList(current, id));
|
||||
}, []);
|
||||
|
||||
const addMembers = useCallback(async () => {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (addingMembers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idsToUse = Object.keys(selectedIds);
|
||||
if (!idsToUse.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAddingMembers(true);
|
||||
const result = await addMembersToChannel(serverUrl, channel.id, idsToUse);
|
||||
|
||||
if (result.error) {
|
||||
alertErrorWithFallback(intl, result.error, {id: t('mobile.channel_add_members.error'), defaultMessage: 'There has been an error and we could not add those users to the channel.'});
|
||||
setAddingMembers(false);
|
||||
} else {
|
||||
close();
|
||||
showAddChannelMembersSnackbar(idsToUse.length);
|
||||
}
|
||||
}, [channel, addingMembers, selectedIds, serverUrl, intl]);
|
||||
|
||||
const handleSelectProfile = useCallback((user: UserProfile) => {
|
||||
clearSearch();
|
||||
setSelectedIds((current) => {
|
||||
if (current[user.id]) {
|
||||
return removeProfileFromList(current, user.id);
|
||||
}
|
||||
|
||||
const newSelectedIds = Object.assign({}, current);
|
||||
newSelectedIds[user.id] = user;
|
||||
|
||||
return newSelectedIds;
|
||||
});
|
||||
}, [currentUserId, clearSearch]);
|
||||
|
||||
const onTextChange = useCallback((searchTerm: string) => {
|
||||
setTerm(searchTerm);
|
||||
}, []);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const updateNavigationButtons = useCallback(async () => {
|
||||
const options = await getHeaderOptions(theme, channel?.displayName || '', inModal);
|
||||
mergeNavigationOptions(componentId, options);
|
||||
}, [theme, channel?.displayName, inModal, componentId]);
|
||||
|
||||
const userFetchFunction = useCallback(async (page: number) => {
|
||||
if (!channel) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await fetchProfilesNotInChannel(serverUrl, channel.teamId, channel.id, channel.isGroupConstrained, page, General.PROFILE_CHUNK_SIZE);
|
||||
if (result.users?.length) {
|
||||
return result.users;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [serverUrl, channel]);
|
||||
|
||||
const userSearchFunction = useCallback(async (searchTerm: string) => {
|
||||
if (!channel) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lowerCasedTerm = searchTerm.toLowerCase();
|
||||
const results = await searchProfiles(serverUrl, lowerCasedTerm, {team_id: channel.teamId, not_in_channel_id: channel.id, allow_inactive: true});
|
||||
|
||||
if (results.data) {
|
||||
return results.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [serverUrl, channel]);
|
||||
|
||||
const createUserFilter = useCallback((exactMatches: UserProfile[], searchTerm: string) => {
|
||||
return (p: UserProfile) => {
|
||||
if (p.username === searchTerm || p.username.startsWith(searchTerm)) {
|
||||
exactMatches.push(p);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useNavButtonPressed(CLOSE_BUTTON_ID, componentId, close, [close]);
|
||||
useAndroidHardwareBackHandler(componentId, close);
|
||||
|
||||
useEffect(() => {
|
||||
updateNavigationButtons();
|
||||
}, [updateNavigationButtons]);
|
||||
|
||||
if (addingMembers) {
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={style.container}
|
||||
testID={`${TEST_ID}.screen`}
|
||||
onLayout={onLayout}
|
||||
ref={mainView}
|
||||
edges={['top', 'left', 'right']}
|
||||
>
|
||||
<View style={style.searchBar}>
|
||||
<Search
|
||||
testID={`${TEST_ID}.search_bar`}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelButtonTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
onChangeText={onTextChange}
|
||||
onCancel={clearSearch}
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
<ServerUserList
|
||||
currentUserId={currentUserId}
|
||||
handleSelectProfile={handleSelectProfile}
|
||||
selectedIds={selectedIds}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
term={term}
|
||||
testID={`${TEST_ID}.user_list`}
|
||||
tutorialWatched={tutorialWatched}
|
||||
fetchFunction={userFetchFunction}
|
||||
searchFunction={userSearchFunction}
|
||||
createFilter={createUserFilter}
|
||||
/>
|
||||
<SelectedUsers
|
||||
containerHeight={containerHeight}
|
||||
modalPosition={modalPosition}
|
||||
selectedIds={selectedIds}
|
||||
onRemove={handleRemoveProfile}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
onPress={addMembers}
|
||||
buttonIcon={'account-plus-outline'}
|
||||
buttonText={formatMessage({id: 'channel_add_members.add_members.button', defaultMessage: 'Add Members'})}
|
||||
testID={`${TEST_ID}.selected`}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
29
app/screens/channel_add_members/index.ts
Normal file
29
app/screens/channel_add_members/index.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {Tutorial} from '@constants';
|
||||
import {observeTutorialWatched} from '@queries/app/global';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
||||
|
||||
import ChannelAddMembers from './channel_add_members';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type OwnProps = {
|
||||
channelId: string;
|
||||
} & WithDatabaseArgs;
|
||||
const enhanced = withObservables(['channelId'], ({database, channelId}: OwnProps) => {
|
||||
const channel = observeChannel(database, channelId);
|
||||
|
||||
return {
|
||||
channel,
|
||||
teammateNameDisplay: observeTeammateNameDisplay(database),
|
||||
tutorialWatched: observeTutorialWatched(Tutorial.PROFILE_LONG_PRESS),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(ChannelAddMembers));
|
||||
|
|
@ -29,6 +29,7 @@ type Props = {
|
|||
type?: ChannelType;
|
||||
canEnableDisableCalls: boolean;
|
||||
isCallsEnabledInChannel: boolean;
|
||||
canManageMembers: boolean;
|
||||
}
|
||||
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
|
@ -55,6 +56,7 @@ const ChannelInfo = ({
|
|||
type,
|
||||
canEnableDisableCalls,
|
||||
isCallsEnabledInChannel,
|
||||
canManageMembers,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
|
@ -100,6 +102,7 @@ const ChannelInfo = ({
|
|||
channelId={channelId}
|
||||
type={type}
|
||||
callsEnabled={callsAvailable}
|
||||
canManageMembers={canManageMembers}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
{canEnableDisableCalls &&
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@
|
|||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
import {distinctUntilChanged, switchMap, combineLatestWith} from 'rxjs/operators';
|
||||
|
||||
import {observeIsCallsEnabledInChannel} from '@calls/observers';
|
||||
import {observeCallsConfig} from '@calls/state';
|
||||
import {withServerUrl} from '@context/server';
|
||||
import {observeCurrentChannel} from '@queries/servers/channel';
|
||||
import {observeCanManageChannelMembers} from '@queries/servers/role';
|
||||
import {
|
||||
observeConfigValue,
|
||||
observeCurrentChannelId,
|
||||
|
|
@ -98,10 +99,16 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
|
|||
);
|
||||
const isCallsEnabledInChannel = observeIsCallsEnabledInChannel(database, serverUrl, observeCurrentChannelId(database));
|
||||
|
||||
const canManageMembers = observeCurrentUser(database).pipe(
|
||||
combineLatestWith(channelId),
|
||||
switchMap(([u, cId]) => (u ? observeCanManageChannelMembers(database, cId, u) : of$(false))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
return {
|
||||
type,
|
||||
canEnableDisableCalls,
|
||||
isCallsEnabledInChannel,
|
||||
canManageMembers,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
41
app/screens/channel_info/options/add_members/add_members.tsx
Normal file
41
app/screens/channel_info/options/add_members/add_members.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import {getHeaderOptions} from '@app/screens/channel_add_members/channel_add_members';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {goToScreen} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const AddMembers = ({displayName, channelId}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
const title = formatMessage({id: 'channel_info.add_members', defaultMessage: 'Add members'});
|
||||
|
||||
const goToAddMembers = preventDoubleTap(async () => {
|
||||
const options = await getHeaderOptions(theme, displayName, true);
|
||||
goToScreen(Screens.CHANNEL_ADD_MEMBERS, title, {channelId, inModal: true}, options);
|
||||
});
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={goToAddMembers}
|
||||
label={title}
|
||||
icon='account-plus-outline'
|
||||
type={Platform.select({ios: 'arrow', default: 'default'})}
|
||||
testID='channel_info.options.add_members.option'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddMembers;
|
||||
29
app/screens/channel_info/options/add_members/index.ts
Normal file
29
app/screens/channel_info/options/add_members/index.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
|
||||
import AddMembers from './add_members';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const displayName = observeChannel(database, channelId).pipe(
|
||||
switchMap((c) => of$(c?.displayName)),
|
||||
);
|
||||
|
||||
return {
|
||||
displayName,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(AddMembers));
|
||||
|
|
@ -7,6 +7,7 @@ import CopyChannelLinkOption from '@components/channel_actions/copy_channel_link
|
|||
import {General} from '@constants';
|
||||
import {isTypeDMorGM} from '@utils/channel';
|
||||
|
||||
import AddMembers from './add_members';
|
||||
import EditChannel from './edit_channel';
|
||||
import IgnoreMentions from './ignore_mentions';
|
||||
import Members from './members';
|
||||
|
|
@ -17,9 +18,15 @@ type Props = {
|
|||
channelId: string;
|
||||
type?: ChannelType;
|
||||
callsEnabled: boolean;
|
||||
canManageMembers: boolean;
|
||||
}
|
||||
|
||||
const Options = ({channelId, type, callsEnabled}: Props) => {
|
||||
const Options = ({
|
||||
channelId,
|
||||
type,
|
||||
callsEnabled,
|
||||
canManageMembers,
|
||||
}: Props) => {
|
||||
const isDMorGM = isTypeDMorGM(type);
|
||||
|
||||
return (
|
||||
|
|
@ -32,6 +39,9 @@ const Options = ({channelId, type, callsEnabled}: Props) => {
|
|||
{type !== General.DM_CHANNEL &&
|
||||
<Members channelId={channelId}/>
|
||||
}
|
||||
{canManageMembers &&
|
||||
<AddMembers channelId={channelId}/>
|
||||
}
|
||||
{callsEnabled && !isDMorGM && // if calls is not enabled, copy link will show in the channel actions
|
||||
<CopyChannelLinkOption
|
||||
channelId={channelId}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {Keyboard, LayoutChangeEvent, Platform, View} from 'react-native';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
|
@ -12,11 +12,10 @@ 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 ServerUserList from '@components/server_user_list';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useModalPosition} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
|
|
@ -25,7 +24,7 @@ import {dismissModal, setButtons} from '@screens/navigation';
|
|||
import {alertErrorWithFallback} from '@utils/draft';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {displayUsername, filterProfilesMatchingTerm} from '@utils/user';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
|
|
@ -96,13 +95,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
function reduceProfiles(state: UserProfile[], action: {type: 'add'; values?: UserProfile[]}) {
|
||||
if (action.type === 'add' && action.values?.length) {
|
||||
return [...state, ...action.values];
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function removeProfileFromList(list: {[id: string]: UserProfile}, id: string) {
|
||||
const newSelectedIds = Object.assign({}, list);
|
||||
|
||||
|
|
@ -124,16 +116,9 @@ export default function CreateDirectMessage({
|
|||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
|
||||
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||
const next = useRef(true);
|
||||
const page = useRef(-1);
|
||||
const mounted = useRef(false);
|
||||
const mainView = useRef<View>(null);
|
||||
const modalPosition = useModalPosition(mainView);
|
||||
|
||||
const [profiles, dispatchProfiles] = useReducer(reduceProfiles, []);
|
||||
const [searchResults, setSearchResults] = useState<UserProfile[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [term, setTerm] = useState('');
|
||||
const [startingConversation, setStartingConversation] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
|
||||
|
|
@ -141,58 +126,10 @@ export default function CreateDirectMessage({
|
|||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
const selectedCount = Object.keys(selectedIds).length;
|
||||
|
||||
const isSearch = Boolean(term);
|
||||
|
||||
const loadedProfiles = ({users}: {users?: UserProfile[]}) => {
|
||||
if (mounted.current) {
|
||||
if (users && !users.length) {
|
||||
next.current = false;
|
||||
}
|
||||
|
||||
page.current += 1;
|
||||
setLoading(false);
|
||||
dispatchProfiles({type: 'add', values: users});
|
||||
}
|
||||
};
|
||||
|
||||
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([]);
|
||||
}, []);
|
||||
|
||||
const getProfiles = useCallback(debounce(() => {
|
||||
if (next.current && !loading && !term && mounted.current) {
|
||||
setLoading(true);
|
||||
if (restrictDirectMessage) {
|
||||
fetchProfilesInTeam(serverUrl, currentTeamId, page.current + 1, General.PROFILE_CHUNK_SIZE).then(loadedProfiles);
|
||||
} else {
|
||||
fetchProfiles(serverUrl, page.current + 1, General.PROFILE_CHUNK_SIZE).then(loadedProfiles);
|
||||
}
|
||||
}
|
||||
}, 100), [loading, isSearch, restrictDirectMessage, serverUrl, currentTeamId]);
|
||||
|
||||
const handleRemoveProfile = useCallback((id: string) => {
|
||||
setSelectedIds((current) => removeProfileFromList(current, id));
|
||||
}, []);
|
||||
|
|
@ -274,49 +211,10 @@ export default function CreateDirectMessage({
|
|||
}
|
||||
}, [currentUserId, clearSearch]);
|
||||
|
||||
const searchUsers = useCallback(async (searchTerm: string) => {
|
||||
const lowerCasedTerm = searchTerm.toLowerCase();
|
||||
setLoading(true);
|
||||
let results;
|
||||
|
||||
if (restrictDirectMessage) {
|
||||
results = await searchProfiles(serverUrl, lowerCasedTerm, {team_id: currentTeamId, allow_inactive: true});
|
||||
} else {
|
||||
results = await searchProfiles(serverUrl, lowerCasedTerm, {allow_inactive: true});
|
||||
}
|
||||
|
||||
let searchData: UserProfile[] = [];
|
||||
if (results.data) {
|
||||
searchData = results.data;
|
||||
}
|
||||
|
||||
setSearchResults(searchData);
|
||||
setLoading(false);
|
||||
}, [restrictDirectMessage, serverUrl, currentTeamId]);
|
||||
|
||||
const search = useCallback(() => {
|
||||
searchUsers(term);
|
||||
}, [searchUsers, term]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const onSearch = useCallback((text: string) => {
|
||||
if (text) {
|
||||
setTerm(text);
|
||||
if (searchTimeoutId.current) {
|
||||
clearTimeout(searchTimeoutId.current);
|
||||
}
|
||||
|
||||
searchTimeoutId.current = setTimeout(() => {
|
||||
searchUsers(text);
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
} else {
|
||||
clearSearch();
|
||||
}
|
||||
}, [searchUsers, clearSearch]);
|
||||
|
||||
const updateNavigationButtons = useCallback(async () => {
|
||||
const closeIcon = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
setButtons(componentId, {
|
||||
|
|
@ -328,17 +226,62 @@ export default function CreateDirectMessage({
|
|||
});
|
||||
}, [intl.locale, theme]);
|
||||
|
||||
const onChangeText = useCallback((searchTerm: string) => {
|
||||
setTerm(searchTerm);
|
||||
}, []);
|
||||
|
||||
const userFetchFunction = useCallback(async (page: number) => {
|
||||
let results;
|
||||
if (restrictDirectMessage) {
|
||||
results = await fetchProfilesInTeam(serverUrl, currentTeamId, page, General.PROFILE_CHUNK_SIZE);
|
||||
} else {
|
||||
results = await fetchProfiles(serverUrl, page, General.PROFILE_CHUNK_SIZE);
|
||||
}
|
||||
|
||||
if (results.users?.length) {
|
||||
return results.users;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [serverUrl, currentTeamId, restrictDirectMessage]);
|
||||
|
||||
const userSearchFunction = useCallback(async (searchTerm: string) => {
|
||||
const lowerCasedTerm = searchTerm.toLowerCase();
|
||||
let results;
|
||||
if (restrictDirectMessage) {
|
||||
results = await searchProfiles(serverUrl, lowerCasedTerm, {team_id: currentTeamId, allow_inactive: true});
|
||||
} else {
|
||||
results = await searchProfiles(serverUrl, lowerCasedTerm, {allow_inactive: true});
|
||||
}
|
||||
|
||||
if (results.data) {
|
||||
return results.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [serverUrl, currentTeamId, restrictDirectMessage]);
|
||||
|
||||
const createUserFilter = useCallback((exactMatches: UserProfile[], searchTerm: string) => {
|
||||
return (p: UserProfile) => {
|
||||
if (selectedCount > 0 && p.id === currentUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (p.username === searchTerm || p.username.startsWith(searchTerm)) {
|
||||
exactMatches.push(p);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}, [selectedCount > 0, currentUserId]);
|
||||
|
||||
useNavButtonPressed(CLOSE_BUTTON, componentId, close, [close]);
|
||||
useAndroidHardwareBackHandler(componentId, close);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
updateNavigationButtons();
|
||||
getProfiles();
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
}, [updateNavigationButtons]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowToast(selectedCount >= General.MAX_USERS_IN_GM);
|
||||
|
|
@ -366,26 +309,24 @@ export default function CreateDirectMessage({
|
|||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelButtonTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
onChangeText={onSearch}
|
||||
onSubmitEditing={search}
|
||||
onChangeText={onChangeText}
|
||||
onCancel={clearSearch}
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
<UserList
|
||||
<ServerUserList
|
||||
currentUserId={currentUserId}
|
||||
handleSelectProfile={handleSelectProfile}
|
||||
loading={loading}
|
||||
profiles={data}
|
||||
selectedIds={selectedIds}
|
||||
showNoResults={!loading && page.current !== -1}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
fetchMore={getProfiles}
|
||||
term={term}
|
||||
testID='create_direct_message.user_list'
|
||||
tutorialWatched={tutorialWatched}
|
||||
fetchFunction={userFetchFunction}
|
||||
searchFunction={userSearchFunction}
|
||||
createFilter={createUserFilter}
|
||||
/>
|
||||
<SelectedUsers
|
||||
containerHeight={containerHeight}
|
||||
|
|
@ -401,6 +342,7 @@ export default function CreateDirectMessage({
|
|||
buttonIcon={'forum-outline'}
|
||||
buttonText={formatMessage(messages.buttonText)}
|
||||
testID='create_direct_message'
|
||||
maxUsers={General.MAX_USERS_IN_GM}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.CREATE_DIRECT_MESSAGE:
|
||||
screen = withServerDatabase(require('@screens/create_direct_message').default);
|
||||
break;
|
||||
case Screens.CHANNEL_ADD_MEMBERS:
|
||||
screen = withServerDatabase(require('@screens/channel_add_members').default);
|
||||
break;
|
||||
case Screens.EDIT_POST:
|
||||
screen = withServerDatabase(require('@screens/edit_post').default);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {View} from 'react-native';
|
|||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {fetchChannels, searchChannels} from '@actions/remote/channel';
|
||||
import {fetchProfiles, searchProfiles} from '@actions/remote/user';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import SearchBar from '@components/search';
|
||||
import ServerUserList from '@components/server_user_list';
|
||||
|
|
@ -516,18 +517,52 @@ function IntegrationSelector(
|
|||
);
|
||||
}, [multiselectSelected, selectedIds, style, theme]);
|
||||
|
||||
const userFetchFunction = useCallback(async (userFetchPage: number) => {
|
||||
const result = await fetchProfiles(serverUrl, userFetchPage, General.PROFILE_CHUNK_SIZE);
|
||||
if (result.users?.length) {
|
||||
return result.users;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [serverUrl]);
|
||||
|
||||
const userSearchFunction = useCallback(async (searchTerm: string) => {
|
||||
const lowerCasedTerm = searchTerm.toLowerCase();
|
||||
const results = await searchProfiles(serverUrl, lowerCasedTerm, {allow_inactive: false});
|
||||
|
||||
if (results.data) {
|
||||
return results.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [serverUrl]);
|
||||
|
||||
const createUserFilter = useCallback((exactMatches: UserProfile[], searchTerm: string) => {
|
||||
return (p: UserProfile) => {
|
||||
if (p.username === searchTerm || p.username.startsWith(searchTerm)) {
|
||||
exactMatches.push(p);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderDataTypeList = () => {
|
||||
switch (dataSource) {
|
||||
case ViewConstants.DATA_SOURCE_USERS:
|
||||
return (
|
||||
<ServerUserList
|
||||
currentTeamId={currentTeamId}
|
||||
currentUserId={currentUserId}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
term={term}
|
||||
tutorialWatched={true}
|
||||
handleSelectProfile={handleSelectProfile}
|
||||
selectedIds={selectedIds as {[id: string]: UserProfile}}
|
||||
fetchFunction={userFetchFunction}
|
||||
searchFunction={userSearchFunction}
|
||||
createFilter={createUserFilter}
|
||||
testID={'integration_selector.user_list'}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -441,7 +441,7 @@ export function resetToTeams() {
|
|||
});
|
||||
}
|
||||
|
||||
export function goToScreen(name: AvailableScreens, title: string, passProps = {}, options = {}) {
|
||||
export function goToScreen(name: AvailableScreens, title: string, passProps = {}, options: Options = {}) {
|
||||
if (!isScreenRegistered(name)) {
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,13 +28,12 @@ import {makeStyleSheetFromTheme} from '@utils/theme';
|
|||
import {typography} from '@utils/typography';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
import type {ShowSnackBarArgs} from '@utils/snack_bar';
|
||||
|
||||
type SnackBarProps = {
|
||||
componentId: AvailableScreens;
|
||||
onAction?: () => void;
|
||||
barType: keyof typeof SNACK_BAR_TYPE;
|
||||
sourceScreen: AvailableScreens;
|
||||
}
|
||||
} & ShowSnackBarArgs;
|
||||
|
||||
const SNACK_BAR_WIDTH = 96;
|
||||
const SNACK_BAR_HEIGHT = 56;
|
||||
|
|
@ -81,7 +80,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const SnackBar = ({barType, componentId, onAction, sourceScreen}: SnackBarProps) => {
|
||||
const SnackBar = ({
|
||||
barType,
|
||||
messageValues,
|
||||
componentId,
|
||||
onAction,
|
||||
sourceScreen,
|
||||
}: SnackBarProps) => {
|
||||
const [showSnackBar, setShowSnackBar] = useState<boolean | undefined>();
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
|
|
@ -245,7 +250,10 @@ const SnackBar = ({barType, componentId, onAction, sourceScreen}: SnackBarProps)
|
|||
<Toast
|
||||
animatedStyle={snackBarStyle}
|
||||
iconName={config.iconName}
|
||||
message={intl.formatMessage({id: config.id, defaultMessage: config.defaultMessage})}
|
||||
message={intl.formatMessage(
|
||||
{id: config.id, defaultMessage: config.defaultMessage},
|
||||
messageValues,
|
||||
)}
|
||||
style={[styles.toast, barType === SNACK_BAR_TYPE.LINK_COPIED && {backgroundColor: theme.onlineIndicator}]}
|
||||
textStyle={styles.text}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import {SNACK_BAR_TYPE} from '@constants/snack_bar';
|
|||
import {showOverlay} from '@screens/navigation';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
import type {PrimitiveType} from 'react-intl';
|
||||
|
||||
type ShowSnackBarArgs = {
|
||||
export type ShowSnackBarArgs = {
|
||||
barType: keyof typeof SNACK_BAR_TYPE;
|
||||
onAction?: () => void;
|
||||
sourceScreen?: AvailableScreens;
|
||||
messageValues?: Record<string, PrimitiveType>;
|
||||
};
|
||||
|
||||
export const showSnackBar = (passProps: ShowSnackBarArgs) => {
|
||||
|
|
@ -31,6 +33,14 @@ export const showFavoriteChannelSnackbar = (favorited: boolean, onAction: () =>
|
|||
});
|
||||
};
|
||||
|
||||
export const showAddChannelMembersSnackbar = (count: number) => {
|
||||
return showSnackBar({
|
||||
barType: SNACK_BAR_TYPE.ADD_CHANNEL_MEMBERS,
|
||||
sourceScreen: Screens.CHANNEL_ADD_MEMBERS,
|
||||
messageValues: {numMembers: count},
|
||||
});
|
||||
};
|
||||
|
||||
export const showRemoveChannelUserSnackbar = () => {
|
||||
return showSnackBar({
|
||||
barType: SNACK_BAR_TYPE.REMOVE_CHANNEL_USER,
|
||||
|
|
|
|||
|
|
@ -95,9 +95,11 @@
|
|||
"camera_type.photo.option": "Capture Photo",
|
||||
"camera_type.video.option": "Record Video",
|
||||
"center_panel.archived.closeChannel": "Close Channel",
|
||||
"channel_add_members.add_members.button": "Add Members",
|
||||
"channel_header.directchannel.you": "{displayName} (you)",
|
||||
"channel_header.info": "View info",
|
||||
"channel_header.member_count": "{count, plural, one {# member} other {# members}}",
|
||||
"channel_info.add_members": "Add members",
|
||||
"channel_info.alert_retry": "Try Again",
|
||||
"channel_info.alertNo": "No",
|
||||
"channel_info.alertYes": "Yes",
|
||||
|
|
@ -329,7 +331,7 @@
|
|||
"home.header.plus_menu": "Options",
|
||||
"integration_selector.multiselect.submit": "Done",
|
||||
"interactive_dialog.submit": "Submit",
|
||||
"intro.add_people": "Add People",
|
||||
"intro.add_members": "Add members",
|
||||
"intro.channel_info": "Info",
|
||||
"intro.created_by": "created by {creator} on {date}.",
|
||||
"intro.direct_message": "This is the start of your conversation with {teammate}. Messages and files shared here are not shown to anyone else.",
|
||||
|
|
@ -476,6 +478,7 @@
|
|||
"mobile.camera_photo_permission_denied_description": "Take photos and upload them to your server or save them to your device. Open Settings to grant {applicationName} read and write access to your camera.",
|
||||
"mobile.camera_photo_permission_denied_title": "{applicationName} would like to access your camera",
|
||||
"mobile.camera_type.title": "Camera options",
|
||||
"mobile.channel_add_members.error": "There has been an error and we could not add those users to the channel.",
|
||||
"mobile.channel_info.alertNo": "No",
|
||||
"mobile.channel_info.alertYes": "Yes",
|
||||
"mobile.channel_list.recent": "Recent",
|
||||
|
|
@ -921,6 +924,7 @@
|
|||
"skintone_selector.tooltip.description": "You can now choose the skin tone you prefer to use for your emojis.",
|
||||
"skintone_selector.tooltip.title": "Choose your default skin tone",
|
||||
"smobile.search.recent_title": "Recent searches in {teamName}",
|
||||
"snack.bar.channel.members.added": "{numMembers, number} {numMembers, plural, one {member} other {members}} added",
|
||||
"snack.bar.favorited.channel": "This channel was favorited",
|
||||
"snack.bar.link.copied": "Link copied to clipboard",
|
||||
"snack.bar.message.copied": "Text copied to clipboard",
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@ class ChannelScreen {
|
|||
muteQuickAction: 'channel.quick_actions.mute.action',
|
||||
unmuteQuickAction: 'channel.quick_actions.unmute.action',
|
||||
setHeaderQuickAction: 'channel.quick_actions.set_header.action',
|
||||
addPeopleQuickAction: 'channel.quick_actions.add_people.action',
|
||||
addMembersQuickAction: 'channel.quick_actions.add_members.action',
|
||||
copyChannelLinkQuickAction: 'channel.quick_actions.copy_channel_link.action',
|
||||
channelInfoQuickAction: 'channel.quick_actions.channel_info.action',
|
||||
leaveChannelQuickAction: 'channel.quick_actions.leave_channel.action',
|
||||
introDisplayName: 'channel_post_list.intro.display_name',
|
||||
introAddPeopleAction: 'channel_post_list.intro_options.add_people.action',
|
||||
introAddMembersAction: 'channel_post_list.intro_options.add_members.action',
|
||||
introSetHeaderAction: 'channel_post_list.intro_options.set_header.action',
|
||||
introFavoriteAction: 'channel_post_list.intro_options.favorite.action',
|
||||
introUnfavoriteAction: 'channel_post_list.intro_options.unfavorite.action',
|
||||
|
|
@ -50,12 +50,12 @@ class ChannelScreen {
|
|||
muteQuickAction = element(by.id(this.testID.muteQuickAction));
|
||||
unmuteQuickAction = element(by.id(this.testID.unmuteQuickAction));
|
||||
setHeaderQuickAction = element(by.id(this.testID.setHeaderQuickAction));
|
||||
addPeopleQuickAction = element(by.id(this.testID.addPeopleQuickAction));
|
||||
addMembersQuickAction = element(by.id(this.testID.addMembersQuickAction));
|
||||
copyChannelLinkQuickAction = element(by.id(this.testID.copyChannelLinkQuickAction));
|
||||
channelInfoQuickAction = element(by.id(this.testID.channelInfoQuickAction));
|
||||
leaveChannelQuickAction = element(by.id(this.testID.leaveChannelQuickAction));
|
||||
introDisplayName = element(by.id(this.testID.introDisplayName));
|
||||
introAddPeopleAction = element(by.id(this.testID.introAddPeopleAction));
|
||||
introAddMembersAction = element(by.id(this.testID.introAddMembersAction));
|
||||
introSetHeaderAction = element(by.id(this.testID.introSetHeaderAction));
|
||||
introFavoriteAction = element(by.id(this.testID.introFavoriteAction));
|
||||
introUnfavoriteAction = element(by.id(this.testID.introUnfavoriteAction));
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class ChannelInfoScreen {
|
|||
muteAction: 'channel_info.channel_actions.mute.action',
|
||||
unmuteAction: 'channel_info.channel_actions.unmute.action',
|
||||
setHeaderAction: 'channel_info.channel_actions.set_header.action',
|
||||
addPeopleAction: 'channel_info.channel_actions.add_people.action',
|
||||
addMembersAction: 'channel_info.channel_actions.add_members.action',
|
||||
copyChannelLinkAction: 'channel_info.channel_actions.copy_channel_link.action',
|
||||
joinStartCallAction: 'channel_info.channel_actions.join_start_call.action',
|
||||
extraHeader: 'channel_info.extra.header',
|
||||
|
|
@ -53,7 +53,7 @@ class ChannelInfoScreen {
|
|||
muteAction = element(by.id(this.testID.muteAction));
|
||||
unmuteAction = element(by.id(this.testID.unmuteAction));
|
||||
setHeaderAction = element(by.id(this.testID.setHeaderAction));
|
||||
addPeopleAction = element(by.id(this.testID.addPeopleAction));
|
||||
addMembersAction = element(by.id(this.testID.addMembersAction));
|
||||
copyChannelLinkAction = element(by.id(this.testID.copyChannelLinkAction));
|
||||
joinStartCallAction = element(by.id(this.testID.joinStartCallAction));
|
||||
extraHeader = element(by.id(this.testID.extraHeader));
|
||||
|
|
|
|||
1
types/api/users.d.ts
vendored
1
types/api/users.d.ts
vendored
|
|
@ -103,6 +103,7 @@ type SearchUserOptions = {
|
|||
team_id?: string;
|
||||
not_in_team?: string;
|
||||
in_channel_id?: string;
|
||||
not_in_channel_id?: string;
|
||||
in_group_id?: string;
|
||||
group_constrained?: boolean;
|
||||
allow_inactive?: boolean;
|
||||
|
|
|
|||
Loading…
Reference in a new issue