forked from Ivasoft/mattermost-mobile
MM-39716: Added + button bottomsheet layout (#5957)
* [gekidou] feat: MM-39716 Added + Button Bottom sheet layout * feat: Added on click to the channel header listener * chore: Ran i18n command and did the requested changes * chore: updated test snapshot * Refactor PlusMenu & fix Browse Channels * Fix snapshot tests * feedback review Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
committed by
GitHub
parent
ea54a8dff3
commit
03d5ac083c
@@ -20,176 +20,6 @@ exports[`components/channel_list should match snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<RNGestureHandlerButton
|
||||
collapsable={false}
|
||||
exclusive={true}
|
||||
onGestureEvent={[Function]}
|
||||
onGestureHandlerEvent={[Function]}
|
||||
onGestureHandlerStateChange={[Function]}
|
||||
onHandlerStateChange={[Function]}
|
||||
rippleColor={0}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
style={
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
animatedStyle={
|
||||
Object {
|
||||
"value": Object {
|
||||
"marginLeft": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
collapsable={false}
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontFamily": "Metropolis-SemiBold",
|
||||
"fontSize": 28,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 36,
|
||||
}
|
||||
}
|
||||
>
|
||||
Test Team!
|
||||
</Text>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={false}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.8)",
|
||||
"fontSize": 24,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={false}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(255,255,255,0.08)",
|
||||
"borderRadius": 14,
|
||||
"height": 28,
|
||||
"justifyContent": "center",
|
||||
"width": 28,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="plus"
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.8)",
|
||||
"fontSize": 18,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.64)",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 11,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</RNGestureHandlerButton>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
|
||||
@@ -26,51 +26,45 @@ exports[`components/channel_list/header Channel List Header Component should mat
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={false}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "space-between",
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontFamily": "Metropolis-SemiBold",
|
||||
"fontSize": 28,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 36,
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
Test!
|
||||
</Text>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontFamily": "Metropolis-SemiBold",
|
||||
"fontSize": 28,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 36,
|
||||
}
|
||||
}
|
||||
>
|
||||
Test!
|
||||
</Text>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={false}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 4,
|
||||
@@ -90,51 +84,37 @@ exports[`components/channel_list/header Channel List Header Component should mat
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(255,255,255,0.08)",
|
||||
"borderRadius": 14,
|
||||
"height": 28,
|
||||
"justifyContent": "center",
|
||||
"opacity": 1,
|
||||
"width": 28,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={false}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
<Icon
|
||||
name="plus"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(255,255,255,0.08)",
|
||||
"borderRadius": 14,
|
||||
"height": 28,
|
||||
"justifyContent": "center",
|
||||
"width": 28,
|
||||
"color": "rgba(255,255,255,0.8)",
|
||||
"fontSize": 18,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="plus"
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.8)",
|
||||
"fontSize": 18,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context';
|
||||
|
||||
import {renderWithIntl} from '@test/intl-test-helper';
|
||||
|
||||
@@ -10,7 +11,13 @@ import Header from './header';
|
||||
describe('components/channel_list/header', () => {
|
||||
it('Channel List Header Component should match snapshot', () => {
|
||||
const {toJSON} = renderWithIntl(
|
||||
<Header displayName={'Test!'}/>,
|
||||
<SafeAreaProvider>
|
||||
<Header
|
||||
canCreateChannels={true}
|
||||
canJoinChannels={true}
|
||||
displayName={'Test!'}
|
||||
/>
|
||||
</SafeAreaProvider>,
|
||||
);
|
||||
|
||||
expect(toJSON()).toMatchSnapshot();
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerDisplayName} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import PlusMenu from './plus_menu';
|
||||
|
||||
type Props = {
|
||||
canCreateChannels: boolean;
|
||||
canJoinChannels: boolean;
|
||||
displayName: string;
|
||||
iconPad?: boolean;
|
||||
onHeaderPress?: () => void;
|
||||
}
|
||||
|
||||
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
@@ -55,8 +62,11 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const ChannelListHeader = ({displayName, iconPad}: Props) => {
|
||||
const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, iconPad, onHeaderPress}: Props) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const intl = useIntl();
|
||||
const insets = useSafeAreaInsets();
|
||||
const serverDisplayName = useServerDisplayName();
|
||||
const marginLeft = useSharedValue(iconPad ? 44 : 0);
|
||||
const styles = getStyles(theme);
|
||||
@@ -64,38 +74,67 @@ const ChannelListHeader = ({displayName, iconPad}: Props) => {
|
||||
marginLeft: withTiming(marginLeft.value, {duration: 350}),
|
||||
}), []);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
marginLeft.value = iconPad ? 44 : 0;
|
||||
}, [iconPad]);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<PlusMenu
|
||||
canCreateChannels={canCreateChannels}
|
||||
canJoinChannels={canJoinChannels}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const closeButtonId = 'close-plus-menu';
|
||||
let items = 1;
|
||||
if (canCreateChannels) {
|
||||
items += 1;
|
||||
}
|
||||
|
||||
if (canJoinChannels) {
|
||||
items += 1;
|
||||
}
|
||||
|
||||
bottomSheet({
|
||||
closeButtonId,
|
||||
renderContent,
|
||||
snapPoints: [(items * ITEM_HEIGHT) + (insets.bottom * 2), 10],
|
||||
theme,
|
||||
title: intl.formatMessage({id: 'home.header.plus_menu', defaultMessage: 'Options'}),
|
||||
});
|
||||
}, [intl, insets, isTablet, theme]);
|
||||
|
||||
return (
|
||||
<Animated.View style={animatedStyle}>
|
||||
{Boolean(displayName) &&
|
||||
<View style={styles.headerRow}>
|
||||
<View style={styles.headerRow}>
|
||||
<Text style={styles.headingStyles}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<TouchableWithFeedback style={styles.chevronButton}>
|
||||
<CompassIcon
|
||||
style={styles.chevronIcon}
|
||||
name={'chevron-down'}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
<TouchableWithFeedback style={styles.plusButton}>
|
||||
<TouchableWithFeedback
|
||||
onPress={onHeaderPress}
|
||||
type='opacity'
|
||||
>
|
||||
<View style={styles.headerRow}>
|
||||
<Text style={styles.headingStyles}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<View style={styles.chevronButton}>
|
||||
<CompassIcon
|
||||
style={styles.chevronIcon}
|
||||
name={'chevron-down'}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
style={styles.plusButton}
|
||||
type='opacity'
|
||||
>
|
||||
<CompassIcon
|
||||
style={styles.plusIcon}
|
||||
name={'plus'}
|
||||
onPress={async () => {
|
||||
const title = intl.formatMessage({id: 'browse_channels.title', defaultMessage: 'More Channels'});
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
showModal(Screens.BROWSE_CHANNELS, title, {
|
||||
closeButton,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
|
||||
@@ -3,25 +3,52 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {catchError, of as of$} from 'rxjs';
|
||||
import {catchError, combineLatest, of as of$, from as from$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Permissions} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {hasPermissionForTeam} from '@utils/role';
|
||||
|
||||
const {SERVER: {SYSTEM, TEAM}} = MM_TABLES;
|
||||
import ChannelListHeader from './header';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const {SERVER: {SYSTEM, TEAM, USER}} = MM_TABLES;
|
||||
const {CURRENT_TEAM_ID, CURRENT_USER_ID} = SYSTEM_IDENTIFIERS;
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const team = database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID).pipe(
|
||||
const team = database.get<SystemModel>(SYSTEM).findAndObserve(CURRENT_TEAM_ID).pipe(
|
||||
switchMap((id) => database.get<TeamModel>(TEAM).findAndObserve(id.value)),
|
||||
catchError(() => of$({displayName: ''})),
|
||||
);
|
||||
|
||||
const currentUser = database.get<SystemModel>(SYSTEM).findAndObserve(CURRENT_USER_ID).pipe(
|
||||
switchMap(({value}) => database.get<UserModel>(USER).findAndObserve(value)),
|
||||
);
|
||||
|
||||
const canJoinChannels = combineLatest([currentUser, team]).pipe(
|
||||
switchMap(([u, t]) => (('id' in t) ? from$(hasPermissionForTeam(t, u, Permissions.JOIN_PUBLIC_CHANNELS, true)) : of$(false))),
|
||||
);
|
||||
|
||||
const canCreatePublicChannels = combineLatest([currentUser, team]).pipe(
|
||||
switchMap(([u, t]) => (('id' in t) ? from$(hasPermissionForTeam(t, u, Permissions.CREATE_PUBLIC_CHANNEL, true)) : of$(false))),
|
||||
);
|
||||
|
||||
const canCreatePrivateChannels = combineLatest([currentUser, team]).pipe(
|
||||
switchMap(([u, t]) => (('id' in t) ? from$(hasPermissionForTeam(t, u, Permissions.CREATE_PRIVATE_CHANNEL, false)) : of$(false))),
|
||||
);
|
||||
|
||||
const canCreateChannels = combineLatest([canCreatePublicChannels, canCreatePrivateChannels]).pipe(
|
||||
switchMap(([open, priv]) => of$(open || priv)),
|
||||
);
|
||||
|
||||
return {
|
||||
canCreateChannels,
|
||||
canJoinChannels,
|
||||
displayName: team.pipe(
|
||||
switchMap((t) => of$(t.displayName)),
|
||||
),
|
||||
|
||||
64
app/components/channel_list/header/plus_menu/index.tsx
Normal file
64
app/components/channel_list/header/plus_menu/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
// 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 CompassIcon from '@components/compass_icon';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
|
||||
import PlusMenuItem from './item';
|
||||
|
||||
type Props = {
|
||||
canCreateChannels: boolean;
|
||||
canJoinChannels: boolean;
|
||||
}
|
||||
|
||||
const PlusMenuList = ({canCreateChannels, canJoinChannels}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
|
||||
const browseChannels = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
|
||||
const title = intl.formatMessage({id: 'browse_channels.title', defaultMessage: 'More Channels'});
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
showModal(Screens.BROWSE_CHANNELS, title, {
|
||||
closeButton,
|
||||
});
|
||||
}, [intl, theme]);
|
||||
|
||||
const createNewChannel = useCallback(async () => {
|
||||
// To be added
|
||||
}, [intl, theme]);
|
||||
|
||||
const openDirectMessage = useCallback(async () => {
|
||||
// To be added
|
||||
}, [intl, theme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{canJoinChannels &&
|
||||
<PlusMenuItem
|
||||
pickerAction='browseChannels'
|
||||
onPress={browseChannels}
|
||||
/>
|
||||
}
|
||||
{canCreateChannels &&
|
||||
<PlusMenuItem
|
||||
pickerAction='createNewChannel'
|
||||
onPress={createNewChannel}
|
||||
/>
|
||||
}
|
||||
<PlusMenuItem
|
||||
pickerAction='openDirectMessage'
|
||||
onPress={openDirectMessage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlusMenuList;
|
||||
46
app/components/channel_list/header/plus_menu/item.tsx
Normal file
46
app/components/channel_list/header/plus_menu/item.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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 {View} from 'react-native';
|
||||
|
||||
import SlideUpPanelItem from '@components/slide_up_panel_item';
|
||||
|
||||
type PlusMenuItemProps = {
|
||||
pickerAction: 'browseChannels' | 'createNewChannel' | 'openDirectMessage';
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const PlusMenuItem = ({pickerAction, onPress}: PlusMenuItemProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const menuItems = {
|
||||
browseChannels: {
|
||||
icon: 'globe',
|
||||
text: intl.formatMessage({id: 'plus_menu.browse_channels.title', defaultMessage: 'Browse Channels'}),
|
||||
},
|
||||
|
||||
createNewChannel: {
|
||||
icon: 'plus',
|
||||
text: intl.formatMessage({id: 'plus_menu.create_new_channel.title', defaultMessage: 'Create New Channel'}),
|
||||
},
|
||||
|
||||
openDirectMessage: {
|
||||
icon: 'account-outline',
|
||||
text: intl.formatMessage({id: 'plus_menu.open_direct_message.title', defaultMessage: 'Open a Direct Message'}),
|
||||
},
|
||||
};
|
||||
const itemType = menuItems[pickerAction];
|
||||
return (
|
||||
<View>
|
||||
<SlideUpPanelItem
|
||||
text={itemType.text}
|
||||
icon={itemType.icon}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlusMenuItem;
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {TeamModel} from '@database/models/server';
|
||||
@@ -27,10 +28,12 @@ describe('components/channel_list', () => {
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelsList
|
||||
isTablet={false}
|
||||
teamsCount={1}
|
||||
/>,
|
||||
<SafeAreaProvider>
|
||||
<ChannelsList
|
||||
isTablet={false}
|
||||
teamsCount={1}
|
||||
/>
|
||||
</SafeAreaProvider>,
|
||||
{database},
|
||||
);
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {TouchableOpacity} from 'react-native-gesture-handler';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import {TABLET_SIDEBAR_WIDTH, TEAM_SIDEBAR_WIDTH} from '@constants/view';
|
||||
@@ -82,11 +81,10 @@ const ChannelList = ({currentTeamId, iconPad, isTablet, teamsCount}: ChannelList
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, tabletStyle]}>
|
||||
<TouchableOpacity onPress={() => setShowCats(!showCats)}>
|
||||
<ChannelListHeader
|
||||
iconPad={iconPad}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<ChannelListHeader
|
||||
iconPad={iconPad}
|
||||
onHeaderPress={() => setShowCats(!showCats)}
|
||||
/>
|
||||
{content}
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
@@ -26,10 +26,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
titleContainer: {
|
||||
marginTop: 4,
|
||||
marginBottom: 4,
|
||||
},
|
||||
titleContainer: {marginVertical: 4},
|
||||
titleText: {
|
||||
color: theme.centerChannelColor,
|
||||
lineHeight: 30,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View, Text} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
@@ -39,8 +40,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
const BOTTOM_SHEET_HEIGHT_BASE = 55; // Magic number
|
||||
|
||||
export default function ChannelDropdown({
|
||||
typeOfChannels,
|
||||
onPress,
|
||||
@@ -48,6 +47,7 @@ export default function ChannelDropdown({
|
||||
sharedChannelsEnabled,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
@@ -72,10 +72,11 @@ export default function ChannelDropdown({
|
||||
items += 1;
|
||||
}
|
||||
|
||||
const itemsSnap = ((items + 1) * ITEM_HEIGHT) + (insets.bottom * 2) + TITLE_HEIGHT;
|
||||
bottomSheet({
|
||||
title: intl.formatMessage({id: 'browse_channels.dropdownTitle', defaultMessage: 'Show'}),
|
||||
renderContent,
|
||||
snapPoints: [(items * ITEM_HEIGHT) + TITLE_HEIGHT + BOTTOM_SHEET_HEIGHT_BASE, 10],
|
||||
snapPoints: [itemsSnap, 10],
|
||||
closeButtonId: 'close',
|
||||
theme,
|
||||
});
|
||||
|
||||
@@ -48,6 +48,22 @@ export async function hasPermissionForChannel(channel: ChannelModel, user: UserM
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export async function hasPermissionForTeam(team: TeamModel, user: UserModel, permission: string, defaultValue: boolean) {
|
||||
const rolesArray = [...user.roles.split(' ')];
|
||||
|
||||
const myTeam = await team.myTeam.fetch() as MyTeamModel | undefined;
|
||||
if (myTeam) {
|
||||
rolesArray.push(...myTeam.roles.split(' '));
|
||||
}
|
||||
|
||||
if (rolesArray.length) {
|
||||
const roles = await user.collections.get(MM_TABLES.SERVER.ROLE).query(Q.where('name', Q.oneOf(rolesArray))).fetch() as RoleModel[];
|
||||
return hasPermission(roles, permission, defaultValue);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export async function hasPermissionForPost(post: PostModel, user: UserModel, permission: string, defaultValue: boolean) {
|
||||
const channel = await post.channel.fetch() as ChannelModel | undefined;
|
||||
if (channel) {
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
"emoji_skin.medium_light_skin_tone": "medium light skin tone",
|
||||
"emoji_skin.medium_skin_tone": "medium skin tone",
|
||||
"file_upload.fileAbove": "Files must be less than {max}",
|
||||
"home.header.plus_menu": "Options",
|
||||
"get_post_link_modal.title": "Copy Link",
|
||||
"intro.add_people": "Add People",
|
||||
"intro.channel_details": "Details",
|
||||
@@ -365,6 +366,9 @@
|
||||
"permalink.show_dialog_warn.description": "You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?",
|
||||
"permalink.show_dialog_warn.join": "Join",
|
||||
"permalink.show_dialog_warn.title": "Join private channel",
|
||||
"plus_menu.browse_channels.title": "Browse Channels",
|
||||
"plus_menu.create_new_channel.title": "Create New Channel",
|
||||
"plus_menu.open_direct_message.title": "Open a Direct Message",
|
||||
"post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They are also not a member of the groups linked to this channel.",
|
||||
"post_body.check_for_out_of_channel_mentions.link.and": " and ",
|
||||
"post_body.check_for_out_of_channel_mentions.link.private": "add them to this private channel",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import MockAsyncStorage from 'mock-async-storage';
|
||||
import * as ReactNative from 'react-native';
|
||||
import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock';
|
||||
|
||||
import 'react-native-gesture-handler/jestSetup';
|
||||
require('react-native-reanimated/lib/reanimated2/jestUtils').setUpTests();
|
||||
@@ -310,6 +311,8 @@ jest.mock('@screens/navigation', () => ({
|
||||
|
||||
jest.mock('@mattermost/react-native-emm');
|
||||
|
||||
jest.mock('react-native-safe-area-context', () => mockSafeAreaContext);
|
||||
|
||||
declare const global: {requestAnimationFrame: (callback: any) => void};
|
||||
global.requestAnimationFrame = (callback) => {
|
||||
setTimeout(callback, 0);
|
||||
|
||||
4
types/modules/mock-safe-area-context.d.ts
vendored
Normal file
4
types/modules/mock-safe-area-context.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
declare module 'react-native-safe-area-context/jest/mock';
|
||||
Reference in New Issue
Block a user