forked from Ivasoft/mattermost-mobile
Compare commits
59 Commits
release-2.
...
MM-47655-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af8f7fcccf | ||
|
|
794b16b327 | ||
|
|
052e2c66f5 | ||
|
|
d2ebfb1a9f | ||
|
|
cd97f29a2e | ||
|
|
76a2c277aa | ||
|
|
48dd301b86 | ||
|
|
de48d2c152 | ||
|
|
966ecb5508 | ||
|
|
0e6da10728 | ||
|
|
77fdb1858f | ||
|
|
c6efab0315 | ||
|
|
78ceab9e07 | ||
|
|
0406619bbc | ||
|
|
1c9ea94867 | ||
|
|
a7f8226eb9 | ||
|
|
b6fe56fc76 | ||
|
|
1f73625078 | ||
|
|
49a171c92a | ||
|
|
944a1dc3a8 | ||
|
|
1e5a32cd1c | ||
|
|
f9ff339133 | ||
|
|
da8f13283f | ||
|
|
6e89d4458e | ||
|
|
4c00e58c4c | ||
|
|
05f4bd963a | ||
|
|
5b1334d6c2 | ||
|
|
7a4b6e9512 | ||
|
|
f31d6c0d9b | ||
|
|
10718989dd | ||
|
|
419a5e8488 | ||
|
|
772e6a24fd | ||
|
|
02f5d8fa72 | ||
|
|
76facd2f2c | ||
|
|
84a6443042 | ||
|
|
205fe2dae9 | ||
|
|
40444118a5 | ||
|
|
b9d40b59e3 | ||
|
|
86c13f2be1 | ||
|
|
2c3564623d | ||
|
|
f920a5b5b2 | ||
|
|
2317c0e1b8 | ||
|
|
bbee41de31 | ||
|
|
9c4d9c659b | ||
|
|
4d9ab6de8a | ||
|
|
57187326b9 | ||
|
|
b2395ab5ef | ||
|
|
0c5d1b267e | ||
|
|
b25a375762 | ||
|
|
a5c4bc9b6a | ||
|
|
6503bbb6ef | ||
|
|
f6db4c9cc9 | ||
|
|
ff5e6d7892 | ||
|
|
23a29a05a8 | ||
|
|
7be1abebfd | ||
|
|
b261d0dc16 | ||
|
|
f54bb59aef | ||
|
|
2d10f99a94 | ||
|
|
a6b51436ac |
@@ -145,29 +145,21 @@ export async function fetchChannelMemberships(serverUrl: string, channelId: stri
|
||||
}
|
||||
|
||||
export async function addMembersToChannel(serverUrl: string, channelId: string, userIds: string[], postRootId = '', fetchOnly = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const promises = userIds.map((id) => client.addToChannel(id, channelId, postRootId));
|
||||
const channelMemberships: ChannelMembership[] = await Promise.all(promises);
|
||||
const {users} = await fetchUsersByIds(serverUrl, userIds, true);
|
||||
|
||||
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,
|
||||
|
||||
@@ -531,6 +531,36 @@ export const fetchProfilesInTeam = async (serverUrl: string, teamId: string, pag
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchProfilesNotInChannel = async (
|
||||
serverUrl: string,
|
||||
teamId: string,
|
||||
channelId: string,
|
||||
groupConstrained: boolean,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
return {users};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const searchProfiles = async (serverUrl: string, term: string, options: SearchUserOptions, fetchOnly = false) => {
|
||||
let client: Client;
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import OptionBox from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {t} from '@i18n';
|
||||
import {goToScreen, showModal} from '@screens/navigation';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import type {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
displayName: string;
|
||||
inModal?: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: t('mobile.channel_add_people.title'),
|
||||
defaultMessage: 'Add Members',
|
||||
},
|
||||
boxText: {
|
||||
id: t('intro.add_people'),
|
||||
defaultMessage: 'Add People',
|
||||
},
|
||||
});
|
||||
|
||||
const {CHANNEL_ADD_PEOPLE} = Screens;
|
||||
const closeButtonId = 'close-add-people';
|
||||
|
||||
const AddPeopleBox = ({channelId, containerStyle, displayName, inModal, testID}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
|
||||
const onAddPeople = useCallback(async () => {
|
||||
const closeButton = await CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const title = formatMessage(messages.title);
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
subtitle: {
|
||||
color: changeOpacity(theme.sidebarHeaderTextColor, 0.72),
|
||||
text: displayName,
|
||||
},
|
||||
leftButtons: inModal ? [] : [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: 'close.channel_info.button',
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
if (inModal) {
|
||||
goToScreen(CHANNEL_ADD_PEOPLE, title, {channelId}, options);
|
||||
return;
|
||||
}
|
||||
|
||||
showModal(CHANNEL_ADD_PEOPLE, title, {channelId, closeButtonId}, options);
|
||||
}, [formatMessage, channelId, inModal]);
|
||||
|
||||
return (
|
||||
<OptionBox
|
||||
containerStyle={containerStyle}
|
||||
iconName='account-plus-outline'
|
||||
onPress={onAddPeople}
|
||||
testID={testID}
|
||||
text={formatMessage(messages.boxText)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPeopleBox;
|
||||
@@ -1,44 +1,28 @@
|
||||
// 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 {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import OptionBox from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {dismissBottomSheet, goToScreen, showModal} from '@screens/navigation';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
|
||||
import type {StyleProp, ViewStyle} from 'react-native';
|
||||
import AddPeopleBox from './add_people_box';
|
||||
|
||||
type Props = {
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
inModal?: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const AddPeopleBox = ({channelId, containerStyle, inModal, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const channel = observeChannel(database, channelId);
|
||||
const displayName = channel.pipe(switchMap((c) => of$(c?.displayName)));
|
||||
|
||||
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 {
|
||||
displayName,
|
||||
};
|
||||
});
|
||||
|
||||
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;
|
||||
export default withDatabase(enhanced(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 AddPeopleBox from '@components/channel_actions/add_people_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;
|
||||
@@ -70,7 +69,6 @@ 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
|
||||
channelId={channelId}
|
||||
@@ -78,7 +76,6 @@ const ChannelActions = ({channelId, channelType, inModal = false, dismissChannel
|
||||
testID={`${testID}.add_people.action`}
|
||||
/>
|
||||
}
|
||||
*/}
|
||||
{!isDM && !callsEnabled &&
|
||||
<>
|
||||
<View style={styles.separator}/>
|
||||
|
||||
74
app/components/no_results_with_button/index.tsx
Normal file
74
app/components/no_results_with_button/index.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {popToRoot} from '@app/screens/navigation';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {t} from '@i18n';
|
||||
import Button from '@screens/bottom_sheet/button';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const messages = defineMessages({
|
||||
no_more_members_title: {
|
||||
id: t('mobile.no_more_members.title'),
|
||||
defaultMessage: 'No other members to add',
|
||||
},
|
||||
no_more_members_subtext: {
|
||||
id: t('mobile.no_more_members.subtext'),
|
||||
defaultMessage: 'All team members are already in this channel.',
|
||||
},
|
||||
go_back: {
|
||||
id: t('mobile.no_more_members.go_back'),
|
||||
defaultMessage: 'Go back',
|
||||
},
|
||||
});
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center' as const,
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
justifyContent: 'center' as const,
|
||||
},
|
||||
buttonContainer: {
|
||||
marginTop: 24,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 400, 'SemiBold'),
|
||||
},
|
||||
subText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
marginTop: 8,
|
||||
...typography('Body', 200),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const NoResultsWithButton = () => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>{formatMessage(messages.no_more_members_title)}</Text>
|
||||
<Text style={styles.subText}>{formatMessage(messages.no_more_members_subtext)}</Text>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
onPress={popToRoot}
|
||||
icon={'arrow-left'}
|
||||
text={formatMessage(messages.go_back)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoResultsWithButton;
|
||||
68
app/components/selected_users/button.tsx
Normal file
68
app/components/selected_users/button.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {GestureResponderEvent, StyleSheet, Text, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
onPress?: (e: GestureResponderEvent) => void;
|
||||
icon?: string;
|
||||
testID?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
icon_container: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
top: -1,
|
||||
marginRight: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Button({disabled = false, onPress, icon, testID, text}: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
const buttonType = disabled ? 'disabled' : 'default';
|
||||
const styleButtonText = buttonTextStyle(theme, 'lg', 'primary', buttonType);
|
||||
const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', buttonType);
|
||||
|
||||
const iconColor = disabled ? changeOpacity(theme.centerChannelColor, 0.32) : theme.buttonColor;
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
type='opacity'
|
||||
style={[styles.button, styleButtonBackground]}
|
||||
testID={testID}
|
||||
>
|
||||
{icon && (
|
||||
<View style={styles.icon_container}>
|
||||
<CompassIcon
|
||||
size={24}
|
||||
name={icon}
|
||||
color={iconColor}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{text && (
|
||||
<Text
|
||||
style={styleButtonText}
|
||||
>{text}</Text>
|
||||
)}
|
||||
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
@@ -11,9 +11,9 @@ 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 Button from './button';
|
||||
import SelectedUser from './selected_user';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {FlatList, Keyboard, ListRenderItemInfo, Platform, SectionList, SectionLi
|
||||
|
||||
import {storeProfile} from '@actions/local/user';
|
||||
import Loading from '@components/loading';
|
||||
import NoResultsWithButton from '@components/no_results_with_button';
|
||||
import NoResultsWithTerm from '@components/no_results_with_term';
|
||||
import UserListRow from '@components/user_list_row';
|
||||
import {General, Screens} from '@constants';
|
||||
@@ -183,7 +184,7 @@ export default function UserList({
|
||||
const serverUrl = useServerUrl();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
const noResutsStyle = useMemo(() => [
|
||||
const noResultsStyle = useMemo(() => [
|
||||
style.noResultContainer,
|
||||
{paddingBottom: keyboardHeight},
|
||||
], [style, keyboardHeight]);
|
||||
@@ -261,16 +262,16 @@ export default function UserList({
|
||||
}, [loading, theme]);
|
||||
|
||||
const renderNoResults = useCallback(() => {
|
||||
if (!showNoResults || !term) {
|
||||
if (!showNoResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={noResutsStyle}>
|
||||
<NoResultsWithTerm term={term}/>
|
||||
<View style={noResultsStyle}>
|
||||
{term ? <NoResultsWithTerm term={term}/> : <NoResultsWithButton/>}
|
||||
</View>
|
||||
);
|
||||
}, [showNoResults && style, term, noResutsStyle]);
|
||||
}, [showNoResults && style, term, noResultsStyle]);
|
||||
|
||||
const renderSectionHeader = useCallback(({section}: {section: SectionListData<UserProfile>}) => {
|
||||
return (
|
||||
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
SHOW_FULLNAME: 'full_name',
|
||||
},
|
||||
SPECIAL_MENTIONS: new Set(['all', 'channel', 'here']),
|
||||
MAX_USERS_ADD_TO_CHANNEL: 25,
|
||||
MAX_USERS_IN_GM: 7,
|
||||
MIN_USERS_IN_GM: 3,
|
||||
MAX_GROUP_CHANNELS_FOR_PROFILES: 50,
|
||||
|
||||
@@ -142,6 +142,7 @@ export default {
|
||||
export const MODAL_SCREENS_WITHOUT_BACK = new Set<string>([
|
||||
BROWSE_CHANNELS,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
CREATE_TEAM,
|
||||
CUSTOM_STATUS,
|
||||
@@ -171,7 +172,6 @@ export const SCREENS_AS_BOTTOM_SHEET = new Set<string>([
|
||||
]);
|
||||
|
||||
export const NOT_READY = [
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_MENTION,
|
||||
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',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
// import AddPeopleBox from '@components/channel_actions/add_people_box';
|
||||
import AddPeopleBox from '@components/channel_actions/add_people_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';
|
||||
@@ -39,10 +39,9 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
const IntroOptions = ({channelId, header, favorite}: Props) => {
|
||||
const IntroOptions = ({channelId, header, favorite, people}: Props) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Add back in after MM-47655 is resolved. https://mattermost.atlassian.net/browse/MM-47655
|
||||
{people &&
|
||||
<AddPeopleBox
|
||||
channelId={channelId}
|
||||
@@ -50,7 +49,6 @@ const IntroOptions = ({channelId, header, favorite}: Props) => {
|
||||
testID='channel_post_list.intro_options.add_people.action'
|
||||
/>
|
||||
}
|
||||
*/}
|
||||
{header &&
|
||||
<SetHeaderBox
|
||||
channelId={channelId}
|
||||
|
||||
366
app/screens/channel_add_people/channel_add_people.tsx
Normal file
366
app/screens/channel_add_people/channel_add_people.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {Keyboard, LayoutChangeEvent, Platform, StyleSheet, View} from 'react-native';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {addMembersToChannel} from '@actions/remote/channel';
|
||||
import {fetchProfilesNotInChannel, searchProfiles} from '@actions/remote/user';
|
||||
import useNavButtonPressed from '@app/hooks/navigation_button_pressed';
|
||||
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 {t} from '@i18n';
|
||||
import {dismissModal, popTopScreen} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {alertErrorWithFallback} from '@utils/draft';
|
||||
import {showAddChannelMembersSnackbar} from '@utils/snack_bar';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
import {filterProfilesMatchingTerm} from '@utils/user';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
searchBar: {
|
||||
marginLeft: 12,
|
||||
marginRight: Platform.select({ios: 4, default: 12}),
|
||||
marginVertical: 12,
|
||||
},
|
||||
});
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: {
|
||||
id: t('mobile.post.cancel'),
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
error: {
|
||||
id: t('mobile.channel_add_people.error'),
|
||||
defaultMessage: 'We could not add those users to the channel. Please check your connection and try again.',
|
||||
},
|
||||
button: {
|
||||
id: t('mobile.channel_add_people.title'),
|
||||
defaultMessage: 'Add Members',
|
||||
},
|
||||
search: {
|
||||
id: t('search_bar.search'),
|
||||
defaultMessage: 'Search',
|
||||
},
|
||||
toastMessage: {
|
||||
id: t('mobile.channel_add_people.max_limit_reached'),
|
||||
defaultMessage: 'Max selected users are limited to {maxCount} members',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
closeButtonId: string;
|
||||
componentId: AvailableScreens;
|
||||
currentTeamId: string;
|
||||
currentUserId: string;
|
||||
isGroupConstrained: boolean;
|
||||
teammateNameDisplay: string;
|
||||
tutorialWatched: boolean;
|
||||
}
|
||||
|
||||
const MAX_SELECTED_USERS = General.MAX_USERS_ADD_TO_CHANNEL;
|
||||
const EMPTY: UserProfile[] = [];
|
||||
const EDGES: Edge[] = ['top', 'left', 'right'];
|
||||
|
||||
function removeProfileFromList(list: {[id: string]: UserProfile}, id: string) {
|
||||
const newSelectedIds = Object.assign({}, list);
|
||||
|
||||
Reflect.deleteProperty(newSelectedIds, id);
|
||||
return newSelectedIds;
|
||||
}
|
||||
|
||||
export default function ChannelAddPeople({
|
||||
channelId,
|
||||
closeButtonId,
|
||||
componentId,
|
||||
currentTeamId,
|
||||
currentUserId,
|
||||
isGroupConstrained,
|
||||
teammateNameDisplay,
|
||||
tutorialWatched,
|
||||
}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
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, setProfiles] = useState<UserProfile[]>(EMPTY);
|
||||
const [searchResults, setSearchResults] = useState<UserProfile[]>(EMPTY);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [term, setTerm] = useState('');
|
||||
const [startingAddPeople, setStartingAddPeople] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
const selectedCount = Object.keys(selectedIds).length;
|
||||
|
||||
const isSearch = Boolean(term);
|
||||
const hasProfiles = useMemo(() => Boolean(profiles.length), [profiles]);
|
||||
|
||||
const close = () => {
|
||||
const screens = NavigationStore.getScreensInStack();
|
||||
|
||||
if (screens.includes('BottomSheet')) {
|
||||
// from ...
|
||||
dismissModal({componentId});
|
||||
} else {
|
||||
// from Channel Info Screen
|
||||
popTopScreen();
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
};
|
||||
|
||||
useNavButtonPressed(closeButtonId, componentId, close, [close]);
|
||||
|
||||
const loadedProfiles = ({users}: {users: UserProfile[]}) => {
|
||||
if (mounted.current) {
|
||||
if (users && !users.length) {
|
||||
next.current = false;
|
||||
}
|
||||
|
||||
page.current += 1;
|
||||
setLoading(false);
|
||||
setProfiles((prev: UserProfile[]) => [...prev, ...users]);
|
||||
}
|
||||
};
|
||||
|
||||
const getProfiles = useCallback(debounce(() => {
|
||||
if (next.current && !loading && !term && mounted.current) {
|
||||
setLoading(true);
|
||||
fetchProfilesNotInChannel(serverUrl,
|
||||
currentTeamId,
|
||||
channelId,
|
||||
isGroupConstrained,
|
||||
page.current + 1,
|
||||
General.PROFILE_CHUNK_SIZE).then(loadedProfiles);
|
||||
}
|
||||
}, 100), [loading, isSearch, serverUrl, currentTeamId]);
|
||||
|
||||
const handleRemoveProfile = useCallback((id: string) => {
|
||||
setSelectedIds((current) => removeProfileFromList(current, id));
|
||||
}, [selectedIds]);
|
||||
|
||||
const addPeopleToChannel = useCallback(async (ids: string[]): Promise<boolean> => {
|
||||
const result = await addMembersToChannel(serverUrl, channelId, ids, '', false);
|
||||
|
||||
if (result.error) {
|
||||
alertErrorWithFallback(intl, result.error, messages.error);
|
||||
}
|
||||
|
||||
return !result.error;
|
||||
}, [serverUrl]);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setLoading(false);
|
||||
setTerm('');
|
||||
setSearchResults(EMPTY);
|
||||
}, []);
|
||||
|
||||
const startAddPeople = useCallback(async (selectedId?: {[id: string]: boolean}) => {
|
||||
if (startingAddPeople) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStartingAddPeople(true);
|
||||
|
||||
const idsToUse = selectedId ? Object.keys(selectedId) : Object.keys(selectedIds);
|
||||
let success;
|
||||
if (idsToUse.length === 0) {
|
||||
success = false;
|
||||
} else {
|
||||
success = await addPeopleToChannel(idsToUse);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
close();
|
||||
showAddChannelMembersSnackbar(idsToUse);
|
||||
} else {
|
||||
setStartingAddPeople(false);
|
||||
}
|
||||
}, [startingAddPeople, selectedIds, addPeopleToChannel]);
|
||||
|
||||
const handleSelectProfile = useCallback((user: UserProfile) => {
|
||||
clearSearch();
|
||||
setSelectedIds((current) => {
|
||||
if (current[user.id]) {
|
||||
return removeProfileFromList(current, user.id);
|
||||
}
|
||||
|
||||
const wasSelected = current[user.id];
|
||||
|
||||
if (!wasSelected && selectedCount >= MAX_SELECTED_USERS) {
|
||||
setShowToast(true);
|
||||
return current;
|
||||
}
|
||||
|
||||
const newSelectedIds = Object.assign({}, current);
|
||||
if (!wasSelected) {
|
||||
newSelectedIds[user.id] = user;
|
||||
}
|
||||
|
||||
return newSelectedIds;
|
||||
});
|
||||
}, [selectedIds, clearSearch]);
|
||||
|
||||
const searchUsers = useCallback(async (searchTerm: string) => {
|
||||
const lowerCasedTerm = searchTerm.toLowerCase();
|
||||
setLoading(true);
|
||||
|
||||
const results = await searchProfiles(serverUrl, lowerCasedTerm, {
|
||||
team_id: currentTeamId,
|
||||
not_in_channel_id: channelId,
|
||||
allow_inactive: true,
|
||||
});
|
||||
|
||||
let data: UserProfile[] = EMPTY;
|
||||
if (results.data) {
|
||||
data = results.data;
|
||||
}
|
||||
|
||||
setSearchResults(data);
|
||||
setLoading(false);
|
||||
}, [channelId, serverUrl, currentTeamId]);
|
||||
|
||||
const search = useCallback(() => {
|
||||
searchUsers(term);
|
||||
}, [searchUsers, term]);
|
||||
|
||||
const onSearch = useCallback((text: string) => {
|
||||
setLoading(true);
|
||||
if (text) {
|
||||
setTerm(text);
|
||||
if (searchTimeoutId.current) {
|
||||
clearTimeout(searchTimeoutId.current);
|
||||
}
|
||||
|
||||
searchTimeoutId.current = setTimeout(() => {
|
||||
searchUsers(text);
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
} else {
|
||||
clearSearch();
|
||||
}
|
||||
}, [searchUsers, clearSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
getProfiles();
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setShowToast(selectedCount >= MAX_SELECTED_USERS);
|
||||
}, [selectedCount >= MAX_SELECTED_USERS]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (isSearch) {
|
||||
const exactMatches: UserProfile[] = EMPTY;
|
||||
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]);
|
||||
|
||||
if (startingAddPeople) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges={EDGES}
|
||||
onLayout={onLayout}
|
||||
style={styles.container}
|
||||
testID='add_members.screen'
|
||||
>
|
||||
{hasProfiles &&
|
||||
<View style={styles.searchBar}>
|
||||
<Search
|
||||
autoCapitalize='none'
|
||||
cancelButtonTitle={formatMessage(messages.cancel)}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
onCancel={clearSearch}
|
||||
onChangeText={onSearch}
|
||||
onSubmitEditing={search}
|
||||
placeholder={formatMessage(messages.search)}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
testID='add_members.search_bar'
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
<UserList
|
||||
currentUserId={currentUserId}
|
||||
fetchMore={getProfiles}
|
||||
handleSelectProfile={handleSelectProfile}
|
||||
loading={loading}
|
||||
profiles={data}
|
||||
selectedIds={selectedIds}
|
||||
showNoResults={!loading && page.current !== -1}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
term={term}
|
||||
testID='add_members.user_list'
|
||||
tutorialWatched={tutorialWatched}
|
||||
/>
|
||||
<SelectedUsers
|
||||
buttonIcon={'account-plus-outline'}
|
||||
buttonText={formatMessage(messages.button)}
|
||||
containerHeight={containerHeight}
|
||||
modalPosition={modalPosition}
|
||||
onPress={startAddPeople}
|
||||
onRemove={handleRemoveProfile}
|
||||
selectedIds={selectedIds}
|
||||
setShowToast={setShowToast}
|
||||
showToast={showToast}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
toastIcon={'check'}
|
||||
toastMessage={formatMessage(messages.toastMessage, {maxCount: MAX_SELECTED_USERS})}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
33
app/screens/channel_add_people/index.tsx
Normal file
33
app/screens/channel_add_people/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
// 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 {Tutorial} from '@constants';
|
||||
import {observeTutorialWatched} from '@queries/app/global';
|
||||
import {observeCurrentChannel} from '@queries/servers/channel';
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
||||
|
||||
import ChannelAddPeople from './channel_add_people';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const channel = observeCurrentChannel(database);
|
||||
const isGroupConstrained = channel.pipe(switchMap((c) => of$(Boolean(c?.isGroupConstrained))));
|
||||
const channelId = channel.pipe(switchMap((c) => of$(c?.id)));
|
||||
|
||||
return {
|
||||
channelId,
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
isGroupConstrained,
|
||||
teammateNameDisplay: observeTeammateNameDisplay(database),
|
||||
tutorialWatched: observeTutorialWatched(Tutorial.PROFILE_LONG_PRESS),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(ChannelAddPeople));
|
||||
@@ -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, useMemo, 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';
|
||||
@@ -59,6 +59,10 @@ type Props = {
|
||||
tutorialWatched: boolean;
|
||||
}
|
||||
|
||||
const MAX_SELECTED_USERS = General.MAX_USERS_IN_GM;
|
||||
const EMPTY: UserProfile[] = [];
|
||||
const EMPTY_IDS = {};
|
||||
|
||||
const close = () => {
|
||||
Keyboard.dismiss();
|
||||
dismissModal();
|
||||
@@ -96,15 +100,8 @@ 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);
|
||||
const newSelectedIds = Object.assign(EMPTY_IDS, list);
|
||||
|
||||
Reflect.deleteProperty(newSelectedIds, id);
|
||||
return newSelectedIds;
|
||||
@@ -131,19 +128,19 @@ export default function CreateDirectMessage({
|
||||
const mainView = useRef<View>(null);
|
||||
const modalPosition = useModalPosition(mainView);
|
||||
|
||||
const [profiles, dispatchProfiles] = useReducer(reduceProfiles, []);
|
||||
const [searchResults, setSearchResults] = useState<UserProfile[]>([]);
|
||||
const [profiles, setProfiles] = useState<UserProfile[]>(EMPTY);
|
||||
const [searchResults, setSearchResults] = useState<UserProfile[]>(EMPTY);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [term, setTerm] = useState('');
|
||||
const [startingConversation, setStartingConversation] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>(EMPTY_IDS);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
const selectedCount = Object.keys(selectedIds).length;
|
||||
|
||||
const isSearch = Boolean(term);
|
||||
|
||||
const loadedProfiles = ({users}: {users?: UserProfile[]}) => {
|
||||
const loadedProfiles = ({users}: {users: UserProfile[]}) => {
|
||||
if (mounted.current) {
|
||||
if (users && !users.length) {
|
||||
next.current = false;
|
||||
@@ -151,13 +148,13 @@ export default function CreateDirectMessage({
|
||||
|
||||
page.current += 1;
|
||||
setLoading(false);
|
||||
dispatchProfiles({type: 'add', values: users});
|
||||
setProfiles((prev: UserProfile[]) => [...prev, ...users]);
|
||||
}
|
||||
};
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (term) {
|
||||
const exactMatches: UserProfile[] = [];
|
||||
const exactMatches: UserProfile[] = EMPTY;
|
||||
const filterByTerm = (p: UserProfile) => {
|
||||
if (selectedCount > 0 && p.id === currentUserId) {
|
||||
return false;
|
||||
@@ -250,29 +247,30 @@ export default function CreateDirectMessage({
|
||||
};
|
||||
|
||||
startConversation(selectedId);
|
||||
} else {
|
||||
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;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}, [currentUserId, clearSearch]);
|
||||
|
||||
clearSearch();
|
||||
setSelectedIds((current) => {
|
||||
if (current[user.id]) {
|
||||
return removeProfileFromList(current, user.id);
|
||||
}
|
||||
|
||||
const wasSelected = current[user.id];
|
||||
|
||||
if (!wasSelected && selectedCount >= MAX_SELECTED_USERS) {
|
||||
setShowToast(true);
|
||||
return current;
|
||||
}
|
||||
|
||||
const newSelectedIds = Object.assign(EMPTY_IDS, current);
|
||||
if (!wasSelected) {
|
||||
newSelectedIds[user.id] = user;
|
||||
}
|
||||
|
||||
return newSelectedIds;
|
||||
});
|
||||
}, [currentUserId, clearSearch, selectedCount]);
|
||||
|
||||
const searchUsers = useCallback(async (searchTerm: string) => {
|
||||
const lowerCasedTerm = searchTerm.toLowerCase();
|
||||
@@ -285,7 +283,7 @@ export default function CreateDirectMessage({
|
||||
results = await searchProfiles(serverUrl, lowerCasedTerm, {allow_inactive: true});
|
||||
}
|
||||
|
||||
let searchData: UserProfile[] = [];
|
||||
let searchData: UserProfile[] = EMPTY;
|
||||
if (results.data) {
|
||||
searchData = results.data;
|
||||
}
|
||||
@@ -303,6 +301,7 @@ export default function CreateDirectMessage({
|
||||
}, []);
|
||||
|
||||
const onSearch = useCallback((text: string) => {
|
||||
setLoading(true);
|
||||
if (text) {
|
||||
setTerm(text);
|
||||
if (searchTimeoutId.current) {
|
||||
@@ -341,8 +340,8 @@ export default function CreateDirectMessage({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setShowToast(selectedCount >= General.MAX_USERS_IN_GM);
|
||||
}, [selectedCount >= General.MAX_USERS_IN_GM]);
|
||||
setShowToast(selectedCount >= MAX_SELECTED_USERS);
|
||||
}, [selectedCount >= MAX_SELECTED_USERS]);
|
||||
|
||||
if (startingConversation) {
|
||||
return (
|
||||
@@ -393,7 +392,7 @@ export default function CreateDirectMessage({
|
||||
showToast={showToast}
|
||||
setShowToast={setShowToast}
|
||||
toastIcon={'check'}
|
||||
toastMessage={formatMessage(messages.toastMessage, {maxCount: General.MAX_USERS_IN_GM})}
|
||||
toastMessage={formatMessage(messages.toastMessage, {maxCount: MAX_SELECTED_USERS})}
|
||||
selectedIds={selectedIds}
|
||||
onRemove={handleRemoveProfile}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
|
||||
@@ -97,6 +97,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
case Screens.CREATE_DIRECT_MESSAGE:
|
||||
screen = withServerDatabase(require('@screens/create_direct_message').default);
|
||||
break;
|
||||
case Screens.CHANNEL_ADD_PEOPLE:
|
||||
screen = withServerDatabase(require('@screens/channel_add_people').default);
|
||||
break;
|
||||
case Screens.EDIT_POST:
|
||||
screen = withServerDatabase(require('@screens/edit_post').default);
|
||||
break;
|
||||
|
||||
@@ -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,9 +80,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
const SnackBar = ({barType, componentId, onAction, sourceScreen}: SnackBarProps) => {
|
||||
const SnackBar = ({barType, componentId, messageValues = {}, onAction, sourceScreen}: SnackBarProps) => {
|
||||
const [showSnackBar, setShowSnackBar] = useState<boolean | undefined>();
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const {width: windowWidth, height: windowHeight} = useWindowDimensions();
|
||||
@@ -245,14 +244,17 @@ const SnackBar = ({barType, componentId, onAction, sourceScreen}: SnackBarProps)
|
||||
<Toast
|
||||
animatedStyle={snackBarStyle}
|
||||
iconName={config.iconName}
|
||||
message={intl.formatMessage({id: config.id, defaultMessage: config.defaultMessage})}
|
||||
message={formatMessage({
|
||||
id: config.id,
|
||||
defaultMessage: config.defaultMessage,
|
||||
}, messageValues)}
|
||||
style={[styles.toast, barType === SNACK_BAR_TYPE.LINK_COPIED && {backgroundColor: theme.onlineIndicator}]}
|
||||
textStyle={styles.text}
|
||||
>
|
||||
{config.canUndo && onAction && (
|
||||
<TouchableOpacity onPress={onUndoPressHandler}>
|
||||
<Text style={styles.undo}>
|
||||
{intl.formatMessage({
|
||||
{formatMessage({
|
||||
id: 'snack.bar.undo',
|
||||
defaultMessage: 'Undo',
|
||||
})}
|
||||
|
||||
@@ -6,10 +6,16 @@ import {showOverlay} from '@screens/navigation';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type ShowSnackBarArgs = {
|
||||
type AddChannelMemberValues = {
|
||||
numMembers: number;
|
||||
};
|
||||
|
||||
export type ShowSnackBarArgs = {
|
||||
barType: keyof typeof SNACK_BAR_TYPE;
|
||||
onAction?: () => void;
|
||||
sourceScreen?: AvailableScreens;
|
||||
messageValues?: AddChannelMemberValues | {};
|
||||
|
||||
};
|
||||
|
||||
export const showSnackBar = (passProps: ShowSnackBarArgs) => {
|
||||
@@ -31,6 +37,14 @@ export const showFavoriteChannelSnackbar = (favorited: boolean, onAction: () =>
|
||||
});
|
||||
};
|
||||
|
||||
export const showAddChannelMembersSnackbar = (ids: string[]) => {
|
||||
return showSnackBar({
|
||||
barType: SNACK_BAR_TYPE.ADD_CHANNEL_MEMBERS,
|
||||
sourceScreen: Screens.CHANNEL_ADD_PEOPLE,
|
||||
messageValues: {numMembers: ids.length},
|
||||
});
|
||||
};
|
||||
|
||||
export const showRemoveChannelUserSnackbar = () => {
|
||||
return showSnackBar({
|
||||
barType: SNACK_BAR_TYPE.REMOVE_CHANNEL_USER,
|
||||
|
||||
@@ -457,7 +457,9 @@
|
||||
"mobile.calls_you": "(you)",
|
||||
"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_people.error": "We could not add those users to the channel. Please check your connection and try again.",
|
||||
"mobile.channel_add_people.max_limit_reached": "Max selected users are limited to {maxCount} members",
|
||||
"mobile.channel_add_people.title": "Add Members",
|
||||
"mobile.channel_info.alertNo": "No",
|
||||
"mobile.channel_info.alertYes": "Yes",
|
||||
"mobile.channel_list.recent": "Recent",
|
||||
@@ -562,6 +564,9 @@
|
||||
"mobile.message_length.message": "Your current message is too long. Current character count: {count}/{max}",
|
||||
"mobile.message_length.message_split_left": "Message exceeds the character limit",
|
||||
"mobile.message_length.title": "Message Length",
|
||||
"mobile.no_more_members.go_back": "Go back",
|
||||
"mobile.no_more_members.subtext": "All team members are already in this channel.",
|
||||
"mobile.no_more_members.title": "No other members to add",
|
||||
"mobile.no_results_with_term": "No results for “{term}”",
|
||||
"mobile.no_results_with_term.files": "No files matching “{term}”",
|
||||
"mobile.no_results_with_term.messages": "No matches found for “{term}”",
|
||||
@@ -902,6 +907,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",
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user