[Gekidou] find channels (default state) (#6191)
* Find channels unfiltered state * ux feedback review * Update snapshots after ux review * Fix channel item textStyles * Center Channel item content * feedback review
This commit is contained in:
parent
bb3a3112b1
commit
1132bd0e51
37 changed files with 1607 additions and 527 deletions
|
|
@ -574,7 +574,7 @@ export const setStatus = async (serverUrl: string, status: UserStatus) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const updateCustomStatus = async (serverUrl: string, user: UserModel, customStatus: UserCustomStatus) => {
|
||||
export const updateCustomStatus = async (serverUrl: string, customStatus: UserCustomStatus) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
|
|
@ -583,6 +583,9 @@ export const updateCustomStatus = async (serverUrl: string, user: UserModel, cus
|
|||
}
|
||||
|
||||
try {
|
||||
if (!customStatus.duration) {
|
||||
delete customStatus.expires_at;
|
||||
}
|
||||
await client.updateCustomStatus(customStatus);
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
|
@ -11,26 +12,33 @@ import type UserModel from '@typings/database/models/servers/user';
|
|||
|
||||
type Props = {
|
||||
author?: UserModel;
|
||||
isInfo?: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {marginLeft: 4},
|
||||
status: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
borderWidth: 0,
|
||||
},
|
||||
statusInfo: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
}));
|
||||
|
||||
const DmAvatar = ({author}: Props) => {
|
||||
const DmAvatar = ({author, isInfo}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
return (
|
||||
<ProfilePicture
|
||||
author={author}
|
||||
size={24}
|
||||
showStatus={true}
|
||||
statusSize={12}
|
||||
statusStyle={style.status}
|
||||
/>
|
||||
<View style={style.container}>
|
||||
<ProfilePicture
|
||||
author={author}
|
||||
size={24}
|
||||
showStatus={true}
|
||||
statusSize={12}
|
||||
statusStyle={[style.status, isInfo && style.statusInfo]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
color: theme.sidebarUnreadText,
|
||||
},
|
||||
iconInfo: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
},
|
||||
iconInfoUnread: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
groupBox: {
|
||||
|
|
@ -72,6 +75,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
color: theme.sidebarUnreadText,
|
||||
},
|
||||
groupInfo: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
},
|
||||
groupInfoUnread: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
muted: {
|
||||
|
|
@ -81,19 +87,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
});
|
||||
|
||||
const ChannelIcon = ({
|
||||
hasDraft = false,
|
||||
isActive = false,
|
||||
isArchived = false,
|
||||
isInfo = false,
|
||||
isUnread = false,
|
||||
isMuted = false,
|
||||
membersCount = 0,
|
||||
name,
|
||||
shared,
|
||||
size = 12,
|
||||
style,
|
||||
testID,
|
||||
type,
|
||||
hasDraft = false, isActive = false, isArchived = false,
|
||||
isInfo = false, isUnread = false, isMuted = false,
|
||||
membersCount = 0, name,
|
||||
shared, size = 12, style, testID, type,
|
||||
}: ChannelIconProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
|
@ -119,9 +116,9 @@ const ChannelIcon = ({
|
|||
}
|
||||
|
||||
if (isInfo) {
|
||||
activeIcon = styles.iconInfo;
|
||||
activeIcon = isUnread ? styles.iconInfoUnread : styles.iconInfo;
|
||||
activeGroupBox = styles.groupBoxInfo;
|
||||
activeGroup = styles.groupInfo;
|
||||
activeGroup = isUnread ? styles.groupInfoUnread : styles.groupInfo;
|
||||
}
|
||||
|
||||
if (isMuted) {
|
||||
|
|
@ -186,7 +183,11 @@ const ChannelIcon = ({
|
|||
</View>
|
||||
);
|
||||
} else if (type === General.DM_CHANNEL) {
|
||||
icon = (<DmAvatar channelName={name}/>);
|
||||
icon = (
|
||||
<DmAvatar
|
||||
channelName={name}
|
||||
isInfo={isInfo}
|
||||
/>);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,286 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/channel_list/categories/body/channel_item should match snapshot 1`] = `
|
||||
<View
|
||||
animatedStyle={
|
||||
Object {
|
||||
"value": Object {
|
||||
"height": 40,
|
||||
"marginVertical": 2,
|
||||
"opacity": 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
collapsable={false}
|
||||
style={
|
||||
Object {
|
||||
"height": 40,
|
||||
"marginVertical": 2,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"minHeight": 40,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
Object {
|
||||
"minHeight": 40,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="channel_list_item.hello.collapsed.false"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
Object {
|
||||
"height": 24,
|
||||
"width": 24,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(255,255,255,0.16)",
|
||||
"borderRadius": 4,
|
||||
"justifyContent": "center",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
Object {
|
||||
"height": 24,
|
||||
"width": 24,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 16,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="undefined.gm_member_count"
|
||||
>
|
||||
1
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"marginTop": -1,
|
||||
"paddingLeft": 12,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
}
|
||||
testID="channel_list_item.hello.display_name"
|
||||
>
|
||||
Hello!
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/channel_list/categories/body/channel_item should match snapshot when it has a draft 1`] = `
|
||||
<View
|
||||
animatedStyle={
|
||||
Object {
|
||||
"value": Object {
|
||||
"height": 40,
|
||||
"marginVertical": 2,
|
||||
"opacity": 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
collapsable={false}
|
||||
style={
|
||||
Object {
|
||||
"height": 40,
|
||||
"marginVertical": 2,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"minHeight": 40,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
Object {
|
||||
"minHeight": 40,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="channel_list_item.hello.collapsed.false"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
Object {
|
||||
"height": 24,
|
||||
"width": 24,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="pencil-outline"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.4)",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
Object {
|
||||
"fontSize": 24,
|
||||
"left": 2,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="undefined.draft"
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"marginTop": -1,
|
||||
"paddingLeft": 12,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
}
|
||||
testID="channel_list_item.hello.display_name"
|
||||
>
|
||||
Hello!
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
|
@ -32,6 +32,7 @@ describe('components/channel_list/categories/body/channel_item', () => {
|
|||
const wrapper = renderWithIntlAndTheme(
|
||||
<ChannelItem
|
||||
channel={{displayName: 'Hello!', type: 'G', shared: false, name: 'hello', deleteAt: 0} as ChannelModel}
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
myChannel={myChannel}
|
||||
isMuted={false}
|
||||
|
|
@ -39,6 +40,26 @@ describe('components/channel_list/categories/body/channel_item', () => {
|
|||
currentUserId={'id'}
|
||||
testID='channel_list_item'
|
||||
isVisible={true}
|
||||
onPress={() => undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot when it has a draft', () => {
|
||||
const wrapper = renderWithIntlAndTheme(
|
||||
<ChannelItem
|
||||
channel={{displayName: 'Hello!', type: 'G', shared: false, name: 'hello', deleteAt: 0} as ChannelModel}
|
||||
hasDraft={true}
|
||||
isActive={false}
|
||||
myChannel={myChannel}
|
||||
isMuted={false}
|
||||
collapsed={false}
|
||||
currentUserId={'id'}
|
||||
testID='channel_list_item'
|
||||
isVisible={true}
|
||||
onPress={() => undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
256
app/components/channel_item/channel_item.tsx
Normal file
256
app/components/channel_item/channel_item.tsx
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
|
||||
import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import Badge from '@components/badge';
|
||||
import ChannelIcon from '@components/channel_icon';
|
||||
import {General} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import CustomStatus from './custom_status';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
|
||||
type Props = {
|
||||
channel: ChannelModel;
|
||||
collapsed: boolean;
|
||||
currentUserId: string;
|
||||
hasDraft: boolean;
|
||||
isActive: boolean;
|
||||
isInfo?: boolean;
|
||||
isMuted: boolean;
|
||||
isVisible: boolean;
|
||||
myChannel?: MyChannelModel;
|
||||
onPress: (channelId: string) => void;
|
||||
teamDisplayName?: string;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 20,
|
||||
minHeight: 40,
|
||||
alignItems: 'center',
|
||||
},
|
||||
infoItem: {
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 24,
|
||||
lineHeight: 28,
|
||||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
},
|
||||
text: {
|
||||
marginTop: -1,
|
||||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
paddingLeft: 12,
|
||||
},
|
||||
highlight: {
|
||||
color: theme.sidebarText,
|
||||
},
|
||||
textInfo: {
|
||||
color: theme.centerChannelColor,
|
||||
paddingRight: 20,
|
||||
},
|
||||
muted: {
|
||||
color: changeOpacity(theme.sidebarText, 0.4),
|
||||
},
|
||||
badge: {
|
||||
position: 'relative',
|
||||
left: 0,
|
||||
top: 0,
|
||||
alignSelf: undefined,
|
||||
},
|
||||
infoBadge: {
|
||||
color: theme.buttonColor,
|
||||
backgroundColor: theme.buttonBg,
|
||||
borderColor: theme.centerChannelBg,
|
||||
},
|
||||
mutedBadge: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
activeItem: {
|
||||
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.1),
|
||||
borderLeftColor: theme.sidebarTextActiveBorder,
|
||||
borderLeftWidth: 5,
|
||||
marginLeft: 0,
|
||||
paddingLeft: 14,
|
||||
},
|
||||
textActive: {
|
||||
color: theme.sidebarText,
|
||||
},
|
||||
teamName: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
paddingLeft: 12,
|
||||
marginTop: 4,
|
||||
...typography('Body', 75),
|
||||
},
|
||||
teamNameTablet: {
|
||||
marginLeft: -12,
|
||||
paddingLeft: 0,
|
||||
marginTop: 0,
|
||||
paddingBottom: 0,
|
||||
top: 5,
|
||||
},
|
||||
}));
|
||||
|
||||
const textStyle = StyleSheet.create({
|
||||
bright: typography('Body', 200, 'SemiBold'),
|
||||
regular: typography('Body', 200, 'Regular'),
|
||||
});
|
||||
|
||||
const ChannelListItem = ({
|
||||
channel, collapsed, currentUserId, hasDraft,
|
||||
isActive, isInfo, isMuted, isVisible,
|
||||
myChannel, onPress, teamDisplayName, testID}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
// Make it brighter if it's not muted, and highlighted or has unreads
|
||||
const isBright = !isMuted && (myChannel && (myChannel.isUnread || myChannel.mentionsCount > 0));
|
||||
|
||||
const shouldCollapse = (collapsed && !isBright) && !isActive;
|
||||
const sharedValue = useSharedValue(shouldCollapse);
|
||||
const height = useMemo(() => {
|
||||
let h = 40;
|
||||
if (isInfo) {
|
||||
h = (teamDisplayName && !isTablet) ? 58 : 44;
|
||||
}
|
||||
return h;
|
||||
}, [teamDisplayName, isInfo, isTablet]);
|
||||
|
||||
useEffect(() => {
|
||||
sharedValue.value = shouldCollapse;
|
||||
}, [shouldCollapse]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
marginVertical: withTiming(sharedValue.value ? 0 : 2, {duration: 500}),
|
||||
height: withTiming(sharedValue.value ? 0 : height, {duration: 500}),
|
||||
opacity: withTiming(sharedValue.value ? 0 : 1, {duration: 500, easing: Easing.inOut(Easing.exp)}),
|
||||
};
|
||||
}, [height]);
|
||||
|
||||
const handleOnPress = useCallback(() => {
|
||||
onPress(myChannel?.id || channel.id);
|
||||
}, [channel.id, myChannel?.id]);
|
||||
|
||||
const membersCount = useMemo(() => {
|
||||
if (channel.type === General.GM_CHANNEL) {
|
||||
return channel.name.split(',').length;
|
||||
}
|
||||
return 0;
|
||||
}, [channel.type, channel.name]);
|
||||
|
||||
const textStyles = useMemo(() => [
|
||||
isBright ? textStyle.bright : textStyle.regular,
|
||||
styles.text,
|
||||
isBright && styles.highlight,
|
||||
isMuted && styles.muted,
|
||||
isActive && !isInfo ? styles.textActive : null,
|
||||
isInfo ? styles.textInfo : null,
|
||||
], [isBright, styles, isMuted, isActive, isInfo]);
|
||||
|
||||
const containerStyle = useMemo(() => [
|
||||
styles.container,
|
||||
isActive && !isInfo && styles.activeItem,
|
||||
isInfo && styles.infoItem,
|
||||
{minHeight: height},
|
||||
],
|
||||
[height, isActive, isInfo, styles]);
|
||||
|
||||
if ((channel.deleteAt > 0 && !isActive) || !myChannel || !isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const teammateId = (channel.type === General.DM_CHANNEL) ? getUserIdFromChannelName(currentUserId, channel.name) : undefined;
|
||||
const isOwnDirectMessage = (channel.type === General.DM_CHANNEL) && currentUserId === teammateId;
|
||||
|
||||
let displayName = channel.displayName;
|
||||
if (isOwnDirectMessage) {
|
||||
displayName = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View style={animatedStyle}>
|
||||
<TouchableOpacity onPress={handleOnPress}>
|
||||
<>
|
||||
<View
|
||||
style={containerStyle}
|
||||
testID={`${testID}.${channel.name}.collapsed.${collapsed && !isActive}`}
|
||||
>
|
||||
<View style={styles.wrapper}>
|
||||
<ChannelIcon
|
||||
hasDraft={hasDraft}
|
||||
isActive={isInfo ? false : isActive}
|
||||
isInfo={isInfo}
|
||||
isUnread={isBright}
|
||||
isArchived={channel.deleteAt > 0}
|
||||
membersCount={membersCount}
|
||||
name={channel.name}
|
||||
shared={channel.shared}
|
||||
size={24}
|
||||
type={channel.type}
|
||||
isMuted={isMuted}
|
||||
/>
|
||||
<View>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={textStyles}
|
||||
testID={`${testID}.${channel.name}.display_name`}
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
{isInfo && Boolean(teamDisplayName) && !isTablet &&
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
testID={`${testID}.${teamDisplayName}.display_name`}
|
||||
style={styles.teamName}
|
||||
>
|
||||
{teamDisplayName}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
{Boolean(teammateId) && <CustomStatus userId={teammateId!}/>}
|
||||
{isInfo && Boolean(teamDisplayName) && isTablet &&
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
testID={`${testID}.${teamDisplayName}.display_name`}
|
||||
style={[styles.teamName, styles.teamNameTablet]}
|
||||
>
|
||||
{teamDisplayName}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
<Badge
|
||||
visible={myChannel.mentionsCount > 0}
|
||||
value={myChannel.mentionsCount}
|
||||
style={[styles.badge, isMuted && styles.mutedBadge, isInfo && styles.infoBadge]}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelListItem;
|
||||
39
app/components/channel_item/custom_status/custom_status.tsx
Normal file
39
app/components/channel_item/custom_status/custom_status.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
|
||||
type Props = {
|
||||
customStatus?: UserCustomStatus;
|
||||
customStatusExpired: boolean;
|
||||
isCustomStatusEnabled: boolean;
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
customStatusEmoji: {
|
||||
color: '#000',
|
||||
marginHorizontal: 5,
|
||||
marginTop: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled}: Props) => {
|
||||
const showCustomStatusEmoji = Boolean(isCustomStatusEnabled && customStatus && !customStatusExpired);
|
||||
|
||||
if (!showCustomStatusEmoji) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomStatusEmoji
|
||||
customStatus={customStatus!}
|
||||
style={style.customStatusEmoji}
|
||||
testID={`channel_list_item.custom_status.${customStatus!.emoji}-${customStatus!.text}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomStatus;
|
||||
37
app/components/channel_item/custom_status/index.ts
Normal file
37
app/components/channel_item/custom_status/index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// 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 {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfig} from '@queries/servers/system';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
import {getUserCustomStatus, isCustomStatusExpired} from '@utils/user';
|
||||
|
||||
import CustomStatus from './custom_status';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type HeaderInputProps = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const enhanced = withObservables(
|
||||
['userId'],
|
||||
({userId, database}: WithDatabaseArgs & HeaderInputProps) => {
|
||||
const config = observeConfig(database);
|
||||
const user = observeUser(database, userId);
|
||||
const isCustomStatusEnabled = config.pipe(map((cfg) => cfg?.EnableCustomUserStatuses === 'true'));
|
||||
const customStatus = user.pipe(switchMap((u) => (u?.isBot ? of$(undefined) : of$(getUserCustomStatus(u)))));
|
||||
const customStatusExpired = user.pipe(switchMap((u) => (u?.isBot ? of$(false) : of$(isCustomStatusExpired(u)))));
|
||||
|
||||
return {
|
||||
customStatus,
|
||||
customStatusExpired,
|
||||
isCustomStatusEnabled,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(CustomStatus));
|
||||
|
|
@ -10,6 +10,7 @@ import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
|||
import {Preferences} from '@constants';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {observeMyChannel} from '@queries/servers/channel';
|
||||
import {queryDraft} from '@queries/servers/drafts';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
|
@ -20,12 +21,23 @@ import ChannelItem from './channel_item';
|
|||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
|
||||
type EnhanceProps = WithDatabaseArgs & {
|
||||
channel: ChannelModel;
|
||||
isInfo?: boolean;
|
||||
isUnreads?: boolean;
|
||||
showTeamName?: boolean;
|
||||
}
|
||||
|
||||
const observeIsMutedSetting = (mc: MyChannelModel) => mc.settings.observe().pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === 'mention')));
|
||||
|
||||
const enhance = withObservables(['channel', 'isUnreads'], ({channel, isUnreads, database}: {channel: ChannelModel; isUnreads?: boolean} & WithDatabaseArgs) => {
|
||||
const enhance = withObservables(['channel', 'isUnreads', 'showTeamName'], ({channel, database, isInfo, isUnreads, showTeamName}: EnhanceProps) => {
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
const myChannel = observeMyChannel(database, channel.id);
|
||||
|
||||
const hasDraft = queryDraft(database, channel.id).observeWithColumns(['message', 'files']).pipe(
|
||||
switchMap((draft) => of$(draft.length > 0)),
|
||||
);
|
||||
|
||||
const isActive = observeCurrentChannelId(database).pipe(switchMap((id) => of$(id ? id === channel.id : false)), distinctUntilChanged());
|
||||
const unreadsOnTop = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
|
||||
observeWithColumns(['value']).
|
||||
|
|
@ -38,6 +50,11 @@ const enhance = withObservables(['channel', 'isUnreads'], ({channel, isUnreads,
|
|||
if (!mc) {
|
||||
return of$(false);
|
||||
}
|
||||
|
||||
if (isInfo) {
|
||||
return of$(true);
|
||||
}
|
||||
|
||||
if (isUnreads) {
|
||||
return of$(u);
|
||||
}
|
||||
|
|
@ -55,13 +72,22 @@ const enhance = withObservables(['channel', 'isUnreads'], ({channel, isUnreads,
|
|||
}),
|
||||
);
|
||||
|
||||
let teamDisplayName = of$('');
|
||||
if (channel.teamId && showTeamName) {
|
||||
teamDisplayName = channel.team.observe().pipe(
|
||||
switchMap((team) => of$(team?.displayName || '')),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
channel: channel.observe(),
|
||||
currentUserId,
|
||||
isMuted,
|
||||
hasDraft,
|
||||
isActive,
|
||||
isMuted,
|
||||
isVisible,
|
||||
myChannel,
|
||||
channel: channel.observe(),
|
||||
teamDisplayName,
|
||||
};
|
||||
});
|
||||
|
||||
66
app/components/option_box/index.tsx
Normal file
66
app/components/option_box/index.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text, TextStyle, TouchableHighlight, ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type OptionBoxProps = {
|
||||
iconColor?: string;
|
||||
iconName: string;
|
||||
onPress: () => void;
|
||||
text: string;
|
||||
textStyle?: TextStyle;
|
||||
style?: ViewStyle;
|
||||
underlayColor?: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
maxHeight: 60,
|
||||
justifyContent: 'center',
|
||||
minWidth: 115,
|
||||
},
|
||||
text: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
paddingHorizontal: 5,
|
||||
...typography('Body', 50, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const OptionBox = ({iconColor, iconName, onPress, style, text, textStyle, underlayColor}: OptionBoxProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={onPress}
|
||||
style={[styles.container, style]}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.32) || underlayColor}
|
||||
>
|
||||
<>
|
||||
<CompassIcon
|
||||
color={iconColor || changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
name={iconName}
|
||||
size={24}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[styles.text, textStyle]}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptionBox;
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ActivityIndicatorProps, Platform, StyleProp, TextInput, TextInputProps, TextStyle, TouchableOpacityProps, ViewStyle} from 'react-native';
|
||||
import {ActivityIndicatorProps, Keyboard, Platform, StyleProp, TextInput, TextInputProps, TextStyle, TouchableOpacityProps, ViewStyle} from 'react-native';
|
||||
import {SearchBar} from 'react-native-elements';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
|
|
@ -82,9 +82,10 @@ const Search = forwardRef<SearchRef, SearchProps>((props: SearchProps, ref) => {
|
|||
const searchInputTestID = `${props.testID}.search.input`;
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
setValue('');
|
||||
props.onCancel?.();
|
||||
}, []);
|
||||
}, [props.onCancel]);
|
||||
|
||||
const onClear = useCallback(() => {
|
||||
setValue('');
|
||||
|
|
@ -94,7 +95,7 @@ const Search = forwardRef<SearchRef, SearchProps>((props: SearchProps, ref) => {
|
|||
const onChangeText = useCallback((text: string) => {
|
||||
setValue(text);
|
||||
props.onChangeText?.(text);
|
||||
}, []);
|
||||
}, [props.onChangeText]);
|
||||
|
||||
const cancelButtonProps = useMemo(() => ({
|
||||
buttonTextStyle: {
|
||||
|
|
@ -107,38 +108,34 @@ const Search = forwardRef<SearchRef, SearchProps>((props: SearchProps, ref) => {
|
|||
setValue(props.defaultValue || value || '');
|
||||
}, [props.defaultValue]);
|
||||
|
||||
const clearIcon = useMemo(() => {
|
||||
return (
|
||||
<CompassIcon
|
||||
color={changeOpacity(props.clearIconColor || theme.centerChannelColor, Platform.select({android: 0.56, default: 0.72}))}
|
||||
name={Platform.select({android: 'close', default: 'close-circle'})}
|
||||
onPress={searchRef.current?.clear}
|
||||
size={Platform.select({android: 24, default: 18})}
|
||||
testID={searchClearButtonTestID}
|
||||
/>
|
||||
);
|
||||
}, [searchRef.current, theme]);
|
||||
const clearIcon = (
|
||||
<CompassIcon
|
||||
color={changeOpacity(props.clearIconColor || theme.centerChannelColor, Platform.select({android: 0.56, default: 0.72}))}
|
||||
name={Platform.select({android: 'close', default: 'close-circle'})}
|
||||
onPress={searchRef.current?.clear}
|
||||
size={Platform.select({android: 24, default: 18})}
|
||||
testID={searchClearButtonTestID}
|
||||
/>
|
||||
);
|
||||
|
||||
const searchIcon = useMemo(() => (
|
||||
const searchIcon = (
|
||||
<CompassIcon
|
||||
color={changeOpacity(props.searchIconColor || theme.centerChannelColor, Platform.select({android: 0.56, default: 0.72}))}
|
||||
name='magnify'
|
||||
onPress={searchRef.current?.focus}
|
||||
size={24}
|
||||
/>
|
||||
), [searchRef.current, theme]);
|
||||
);
|
||||
|
||||
const cancelIcon = useMemo(() => (
|
||||
const cancelIcon = (
|
||||
<CompassIcon
|
||||
color={changeOpacity(props.cancelButtonProps?.color || theme.centerChannelColor, Platform.select({android: 0.56, default: 0.72}))}
|
||||
name='arrow-left'
|
||||
|
||||
// @ts-expect-error cancel is not part of TextInput does exist in SearchBar
|
||||
onPress={searchRef.current?.cancel}
|
||||
onPress={onCancel}
|
||||
size={24}
|
||||
testID={searchCancelButtonTestID}
|
||||
/>
|
||||
), [searchRef.current, theme]);
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
blur: () => {
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ const UserItem = ({
|
|||
isActive={false}
|
||||
isArchived={false}
|
||||
isInfo={true}
|
||||
isUnread={true}
|
||||
isUnread={false}
|
||||
size={18}
|
||||
shared={true}
|
||||
type={General.DM_CHANNEL}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter';
|
|||
export const EDIT_POST = 'EditPost';
|
||||
export const EDIT_PROFILE = 'EditProfile';
|
||||
export const EDIT_SERVER = 'EditServer';
|
||||
export const FIND_CHANNELS = 'FindChannels';
|
||||
export const FORGOT_PASSWORD = 'ForgotPassword';
|
||||
export const GALLERY = 'Gallery';
|
||||
export const HOME = 'Home';
|
||||
|
|
@ -59,6 +60,7 @@ export default {
|
|||
EDIT_POST,
|
||||
EDIT_PROFILE,
|
||||
EDIT_SERVER,
|
||||
FIND_CHANNELS,
|
||||
FORGOT_PASSWORD,
|
||||
GALLERY,
|
||||
HOME,
|
||||
|
|
@ -85,5 +87,6 @@ export const MODAL_SCREENS_WITHOUT_BACK = [
|
|||
CREATE_DIRECT_MESSAGE,
|
||||
EMOJI_PICKER,
|
||||
EDIT_POST,
|
||||
FIND_CHANNELS,
|
||||
PERMALINK,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -416,3 +416,18 @@ export function observeMyChannelMentionCount(database: Database, teamId?: string
|
|||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
export function queryMyChannelsByUnread(database: Database, isUnread: boolean, sortBy: 'last_viewed_at' | 'last_post_at', take: number, excludeIds?: string[]) {
|
||||
const clause: Q.Clause[] = [Q.where('is_unread', Q.eq(isUnread))];
|
||||
|
||||
if (excludeIds?.length) {
|
||||
clause.push(Q.where('id', Q.notIn(excludeIds)));
|
||||
}
|
||||
|
||||
return queryAllMyChannel(database).extend(
|
||||
Q.on(CHANNEL, Q.where('delete_at', Q.eq(0))),
|
||||
...clause,
|
||||
Q.sortBy(sortBy, Q.desc),
|
||||
Q.take(take),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
|||
status.duration = duration;
|
||||
status.expires_at = expiresAt;
|
||||
}
|
||||
const {error} = await updateCustomStatus(serverUrl, currentUser, status);
|
||||
const {error} = await updateCustomStatus(serverUrl, status);
|
||||
if (error) {
|
||||
DeviceEventEmitter.emit(SET_CUSTOM_STATUS_FAILURE);
|
||||
return;
|
||||
|
|
|
|||
84
app/screens/find_channels/filtered_list/filtered_list.tsx
Normal file
84
app/screens/find_channels/filtered_list/filtered_list.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {FlatList, ListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||
import Animated, {FadeInDown, FadeOutUp} from 'react-native-reanimated';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import ChannelItem from '@components/channel_item';
|
||||
import NoResultsWithTerm from '@components/no_results_with_term';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
type Props = {
|
||||
close: () => Promise<void>;
|
||||
keyboardHeight: number;
|
||||
showTeamName: boolean;
|
||||
term: string;
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
flex: {flex: 1},
|
||||
noResultContainer: {
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
const UnfilteredList = ({close, keyboardHeight, showTeamName, term}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const [data] = useState([]);
|
||||
const flatListStyle = useMemo(() => ({flexGrow: 1, paddingBottom: keyboardHeight}), [keyboardHeight]);
|
||||
|
||||
const onPress = useCallback(async (channelId: string) => {
|
||||
await close();
|
||||
switchToChannelById(serverUrl, channelId);
|
||||
}, [serverUrl, close]);
|
||||
|
||||
const renderNoResults = useCallback(() => {
|
||||
if (term) {
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<NoResultsWithTerm term={term}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [term]);
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<ChannelModel>) => {
|
||||
return (
|
||||
<ChannelItem
|
||||
channel={item}
|
||||
collapsed={false}
|
||||
isInfo={true}
|
||||
onPress={onPress}
|
||||
showTeamName={showTeamName}
|
||||
/>
|
||||
);
|
||||
}, [onPress, showTeamName]);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(200)}
|
||||
exiting={FadeOutUp.duration(200)}
|
||||
style={style.flex}
|
||||
>
|
||||
<FlatList
|
||||
contentContainerStyle={flatListStyle}
|
||||
keyboardDismissMode='interactive'
|
||||
keyboardShouldPersistTaps='handled'
|
||||
ListEmptyComponent={renderNoResults}
|
||||
renderItem={renderItem}
|
||||
data={data}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnfilteredList;
|
||||
27
app/screens/find_channels/filtered_list/index.ts
Normal file
27
app/screens/find_channels/filtered_list/index.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// 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 {queryJoinedTeams} from '@queries/servers/team';
|
||||
|
||||
import FilteredList from './filtered_list';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type EnhanceProps = WithDatabaseArgs & {
|
||||
term: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables([], ({database}: EnhanceProps) => {
|
||||
const teamsCount = queryJoinedTeams(database).observeCount();
|
||||
|
||||
return {
|
||||
showTeamName: teamsCount.pipe(switchMap((count) => of$(count > 1))),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(FilteredList));
|
||||
118
app/screens/find_channels/index.tsx
Normal file
118
app/screens/find_channels/index.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {DeviceEventEmitter, Keyboard, View} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import SearchBar from '@components/search';
|
||||
import {Events} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useKeyboardHeight} from '@hooks/device';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import FilteredList from './filtered_list';
|
||||
import QuickOptions from './quick_options';
|
||||
import UnfilteredList from './unfiltered_list';
|
||||
|
||||
type Props = {
|
||||
closeButtonId: string;
|
||||
componentId: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginHorizontal: 20,
|
||||
marginTop: 20,
|
||||
},
|
||||
inputContainerStyle: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.12),
|
||||
},
|
||||
inputStyle: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
listContainer: {
|
||||
flex: 1,
|
||||
marginTop: 8,
|
||||
},
|
||||
}));
|
||||
|
||||
const FindChannels = ({closeButtonId, componentId}: Props) => {
|
||||
const theme = useTheme();
|
||||
const [term, setTerm] = useState('');
|
||||
const styles = getStyleSheet(theme);
|
||||
const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]);
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
|
||||
const cancelButtonProps = useMemo(() => ({
|
||||
color,
|
||||
buttonTextStyle: {
|
||||
...typography('Body', 100),
|
||||
},
|
||||
}), [color]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
DeviceEventEmitter.emit(Events.PAUSE_KEYBOARD_TRACKING_VIEW, false);
|
||||
return dismissModal({componentId});
|
||||
}, []);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
dismissModal({componentId});
|
||||
}, []);
|
||||
|
||||
const onChangeText = useCallback((text) => {
|
||||
setTerm(text);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const navigationEvents = Navigation.events().registerNavigationButtonPressedListener(({buttonId}) => {
|
||||
if (closeButtonId && buttonId === closeButtonId) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
return () => navigationEvents.remove();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SearchBar
|
||||
autoCapitalize='none'
|
||||
autoFocus={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
clearIconColor={color}
|
||||
searchIconColor={color}
|
||||
cancelButtonProps={cancelButtonProps}
|
||||
placeholderTextColor={color}
|
||||
inputStyle={styles.inputStyle}
|
||||
inputContainerStyle={styles.inputContainerStyle}
|
||||
selectionColor={color}
|
||||
onCancel={onCancel}
|
||||
onChangeText={onChangeText}
|
||||
value={term}
|
||||
/>
|
||||
{term === '' && <QuickOptions close={close}/>}
|
||||
<View style={styles.listContainer}>
|
||||
{term === '' &&
|
||||
<UnfilteredList
|
||||
close={close}
|
||||
keyboardHeight={keyboardHeight}
|
||||
/>
|
||||
}
|
||||
{Boolean(term) &&
|
||||
<FilteredList
|
||||
close={close}
|
||||
keyboardHeight={keyboardHeight}
|
||||
term={term}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default FindChannels;
|
||||
45
app/screens/find_channels/quick_options/index.ts
Normal file
45
app/screens/find_channels/quick_options/index.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// 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 {combineLatest, of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Permissions} from '@constants';
|
||||
import {observePermissionForTeam} from '@queries/servers/role';
|
||||
import {observeCurrentTeam} from '@queries/servers/team';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import QuickOptions from './quick_options';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const team = observeCurrentTeam(database);
|
||||
|
||||
const currentUser = observeCurrentUser(database);
|
||||
|
||||
const canJoinChannels = combineLatest([currentUser, team]).pipe(
|
||||
switchMap(([u, t]) => (t && u ? observePermissionForTeam(t, u, Permissions.JOIN_PUBLIC_CHANNELS, true) : of$(false))),
|
||||
);
|
||||
|
||||
const canCreatePublicChannels = combineLatest([currentUser, team]).pipe(
|
||||
switchMap(([u, t]) => (t && u ? observePermissionForTeam(t, u, Permissions.CREATE_PUBLIC_CHANNEL, true) : of$(false))),
|
||||
);
|
||||
|
||||
const canCreatePrivateChannels = combineLatest([currentUser, team]).pipe(
|
||||
switchMap(([u, t]) => (t && u ? observePermissionForTeam(t, u, Permissions.CREATE_PRIVATE_CHANNEL, false) : of$(false))),
|
||||
);
|
||||
|
||||
const canCreateChannels = combineLatest([canCreatePublicChannels, canCreatePrivateChannels]).pipe(
|
||||
switchMap(([open, priv]) => of$(open || priv)),
|
||||
);
|
||||
|
||||
return {
|
||||
canCreateChannels,
|
||||
canJoinChannels,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(QuickOptions));
|
||||
105
app/screens/find_channels/quick_options/quick_options.tsx
Normal file
105
app/screens/find_channels/quick_options/quick_options.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// 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 {StyleSheet, View} from 'react-native';
|
||||
import Animated, {FadeInDown, FadeOutUp} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import OptionBox from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {showModal} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
canCreateChannels: boolean;
|
||||
canJoinChannels: boolean;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
const OPTIONS_HEIGHT = 60;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
wrapper: {
|
||||
flexDirection: 'row',
|
||||
height: OPTIONS_HEIGHT,
|
||||
},
|
||||
separator: {
|
||||
width: 8,
|
||||
},
|
||||
});
|
||||
|
||||
const QuickOptions = ({canCreateChannels, canJoinChannels, close}: Props) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
|
||||
const browseChannels = useCallback(async () => {
|
||||
const title = intl.formatMessage({id: 'browse_channels.title', defaultMessage: 'Browse channels'});
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
await close();
|
||||
showModal(Screens.BROWSE_CHANNELS, title, {
|
||||
closeButton,
|
||||
}, {modal: {swipeToDismiss: false}});
|
||||
}, [intl, theme]);
|
||||
|
||||
const createNewChannel = useCallback(async () => {
|
||||
const title = intl.formatMessage({id: 'mobile.create_channel.title', defaultMessage: 'New channel'});
|
||||
|
||||
await close();
|
||||
showModal(Screens.CREATE_OR_EDIT_CHANNEL, title, undefined, {modal: {swipeToDismiss: false}});
|
||||
}, [intl]);
|
||||
|
||||
const openDirectMessage = useCallback(async () => {
|
||||
const title = intl.formatMessage({id: 'create_direct_message.title', defaultMessage: 'Create Direct Message'});
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
await close();
|
||||
showModal(Screens.CREATE_DIRECT_MESSAGE, title, {
|
||||
closeButton,
|
||||
}, {modal: {swipeToDismiss: false}});
|
||||
}, [intl, theme]);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(200)}
|
||||
exiting={FadeOutUp.duration(200)}
|
||||
style={styles.container}
|
||||
>
|
||||
<Animated.View style={styles.wrapper}>
|
||||
{canJoinChannels &&
|
||||
<>
|
||||
<OptionBox
|
||||
iconName='globe'
|
||||
onPress={browseChannels}
|
||||
text={intl.formatMessage({id: 'find_channels.directory', defaultMessage: 'Directory'})}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
</>
|
||||
}
|
||||
<OptionBox
|
||||
iconName='account-outline'
|
||||
onPress={openDirectMessage}
|
||||
text={intl.formatMessage({id: 'find_channels.open_dm', defaultMessage: 'Open a DM'})}
|
||||
/>
|
||||
{canCreateChannels &&
|
||||
<>
|
||||
<View style={styles.separator}/>
|
||||
<OptionBox
|
||||
iconName='plus'
|
||||
onPress={createNewChannel}
|
||||
text={intl.formatMessage({id: 'find_channels.new_channel', defaultMessage: 'New Channel'})}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickOptions;
|
||||
46
app/screens/find_channels/unfiltered_list/header.tsx
Normal file
46
app/screens/find_channels/unfiltered_list/header.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 {Text, View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
paddingVertical: 8,
|
||||
paddingTop: 12,
|
||||
paddingLeft: 2,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
heading: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
...typography('Heading', 75, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
sectionName: string;
|
||||
}
|
||||
|
||||
const FindChannelsHeader = ({sectionName}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text
|
||||
style={styles.heading}
|
||||
testID={`find_channels.header.${sectionName}`}
|
||||
>
|
||||
{sectionName.toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default FindChannelsHeader;
|
||||
54
app/screens/find_channels/unfiltered_list/index.ts
Normal file
54
app/screens/find_channels/unfiltered_list/index.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryMyChannelsByUnread} from '@queries/servers/channel';
|
||||
import {queryJoinedTeams} from '@queries/servers/team';
|
||||
|
||||
import UnfilteredList from './unfiltered_list';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
|
||||
const MAX_UNREAD_CHANNELS = 10;
|
||||
const MAX_CHANNELS = 20;
|
||||
|
||||
const observeChannels = async (myChannels: MyChannelModel[]) => {
|
||||
const channels = await Promise.all(myChannels.map((m) => m.channel.fetch()));
|
||||
return channels.filter((c): c is ChannelModel => c !== null);
|
||||
};
|
||||
|
||||
const observeRecentChannels = (database: Database, unreads: ChannelModel[]) => {
|
||||
const count = MAX_CHANNELS - unreads.length;
|
||||
const unreadIds = unreads.map((u) => u.id);
|
||||
return queryMyChannelsByUnread(database, false, 'last_viewed_at', count, unreadIds).observe().pipe(
|
||||
switchMap((myChannels) => observeChannels(myChannels)),
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const teamsCount = queryJoinedTeams(database).observeCount();
|
||||
|
||||
const unreadChannels = queryMyChannelsByUnread(database, true, 'last_post_at', MAX_UNREAD_CHANNELS).
|
||||
observeWithColumns(['last_post_at']).pipe(
|
||||
switchMap((myChannels) => observeChannels(myChannels)),
|
||||
);
|
||||
|
||||
const recentChannels = unreadChannels.pipe(
|
||||
switchMap((unreads) => observeRecentChannels(database, unreads)),
|
||||
);
|
||||
|
||||
return {
|
||||
recentChannels,
|
||||
showTeamName: teamsCount.pipe(switchMap((count) => of$(count > 1))),
|
||||
unreadChannels,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(UnfilteredList));
|
||||
111
app/screens/find_channels/unfiltered_list/unfiltered_list.tsx
Normal file
111
app/screens/find_channels/unfiltered_list/unfiltered_list.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {SectionList, SectionListRenderItemInfo, StyleSheet} from 'react-native';
|
||||
import Animated, {FadeInDown, FadeOutUp} from 'react-native-reanimated';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import ChannelItem from '@components/channel_item';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {t} from '@i18n';
|
||||
|
||||
import FindChannelsHeader from './header';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
type Props = {
|
||||
close: () => Promise<void>;
|
||||
keyboardHeight: number;
|
||||
recentChannels: ChannelModel[];
|
||||
showTeamName: boolean;
|
||||
unreadChannels: ChannelModel[];
|
||||
}
|
||||
|
||||
const sectionNames = {
|
||||
unreads: {
|
||||
id: t('mobile.channel_list.unreads'),
|
||||
defaultMessage: 'UNREADS',
|
||||
},
|
||||
recent: {
|
||||
id: t('mobile.channel_list.recent'),
|
||||
defaultMessage: 'RECENT',
|
||||
},
|
||||
};
|
||||
|
||||
const style = StyleSheet.create({
|
||||
flex: {flex: 1},
|
||||
});
|
||||
|
||||
const buildSections = (unreadChannels: ChannelModel[], recentChannels: ChannelModel[]) => {
|
||||
const sections = [];
|
||||
if (unreadChannels.length) {
|
||||
sections.push({
|
||||
...sectionNames.unreads,
|
||||
data: unreadChannels,
|
||||
});
|
||||
}
|
||||
|
||||
if (recentChannels.length) {
|
||||
sections.push({
|
||||
...sectionNames.recent,
|
||||
data: recentChannels,
|
||||
});
|
||||
}
|
||||
|
||||
return sections;
|
||||
};
|
||||
|
||||
const UnfilteredList = ({close, keyboardHeight, recentChannels, showTeamName, unreadChannels}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const [sections, setSections] = useState(buildSections(unreadChannels, recentChannels));
|
||||
const sectionListStyle = useMemo(() => ({paddingBottom: keyboardHeight}), [keyboardHeight]);
|
||||
|
||||
const onPress = useCallback(async (channelId: string) => {
|
||||
await close();
|
||||
switchToChannelById(serverUrl, channelId);
|
||||
}, [serverUrl, close]);
|
||||
|
||||
const renderSectionHeader = useCallback(({section}) => (
|
||||
<FindChannelsHeader sectionName={intl.formatMessage({id: section.id, defaultMessage: section.defaultMessage})}/>
|
||||
), [intl.locale]);
|
||||
|
||||
const renderSectionItem = useCallback(({item}: SectionListRenderItemInfo<ChannelModel>) => {
|
||||
return (
|
||||
<ChannelItem
|
||||
channel={item}
|
||||
collapsed={false}
|
||||
isInfo={true}
|
||||
onPress={onPress}
|
||||
showTeamName={showTeamName}
|
||||
/>
|
||||
);
|
||||
}, [onPress, showTeamName]);
|
||||
|
||||
useEffect(() => {
|
||||
setSections(buildSections(unreadChannels, recentChannels));
|
||||
}, [unreadChannels, recentChannels]);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(200)}
|
||||
exiting={FadeOutUp.duration(200)}
|
||||
style={style.flex}
|
||||
>
|
||||
<SectionList
|
||||
contentContainerStyle={sectionListStyle}
|
||||
keyboardDismissMode='interactive'
|
||||
keyboardShouldPersistTaps='handled'
|
||||
renderItem={renderSectionItem}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
sections={sections}
|
||||
showsVerticalScrollIndicator={false}
|
||||
stickySectionHeadersEnabled={true}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnfilteredList;
|
||||
|
|
@ -356,17 +356,23 @@ exports[`components/categories_list should render team error 1`] = `
|
|||
</Text>
|
||||
</View>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(255,255,255,0.12)",
|
||||
"borderRadius": 8,
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"justifyContent": "flex-start",
|
||||
"marginVertical": 20,
|
||||
"maxHeight": 40,
|
||||
"padding": 8,
|
||||
"width": "100%",
|
||||
}
|
||||
|
|
@ -382,35 +388,21 @@ exports[`components/categories_list should render team error 1`] = `
|
|||
}
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Find Channels"
|
||||
placeholderTextColor="rgba(255,255,255,0.72)"
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"alignContent": "center",
|
||||
"alignItems": "center",
|
||||
"color": "#ffffff",
|
||||
"flex": 1,
|
||||
"height": 40,
|
||||
"marginLeft": 5,
|
||||
"marginTop": -2,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
"marginLeft": 5,
|
||||
"marginTop": 1,
|
||||
}
|
||||
}
|
||||
testID="channel_list.search_field.find_channels.input"
|
||||
/>
|
||||
>
|
||||
Find channels...
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
|
|
|
|||
|
|
@ -50,74 +50,89 @@ Object {
|
|||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"minHeight": 40,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
Object {
|
||||
"minHeight": 40,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="category.test_category.channel_list_item.channel.collapsed.false"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
Object {
|
||||
"height": 24,
|
||||
"width": 24,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="globe"
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.4)",
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
Object {
|
||||
"fontSize": 24,
|
||||
"left": 1,
|
||||
"height": 24,
|
||||
"width": 24,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="undefined.public"
|
||||
/>
|
||||
>
|
||||
<Icon
|
||||
name="globe"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.4)",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
Object {
|
||||
"fontSize": 24,
|
||||
"left": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="undefined.public"
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"marginTop": -1,
|
||||
"paddingLeft": 12,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
}
|
||||
testID="category.test_category.channel_list_item.channel.display_name"
|
||||
>
|
||||
Channel
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"flex": 1,
|
||||
"marginTop": -1,
|
||||
"paddingLeft": 12,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
]
|
||||
}
|
||||
testID="category.test_category.channel_list_item.channel.display_name"
|
||||
>
|
||||
Channel
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ describe('components/channel_list/categories/body', () => {
|
|||
<CategoryBody
|
||||
category={category}
|
||||
locale={DEFAULT_LOCALE}
|
||||
onChannelSwitch={() => undefined}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@
|
|||
import React, {useCallback, useMemo} from 'react';
|
||||
import {FlatList} from 'react-native';
|
||||
|
||||
import ChannelItem from '@components/channel_item';
|
||||
import {DMS_CATEGORY} from '@constants/categories';
|
||||
import ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
import ChannelItem from './channel_item';
|
||||
|
||||
import type CategoryModel from '@typings/database/models/servers/category';
|
||||
|
||||
type Props = {
|
||||
|
|
@ -16,11 +15,12 @@ type Props = {
|
|||
hiddenChannelIds: Set<string>;
|
||||
category: CategoryModel;
|
||||
limit: number;
|
||||
onChannelSwitch: (channelId: string) => void;
|
||||
};
|
||||
|
||||
const extractKey = (item: ChannelModel) => item.id;
|
||||
|
||||
const CategoryBody = ({sortedChannels, category, hiddenChannelIds, limit}: Props) => {
|
||||
const CategoryBody = ({sortedChannels, category, hiddenChannelIds, limit, onChannelSwitch}: Props) => {
|
||||
const ids = useMemo(() => {
|
||||
let filteredChannels = sortedChannels;
|
||||
|
||||
|
|
@ -41,9 +41,10 @@ const CategoryBody = ({sortedChannels, category, hiddenChannelIds, limit}: Props
|
|||
channel={item}
|
||||
collapsed={category.collapsed}
|
||||
testID={`category.${category.displayName.replace(/ /g, '_').toLocaleLowerCase()}.channel_list_item`}
|
||||
onPress={onChannelSwitch}
|
||||
/>
|
||||
);
|
||||
}, [category.collapsed]);
|
||||
}, [category.collapsed, onChannelSwitch]);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
|
|
|
|||
|
|
@ -1,140 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/channel_list/categories/body/channel_item should match snapshot 1`] = `
|
||||
<View
|
||||
animatedStyle={
|
||||
Object {
|
||||
"value": Object {
|
||||
"height": 40,
|
||||
"marginVertical": 2,
|
||||
"opacity": 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
collapsable={false}
|
||||
style={
|
||||
Object {
|
||||
"height": 40,
|
||||
"marginVertical": 2,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
false,
|
||||
]
|
||||
}
|
||||
testID="channel_list_item.hello.collapsed.false"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
Object {
|
||||
"height": 24,
|
||||
"width": 24,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(255,255,255,0.16)",
|
||||
"borderRadius": 4,
|
||||
"justifyContent": "center",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
Object {
|
||||
"height": 24,
|
||||
"width": 24,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 16,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="undefined.gm_member_count"
|
||||
>
|
||||
1
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"flex": 1,
|
||||
"marginTop": -1,
|
||||
"paddingLeft": 12,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
]
|
||||
}
|
||||
testID="channel_list_item.hello.display_name"
|
||||
>
|
||||
Hello!
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
|
||||
import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import Badge from '@components/badge';
|
||||
import ChannelIcon from '@components/channel_icon';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 20,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 24,
|
||||
lineHeight: 28,
|
||||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
},
|
||||
text: {
|
||||
marginTop: -1,
|
||||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
paddingLeft: 12,
|
||||
flex: 1,
|
||||
},
|
||||
highlight: {
|
||||
color: theme.sidebarText,
|
||||
},
|
||||
muted: {
|
||||
color: changeOpacity(theme.sidebarText, 0.4),
|
||||
},
|
||||
badge: {
|
||||
position: 'relative',
|
||||
left: 0,
|
||||
top: 0,
|
||||
alignSelf: undefined,
|
||||
},
|
||||
mutedBadge: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
activeItem: {
|
||||
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.1),
|
||||
borderLeftColor: theme.sidebarTextActiveBorder,
|
||||
borderLeftWidth: 5,
|
||||
marginLeft: 0,
|
||||
paddingLeft: 14,
|
||||
},
|
||||
textActive: {
|
||||
color: theme.sidebarText,
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
const textStyle = StyleSheet.create({
|
||||
bright: typography('Body', 200, 'SemiBold'),
|
||||
regular: typography('Body', 200, 'Regular'),
|
||||
});
|
||||
|
||||
type Props = {
|
||||
channel: ChannelModel;
|
||||
isActive: boolean;
|
||||
isMuted: boolean;
|
||||
myChannel?: MyChannelModel;
|
||||
collapsed: boolean;
|
||||
currentUserId: string;
|
||||
isVisible: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const ChannelListItem = ({channel, isActive, currentUserId, isMuted, isVisible, myChannel, collapsed, testID}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const isOwnDirectMessage = (channel.type === General.DM_CHANNEL) && currentUserId === getUserIdFromChannelName(currentUserId, channel.name);
|
||||
|
||||
// Make it brighter if it's not muted, and highlighted or has unreads
|
||||
const isUnread = !isMuted && (myChannel && (myChannel.isUnread || myChannel.mentionsCount > 0));
|
||||
|
||||
const shouldCollapse = (collapsed && !isUnread) && !isActive;
|
||||
const sharedValue = useSharedValue(shouldCollapse);
|
||||
|
||||
useEffect(() => {
|
||||
sharedValue.value = shouldCollapse;
|
||||
}, [shouldCollapse]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
marginVertical: withTiming(sharedValue.value ? 0 : 2, {duration: 500}),
|
||||
height: withTiming(sharedValue.value ? 0 : 40, {duration: 500}),
|
||||
opacity: withTiming(sharedValue.value ? 0 : 1, {duration: 500, easing: Easing.inOut(Easing.exp)}),
|
||||
};
|
||||
});
|
||||
|
||||
const switchChannels = useCallback(() => {
|
||||
if (myChannel) {
|
||||
switchToChannelById(serverUrl, myChannel.id);
|
||||
}
|
||||
}, [myChannel?.id, serverUrl]);
|
||||
|
||||
const membersCount = useMemo(() => {
|
||||
if (channel.type === General.GM_CHANNEL) {
|
||||
return channel.displayName.split(',').length;
|
||||
}
|
||||
return 0;
|
||||
}, [channel.type, channel.displayName]);
|
||||
|
||||
const textStyles = useMemo(() => [
|
||||
isUnread ? textStyle.bright : textStyle.regular,
|
||||
styles.text,
|
||||
isUnread && styles.highlight,
|
||||
isMuted && styles.muted,
|
||||
isActive ? styles.textActive : null,
|
||||
], [isUnread, styles, isMuted]);
|
||||
|
||||
let displayName = channel.displayName;
|
||||
if (isOwnDirectMessage) {
|
||||
displayName = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
|
||||
}
|
||||
|
||||
if ((channel.deleteAt > 0 && !isActive) || !myChannel || !isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View style={animatedStyle}>
|
||||
<TouchableOpacity onPress={switchChannels}>
|
||||
<View
|
||||
style={[styles.container, isActive && styles.activeItem]}
|
||||
testID={`${testID}.${channel.name}.collapsed.${collapsed && !isActive}`}
|
||||
>
|
||||
<ChannelIcon
|
||||
isActive={isActive}
|
||||
isArchived={channel.deleteAt > 0}
|
||||
membersCount={membersCount}
|
||||
name={channel.name}
|
||||
shared={channel.shared}
|
||||
size={24}
|
||||
type={channel.type}
|
||||
isMuted={isMuted}
|
||||
/>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={textStyles}
|
||||
testID={`${testID}.${channel.name}.display_name`}
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Badge
|
||||
visible={myChannel.mentionsCount > 0}
|
||||
value={myChannel.mentionsCount}
|
||||
style={[styles.badge, isMuted && styles.mutedBadge]}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelListItem;
|
||||
|
|
@ -5,6 +5,9 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react';
|
|||
import {useIntl} from 'react-intl';
|
||||
import {FlatList, StyleSheet} from 'react-native';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
||||
import CategoryBody from './body';
|
||||
import LoadCategoriesError from './error';
|
||||
import CategoryHeader from './header';
|
||||
|
|
@ -31,10 +34,20 @@ const extractKey = (item: CategoryModel | 'UNREADS') => (item === 'UNREADS' ? 'U
|
|||
const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
|
||||
const intl = useIntl();
|
||||
const listRef = useRef<FlatList>(null);
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const onChannelSwitch = useCallback(async (channelId: string) => {
|
||||
switchToChannelById(serverUrl, channelId);
|
||||
}, [serverUrl]);
|
||||
|
||||
const renderCategory = useCallback((data: {item: CategoryModel | 'UNREADS'}) => {
|
||||
if (data.item === 'UNREADS') {
|
||||
return <UnreadCategories currentTeamId={currentTeamId}/>;
|
||||
return (
|
||||
<UnreadCategories
|
||||
currentTeamId={currentTeamId}
|
||||
onChannelSwitch={onChannelSwitch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
|
|
@ -42,10 +55,11 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
|
|||
<CategoryBody
|
||||
category={data.item}
|
||||
locale={intl.locale}
|
||||
onChannelSwitch={onChannelSwitch}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, [intl.locale]);
|
||||
}, [currentTeamId, intl.locale, onChannelSwitch]);
|
||||
|
||||
useEffect(() => {
|
||||
listRef.current?.scrollToOffset({animated: false, offset: 0});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ describe('components/channel_list/categories/body', () => {
|
|||
const wrapper = renderWithEverything(
|
||||
<UnreadsCategory
|
||||
unreadChannels={[]}
|
||||
onChannelSwitch={() => undefined}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FlatList, Text} from 'react-native';
|
||||
|
||||
import ChannelItem from '@components/channel_item';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import ChannelItem from '../body/channel_item';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
|
|
@ -23,24 +22,27 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
const renderItem = ({item}: {item: ChannelModel}) => {
|
||||
return (
|
||||
<ChannelItem
|
||||
channel={item}
|
||||
collapsed={false}
|
||||
isUnreads={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
type UnreadCategoriesProps = {
|
||||
unreadChannels: ChannelModel[];
|
||||
onChannelSwitch: (channelId: string) => void;
|
||||
}
|
||||
|
||||
const UnreadCategories = ({unreadChannels}: UnreadCategoriesProps) => {
|
||||
const UnreadCategories = ({onChannelSwitch, unreadChannels}: UnreadCategoriesProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
|
||||
const renderItem = useCallback(({item}: {item: ChannelModel}) => {
|
||||
return (
|
||||
<ChannelItem
|
||||
channel={item}
|
||||
collapsed={false}
|
||||
isUnreads={true}
|
||||
onPress={onChannelSwitch}
|
||||
/>
|
||||
);
|
||||
}, [onChannelSwitch]);
|
||||
|
||||
if (!unreadChannels.length) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,23 @@
|
|||
|
||||
exports[`Search Field should match snapshot 1`] = `
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(255,255,255,0.12)",
|
||||
"borderRadius": 8,
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"justifyContent": "flex-start",
|
||||
"marginVertical": 20,
|
||||
"maxHeight": 40,
|
||||
"padding": 8,
|
||||
"width": "100%",
|
||||
}
|
||||
|
|
@ -28,34 +34,20 @@ exports[`Search Field should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Find Channels"
|
||||
placeholderTextColor="rgba(255,255,255,0.72)"
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"alignContent": "center",
|
||||
"alignItems": "center",
|
||||
"color": "#ffffff",
|
||||
"flex": 1,
|
||||
"height": 40,
|
||||
"marginLeft": 5,
|
||||
"marginTop": -2,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
"marginLeft": 5,
|
||||
"marginTop": 1,
|
||||
}
|
||||
}
|
||||
testID="channel_list.search_field.find_channels.input"
|
||||
/>
|
||||
>
|
||||
Find channels...
|
||||
</Text>
|
||||
</View>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {TextInput, StyleSheet, View} from 'react-native';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter, TouchableHighlight} from 'react-native';
|
||||
import {Options} from 'react-native-navigation';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
backgroundColor: changeOpacity(theme.sidebarText, 0.12),
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
marginVertical: 20,
|
||||
height: 40,
|
||||
maxHeight: 40,
|
||||
},
|
||||
icon: {
|
||||
width: 24,
|
||||
|
|
@ -29,40 +31,57 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
height: 40,
|
||||
color: theme.sidebarText,
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
marginLeft: 5,
|
||||
marginTop: -2,
|
||||
marginTop: 1,
|
||||
...typography('Body', 200),
|
||||
},
|
||||
}));
|
||||
|
||||
const textStyles = StyleSheet.create([
|
||||
typography('Body', 200),
|
||||
{textAlignVertical: 'center'},
|
||||
]);
|
||||
|
||||
const SearchField = () => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
const options: Options = {modal: {swipeToDismiss: false}};
|
||||
const closeButtonId = 'close-find-channels';
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
options.topBar = {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: closeButtonId,
|
||||
}],
|
||||
};
|
||||
|
||||
DeviceEventEmitter.emit(Events.PAUSE_KEYBOARD_TRACKING_VIEW, true);
|
||||
showModal(
|
||||
Screens.FIND_CHANNELS,
|
||||
intl.formatMessage({id: 'find_channels.title', defaultMessage: 'Find Channels'}),
|
||||
{closeButtonId},
|
||||
options,
|
||||
);
|
||||
}, [intl.locale, theme]);
|
||||
|
||||
return (
|
||||
<View
|
||||
<TouchableHighlight
|
||||
style={styles.container}
|
||||
onPress={onPress}
|
||||
underlayColor={changeOpacity(theme.sidebarText, 0.32)}
|
||||
>
|
||||
<CompassIcon
|
||||
name='magnify'
|
||||
style={styles.icon}
|
||||
/>
|
||||
<TextInput
|
||||
style={[textStyles, styles.input]}
|
||||
placeholder='Find Channels'
|
||||
placeholderTextColor={changeOpacity(theme.sidebarText, 0.72)}
|
||||
testID='channel_list.search_field.find_channels.input'
|
||||
/>
|
||||
</View>
|
||||
<>
|
||||
<CompassIcon
|
||||
name='magnify'
|
||||
style={styles.icon}
|
||||
/>
|
||||
<FormattedText
|
||||
defaultMessage='Find channels...'
|
||||
id='channel_list.find_channels'
|
||||
style={styles.input}
|
||||
/>
|
||||
</>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -101,9 +101,10 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
screen = withIntl(require('@screens/edit_server').default);
|
||||
break;
|
||||
case Screens.EMOJI_PICKER:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/emoji_picker').default,
|
||||
);
|
||||
screen = withServerDatabase(require('@screens/emoji_picker').default);
|
||||
break;
|
||||
case Screens.FIND_CHANNELS:
|
||||
screen = withServerDatabase(require('@screens/find_channels').default);
|
||||
break;
|
||||
case Screens.FORGOT_PASSWORD:
|
||||
screen = withIntl(require('@screens/forgot_password').default);
|
||||
|
|
|
|||
|
|
@ -135,7 +135,11 @@ export const getUserCustomStatus = (user?: UserModel | UserProfile): UserCustomS
|
|||
}
|
||||
};
|
||||
|
||||
export function isCustomStatusExpired(user: UserModel) {
|
||||
export function isCustomStatusExpired(user?: UserModel) {
|
||||
if (!user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const customStatus = getUserCustomStatus(user);
|
||||
|
||||
if (!customStatus) {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@
|
|||
"center_panel.archived.closeChannel": "Close Channel",
|
||||
"channel": "{count, plural, one {# member} other {# members}}",
|
||||
"channel_header.directchannel.you": "{displayName} (you)",
|
||||
"channel_list.find_channels": "Find channels...",
|
||||
"channel_loader.someone": "Someone",
|
||||
"channel_modal.descriptionHelp": "Describe how this channel should be used.",
|
||||
"channel_modal.header": "Header",
|
||||
|
|
@ -168,6 +169,10 @@
|
|||
"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}",
|
||||
"find_channels.directory": "Directory",
|
||||
"find_channels.new_channel": "New Channel",
|
||||
"find_channels.open_dm": "Open a DM",
|
||||
"find_channels.title": "Find Channels",
|
||||
"gallery.copy_link.failed": "Failed to copy link to clipboard",
|
||||
"gallery.downloading": "Downloading...",
|
||||
"gallery.footer.channel_name": "Shared in {channelName}",
|
||||
|
|
@ -240,6 +245,7 @@
|
|||
"mobile.camera_photo_permission_denied_title": "{applicationName} would like to access your camera",
|
||||
"mobile.channel_info.alertNo": "No",
|
||||
"mobile.channel_info.alertYes": "Yes",
|
||||
"mobile.channel_list.recent": "RECENT",
|
||||
"mobile.channel_list.unreads": "UNREADS",
|
||||
"mobile.commands.error_title": "Error Executing Command",
|
||||
"mobile.components.select_server_view.connect": "Connect",
|
||||
|
|
@ -520,7 +526,7 @@
|
|||
"threads.following": "Following",
|
||||
"threads.followMessage": "Follow Message",
|
||||
"threads.followThread": "Follow Thread",
|
||||
"threads.newReplies": "{count} new {count, plural, one {reply} other {replies}}",
|
||||
"threads.replies": "{count} {count, plural, one {reply} other {replies}}",
|
||||
"threads.unfollowMessage": "Unfollow Message",
|
||||
"threads.unfollowThread": "Unfollow Thread",
|
||||
"user.edit_profile.email.auth_service": "Login occurs through {service}. Email cannot be updated. Email address used for notifications is {email}.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue