diff --git a/app/actions/local/thread.ts b/app/actions/local/thread.ts index 9d95670dc..cbc02271d 100644 --- a/app/actions/local/thread.ts +++ b/app/actions/local/thread.ts @@ -156,6 +156,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo noBorder: true, scrollEdgeAppearance: { noBorder: true, + active: true, }, rightButtons, }, diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 8f75d80b9..111168428 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -164,10 +164,12 @@ export async function addMembersToChannel(serverUrl: string, channelId: string, if (!fetchOnly) { const modelPromises: Array> = []; - modelPromises.push(operator.handleUsers({ - users, - prepareRecordsOnly: true, - })); + if (users?.length) { + modelPromises.push(operator.handleUsers({ + users, + prepareRecordsOnly: true, + })); + } modelPromises.push(operator.handleChannelMembership({ channelMemberships, prepareRecordsOnly: true, diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index 5a2ae19e9..d4de4940e 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -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, diff --git a/app/components/button/index.tsx b/app/components/button/index.tsx index 317a7c31c..0b9fecea4 100644 --- a/app/components/button/index.tsx +++ b/app/components/button/index.tsx @@ -52,13 +52,22 @@ const Button = ({ textStyle, ], [theme, textStyle, size, emphasis, buttonType]); + const containerStyle = useMemo( + () => + (iconSize ? [ + styles.container, + {minHeight: iconSize}, + ] : styles.container), + [iconSize], + ); + return ( - + {Boolean(iconName) && ; + 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 ( + + ); +}; + +export default AddMembersBox; diff --git a/app/components/channel_actions/add_members_box/index.ts b/app/components/channel_actions/add_members_box/index.ts new file mode 100644 index 000000000..6e18bc003 --- /dev/null +++ b/app/components/channel_actions/add_members_box/index.ts @@ -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)); diff --git a/app/components/channel_actions/add_people_box/index.tsx b/app/components/channel_actions/add_people_box/index.tsx deleted file mode 100644 index 4ef09ef6b..000000000 --- a/app/components/channel_actions/add_people_box/index.tsx +++ /dev/null @@ -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; - 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 ( - - ); -}; - -export default AddPeopleBox; diff --git a/app/components/channel_actions/channel_actions.tsx b/app/components/channel_actions/channel_actions.tsx index e8ecfcdd6..12f11ca42 100644 --- a/app/components/channel_actions/channel_actions.tsx +++ b/app/components/channel_actions/channel_actions.tsx @@ -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 && - } - */} {!isDM && !callsEnabled && <> diff --git a/app/components/channel_actions/index.ts b/app/components/channel_actions/index.ts index 89d01bac5..bff50d6d4 100644 --- a/app/components/channel_actions/index.ts +++ b/app/components/channel_actions/index.ts @@ -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, }; }); diff --git a/app/components/option_box/index.tsx b/app/components/option_box/index.tsx index 1406e3cd9..0fc077055 100644 --- a/app/components/option_box/index.tsx +++ b/app/components/option_box/index.tsx @@ -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'), }, })); diff --git a/app/components/selected_users/index.tsx b/app/components/selected_users/index.tsx index aa1522819..6abd1bc85 100644 --- a/app/components/selected_users/index.tsx +++ b/app/components/selected_users/index.tsx @@ -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 ( {showToast && @@ -270,7 +294,7 @@ export default function SelectedUsers({ /> } - +