Fix and unify channel and user list items (#7175)

* Fix channel and user list items

* Fixes on members and create a dm lists

* Fix tutorial and ipad

* Fix test

* Address feedback

* Several fixes on Android

* Fix tests

* Address feedback

* Add more non breaking strings

---------

Co-authored-by: Daniel Espino <danielespino@MacBook-Pro-de-Daniel.local>
This commit is contained in:
Daniel Espino García
2023-04-19 10:13:14 +02:00
committed by GitHub
parent 67f1a2f0c9
commit a8ee3a1b5a
75 changed files with 1682 additions and 2311 deletions

View File

@@ -20,7 +20,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
height: 40,
paddingVertical: 8,
paddingTop: 4,
paddingHorizontal: 16,
flexDirection: 'row',
alignItems: 'center',
},

View File

@@ -2,12 +2,8 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import UserItem from '@components/user_item';
import {useTheme} from '@context/theme';
import {changeOpacity} from '@utils/theme';
import type UserModel from '@typings/database/models/servers/user';
@@ -22,26 +18,16 @@ const AtMentionItem = ({
onPress,
testID,
}: AtMentionItemProps) => {
const insets = useSafeAreaInsets();
const theme = useTheme();
const completeMention = useCallback(() => {
onPress?.(user.username);
}, [user.username]);
const completeMention = useCallback((u: UserModel | UserProfile) => {
onPress?.(u.username);
}, []);
return (
<TouchableWithFeedback
key={user.id}
onPress={completeMention}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
style={{marginLeft: insets.left, marginRight: insets.right}}
type={'native'}
>
<UserItem
user={user}
testID={testID}
/>
</TouchableWithFeedback>
<UserItem
user={user}
testID={testID}
onUserPress={completeMention}
/>
);
};

View File

@@ -43,6 +43,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
listStyle: {
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
paddingHorizontal: 16,
},
};
});

View File

@@ -8,16 +8,15 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {typography} from '@utils/typography';
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
section: {
flexDirection: 'row',
paddingHorizontal: 16,
},
sectionText: {
fontSize: 12,
fontWeight: '600',
...typography('Body', 75, 'SemiBold'),
textTransform: 'uppercase',
color: changeOpacity(theme.centerChannelColor, 0.56),
paddingTop: 16,

View File

@@ -7,13 +7,16 @@ import {Platform, SectionList, SectionListData, SectionListRenderItemInfo, Style
import {searchChannels} from '@actions/remote/channel';
import AutocompleteSectionHeader from '@components/autocomplete/autocomplete_section_header';
import ChannelMentionItem from '@components/autocomplete/channel_mention_item';
import ChannelItem from '@components/channel_item';
import {General} from '@constants';
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
import {useServerUrl} from '@context/server';
import DatabaseManager from '@database/manager';
import useDidUpdate from '@hooks/did_update';
import {t} from '@i18n';
import {getUserById} from '@queries/servers/user';
import {hasTrailingSpaces} from '@utils/helpers';
import {getUserIdFromChannelName} from '@utils/user';
import type ChannelModel from '@typings/database/models/servers/channel';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
@@ -138,6 +141,7 @@ type Props = {
matchTerm: string;
localChannels: ChannelModel[];
teamId: string;
currentUserId: string;
}
const emptySections: Array<SectionListData<Channel>> = [];
@@ -155,6 +159,7 @@ const ChannelMention = ({
matchTerm,
localChannels,
teamId,
currentUserId,
}: Props) => {
const serverUrl = useServerUrl();
@@ -193,7 +198,22 @@ const ChannelMention = ({
setLoading(false);
};
const completeMention = useCallback((mention: string) => {
const completeMention = useCallback(async (c: ChannelModel | Channel) => {
let mention = c.name;
const teammateId = getUserIdFromChannelName(currentUserId, c.name);
if (c.type === General.DM_CHANNEL && teammateId) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const user = await getUserById(database, teammateId);
if (user) {
mention = `@${user.username}`;
}
} catch (err) {
// Do nothing
}
}
const mentionPart = value.substring(0, localCursorPosition);
let completedDraft: string;
@@ -231,14 +251,16 @@ const ChannelMention = ({
setSections(emptySections);
setRemoteChannels(emptyChannels);
latestSearchAt.current = Date.now();
}, [value, localCursorPosition, isSearch]);
}, [value, localCursorPosition, isSearch, currentUserId]);
const renderItem = useCallback(({item}: SectionListRenderItemInfo<Channel | ChannelModel>) => {
return (
<ChannelMentionItem
<ChannelItem
channel={item}
onPress={completeMention}
testID='autocomplete.channel_mention_item'
isOnCenterBg={true}
showChannelName={true}
/>
);
}, [completeMention]);

View File

@@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
import {observeChannel, queryAllMyChannel, queryChannelsForAutocomplete} from '@queries/servers/channel';
import {observeCurrentTeamId} from '@queries/servers/system';
import {observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
import ChannelMention from './channel_mention';
@@ -58,6 +58,7 @@ const emptyChannelList: ChannelModel[] = [];
const withMembers = withObservables([], ({database}: WithDatabaseArgs) => {
return {
myMembers: queryAllMyChannel(database).observe(),
currentUserId: observeCurrentUserId(database),
};
});

View File

@@ -1,153 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {Text, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import ChannelIcon from '@components/channel_icon';
import {BotTag, GuestTag} from '@components/tag';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {useTheme} from '@context/theme';
import {isDMorGM} from '@utils/channel';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import type ChannelModel from '@typings/database/models/servers/channel';
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
return {
icon: {
marginRight: 11,
opacity: 0.56,
},
row: {
paddingHorizontal: 16,
height: 40,
flexDirection: 'row',
alignItems: 'center',
},
rowDisplayName: {
flex: 1,
fontSize: 15,
color: theme.centerChannelColor,
},
rowName: {
fontSize: 15,
color: theme.centerChannelColor,
opacity: 0.56,
},
};
});
type Props = {
channel: Channel | ChannelModel;
displayName?: string;
isBot: boolean;
isGuest: boolean;
onPress: (name?: string) => void;
testID?: string;
};
const ChannelMentionItem = ({
channel,
displayName,
isBot,
isGuest,
onPress,
testID,
}: Props) => {
const insets = useSafeAreaInsets();
const theme = useTheme();
const completeMention = () => {
if (isDMorGM(channel)) {
onPress('@' + displayName?.replace(/ /g, ''));
} else {
onPress(channel.name);
}
};
const style = getStyleFromTheme(theme);
const margins = useMemo(() => {
return {marginLeft: insets.left, marginRight: insets.right};
}, [insets]);
const rowStyle = useMemo(() => {
return [style.row, margins];
}, [margins, style]);
let component;
const isArchived = ('delete_at' in channel ? channel.delete_at : channel.deleteAt) > 0;
const channelMentionItemTestId = `${testID}.${channel.name}`;
if (isDMorGM(channel)) {
if (!displayName) {
return null;
}
component = (
<TouchableWithFeedback
key={channel.id}
onPress={completeMention}
style={rowStyle}
testID={channelMentionItemTestId}
type={'opacity'}
>
<Text
style={style.rowDisplayName}
testID={`${channelMentionItemTestId}.display_name`}
>
{'@' + displayName}
</Text>
<BotTag
show={isBot}
testID={`${channelMentionItemTestId}.bot.tag`}
/>
<GuestTag
show={isGuest}
testID={`${channelMentionItemTestId}.guest.tag`}
/>
</TouchableWithFeedback>
);
} else {
component = (
<TouchableWithFeedback
key={channel.id}
onPress={completeMention}
style={margins}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
testID={channelMentionItemTestId}
type={'native'}
>
<View style={style.row}>
<ChannelIcon
name={channel.name}
shared={channel.shared}
type={channel.type}
isInfo={true}
isArchived={isArchived}
size={18}
style={style.icon}
/>
<Text
numberOfLines={1}
style={style.rowDisplayName}
testID={`${channelMentionItemTestId}.display_name`}
>
{displayName}
<Text
style={style.rowName}
testID={`${channelMentionItemTestId}.name`}
>
{` ~${channel.name}`}
</Text>
</Text>
</View>
</TouchableWithFeedback>
);
}
return component;
};
export default ChannelMentionItem;

View File

@@ -1,41 +0,0 @@
// 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 {General} from '@constants';
import {observeUser} from '@queries/servers/user';
import ChannelMentionItem from './channel_mention_item';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type UserModel from '@typings/database/models/servers/user';
type OwnProps = {
channel: Channel | ChannelModel;
}
const enhanced = withObservables([], ({database, channel}: WithDatabaseArgs & OwnProps) => {
let user = of$<UserModel | undefined>(undefined);
const teammateId = 'teammate_id' in channel ? channel.teammate_id : '';
const channelDisplayName = 'display_name' in channel ? channel.display_name : channel.displayName;
if (channel.type === General.DM_CHANNEL && teammateId) {
user = observeUser(database, teammateId!);
}
const isBot = user.pipe(switchMap((u) => of$(u ? u.isBot : false)));
const isGuest = user.pipe(switchMap((u) => of$(u ? u.isGuest : false)));
const displayName = user.pipe(switchMap((u) => of$(u ? u.username : channelDisplayName)));
return {
isBot,
isGuest,
displayName,
};
});
export default withDatabase(enhanced(ChannelMentionItem));

View File

@@ -54,7 +54,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
alignItems: 'center',
overflow: 'hidden',
paddingBottom: 8,
paddingHorizontal: 16,
height: 40,
},
};

View File

@@ -7,7 +7,7 @@ import {useIntl} from 'react-intl';
import {FlatList, Platform, StyleProp, ViewStyle} from 'react-native';
import AtMentionItem from '@components/autocomplete/at_mention_item';
import ChannelMentionItem from '@components/autocomplete/channel_mention_item';
import ChannelItem from '@components/channel_item';
import {COMMAND_SUGGESTION_CHANNEL, COMMAND_SUGGESTION_USER} from '@constants/apps';
import {useServerUrl} from '@context/server';
import analytics from '@managers/analytics';
@@ -98,7 +98,7 @@ const AppSlashSuggestion = ({
}
}, [serverUrl, updateValue]);
const completeIgnoringSuggestion = useCallback((base: string): (toIgnore: string) => void => {
const completeIgnoringSuggestion = useCallback((base: string): () => void => {
return () => {
completeSuggestion(base);
};
@@ -127,10 +127,12 @@ const AppSlashSuggestion = ({
}
return (
<ChannelMentionItem
<ChannelItem
channel={channel}
onPress={completeIgnoringSuggestion(item.Complete)}
testID='autocomplete.slash_suggestion.channel_mention_item'
isOnCenterBg={true}
showChannelName={true}
/>
);
}

View File

@@ -37,7 +37,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
flexDirection: 'row',
alignItems: 'center',
paddingBottom: 8,
paddingHorizontal: 16,
overflow: 'hidden',
},
slashIcon: {

View File

@@ -18,12 +18,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
row: {
height: 40,
paddingVertical: 8,
paddingHorizontal: 9,
flexDirection: 'row',
alignItems: 'center',
},
rowPicture: {
marginHorizontal: 8,
marginRight: 8,
width: 20,
alignItems: 'center',
justifyContent: 'center',

View File

@@ -143,7 +143,7 @@ function AutoCompleteSelector({
const goToSelectorScreen = useCallback(preventDoubleTap(() => {
const screen = Screens.INTEGRATION_SELECTOR;
goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect, teammateNameDisplay});
goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect});
}), [dataSource, options, getDynamicOptions]);
const handleSelect = useCallback((newSelection?: Selection) => {

View File

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import ProfilePicture from '@components/profile_picture';
@@ -10,54 +9,55 @@ import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import type UserModel from '@typings/database/models/servers/user';
import type {StyleProp, ViewStyle} from 'react-native';
type Props = {
author?: UserModel;
isInfo?: boolean;
isOnCenterBg?: boolean;
style: StyleProp<ViewStyle>;
size: number;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {marginLeft: 4},
status: {
backgroundColor: theme.sidebarBg,
borderWidth: 0,
},
statusInfo: {
statusOnCenterBg: {
backgroundColor: theme.centerChannelBg,
},
icon: {
color: changeOpacity(theme.sidebarText, 0.4),
left: 1,
},
iconInfo: {
iconOnCenterBg: {
color: changeOpacity(theme.centerChannelColor, 0.72),
},
}));
const DmAvatar = ({author, isInfo}: Props) => {
const DmAvatar = ({author, isOnCenterBg, style, size}: Props) => {
const theme = useTheme();
const style = getStyleSheet(theme);
const styles = getStyleSheet(theme);
if (author?.deleteAt) {
return (
<CompassIcon
name='archive-outline'
style={[style.icon, isInfo && style.iconInfo]}
style={[styles.icon, style, isOnCenterBg && styles.iconOnCenterBg]}
size={24}
/>
);
}
return (
<View style={style.container}>
<ProfilePicture
author={author}
size={24}
showStatus={true}
statusSize={12}
statusStyle={[style.status, isInfo && style.statusInfo]}
/>
</View>
<ProfilePicture
author={author}
size={size}
showStatus={true}
statusSize={12}
statusStyle={[styles.status, isOnCenterBg && styles.statusOnCenterBg]}
containerStyle={style}
/>
);
};

View File

@@ -16,7 +16,7 @@ type ChannelIconProps = {
hasDraft?: boolean;
isActive?: boolean;
isArchived?: boolean;
isInfo?: boolean;
isOnCenterBg?: boolean;
isUnread?: boolean;
isMuted?: boolean;
membersCount?: number;
@@ -43,10 +43,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
iconUnread: {
color: theme.sidebarUnreadText,
},
iconInfo: {
iconOnCenterBg: {
color: changeOpacity(theme.centerChannelColor, 0.72),
},
iconInfoUnread: {
iconUnreadOnCenterBg: {
color: theme.centerChannelColor,
},
groupBox: {
@@ -61,7 +61,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
groupBoxUnread: {
backgroundColor: changeOpacity(theme.sidebarUnreadText, 0.3),
},
groupBoxInfo: {
groupBoxOnCenterBg: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.3),
},
group: {
@@ -74,10 +74,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
groupUnread: {
color: theme.sidebarUnreadText,
},
groupInfo: {
groupOnCenterBg: {
color: changeOpacity(theme.centerChannelColor, 0.72),
},
groupInfoUnread: {
groupUnreadOnCenterBg: {
color: theme.centerChannelColor,
},
muted: {
@@ -88,7 +88,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const ChannelIcon = ({
hasDraft = false, isActive = false, isArchived = false,
isInfo = false, isUnread = false, isMuted = false,
isOnCenterBg = false, isUnread = false, isMuted = false,
membersCount = 0, name,
shared, size = 12, style, testID, type,
}: ChannelIconProps) => {
@@ -115,22 +115,38 @@ const ChannelIcon = ({
activeGroup = styles.groupActive;
}
if (isInfo) {
activeIcon = isUnread && !isMuted ? styles.iconInfoUnread : styles.iconInfo;
activeGroupBox = styles.groupBoxInfo;
activeGroup = isUnread ? styles.groupInfoUnread : styles.groupInfo;
if (isOnCenterBg) {
activeIcon = isUnread && !isMuted ? styles.iconUnreadOnCenterBg : styles.iconOnCenterBg;
activeGroupBox = styles.groupBoxOnCenterBg;
activeGroup = isUnread ? styles.groupUnreadOnCenterBg : styles.groupOnCenterBg;
}
if (isMuted) {
mutedStyle = styles.muted;
}
let icon;
const commonStyles = [
style,
mutedStyle,
];
const commonIconStyles = [
styles.icon,
unreadIcon,
activeIcon,
commonStyles,
{fontSize: size},
];
let icon = null;
if (isArchived) {
icon = (
<CompassIcon
name='archive-outline'
style={[styles.icon, unreadIcon, activeIcon, {fontSize: size, left: 1}]}
style={[
commonIconStyles,
{left: 1},
]}
testID={`${testID}.archive`}
/>
);
@@ -138,7 +154,10 @@ const ChannelIcon = ({
icon = (
<CompassIcon
name='pencil-outline'
style={[styles.icon, unreadIcon, activeIcon, {fontSize: size, left: 2}]}
style={[
commonIconStyles,
{left: 2},
]}
testID={`${testID}.draft`}
/>
);
@@ -148,7 +167,10 @@ const ChannelIcon = ({
icon = (
<CompassIcon
name={iconName}
style={[styles.icon, unreadIcon, activeIcon, {fontSize: size, left: 0.5}]}
style={[
commonIconStyles,
{left: 0.5},
]}
testID={sharedTestID}
/>
);
@@ -156,7 +178,10 @@ const ChannelIcon = ({
icon = (
<CompassIcon
name='globe'
style={[styles.icon, unreadIcon, activeIcon, {fontSize: size, left: 1}]}
style={[
commonIconStyles,
{left: 1},
]}
testID={`${testID}.public`}
/>
);
@@ -164,7 +189,10 @@ const ChannelIcon = ({
icon = (
<CompassIcon
name='lock-outline'
style={[styles.icon, unreadIcon, activeIcon, {fontSize: size, left: 0.5}]}
style={[
commonIconStyles,
{left: 0.5},
]}
testID={`${testID}.private`}
/>
);
@@ -172,7 +200,7 @@ const ChannelIcon = ({
const fontSize = size - 12;
icon = (
<View
style={[styles.groupBox, unreadGroupBox, activeGroupBox, {width: size, height: size}]}
style={[styles.groupBox, unreadGroupBox, activeGroupBox, commonStyles, {width: size, height: size}]}
>
<Text
style={[styles.group, unreadGroup, activeGroup, {fontSize}]}
@@ -186,16 +214,14 @@ const ChannelIcon = ({
icon = (
<DmAvatar
channelName={name}
isInfo={isInfo}
isOnCenterBg={isOnCenterBg}
style={commonStyles}
size={size}
/>
);
}
return (
<View style={[styles.container, {width: size, height: size}, style, mutedStyle]}>
{icon}
</View>
);
return icon;
};
export default React.memo(ChannelIcon);

View File

@@ -41,11 +41,10 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
{
"alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
false,
false,
{
"minHeight": 40,
},
@@ -53,79 +52,71 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
}
testID="channel_item.hello"
>
<Icon
name="globe"
style={
[
[
{
"color": "rgba(255,255,255,0.4)",
},
undefined,
undefined,
[
{
"marginRight": 12,
},
undefined,
],
{
"fontSize": 24,
},
],
{
"left": 1,
},
]
}
testID="undefined.public"
/>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
[
[
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
},
false,
null,
null,
false,
false,
],
{
"flex": 0,
"flexShrink": 1,
},
]
}
testID="channel_item.hello.display_name"
>
Hello!
</Text>
<View
style={
{
"flex": 1,
"flexDirection": "row",
}
}
>
<View
style={
[
{
"alignItems": "center",
"justifyContent": "center",
},
{
"height": 24,
"width": 24,
},
undefined,
undefined,
]
}
>
<Icon
name="globe"
style={
[
{
"color": "rgba(255,255,255,0.4)",
},
undefined,
undefined,
{
"fontSize": 24,
"left": 1,
},
]
}
testID="undefined.public"
/>
</View>
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
[
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
"paddingRight": 20,
},
false,
null,
null,
false,
false,
]
}
testID="channel_item.hello.display_name"
>
Hello!
</Text>
</View>
</View>
/>
</View>
</View>
`;
@@ -171,11 +162,10 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
{
"alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
false,
false,
{
"minHeight": 40,
},
@@ -183,103 +173,93 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
}
testID="channel_item.hello"
>
<Icon
name="pencil-outline"
style={
[
[
{
"color": "rgba(255,255,255,0.4)",
},
undefined,
undefined,
[
{
"marginRight": 12,
},
undefined,
],
{
"fontSize": 24,
},
],
{
"left": 2,
},
]
}
testID="undefined.draft"
/>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
[
[
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
},
false,
null,
null,
false,
false,
],
{
"flex": 0,
"flexShrink": 1,
},
]
}
testID="channel_item.hello.display_name"
>
Hello!
</Text>
<View
style={
{
"flex": 1,
"flexDirection": "row",
}
}
>
<View
style={
[
{
"alignItems": "center",
"justifyContent": "center",
},
{
"height": 24,
"width": 24,
},
undefined,
undefined,
]
}
>
<Icon
name="pencil-outline"
style={
[
{
"color": "rgba(255,255,255,0.4)",
},
undefined,
undefined,
{
"fontSize": 24,
"left": 2,
},
]
}
testID="undefined.draft"
/>
</View>
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
[
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
"paddingRight": 20,
},
false,
null,
null,
false,
false,
]
}
testID="channel_item.hello.display_name"
>
Hello!
</Text>
</View>
</View>
/>
<Icon
name="phone-in-talk"
size={16}
style={
[
[
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
},
false,
null,
null,
false,
false,
],
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
"paddingRight": 20,
},
false,
null,
null,
false,
false,
{
"paddingRight": 0,
"textAlign": "right",
},
]
@@ -330,11 +310,10 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
{
"alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
false,
false,
{
"minHeight": 40,
},
@@ -342,79 +321,71 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
}
testID="channel_item.hello"
>
<Icon
name="pencil-outline"
style={
[
[
{
"color": "rgba(255,255,255,0.4)",
},
undefined,
undefined,
[
{
"marginRight": 12,
},
undefined,
],
{
"fontSize": 24,
},
],
{
"left": 2,
},
]
}
testID="undefined.draft"
/>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
[
[
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
},
false,
null,
null,
false,
false,
],
{
"flex": 0,
"flexShrink": 1,
},
]
}
testID="channel_item.hello.display_name"
>
Hello!
</Text>
<View
style={
{
"flex": 1,
"flexDirection": "row",
}
}
>
<View
style={
[
{
"alignItems": "center",
"justifyContent": "center",
},
{
"height": 24,
"width": 24,
},
undefined,
undefined,
]
}
>
<Icon
name="pencil-outline"
style={
[
{
"color": "rgba(255,255,255,0.4)",
},
undefined,
undefined,
{
"fontSize": 24,
"left": 2,
},
]
}
testID="undefined.draft"
/>
</View>
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
[
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
"paddingRight": 20,
},
false,
null,
null,
false,
false,
]
}
testID="channel_item.hello.display_name"
>
Hello!
</Text>
</View>
</View>
/>
</View>
</View>
`;

View File

@@ -0,0 +1,125 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {StyleProp, Text, TextStyle, View} from 'react-native';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {nonBreakingString} from '@utils/strings';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import CustomStatus from './custom_status';
type Props = {
displayName: string;
teamDisplayName: string;
teammateId?: string;
isMuted: boolean;
textStyles: StyleProp<TextStyle>;
testId: string;
channelName: string;
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
teamName: {
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Body', 75),
},
teamNameMuted: {
color: changeOpacity(theme.centerChannelColor, 0.32),
},
flex: {
flex: 0,
flexShrink: 1,
},
channelName: {
...typography('Body', 200),
color: changeOpacity(theme.centerChannelColor, 0.64),
},
customStatus: {
marginLeft: 4,
},
};
});
export const ChannelBody = ({
displayName,
channelName,
teamDisplayName,
teammateId,
isMuted,
textStyles,
testId,
}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const isTablet = useIsTablet();
const nonBreakingDisplayName = nonBreakingString(displayName);
const channelText = (
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={[textStyles, styles.flex]}
testID={`${testId}.display_name`}
>
{nonBreakingDisplayName}
{Boolean(channelName) && (
<Text style={styles.channelName}>
{nonBreakingString(` ~${channelName}`)}
</Text>
)}
</Text>
);
if (teamDisplayName) {
const teamText = (
<Text
ellipsizeMode={isTablet ? undefined : 'tail'} // Handled by the parent text on tablets
numberOfLines={isTablet ? undefined : 1} // Handled by the parent text on tablets
style={[styles.teamName, isMuted && styles.teamNameMuted, styles.flex]}
testID={`${testId}.team_display_name`}
>
{nonBreakingString(`${isTablet ? ' ' : ''}${teamDisplayName}`)}
</Text>
);
if (isTablet) {
return (
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={[textStyles, styles.flex]}
testID={`${testId}.display_name`}
>
{nonBreakingDisplayName}
{teamText}
</Text>
);
}
return (
<View style={styles.flex}>
{channelText}
{teamText}
</View>
);
}
if (teammateId) {
const customStatus = (
<CustomStatus
userId={teammateId}
style={styles.customStatus}
/>
);
return (
<>
{channelText}
{customStatus}
</>
);
}
return channelText;
};

View File

@@ -41,7 +41,6 @@ describe('components/channel_list/categories/body/channel_item', () => {
onPress={() => undefined}
isUnread={myChannel.isUnread}
mentionsCount={myChannel.mentionsCount}
hasMember={Boolean(myChannel)}
hasCall={false}
/>,
);
@@ -62,7 +61,6 @@ describe('components/channel_list/categories/body/channel_item', () => {
onPress={() => undefined}
isUnread={myChannel.isUnread}
mentionsCount={myChannel.mentionsCount}
hasMember={Boolean(myChannel)}
hasCall={false}
/>,
);
@@ -83,7 +81,6 @@ describe('components/channel_list/categories/body/channel_item', () => {
onPress={() => undefined}
isUnread={myChannel.isUnread}
mentionsCount={myChannel.mentionsCount}
hasMember={Boolean(myChannel)}
hasCall={true}
/>,
);

View File

@@ -3,85 +3,79 @@
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import {StyleSheet, TouchableOpacity, View} from 'react-native';
import Badge from '@components/badge';
import ChannelIcon from '@components/channel_icon';
import CompassIcon from '@components/compass_icon';
import {General} from '@constants';
import {HOME_PADDING} from '@constants/view';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {isDMorGM} from '@utils/channel';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {getUserIdFromChannelName} from '@utils/user';
import CustomStatus from './custom_status';
import {ChannelBody} from './channel_body';
import type ChannelModel from '@typings/database/models/servers/channel';
type Props = {
channel: ChannelModel;
channel: ChannelModel | Channel;
currentUserId: string;
hasDraft: boolean;
isActive: boolean;
isInfo?: boolean;
isMuted: boolean;
membersCount: number;
isUnread: boolean;
mentionsCount: number;
onPress: (channelId: string) => void;
hasMember: boolean;
onPress: (channel: ChannelModel | Channel) => void;
teamDisplayName?: string;
testID?: string;
hasCall: boolean;
isOnCenterBg?: boolean;
showChannelName?: boolean;
isOnHome?: boolean;
}
export const ROW_HEIGHT = 40;
export const ROW_HEIGHT_WITH_TEAM = 58;
export 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),
marginRight: 12,
},
text: {
marginTop: -1,
color: changeOpacity(theme.sidebarText, 0.72),
paddingLeft: 12,
paddingRight: 20,
},
highlight: {
color: theme.sidebarUnreadText,
},
textInfo: {
textOnCenterBg: {
color: theme.centerChannelColor,
paddingRight: 20,
},
muted: {
color: changeOpacity(theme.sidebarText, 0.32),
},
mutedInfo: {
mutedOnCenterBg: {
color: changeOpacity(theme.centerChannelColor, 0.32),
},
badge: {
borderColor: theme.sidebarBg,
position: 'relative',
left: 0,
top: -2,
marginLeft: 4,
//Overwrite default badge styles
position: undefined,
top: undefined,
left: undefined,
alignSelf: undefined,
},
infoBadge: {
badgeOnCenterBg: {
color: theme.buttonColor,
backgroundColor: theme.buttonBg,
borderColor: theme.centerChannelBg,
@@ -93,31 +87,15 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
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),
},
teamNameMuted: {
color: changeOpacity(theme.centerChannelColor, 0.32),
},
teamNameTablet: {
marginLeft: -12,
paddingLeft: 0,
marginTop: 0,
paddingBottom: 0,
top: 5,
},
hasCall: {
textAlign: 'right',
paddingRight: 0,
},
filler: {
flex: 1,
},
}));
@@ -126,137 +104,118 @@ export const textStyle = StyleSheet.create({
regular: typography('Body', 200, 'Regular'),
});
const ChannelListItem = ({
channel, currentUserId, hasDraft,
isActive, isInfo, isMuted, membersCount, hasMember,
isUnread, mentionsCount, onPress, teamDisplayName, testID, hasCall}: Props) => {
const ChannelItem = ({
channel,
currentUserId,
hasDraft,
isActive,
isMuted,
membersCount,
isUnread,
mentionsCount,
onPress,
teamDisplayName = '',
testID,
hasCall,
isOnCenterBg = false,
showChannelName = false,
isOnHome = false,
}: Props) => {
const {formatMessage} = useIntl();
const theme = useTheme();
const isTablet = useIsTablet();
const styles = getStyleSheet(theme);
const channelName = (showChannelName && !isDMorGM(channel)) ? channel.name : '';
// Make it bolded if it has unreads or mentions
const isBolded = isUnread || mentionsCount > 0;
const showActive = isActive && isTablet;
const teammateId = (channel.type === General.DM_CHANNEL) ? getUserIdFromChannelName(currentUserId, channel.name) : undefined;
const isOwnDirectMessage = (channel.type === General.DM_CHANNEL) && currentUserId === teammateId;
let displayName = 'displayName' in channel ? channel.displayName : channel.display_name;
if (isOwnDirectMessage) {
displayName = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
}
const deleteAt = 'deleteAt' in channel ? channel.deleteAt : channel.delete_at;
const channelItemTestId = `${testID}.${channel.name}`;
const height = useMemo(() => {
let h = 40;
if (isInfo) {
h = (teamDisplayName && !isTablet) ? 58 : 44;
}
return h;
}, [teamDisplayName, isInfo, isTablet]);
return (teamDisplayName && !isTablet) ? ROW_HEIGHT_WITH_TEAM : ROW_HEIGHT;
}, [teamDisplayName, isTablet]);
const handleOnPress = useCallback(() => {
onPress(channel.id);
onPress(channel);
}, [channel.id]);
const textStyles = useMemo(() => [
isBolded && !isMuted ? textStyle.bold : textStyle.regular,
styles.text,
isBolded && styles.highlight,
isActive && isTablet && !isInfo ? styles.textActive : null,
isInfo ? styles.textInfo : null,
showActive ? styles.textActive : null,
isOnCenterBg ? styles.textOnCenterBg : null,
isMuted && styles.muted,
isMuted && isInfo && styles.mutedInfo,
], [isBolded, styles, isMuted, isActive, isInfo, isTablet]);
isMuted && isOnCenterBg && styles.mutedOnCenterBg,
], [isBolded, styles, isMuted, showActive, isOnCenterBg]);
const containerStyle = useMemo(() => [
styles.container,
isActive && isTablet && !isInfo && styles.activeItem,
isInfo && styles.infoItem,
isOnHome && HOME_PADDING,
showActive && styles.activeItem,
showActive && isOnHome && {
paddingLeft: HOME_PADDING.paddingLeft - styles.activeItem.borderLeftWidth,
},
{minHeight: height},
],
[height, isActive, isTablet, isInfo, styles]);
if (!hasMember) {
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});
}
const channelItemTestId = `${testID}.${channel.name}`;
], [height, showActive, styles, isOnHome]);
return (
<TouchableOpacity onPress={handleOnPress}>
<>
<View
style={containerStyle}
testID={channelItemTestId}
>
<View style={styles.wrapper}>
<ChannelIcon
hasDraft={hasDraft}
isActive={isInfo ? false : isTablet && isActive}
isInfo={isInfo}
isUnread={isBolded}
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={`${channelItemTestId}.display_name`}
>
{displayName}
</Text>
{isInfo && Boolean(teamDisplayName) && !isTablet &&
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={[styles.teamName, isMuted && styles.teamNameMuted]}
testID={`${channelItemTestId}.team_display_name`}
>
{teamDisplayName}
</Text>
}
</View>
{Boolean(teammateId) &&
<CustomStatus
isInfo={isInfo}
testID={channelItemTestId}
userId={teammateId!}
/>
}
{isInfo && Boolean(teamDisplayName) && isTablet &&
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={[styles.teamName, styles.teamNameTablet, isMuted && styles.teamNameMuted]}
testID={`${channelItemTestId}.team_display_name`}
>
{teamDisplayName}
</Text>
}
</View>
<Badge
visible={mentionsCount > 0}
value={mentionsCount}
style={[styles.badge, isMuted && styles.mutedBadge, isInfo && styles.infoBadge]}
/>
{hasCall &&
<CompassIcon
name='phone-in-talk'
size={16}
style={[...textStyles, styles.hasCall]}
/>
}
</View>
</>
<View
style={containerStyle}
testID={channelItemTestId}
>
<ChannelIcon
hasDraft={hasDraft}
isActive={isTablet && isActive}
isOnCenterBg={isOnCenterBg}
isUnread={isBolded}
isArchived={deleteAt > 0}
membersCount={membersCount}
name={channel.name}
shared={channel.shared}
size={24}
type={channel.type}
isMuted={isMuted}
style={styles.icon}
/>
<ChannelBody
displayName={displayName}
isMuted={isMuted}
teamDisplayName={teamDisplayName}
teammateId={teammateId}
testId={channelItemTestId}
textStyles={textStyles}
channelName={channelName}
/>
<View style={styles.filler}/>
<Badge
visible={mentionsCount > 0}
value={mentionsCount}
style={[styles.badge, isMuted && styles.mutedBadge, isOnCenterBg && styles.badgeOnCenterBg]}
/>
{hasCall &&
<CompassIcon
name='phone-in-talk'
size={16}
style={[textStyles, styles.hasCall]}
/>
}
</View>
</TouchableOpacity>
);
};
export default ChannelListItem;
export default ChannelItem;

View File

@@ -2,30 +2,20 @@
// See LICENSE.txt for license information.
import React from 'react';
import {StyleSheet} from 'react-native';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
import type {EmojiCommonStyle} from '@typings/components/emoji';
import type {StyleProp} from 'react-native';
type Props = {
customStatus?: UserCustomStatus;
customStatusExpired: boolean;
isCustomStatusEnabled: boolean;
isInfo?: boolean;
testID?: string;
style: StyleProp<EmojiCommonStyle>;
}
const style = StyleSheet.create({
customStatusEmoji: {
color: '#000',
marginHorizontal: -5,
top: 3,
},
info: {
marginHorizontal: -15,
},
});
const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled, isInfo, testID}: Props) => {
const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled, style}: Props) => {
const showCustomStatusEmoji = Boolean(isCustomStatusEnabled && customStatus?.emoji && !customStatusExpired);
if (!showCustomStatusEmoji) {
@@ -35,8 +25,7 @@ const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled,
return (
<CustomStatusEmoji
customStatus={customStatus!}
style={[style.customStatusEmoji, isInfo && style.info]}
testID={testID}
style={style}
/>
);
};

View File

@@ -21,66 +21,68 @@ import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
type EnhanceProps = WithDatabaseArgs & {
channel: ChannelModel;
channel: ChannelModel | Channel;
showTeamName?: boolean;
serverUrl?: string;
shouldHighlightActive?: boolean;
shouldHighlightState?: boolean;
}
const enhance = withObservables(['channel', 'showTeamName'], ({
const enhance = withObservables(['channel', 'showTeamName', 'shouldHighlightActive', 'shouldHighlightState'], ({
channel,
database,
showTeamName,
serverUrl,
showTeamName = false,
shouldHighlightActive = false,
shouldHighlightState = false,
}: 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)),
distinctUntilChanged(),
);
const hasDraft = shouldHighlightState ?
queryDraft(database, channel.id).observeWithColumns(['message', 'files']).pipe(
switchMap((draft) => of$(draft.length > 0)),
distinctUntilChanged(),
) : of$(false);
const isActive = observeCurrentChannelId(database).pipe(
switchMap((id) => of$(id ? id === channel.id : false)),
distinctUntilChanged(),
);
const isActive = shouldHighlightActive ?
observeCurrentChannelId(database).pipe(
switchMap((id) => of$(id ? id === channel.id : false)),
distinctUntilChanged(),
) : of$(false);
const isMuted = myChannel.pipe(
switchMap((mc) => {
if (!mc) {
return of$(false);
}
return observeIsMutedSetting(database, mc.id);
}),
);
const isMuted = shouldHighlightState ?
myChannel.pipe(
switchMap((mc) => {
if (!mc) {
return of$(false);
}
return observeIsMutedSetting(database, mc.id);
}),
) : of$(false);
let teamDisplayName = of$('');
if (channel.teamId && showTeamName) {
teamDisplayName = observeTeam(database, channel.teamId).pipe(
const teamId = 'teamId' in channel ? channel.teamId : channel.team_id;
const teamDisplayName = (teamId && showTeamName) ?
observeTeam(database, teamId).pipe(
switchMap((team) => of$(team?.displayName || '')),
distinctUntilChanged(),
);
}
) : of$('');
let membersCount = of$(0);
if (channel.type === General.GM_CHANNEL) {
membersCount = queryChannelMembers(database, channel.id).observeCount(false);
}
const membersCount = channel.type === General.GM_CHANNEL ?
queryChannelMembers(database, channel.id).observeCount(false) :
of$(0);
const isUnread = myChannel.pipe(
switchMap((mc) => of$(mc?.isUnread)),
distinctUntilChanged(),
);
const isUnread = shouldHighlightState ?
myChannel.pipe(
switchMap((mc) => of$(mc?.isUnread)),
distinctUntilChanged(),
) : of$(false);
const mentionsCount = myChannel.pipe(
switchMap((mc) => of$(mc?.mentionsCount)),
distinctUntilChanged(),
);
const hasMember = myChannel.pipe(
switchMap((mc) => of$(Boolean(mc))),
distinctUntilChanged(),
);
const mentionsCount = shouldHighlightState ?
myChannel.pipe(
switchMap((mc) => of$(mc?.mentionsCount)),
distinctUntilChanged(),
) : of$(0);
const hasCall = observeChannelsWithCalls(serverUrl || '').pipe(
switchMap((calls) => of$(Boolean(calls[channel.id]))),
@@ -88,7 +90,7 @@ const enhance = withObservables(['channel', 'showTeamName'], ({
);
return {
channel: channel.observe(),
channel: 'observe' in channel ? channel.observe() : of$(channel),
currentUserId,
hasDraft,
isActive,
@@ -97,7 +99,6 @@ const enhance = withObservables(['channel', 'showTeamName'], ({
isUnread,
mentionsCount,
teamDisplayName,
hasMember,
hasCall,
};
});

View File

@@ -1,41 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/custom_status/custom_status_emoji should match snapshot 1`] = `
<View
testID="test.custom_status.custom_status_emoji.calendar"
<Text
style={
[
undefined,
undefined,
{
"color": "#000",
"fontSize": 16,
},
]
}
>
<Text
style={
[
undefined,
{
"color": "#000",
"fontSize": 16,
},
]
}
>
📆
</Text>
</View>
📆
</Text>
`;
exports[`components/custom_status/custom_status_emoji should match snapshot with props 1`] = `
<View
testID="test.custom_status.custom_status_emoji.calendar"
<Text
style={
[
undefined,
undefined,
{
"color": "#000",
"fontSize": 34,
},
]
}
>
<Text
style={
[
undefined,
{
"color": "#000",
"fontSize": 34,
},
]
}
>
📆
</Text>
</View>
📆
</Text>
`;

View File

@@ -26,7 +26,6 @@ describe('components/custom_status/custom_status_emoji', () => {
const wrapper = renderWithEverything(
<CustomStatusEmoji
customStatus={customStatus}
testID='test'
/>,
{database},
);
@@ -38,7 +37,6 @@ describe('components/custom_status/custom_status_emoji', () => {
<CustomStatusEmoji
customStatus={customStatus}
emojiSize={34}
testID='test'
/>,
{database},
);

View File

@@ -2,29 +2,26 @@
// See LICENSE.txt for license information.
import React from 'react';
import {StyleProp, TextStyle, View} from 'react-native';
import Emoji from '@components/emoji';
import type {EmojiCommonStyle} from '@typings/components/emoji';
import type {StyleProp} from 'react-native';
interface ComponentProps {
customStatus: UserCustomStatus;
emojiSize?: number;
style?: StyleProp<TextStyle>;
testID?: string;
style?: StyleProp<EmojiCommonStyle>;
}
const CustomStatusEmoji = ({customStatus, emojiSize = 16, style, testID}: ComponentProps) => {
const CustomStatusEmoji = ({customStatus, emojiSize = 16, style}: ComponentProps) => {
if (customStatus.emoji) {
return (
<View
style={style}
testID={`${testID}.custom_status.custom_status_emoji.${customStatus.emoji}`}
>
<Emoji
size={emojiSize}
emojiName={customStatus.emoji}
/>
</View>
<Emoji
size={emojiSize}
emojiName={customStatus.emoji}
commonStyle={style}
/>
);
}

View File

@@ -29,12 +29,13 @@ const assetImages = new Map([['mattermost.png', require('@assets/images/emojis/m
const Emoji = (props: EmojiProps) => {
const {
customEmojis,
customEmojiStyle,
imageStyle,
displayTextOnly,
emojiName,
literal = '',
testID,
textStyle,
commonStyle,
} = props;
const serverUrl = useServerUrl();
let assetImage = '';
@@ -73,7 +74,7 @@ const Emoji = (props: EmojiProps) => {
if (displayTextOnly || (!imageUrl && !assetImage && !unicode)) {
return (
<Text
style={textStyle}
style={[commonStyle, textStyle]}
testID={testID}
>
{literal}
@@ -91,7 +92,7 @@ const Emoji = (props: EmojiProps) => {
return (
<Text
style={[textStyle, {fontSize: size, color: '#000'}]}
style={[commonStyle, textStyle, {fontSize: size, color: '#000'}]}
testID={testID}
>
{code}
@@ -110,7 +111,7 @@ const Emoji = (props: EmojiProps) => {
<FastImage
key={key}
source={image}
style={[customEmojiStyle, {width, height}]}
style={[commonStyle, imageStyle, {width, height}]}
resizeMode={FastImage.resizeMode.contain}
testID={testID}
/>
@@ -128,7 +129,7 @@ const Emoji = (props: EmojiProps) => {
return (
<FastImage
key={key}
style={[customEmojiStyle, {width, height}]}
style={[commonStyle, imageStyle, {width, height}]}
source={{uri: imageUrl}}
resizeMode={FastImage.resizeMode.contain}
testID={testID}

View File

@@ -121,7 +121,6 @@ const HeaderDisplayName = ({
<CustomStatusEmoji
customStatus={customStatus!}
style={[style.customStatusEmoji]}
testID='post_header'
/>
)}
</TouchableOpacity>

View File

@@ -5,8 +5,8 @@ import React, {useCallback, useMemo, useState} from 'react';
import {View, Text, StyleSheet} from 'react-native';
import {switchToChannelById} from '@actions/remote/channel';
import TouchableWithFeedback from '@app/components/touchable_with_feedback';
import {useServerUrl} from '@app/context/server';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useEffect, useMemo} from 'react';
import {Platform, StyleProp, View, ViewStyle} from 'react-native';
import {StyleProp, View, ViewStyle} from 'react-native';
import {fetchStatusInBatch} from '@actions/remote/user';
import {useServerUrl} from '@context/server';
@@ -15,11 +15,6 @@ import Status from './status';
import type UserModel from '@typings/database/models/servers/user';
import type {Source} from 'react-native-fast-image';
const STATUS_BUFFER = Platform.select({
ios: 3,
android: 2,
});
type ProfilePictureProps = {
author?: UserModel | UserProfile;
forwardRef?: React.RefObject<any>;
@@ -27,6 +22,7 @@ type ProfilePictureProps = {
showStatus?: boolean;
size: number;
statusSize?: number;
containerStyle?: StyleProp<ViewStyle>;
statusStyle?: StyleProp<ViewStyle>;
testID?: string;
source?: Source | string;
@@ -38,7 +34,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
container: {
justifyContent: 'center',
alignItems: 'center',
borderRadius: 80,
},
icon: {
color: changeOpacity(theme.centerChannelColor, 0.48),
@@ -67,6 +62,7 @@ const ProfilePicture = ({
showStatus = true,
size = 64,
statusSize = 14,
containerStyle,
statusStyle,
testID,
source,
@@ -77,7 +73,6 @@ const ProfilePicture = ({
serverUrl = url || serverUrl;
const style = getStyleSheet(theme);
const buffer = showStatus ? STATUS_BUFFER || 0 : 0;
const isBot = author && (('isBot' in author) ? author.isBot : author.is_bot);
useEffect(() => {
@@ -86,26 +81,14 @@ const ProfilePicture = ({
}
}, []);
const containerStyle = useMemo(() => {
if (author) {
return {
width: size + (buffer - 1),
height: size + (buffer - 1),
borderRadius: (size + buffer) / 2,
};
}
return {
...style.container,
width: size + buffer,
height: size + buffer,
borderRadius: (size + buffer) / 2,
};
}, [author, size]);
const viewStyle = useMemo(
() => [style.container, {width: size, height: size}, containerStyle],
[style, size, containerStyle],
);
return (
<View
style={containerStyle}
style={viewStyle}
testID={testID}
>
<Image

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {StyleProp, View, ViewStyle} from 'react-native';
import {Platform, StyleProp, View, ViewStyle} from 'react-native';
import UserStatus from '@components/user_status';
import {makeStyleSheetFromTheme} from '@utils/theme';
@@ -16,12 +16,17 @@ type Props = {
theme: Theme;
}
const STATUS_BUFFER = Platform.select({
ios: 3,
default: 2,
});
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
statusWrapper: {
position: 'absolute',
bottom: 0,
right: 0,
bottom: -STATUS_BUFFER,
right: -STATUS_BUFFER,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',

View File

@@ -2,11 +2,12 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {Text, TouchableOpacity, View} from 'react-native';
import {Text, TouchableOpacity, useWindowDimensions} from 'react-native';
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
import {nonBreakingString} from '@utils/strings';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@@ -35,11 +36,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
marginRight: 8,
paddingHorizontal: 7,
},
extraContent: {
flexDirection: 'row',
alignItems: 'center',
color: theme.centerChannelColor,
},
text: {
marginLeft: 8,
color: theme.centerChannelColor,
@@ -61,6 +57,7 @@ export default function SelectedChip({
}: SelectedChipProps) {
const theme = useTheme();
const style = getStyleFromTheme(theme);
const dimensions = useWindowDimensions();
const onPress = useCallback(() => {
onRemove(id);
@@ -73,16 +70,13 @@ export default function SelectedChip({
style={style.container}
testID={testID}
>
{extra && (
<View style={style.extraContent}>
{extra}
</View>
)}
{extra}
<Text
style={style.text}
style={[style.text, {maxWidth: dimensions.width * 0.70}]}
numberOfLines={1}
testID={`${testID}.display_name`}
>
{text}
{nonBreakingString(text)}
</Text>
<TouchableOpacity
style={style.remove}

View File

@@ -8,6 +8,8 @@ import ProfilePicture from '@components/profile_picture';
import SelectedChip from '@components/selected_chip';
import {displayUsername} from '@utils/user';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
/*
@@ -18,7 +20,7 @@ type Props = {
/*
* The user that this component represents.
*/
user: UserProfile;
user: UserProfile|UserModel;
/*
* A handler function that will deselect a user when clicked on.

View File

@@ -11,7 +11,6 @@ import {filterProfilesMatchingTerm} from '@utils/user';
type Props = {
currentUserId: string;
teammateNameDisplay: string;
tutorialWatched: boolean;
handleSelectProfile: (user: UserProfile) => void;
term: string;
@@ -24,7 +23,6 @@ type Props = {
export default function ServerUserList({
currentUserId,
teammateNameDisplay,
tutorialWatched,
handleSelectProfile,
term,
@@ -121,7 +119,6 @@ export default function ServerUserList({
profiles={data}
selectedIds={selectedIds}
showNoResults={!loading && page.current !== -1}
teammateNameDisplay={teammateNameDisplay}
fetchMore={getProfiles}
term={term}
testID={testID}

View File

@@ -25,9 +25,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
alignSelf: 'center',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
borderRadius: 4,
marginRight: 2,
marginBottom: 1,
marginLeft: 2,
paddingVertical: 2,
paddingHorizontal: 4,
},

View File

@@ -38,70 +38,62 @@ exports[`Thread item in the channel list Threads Component should match snapshot
>
<View
style={
{
"marginLeft": -18,
"marginRight": -20,
}
[
{
"alignItems": "center",
"flexDirection": "row",
},
false,
false,
false,
{
"minHeight": 40,
},
]
}
>
<View
<Icon
name="message-text-outline"
style={
[
{
"alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
"color": "rgba(255,255,255,0.5)",
"fontSize": 24,
"marginRight": 12,
},
false,
undefined,
]
}
/>
<Text
style={
[
{
"flex": 1,
},
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
},
false,
false,
undefined,
]
}
>
<Icon
name="message-text-outline"
style={
[
{
"color": "rgba(255,255,255,0.5)",
"fontSize": 24,
},
false,
undefined,
]
}
/>
<Text
style={
[
{
"flex": 1,
},
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
"paddingRight": 20,
},
false,
false,
undefined,
]
}
>
Threads
</Text>
</View>
Threads
</Text>
</View>
</View>
`;
exports[`Thread item in the channel list Threads Component should match snapshot with isInfo 1`] = `
exports[`Thread item in the channel list Threads Component should match snapshot with onCenterBg 1`] = `
<View
accessibilityState={
{
@@ -139,70 +131,61 @@ exports[`Thread item in the channel list Threads Component should match snapshot
>
<View
style={
{
"marginLeft": -18,
"marginRight": -20,
}
[
{
"alignItems": "center",
"flexDirection": "row",
},
false,
false,
false,
{
"minHeight": 40,
},
]
}
>
<View
<Icon
name="message-text-outline"
style={
[
{
"alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
"color": "rgba(255,255,255,0.5)",
"fontSize": 24,
"marginRight": 12,
},
false,
{
"color": "rgba(63,67,80,0.72)",
},
]
}
/>
<Text
style={
[
{
"flex": 1,
},
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
},
false,
false,
{
"color": "#3f4350",
},
]
}
>
<Icon
name="message-text-outline"
style={
[
{
"color": "rgba(255,255,255,0.5)",
"fontSize": 24,
},
false,
{
"color": "rgba(63,67,80,0.72)",
},
]
}
/>
<Text
style={
[
{
"flex": 1,
},
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
"paddingRight": 20,
},
false,
false,
{
"color": "#3f4350",
"paddingRight": 20,
},
]
}
>
Threads
</Text>
</View>
Threads
</Text>
</View>
</View>
`;
@@ -247,65 +230,57 @@ exports[`Thread item in the channel list Threads Component should match snapshot
>
<View
style={
{
"marginLeft": -18,
"marginRight": -20,
}
[
{
"alignItems": "center",
"flexDirection": "row",
},
false,
false,
false,
{
"minHeight": 40,
},
]
}
>
<View
<Icon
name="message-text-outline"
style={
[
{
"alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
"color": "rgba(255,255,255,0.5)",
"fontSize": 24,
"marginRight": 12,
},
false,
undefined,
]
}
/>
<Text
style={
[
{
"flex": 1,
},
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
},
false,
false,
undefined,
]
}
>
<Icon
name="message-text-outline"
style={
[
{
"color": "rgba(255,255,255,0.5)",
"fontSize": 24,
},
false,
undefined,
]
}
/>
<Text
style={
[
{
"flex": 1,
},
{
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
{
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
"paddingRight": 20,
},
false,
false,
undefined,
]
}
>
Threads
</Text>
</View>
Threads
</Text>
</View>
</View>
`;

View File

@@ -36,11 +36,11 @@ describe('Thread item in the channel list', () => {
expect(toJSON()).toMatchSnapshot();
});
test('Threads Component should match snapshot with isInfo', () => {
test('Threads Component should match snapshot with onCenterBg', () => {
const {toJSON} = renderWithIntlAndTheme(
<Threads
{...baseProps}
isInfo={true}
onCenterBg={true}
/>,
);

View File

@@ -8,10 +8,12 @@ import {switchToGlobalThreads} from '@actions/local/thread';
import Badge from '@components/badge';
import {
getStyleSheet as getChannelItemStyleSheet,
ROW_HEIGHT,
textStyle as channelItemTextStyle,
} from '@components/channel_item/channel_item';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {HOME_PADDING} from '@constants/view';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
@@ -19,13 +21,10 @@ import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
baseContainer: {
marginLeft: -18,
marginRight: -20,
},
icon: {
color: changeOpacity(theme.sidebarText, 0.5),
fontSize: 24,
marginRight: 12,
},
iconActive: {
color: theme.sidebarText,
@@ -41,16 +40,27 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
type Props = {
currentChannelId: string;
groupUnreadsSeparately: boolean;
isInfo?: boolean;
onCenterBg?: boolean;
onlyUnreads: boolean;
onPress?: () => void;
shouldHighlighActive?: boolean;
unreadsAndMentions: {
unreads: boolean;
mentions: number;
};
isOnHome?: boolean;
};
const ThreadsButton = ({currentChannelId, groupUnreadsSeparately, isInfo, onlyUnreads, onPress, unreadsAndMentions}: Props) => {
const ThreadsButton = ({
currentChannelId,
groupUnreadsSeparately,
onCenterBg,
onlyUnreads,
onPress,
unreadsAndMentions,
shouldHighlighActive = false,
isOnHome = false,
}: Props) => {
const isTablet = useIsTablet();
const serverUrl = useServerUrl();
@@ -67,18 +77,23 @@ const ThreadsButton = ({currentChannelId, groupUnreadsSeparately, isInfo, onlyUn
}), [onPress, serverUrl]);
const {unreads, mentions} = unreadsAndMentions;
const isActive = isTablet && !currentChannelId;
const isActive = isTablet && shouldHighlighActive && !currentChannelId;
const [containerStyle, iconStyle, textStyle, badgeStyle] = useMemo(() => {
const container = [
styles.container,
isOnHome && HOME_PADDING,
isActive && styles.activeItem,
isActive && isOnHome && {
paddingLeft: HOME_PADDING.paddingLeft - styles.activeItem.borderLeftWidth,
},
{minHeight: ROW_HEIGHT},
];
const icon = [
customStyles.icon,
(isActive || unreads) && customStyles.iconActive,
isInfo && customStyles.iconInfo,
onCenterBg && customStyles.iconInfo,
];
const text = [
@@ -87,16 +102,16 @@ const ThreadsButton = ({currentChannelId, groupUnreadsSeparately, isInfo, onlyUn
styles.text,
unreads && styles.highlight,
isActive && styles.textActive,
isInfo && styles.textInfo,
onCenterBg && styles.textOnCenterBg,
];
const badge = [
styles.badge,
isInfo && styles.infoBadge,
onCenterBg && styles.badgeOnCenterBg,
];
return [container, icon, text, badge];
}, [customStyles, isActive, isInfo, styles, unreads]);
}, [customStyles, isActive, onCenterBg, styles, unreads, isOnHome]);
if (groupUnreadsSeparately && (onlyUnreads && !isActive && !unreads && !mentions)) {
return null;
@@ -107,23 +122,21 @@ const ThreadsButton = ({currentChannelId, groupUnreadsSeparately, isInfo, onlyUn
onPress={handlePress}
testID='channel_list.threads.button'
>
<View style={customStyles.baseContainer}>
<View style={containerStyle}>
<CompassIcon
name='message-text-outline'
style={iconStyle}
/>
<FormattedText
id='threads'
defaultMessage='Threads'
style={textStyle}
/>
<Badge
value={mentions}
style={badgeStyle}
visible={mentions > 0}
/>
</View>
<View style={containerStyle}>
<CompassIcon
name='message-text-outline'
style={iconStyle}
/>
<FormattedText
id='threads'
defaultMessage='Threads'
style={textStyle}
/>
<Badge
value={mentions}
style={badgeStyle}
visible={mentions > 0}
/>
</View>
</TouchableOpacity>
);

View File

@@ -4,7 +4,7 @@
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
import React, {useCallback, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent, PanResponder, StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native';
import {Keyboard, ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent, PanResponder} from 'react-native';
import {FlatList} from 'react-native-gesture-handler';
import UserItem from '@components/user_item';
@@ -23,40 +23,30 @@ type Props = {
type ItemProps = {
channelId: string;
containerStyle: StyleProp<ViewStyle>;
location: string;
user: UserModel;
}
const style = StyleSheet.create({
container: {
paddingLeft: 0,
},
});
const Item = ({channelId, containerStyle, location, user}: ItemProps) => {
const Item = ({channelId, location, user}: ItemProps) => {
const intl = useIntl();
const theme = useTheme();
const openUserProfile = async () => {
if (user) {
await dismissBottomSheet(Screens.BOTTOM_SHEET);
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {closeButtonId, location, userId: user.id, channelId};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
}
};
const openUserProfile = useCallback(async (u: UserModel | UserProfile) => {
await dismissBottomSheet(Screens.BOTTOM_SHEET);
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {closeButtonId, location, userId: u.id, channelId};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
}, [location, channelId, theme, intl]);
return (
<TouchableOpacity onPress={openUserProfile}>
<UserItem
user={user}
containerStyle={containerStyle}
/>
</TouchableOpacity>
<UserItem
user={user}
onUserPress={openUserProfile}
/>
);
};
@@ -89,7 +79,6 @@ const UsersList = ({channelId, location, type = 'FlatList', users}: Props) => {
channelId={channelId}
location={location}
user={item}
containerStyle={style.container}
/>
), [channelId, location]);

View File

@@ -3,8 +3,11 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {observeConfigBooleanValue, observeCurrentUserId} from '@queries/servers/system';
import {observeCurrentUser, observeTeammateNameDisplay} from '@queries/servers/user';
import UserItem from './user_item';
@@ -12,12 +15,17 @@ import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const isCustomStatusEnabled = observeConfigBooleanValue(database, 'EnableCustomUserStatuses');
const showFullName = observeConfigBooleanValue(database, 'ShowFullName');
const currentUserId = observeCurrentUserId(database);
const locale = observeCurrentUser(database).pipe(
switchMap((u) => of$(u?.locale)),
distinctUntilChanged(),
);
const teammateNameDisplay = observeTeammateNameDisplay(database);
return {
isCustomStatusEnabled,
showFullName,
currentUserId,
locale,
teammateNameDisplay,
};
});

View File

@@ -1,104 +1,92 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {IntlShape, useIntl} from 'react-intl';
import {StyleProp, Text, View, ViewStyle} from 'react-native';
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import ChannelIcon from '@components/channel_icon';
import CompassIcon from '@components/compass_icon';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
import FormattedText from '@components/formatted_text';
import ProfilePicture from '@components/profile_picture';
import {BotTag, GuestTag} from '@components/tag';
import {General} from '@constants';
import {useTheme} from '@context/theme';
import {nonBreakingString} from '@utils/strings';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {typography} from '@utils/typography';
import {getUserCustomStatus, isBot, isCustomStatusExpired, isGuest, isShared} from '@utils/user';
import {displayUsername, getUserCustomStatus, isBot, isCustomStatusExpired, isGuest, isShared} from '@utils/user';
import type UserModel from '@typings/database/models/servers/user';
type AtMentionItemProps = {
user?: UserProfile | UserModel;
containerStyle?: StyleProp<ViewStyle>;
user: UserProfile | UserModel;
currentUserId: string;
showFullName: boolean;
testID?: string;
isCustomStatusEnabled: boolean;
pictureContainerStyle?: StyleProp<ViewStyle>;
showBadges?: boolean;
locale?: string;
teammateNameDisplay: string;
rightDecorator?: React.ReactNode;
onUserPress?: (user: UserProfile | UserModel) => void;
onUserLongPress?: (user: UserProfile | UserModel) => void;
disabled?: boolean;
viewRef?: React.LegacyRef<View>;
padding?: number;
}
const getName = (user: UserProfile | UserModel | undefined, showFullName: boolean, isCurrentUser: boolean, intl: IntlShape) => {
let name = '';
if (!user) {
return intl.formatMessage({id: 'channel_loader.someone', defaultMessage: 'Someone'});
}
const hasNickname = user.nickname.length > 0;
if (showFullName) {
const first = 'first_name' in user ? user.first_name : user.firstName;
const last = 'last_name' in user ? user.last_name : user.lastName;
name += `${first} ${last} `;
}
if (hasNickname && !isCurrentUser) {
name += name.length > 0 ? `(${user.nickname})` : user.nickname;
}
return name.trim();
};
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
const getThemedStyles = makeStyleSheetFromTheme((theme: Theme) => {
return {
row: {
height: 40,
paddingVertical: 8,
paddingTop: 4,
paddingHorizontal: 16,
flexDirection: 'row',
alignItems: 'center',
},
rowPicture: {
marginRight: 10,
marginLeft: 2,
width: 24,
alignItems: 'center',
justifyContent: 'center',
},
rowInfo: {
flexDirection: 'row',
overflow: 'hidden',
},
rowFullname: {
...typography('Body', 200),
color: theme.centerChannelColor,
paddingLeft: 4,
flex: 0,
flexShrink: 1,
},
rowUsername: {
...typography('Body', 200),
...typography('Body', 100),
color: changeOpacity(theme.centerChannelColor, 0.64),
fontSize: 15,
fontFamily: 'OpenSans',
},
icon: {
marginLeft: 4,
},
};
});
const nonThemedStyles = StyleSheet.create({
row: {
height: 40,
paddingVertical: 8,
paddingTop: 4,
flexDirection: 'row',
alignItems: 'center',
},
icon: {
marginLeft: 4,
},
profile: {
marginRight: 12,
},
tag: {
marginLeft: 6,
},
flex: {
flex: 1,
},
});
const UserItem = ({
containerStyle,
user,
currentUserId,
showFullName,
testID,
isCustomStatusEnabled,
pictureContainerStyle,
showBadges = false,
locale,
teammateNameDisplay,
rightDecorator,
onUserPress,
onUserLongPress,
disabled = false,
viewRef,
padding,
}: AtMentionItemProps) => {
const theme = useTheme();
const style = getStyleFromTheme(theme);
const style = getThemedStyles(theme);
const intl = useIntl();
const bot = user ? isBot(user) : false;
@@ -106,91 +94,109 @@ const UserItem = ({
const shared = user ? isShared(user) : false;
const isCurrentUser = currentUserId === user?.id;
const name = getName(user, showFullName, isCurrentUser, intl);
const customStatus = getUserCustomStatus(user);
const customStatusExpired = isCustomStatusExpired(user);
const userItemTestId = `${testID}.${user?.id}`;
const deleteAt = 'deleteAt' in user ? user.deleteAt : user.delete_at;
let rowUsernameFlexShrink = 1;
if (user) {
for (const rowInfoElem of [bot, guest, Boolean(name.length), isCurrentUser]) {
if (rowInfoElem) {
rowUsernameFlexShrink++;
}
}
let displayName = displayUsername(user, locale, teammateNameDisplay);
const showTeammateDisplay = displayName !== user?.username;
if (isCurrentUser) {
displayName = intl.formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
}
const usernameTextStyle = useMemo(() => {
return [style.rowUsername, {flexShrink: rowUsernameFlexShrink}];
}, [user, rowUsernameFlexShrink]);
const userItemTestId = `${testID}.${user?.id}`;
const containerStyle = useMemo(() => {
return [
nonThemedStyles.row,
{
opacity: disabled ? 0.32 : 1,
paddingHorizontal: padding || undefined,
},
];
}, [disabled, padding]);
const onPress = useCallback(() => {
onUserPress?.(user);
}, [user, onUserPress]);
const onLongPress = useCallback(() => {
onUserLongPress?.(user);
}, [user, onUserLongPress]);
return (
<View
style={[style.row, containerStyle]}
testID={userItemTestId}
<TouchableOpacity
onPress={onPress}
onLongPress={onLongPress}
disabled={!(onUserPress || onUserLongPress)}
>
<View style={[style.rowPicture, pictureContainerStyle]}>
<View
ref={viewRef}
style={containerStyle}
testID={userItemTestId}
>
<ProfilePicture
author={user}
size={24}
showStatus={false}
testID={`${userItemTestId}.profile_picture`}
containerStyle={nonThemedStyles.profile}
/>
</View>
<View
style={[style.rowInfo, {maxWidth: shared ? '75%' : '85%'}]}
>
{bot && <BotTag testID={`${userItemTestId}.bot.tag`}/>}
{guest && <GuestTag testID={`${userItemTestId}.guest.tag`}/>}
{Boolean(name.length) &&
<Text
style={style.rowFullname}
numberOfLines={1}
testID={`${userItemTestId}.display_name`}
>
{name}
</Text>
}
{isCurrentUser &&
<FormattedText
id='suggestion.mention.you'
defaultMessage=' (you)'
style={style.rowUsername}
testID={`${userItemTestId}.current_user_indicator`}
<Text
style={style.rowFullname}
numberOfLines={1}
testID={`${userItemTestId}.display_name`}
>
{nonBreakingString(displayName)}
{Boolean(showTeammateDisplay) && (
<Text
style={style.rowUsername}
testID={`${userItemTestId}.username`}
>
{nonBreakingString(` @${user!.username}`)}
</Text>
)}
{Boolean(deleteAt) && (
<Text
style={style.rowUsername}
testID={`${userItemTestId}.deactivated`}
>
{nonBreakingString(` ${intl.formatMessage({id: 'mobile.user_list.deactivated', defaultMessage: 'Deactivated'})}`)}
</Text>
)}
</Text>
{showBadges && bot && (
<BotTag
testID={`${userItemTestId}.bot.tag`}
style={nonThemedStyles.tag}
/>
}
{Boolean(user) && (
<Text
style={usernameTextStyle}
numberOfLines={1}
testID={`${userItemTestId}.username`}
>
{` @${user!.username}`}
</Text>
)}
{showBadges && guest && (
<GuestTag
testID={`${userItemTestId}.guest.tag`}
style={nonThemedStyles.tag}
/>
)}
{Boolean(isCustomStatusEnabled && !bot && customStatus?.emoji && !customStatusExpired) && (
<CustomStatusEmoji
customStatus={customStatus!}
style={nonThemedStyles.icon}
/>
)}
{shared && (
<CompassIcon
name={'circle-multiple-outline'}
size={16}
color={theme.centerChannelColor}
style={nonThemedStyles.icon}
/>
)}
<View style={nonThemedStyles.flex}/>
{Boolean(rightDecorator) && rightDecorator}
</View>
{Boolean(isCustomStatusEnabled && !bot && customStatus?.emoji && !customStatusExpired) && (
<CustomStatusEmoji
customStatus={customStatus!}
style={style.icon}
testID={userItemTestId}
/>
)}
{shared && (
<ChannelIcon
name={name}
isActive={false}
isArchived={false}
isInfo={true}
isUnread={false}
size={18}
shared={true}
type={General.DM_CHANNEL}
style={style.icon}
/>
)}
</View>
</TouchableOpacity>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -77,7 +77,6 @@ describe('components/channel_list_row', () => {
profiles={[user]}
testID='UserListRow'
currentUserId={'1'}
teammateNameDisplay={'johndoe'}
handleSelectProfile={() => {
// noop
}}
@@ -101,7 +100,6 @@ describe('components/channel_list_row', () => {
profiles={[user]}
testID='UserListRow'
currentUserId={'1'}
teammateNameDisplay={'johndoe'}
handleSelectProfile={() => {
// noop
}}
@@ -125,7 +123,6 @@ describe('components/channel_list_row', () => {
profiles={[user, user2]}
testID='UserListRow'
currentUserId={'1'}
teammateNameDisplay={'johndoe'}
handleSelectProfile={() => {
// noop
}}
@@ -149,7 +146,6 @@ describe('components/channel_list_row', () => {
profiles={[user]}
testID='UserListRow'
currentUserId={'1'}
teammateNameDisplay={'johndoe'}
handleSelectProfile={() => {
// noop
}}

View File

@@ -21,6 +21,8 @@ import {
} from '@utils/theme';
import {typography} from '@utils/typography';
import type UserModel from '@typings/database/models/servers/user';
type UserProfileWithChannelAdmin = UserProfile & {scheme_admin?: boolean}
type RenderItemType = ListRenderItemInfo<UserProfileWithChannelAdmin> & {section?: SectionListData<UserProfileWithChannelAdmin>}
@@ -147,8 +149,7 @@ type Props = {
profiles: UserProfile[];
channelMembers?: ChannelMembership[];
currentUserId: string;
teammateNameDisplay: string;
handleSelectProfile: (user: UserProfile) => void;
handleSelectProfile: (user: UserProfile | UserModel) => void;
fetchMore?: () => void;
loading: boolean;
manageMode?: boolean;
@@ -165,7 +166,6 @@ export default function UserList({
channelMembers,
selectedIds,
currentUserId,
teammateNameDisplay,
handleSelectProfile,
fetchMore,
loading,
@@ -198,21 +198,29 @@ export default function UserList({
return createProfilesSections(intl, profiles, channelMembers);
}, [channelMembers, loading, profiles, term]);
const openUserProfile = useCallback(async (profile: UserProfile) => {
const {user} = await storeProfile(serverUrl, profile);
if (user) {
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {
closeButtonId,
userId: user.id,
location: Screens.USER_PROFILE,
};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
const openUserProfile = useCallback(async (profile: UserProfile | UserModel) => {
let user: UserModel;
if ('create_at' in profile) {
const res = await storeProfile(serverUrl, profile);
if (!res.user) {
return;
}
user = res.user;
} else {
user = profile;
}
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeButtonId = 'close-user-profile';
const props = {
closeButtonId,
userId: user.id,
location: Screens.USER_PROFILE,
};
Keyboard.dismiss();
openAsBottomSheet({screen, title, theme, closeButtonId, props});
}, []);
const renderItem = useCallback(({item, index, section}: RenderItemType) => {
@@ -237,12 +245,11 @@ export default function UserList({
selected={selected}
showManageMode={showManageMode}
testID='create_direct_message.user_list.user_item'
teammateNameDisplay={teammateNameDisplay}
tutorialWatched={tutorialWatched}
user={item}
/>
);
}, [selectedIds, handleSelectProfile, showManageMode, manageMode, teammateNameDisplay, tutorialWatched]);
}, [selectedIds, handleSelectProfile, showManageMode, manageMode, tutorialWatched]);
const renderLoading = useCallback(() => {
if (!loading) {

View File

@@ -6,24 +6,22 @@ import {useIntl} from 'react-intl';
import {
InteractionManager,
Platform,
Text,
View,
} from 'react-native';
import {storeProfileLongPressTutorial} from '@actions/app/global';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import ProfilePicture from '@components/profile_picture';
import {BotTag, GuestTag} from '@components/tag';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import TutorialHighlight from '@components/tutorial_highlight';
import TutorialLongPress from '@components/tutorial_highlight/long_press';
import UserItem from '@components/user_item';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {t} from '@i18n';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername, isGuest} from '@utils/user';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
highlight?: boolean;
@@ -31,13 +29,12 @@ type Props = {
isMyUser: boolean;
isChannelAdmin: boolean;
manageMode: boolean;
onLongPress: (user: UserProfile) => void;
onPress?: (user: UserProfile) => void;
onLongPress: (user: UserProfile | UserModel) => void;
onPress?: (user: UserProfile | UserModel) => void;
selectable: boolean;
disabled?: boolean;
selected: boolean;
showManageMode: boolean;
teammateNameDisplay: string;
testID: string;
tutorialWatched?: boolean;
user: UserProfile;
@@ -45,54 +42,16 @@ type Props = {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
flexDirection: 'row',
paddingHorizontal: 20,
height: 58,
overflow: 'hidden',
},
profileContainer: {
flexDirection: 'row',
alignItems: 'center',
color: theme.centerChannelColor,
},
textContainer: {
paddingHorizontal: 10,
justifyContent: 'center',
flexDirection: 'column',
flex: 1,
},
username: {
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Body', 75, 'Regular'),
},
displayName: {
height: 24,
color: theme.centerChannelColor,
maxWidth: '80%',
...typography('Body', 200, 'Regular'),
},
indicatorContainer: {
flexDirection: 'row',
},
deactivated: {
marginTop: 2,
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.64),
},
sharedUserIcon: {
alignSelf: 'center',
opacity: 0.75,
},
selector: {
alignItems: 'center',
justifyContent: 'center',
marginLeft: 12,
},
selectorManage: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
marginLeft: 12,
},
manageText: {
color: changeOpacity(theme.centerChannelColor, 0.64),
@@ -107,7 +66,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
};
});
const DISABLED_OPACITY = 0.32;
const DEFAULT_ICON_OPACITY = 0.32;
function UserListRow({
@@ -122,7 +80,6 @@ function UserListRow({
disabled,
selected,
showManageMode = false,
teammateNameDisplay,
testID,
tutorialWatched = false,
user,
@@ -133,8 +90,7 @@ function UserListRow({
const [itemBounds, setItemBounds] = useState<TutorialItemBounds>({startX: 0, startY: 0, endX: 0, endY: 0});
const viewRef = useRef<View>(null);
const style = getStyleFromTheme(theme);
const {formatMessage, locale} = useIntl();
const {username} = user;
const {formatMessage} = useIntl();
const startTutorial = () => {
viewRef.current?.measureInWindow((x, y, w, h) => {
@@ -167,16 +123,12 @@ function UserListRow({
}
}, [highlight, tutorialWatched, isTablet]);
const handlePress = useCallback(() => {
const handlePress = useCallback((u: UserModel | UserProfile) => {
if (isMyUser && manageMode) {
return;
}
onPress?.(user);
}, [onPress, isMyUser, manageMode, user]);
const handleLongPress = useCallback(() => {
onLongPress?.(user);
}, [onLongPress, user]);
onPress?.(u);
}, [onPress, isMyUser, manageMode]);
const manageModeIcon = useMemo(() => {
if (!showManageMode || isMyUser) {
@@ -208,12 +160,11 @@ function UserListRow({
}, []);
const icon = useMemo(() => {
if (!selectable) {
if (!selectable && !selected) {
return null;
}
const iconOpacity = DEFAULT_ICON_OPACITY * (disabled ? DISABLED_OPACITY : 1);
const color = selected ? theme.buttonBg : changeOpacity(theme.centerChannelColor, iconOpacity);
const color = selected ? theme.buttonBg : changeOpacity(theme.centerChannelColor, DEFAULT_ICON_OPACITY);
return (
<View style={style.selector}>
<CompassIcon
@@ -225,85 +176,21 @@ function UserListRow({
);
}, [selectable, disabled, selected, theme]);
let usernameDisplay = `@${username}`;
if (isMyUser) {
usernameDisplay = formatMessage({
id: 'mobile.create_direct_message.you',
defaultMessage: '@{username} - you',
}, {username});
}
const teammateDisplay = displayUsername(user, locale, teammateNameDisplay);
const showTeammateDisplay = teammateDisplay !== username;
const userItemTestID = `${testID}.${id}`;
const opacity = selectable || selected || !disabled ? 1 : DISABLED_OPACITY;
return (
<>
<TouchableWithFeedback
onLongPress={handleLongPress}
onPress={handlePress}
underlayColor={changeOpacity(theme.centerChannelColor, 0.16)}
>
<View
ref={viewRef}
style={style.container}
testID={userItemTestID}
>
<View style={[style.profileContainer, {opacity}]}>
<ProfilePicture
author={user}
size={40}
iconSize={24}
testID={`${userItemTestID}.profile_picture`}
/>
</View>
<View style={[style.textContainer, {opacity}]}>
<View style={style.indicatorContainer}>
<Text
style={style.displayName}
ellipsizeMode='tail'
numberOfLines={1}
testID={`${userItemTestID}.display_name`}
>
{teammateDisplay}
</Text>
<BotTag
show={Boolean(user.is_bot)}
testID={`${userItemTestID}.bot.tag`}
/>
<GuestTag
show={isGuest(user.roles)}
testID={`${userItemTestID}.guest.tag`}
/>
</View>
{showTeammateDisplay &&
<View>
<Text
style={style.username}
ellipsizeMode='tail'
numberOfLines={1}
testID={`${userItemTestID}.team_display_name`}
>
{usernameDisplay}
</Text>
</View>
}
{user.delete_at > 0 &&
<View>
<Text
style={style.deactivated}
testID={`${userItemTestID}.deactivated`}
>
{formatMessage({id: 'mobile.user_list.deactivated', defaultMessage: 'Deactivated'})}
</Text>
</View>
}
</View>
{manageMode ? manageModeIcon : icon}
</View>
</TouchableWithFeedback>
<UserItem
user={user}
onUserLongPress={onLongPress}
onUserPress={handlePress}
showBadges={true}
testID={userItemTestID}
rightDecorator={manageMode ? manageModeIcon : icon}
disabled={!(selectable || selected || !disabled)}
viewRef={viewRef}
padding={20}
/>
{showTutorial &&
<TutorialHighlight
itemBounds={itemBounds}

View File

@@ -27,6 +27,11 @@ export const CALL_ERROR_BAR_HEIGHT = 62;
export const ANNOUNCEMENT_BAR_HEIGHT = 40;
export const HOME_PADDING = {
paddingLeft: 18,
paddingRight: 20,
};
export default {
BOTTOM_TAB_HEIGHT,
BOTTOM_TAB_ICON_SIZE,

View File

@@ -206,7 +206,6 @@ const ChannelHeader = ({
customStatus={customStatus}
emojiSize={13}
style={styles.customStatusEmoji}
testID='channel_header'
/>
}
<View style={styles.customStatusText}>

View File

@@ -273,7 +273,6 @@ export default function ChannelAddMembers({
currentUserId={currentUserId}
handleSelectProfile={handleSelectProfile}
selectedIds={selectedIds}
teammateNameDisplay={teammateNameDisplay}
term={term}
testID={`${TEST_ID}.user_list`}
tutorialWatched={tutorialWatched}

View File

@@ -5,10 +5,10 @@ import React from 'react';
import {useIntl} from 'react-intl';
import {Platform} from 'react-native';
import {getHeaderOptions} from '@app/screens/channel_add_members/channel_add_members';
import OptionItem from '@components/option_item';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {getHeaderOptions} from '@screens/channel_add_members/channel_add_members';
import {goToScreen} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';

View File

@@ -7,13 +7,13 @@ import {View} from 'react-native';
import Animated, {FlipOutXUp} from 'react-native-reanimated';
import {toggleMuteChannel} from '@actions/remote/channel';
import Button from '@app/components/button';
import CompassIcon from '@app/components/compass_icon';
import FormattedText from '@app/components/formatted_text';
import {useServerUrl} from '@app/context/server';
import {useTheme} from '@app/context/theme';
import {preventDoubleTap} from '@app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@app/utils/theme';
import Button from '@components/button';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type Props = {

View File

@@ -320,7 +320,6 @@ export default function CreateDirectMessage({
currentUserId={currentUserId}
handleSelectProfile={handleSelectProfile}
selectedIds={selectedIds}
teammateNameDisplay={teammateNameDisplay}
term={term}
testID='create_direct_message.user_list'
tutorialWatched={tutorialWatched}

View File

@@ -14,14 +14,12 @@ import ChannelItem from '@components/channel_item';
import Loading from '@components/loading';
import NoResultsWithTerm from '@components/no_results_with_term';
import ThreadsButton from '@components/threads_button';
import UserItem from '@components/user_item';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {sortChannelsByDisplayName} from '@utils/channel';
import {displayUsername} from '@utils/user';
import RemoteChannelItem from './remote_channel_item';
import UserItem from './user_item';
import type ChannelModel from '@typings/database/models/servers/channel';
import type UserModel from '@typings/database/models/servers/user';
@@ -144,8 +142,9 @@ const FilteredList = ({
onLoading(false);
};
const onJoinChannel = useCallback(async (channelId: string, displayName: string) => {
const {error} = await joinChannelIfNeeded(serverUrl, channelId);
const onJoinChannel = useCallback(async (c: Channel | ChannelModel) => {
const {error} = await joinChannelIfNeeded(serverUrl, c.id);
const displayName = 'display_name' in c ? c.display_name : c.displayName;
if (error) {
Alert.alert(
'',
@@ -158,11 +157,12 @@ const FilteredList = ({
}
await close();
switchToChannelById(serverUrl, channelId, undefined, true);
switchToChannelById(serverUrl, c.id, undefined, true);
}, [serverUrl, close, locale]);
const onOpenDirectMessage = useCallback(async (teammateId: string, displayName: string) => {
const {data, error} = await makeDirectChannel(serverUrl, teammateId, displayName, false);
const onOpenDirectMessage = useCallback(async (u: UserProfile | UserModel) => {
const displayName = displayUsername(u, locale, teammateDisplayNameSetting);
const {data, error} = await makeDirectChannel(serverUrl, u.id, displayName, false);
if (error || !data) {
Alert.alert(
'',
@@ -176,11 +176,11 @@ const FilteredList = ({
await close();
switchToChannelById(serverUrl, data.id);
}, [serverUrl, close, locale]);
}, [serverUrl, close, locale, teammateDisplayNameSetting]);
const onSwitchToChannel = useCallback(async (channelId: string) => {
const onSwitchToChannel = useCallback(async (c: Channel | ChannelModel) => {
await close();
switchToChannelById(serverUrl, channelId);
switchToChannelById(serverUrl, c.id);
}, [serverUrl, close]);
const onSwitchToThreads = useCallback(async () => {
@@ -214,7 +214,7 @@ const FilteredList = ({
if (item === 'thread') {
return (
<ThreadsButton
isInfo={true}
onCenterBg={true}
onPress={onSwitchToThreads}
/>
);
@@ -223,27 +223,30 @@ const FilteredList = ({
return (
<ChannelItem
channel={item}
isInfo={true}
isOnCenterBg={true}
onPress={onSwitchToChannel}
showTeamName={showTeamName}
shouldHighlightState={true}
testID='find_channels.filtered_list.channel_item'
/>
);
} else if ('username' in item) {
return (
<UserItem
onPress={onOpenDirectMessage}
onUserPress={onOpenDirectMessage}
user={item}
testID='find_channels.filtered_list.user_item'
showBadges={true}
/>
);
}
return (
<RemoteChannelItem
<ChannelItem
channel={item}
onPress={onJoinChannel}
showTeamName={showTeamName}
shouldHighlightState={true}
testID='find_channels.filtered_list.remote_channel_item'
/>
);

View File

@@ -1,34 +0,0 @@
// 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 React from 'react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeTeam} from '@queries/servers/team';
import RemoteChannelItem from './remote_channel_item';
import type {WithDatabaseArgs} from '@typings/database/database';
type EnhanceProps = WithDatabaseArgs & {
channel: Channel;
showTeamName?: boolean;
}
const enhance = withObservables(['channel', 'showTeamName'], ({channel, database, showTeamName}: EnhanceProps) => {
let teamDisplayName = of$('');
if (channel.team_id && showTeamName) {
teamDisplayName = observeTeam(database, channel.team_id).pipe(
switchMap((team) => of$(team?.displayName || '')),
);
}
return {
teamDisplayName,
};
});
export default React.memo(withDatabase(enhance(RemoteChannelItem)));

View File

@@ -1,123 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {Text, TouchableOpacity, View} from 'react-native';
import ChannelIcon from '@components/channel_icon';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type Props = {
onPress: (channelId: string, displayName: string) => void;
channel: Channel;
teamDisplayName?: string;
testID?: string;
}
export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
flexDirection: 'row',
paddingHorizontal: 0,
minHeight: 44,
alignItems: 'center',
marginVertical: 2,
},
wrapper: {
flex: 1,
flexDirection: 'row',
},
text: {
marginTop: -1,
color: theme.centerChannelColor,
paddingLeft: 12,
paddingRight: 20,
...typography('Body', 200, 'Regular'),
},
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 RemoteChannelItem = ({onPress, channel, teamDisplayName, testID}: Props) => {
const theme = useTheme();
const isTablet = useIsTablet();
const styles = getStyleSheet(theme);
const height = (teamDisplayName && !isTablet) ? 58 : 44;
const handleOnPress = useCallback(() => {
onPress(channel.id, channel.display_name);
}, [channel]);
const containerStyle = useMemo(() => [
styles.container,
{minHeight: height},
],
[height, styles]);
return (
<TouchableOpacity onPress={handleOnPress}>
<>
<View
style={containerStyle}
testID={`${testID}.${channel.name}`}
>
<View style={styles.wrapper}>
<ChannelIcon
isInfo={true}
isArchived={channel.delete_at > 0}
name={channel.name}
shared={channel.shared}
size={24}
type={channel.type}
/>
<View>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={styles.text}
testID={`${testID}.${channel.name}.display_name`}
>
{channel.display_name}
</Text>
{Boolean(teamDisplayName) && !isTablet &&
<Text
ellipsizeMode='tail'
numberOfLines={1}
testID={`${testID}.${channel.name}.team_display_name`}
style={styles.teamName}
>
{teamDisplayName}
</Text>
}
</View>
{Boolean(teamDisplayName) && isTablet &&
<Text
ellipsizeMode='tail'
numberOfLines={1}
testID={`${testID}.${channel.name}.team_display_name`}
style={[styles.teamName, styles.teamNameTablet]}
>
{teamDisplayName}
</Text>
}
</View>
</View>
</>
</TouchableOpacity>
);
};
export default RemoteChannelItem;

View File

@@ -1,26 +0,0 @@
// 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 React from 'react';
import {observeCurrentUserId} from '@queries/servers/system';
import {observeTeammateNameDisplay} from '@queries/servers/user';
import UserItem from './user_item';
import type {WithDatabaseArgs} from '@typings/database/database';
import type UserModel from '@typings/database/models/servers/user';
type EnhanceProps = WithDatabaseArgs & {
user: UserModel;
}
const enhance = withObservables(['user'], ({database, user}: EnhanceProps) => ({
currentUserId: observeCurrentUserId(database),
teammateDisplayNameSetting: observeTeammateNameDisplay(database),
user: user.observe(),
}));
export default React.memo(withDatabase(enhance(UserItem)));

View File

@@ -1,99 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Text, TouchableOpacity, View} from 'react-native';
import CustomStatus from '@components/channel_item/custom_status';
import ProfilePicture from '@components/profile_picture';
import {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername} from '@utils/user';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
currentUserId: string;
onPress: (channelId: string, displayName: string) => void;
teammateDisplayNameSetting?: string;
testID?: string;
user: UserModel;
}
export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
flexDirection: 'row',
paddingHorizontal: 0,
height: 44,
alignItems: 'center',
marginVertical: 2,
},
wrapper: {
flex: 1,
flexDirection: 'row',
},
text: {
marginTop: -1,
color: theme.centerChannelColor,
paddingLeft: 12,
paddingRight: 20,
...typography('Body', 200, 'Regular'),
},
avatar: {marginLeft: 4},
status: {
backgroundColor: theme.centerChannelBg,
borderWidth: 0,
},
}));
const UserItem = ({currentUserId, onPress, teammateDisplayNameSetting, testID, user}: Props) => {
const {formatMessage, locale} = useIntl();
const theme = useTheme();
const styles = getStyleSheet(theme);
const isOwnDirectMessage = currentUserId === user.id;
const displayName = displayUsername(user, locale, teammateDisplayNameSetting);
const userItemTestId = `${testID}.${user.id}`;
const handleOnPress = useCallback(() => {
onPress(user.id, displayName);
}, [user.id, displayName, onPress]);
return (
<TouchableOpacity onPress={handleOnPress}>
<>
<View
style={styles.container}
testID={userItemTestId}
>
<View style={styles.wrapper}>
<View style={styles.avatar}>
<ProfilePicture
author={user}
size={24}
showStatus={true}
statusSize={12}
statusStyle={styles.status}
testID={`${userItemTestId}.profile_picture`}
/>
</View>
<View>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={styles.text}
testID={`${userItemTestId}.display_name`}
>
{isOwnDirectMessage ? formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName}) : displayName}
</Text>
</View>
<CustomStatus userId={user.id}/>
</View>
</View>
</>
</TouchableOpacity>
);
};
export default UserItem;

View File

@@ -52,9 +52,9 @@ const UnfilteredList = ({close, keyboardHeight, recentChannels, showTeamName, te
const [sections, setSections] = useState(buildSections(recentChannels));
const sectionListStyle = useMemo(() => ({paddingBottom: keyboardHeight}), [keyboardHeight]);
const onPress = useCallback(async (channelId: string) => {
const onPress = useCallback(async (c: Channel | ChannelModel) => {
await close();
switchToChannelById(serverUrl, channelId);
switchToChannelById(serverUrl, c.id);
}, [serverUrl, close]);
const renderSectionHeader = useCallback(({section}: SectionListRenderItemInfo<ChannelModel>) => (
@@ -65,9 +65,10 @@ const UnfilteredList = ({close, keyboardHeight, recentChannels, showTeamName, te
return (
<ChannelItem
channel={item}
isInfo={true}
onPress={onPress}
isOnCenterBg={true}
showTeamName={showTeamName}
shouldHighlightState={true}
testID={`${testID}.channel_item`}
/>
);

View File

@@ -7,10 +7,10 @@ import {Text, TouchableHighlight, View} from 'react-native';
import {switchToChannelById} from '@actions/remote/channel';
import {fetchAndSwitchToThread} from '@actions/remote/thread';
import TouchableWithFeedback from '@app/components/touchable_with_feedback';
import FormattedText from '@components/formatted_text';
import FriendlyDate from '@components/friendly_date';
import RemoveMarkdown from '@components/remove_markdown';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';

View File

@@ -15,8 +15,6 @@ exports[`components/categories_list should render channels error 1`] = `
"backgroundColor": "#1e325c",
"flex": 1,
"maxWidth": 750,
"paddingLeft": 18,
"paddingRight": 20,
"paddingTop": 10,
}
}
@@ -33,6 +31,8 @@ exports[`components/categories_list should render channels error 1`] = `
style={
{
"marginLeft": 0,
"paddingLeft": 18,
"paddingRight": 20,
}
}
>
@@ -350,8 +350,6 @@ exports[`components/categories_list should render team error 1`] = `
"backgroundColor": "#1e325c",
"flex": 1,
"maxWidth": 750,
"paddingLeft": 18,
"paddingRight": 20,
"paddingTop": 10,
}
}
@@ -368,6 +366,8 @@ exports[`components/categories_list should render team error 1`] = `
style={
{
"marginLeft": 0,
"paddingLeft": 18,
"paddingRight": 20,
}
}
>
@@ -467,6 +467,8 @@ exports[`components/categories_list should render team error 1`] = `
style={
{
"flexDirection": "row",
"paddingLeft": 18,
"paddingRight": 20,
}
}
>

View File

@@ -7,6 +7,7 @@ import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 're
import {fetchDirectChannelsInfo} from '@actions/remote/channel';
import ChannelItem from '@components/channel_item';
import {ROW_HEIGHT as CHANNEL_ROW_HEIGHT} from '@components/channel_item/channel_item';
import {useServerUrl} from '@context/server';
import {isDMorGM} from '@utils/channel';
@@ -16,7 +17,7 @@ import type ChannelModel from '@typings/database/models/servers/channel';
type Props = {
sortedChannels: ChannelModel[];
category: CategoryModel;
onChannelSwitch: (channelId: string) => void;
onChannelSwitch: (channel: Channel | ChannelModel) => void;
unreadIds: Set<string>;
unreadsOnTop: boolean;
};
@@ -46,6 +47,9 @@ const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, onChan
onPress={onChannelSwitch}
key={item.id}
testID={`channel_list.category.${category.displayName.replace(/ /g, '_').toLocaleLowerCase()}.channel_item`}
shouldHighlightActive={true}
shouldHighlightState={true}
isOnHome={true}
/>
);
}, [onChannelSwitch]);
@@ -62,8 +66,8 @@ const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, onChan
}
}, [directChannels.length]);
const height = ids.length ? ids.length * 40 : 0;
const unreadHeight = unreadChannels.length ? unreadChannels.length * 40 : 0;
const height = ids.length ? ids.length * CHANNEL_ROW_HEIGHT : 0;
const unreadHeight = unreadChannels.length ? unreadChannels.length * CHANNEL_ROW_HEIGHT : 0;
const animatedStyle = useAnimatedStyle(() => {
const opacity = unreadHeight > 0 ? 1 : 0;
@@ -74,7 +78,7 @@ const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, onChan
};
}, [height, unreadHeight]);
const listHeight = useMemo(() => ({
const listStyle = useMemo(() => ({
height: category.collapsed ? unreadHeight : height,
}), [category.collapsed, height, unreadHeight]);
@@ -87,7 +91,7 @@ const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, onChan
// @ts-expect-error strictMode not exposed on the types
strictMode={true}
style={listHeight}
style={listStyle}
/>
</Animated.View>
);

View File

@@ -17,6 +17,7 @@ import CategoryHeader from './header';
import UnreadCategories from './unreads';
import type CategoryModel from '@typings/database/models/servers/category';
import type ChannelModel from '@typings/database/models/servers/channel';
type Props = {
categories: CategoryModel[];
@@ -27,8 +28,6 @@ type Props = {
const styles = StyleSheet.create({
mainList: {
flex: 1,
marginLeft: -18,
marginRight: -20,
},
loadingView: {
alignItems: 'center',
@@ -39,7 +38,11 @@ const styles = StyleSheet.create({
const extractKey = (item: CategoryModel | 'UNREADS') => (item === 'UNREADS' ? 'UNREADS' : item.id);
const Categories = ({categories, onlyUnreads, unreadsOnTop}: Props) => {
const Categories = ({
categories,
onlyUnreads,
unreadsOnTop,
}: Props) => {
const intl = useIntl();
const listRef = useRef<FlatList>(null);
const serverUrl = useServerUrl();
@@ -60,8 +63,8 @@ const Categories = ({categories, onlyUnreads, unreadsOnTop}: Props) => {
const [initiaLoad, setInitialLoad] = useState(!categoriesToShow.length);
const onChannelSwitch = useCallback(async (channelId: string) => {
switchToChannelById(serverUrl, channelId);
const onChannelSwitch = useCallback(async (c: Channel | ChannelModel) => {
switchToChannelById(serverUrl, c.id);
}, [serverUrl]);
const renderCategory = useCallback((data: {item: CategoryModel | 'UNREADS'}) => {

View File

@@ -1,11 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {FlatList, Text} from 'react-native';
import ChannelItem from '@components/channel_item';
import {HOME_PADDING} from '@constants/view';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -25,14 +26,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
color: changeOpacity(theme.sidebarText, 0.64),
...typography('Heading', 75),
textTransform: 'uppercase',
paddingLeft: 18,
paddingVertical: 8,
marginTop: 12,
...HOME_PADDING,
},
}));
type UnreadCategoriesProps = {
onChannelSwitch: (channelId: string) => void;
onChannelSwitch: (channel: Channel | ChannelModel) => void;
onlyUnreads: boolean;
unreadChannels: ChannelModel[];
unreadThreads: {unreads: boolean; mentions: number};
@@ -52,11 +53,20 @@ const UnreadCategories = ({onChannelSwitch, onlyUnreads, unreadChannels, unreadT
channel={item}
onPress={onChannelSwitch}
testID='channel_list.category.unreads.channel_item'
shouldHighlightActive={true}
shouldHighlightState={true}
isOnHome={true}
/>
);
}, [onChannelSwitch]);
const showEmptyState = onlyUnreads && !unreadChannels.length;
const containerStyle = useMemo(() => {
return [
showEmptyState && !isTablet && styles.empty,
];
}, [styles, showEmptyState, isTablet]);
const showTitle = !onlyUnreads || (onlyUnreads && !showEmptyState);
const EmptyState = showEmptyState && !isTablet ? (
<Empty onlyUnreads={onlyUnreads}/>
@@ -76,7 +86,7 @@ const UnreadCategories = ({onChannelSwitch, onlyUnreads, unreadChannels, unreadT
</Text>
}
<FlatList
contentContainerStyle={showEmptyState && !isTablet && styles.empty}
contentContainerStyle={containerStyle}
data={unreadChannels}
renderItem={renderItem}
keyExtractor={extractKey}

View File

@@ -13,6 +13,8 @@ exports[`components/channel_list/header Channel List Header Component should mat
style={
{
"marginLeft": 0,
"paddingLeft": 18,
"paddingRight": 20,
}
}
>

View File

@@ -12,6 +12,7 @@ import CompassIcon from '@components/compass_icon';
import {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
import {HOME_PADDING} from '@constants/view';
import {useServerDisplayName, useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
@@ -266,7 +267,7 @@ const ChannelListHeader = ({
}
return (
<Animated.View style={animatedStyle}>
<Animated.View style={[animatedStyle, HOME_PADDING]}>
{header}
</Animated.View>
);

View File

@@ -20,8 +20,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
flex: 1,
backgroundColor: theme.sidebarBg,
paddingLeft: 18,
paddingRight: 20,
paddingTop: 10,
},
}));
@@ -68,7 +66,12 @@ const CategoriesList = ({hasChannels, iconPad, isCRTEnabled, moreThanOneTeam}: C
return (
<>
<SubHeader/>
{isCRTEnabled && <ThreadsButton/>}
{isCRTEnabled &&
<ThreadsButton
isOnHome={true}
shouldHighlighActive={true}
/>
}
<Categories/>
</>
);
@@ -76,9 +79,7 @@ const CategoriesList = ({hasChannels, iconPad, isCRTEnabled, moreThanOneTeam}: C
return (
<Animated.View style={[styles.container, tabletStyle]}>
<ChannelListHeader
iconPad={iconPad}
/>
<ChannelListHeader iconPad={iconPad}/>
{content}
</Animated.View>
);

View File

@@ -5,6 +5,8 @@ import React, {useEffect} from 'react';
import {StyleSheet, View} from 'react-native';
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import {HOME_PADDING} from '@constants/view';
import SearchField from './search_field';
import UnreadFilter from './unread_filter';
@@ -15,6 +17,7 @@ type Props = {
const style = StyleSheet.create({
container: {
flexDirection: 'row',
...HOME_PADDING,
},
});

View File

@@ -118,7 +118,6 @@ export type Props = {
isMultiselect?: boolean;
selected: SelectedDialogValue;
theme: Theme;
teammateNameDisplay: string;
componentId: AvailableScreens;
}
@@ -165,7 +164,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
function IntegrationSelector(
{dataSource, data, isMultiselect = false, selected, handleSelect,
currentTeamId, currentUserId, componentId, getDynamicOptions, options, teammateNameDisplay}: Props) {
currentTeamId, currentUserId, componentId, getDynamicOptions, options}: Props) {
const serverUrl = useServerUrl();
const theme = useTheme();
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
@@ -554,7 +553,6 @@ function IntegrationSelector(
return (
<ServerUserList
currentUserId={currentUserId}
teammateNameDisplay={teammateNameDisplay}
term={term}
tutorialWatched={true}
handleSelectProfile={handleSelectProfile}

View File

@@ -24,6 +24,7 @@ import {isGuest} from '@utils/user';
import Selection from './selection';
import Summary from './summary';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens, NavButtons} from '@typings/screens/navigation';
import type {OptionsTopBarButton} from 'react-native-navigation';
@@ -69,7 +70,7 @@ const getStyleSheet = makeStyleSheetFromTheme(() => {
export type EmailInvite = string;
export type SearchResult = UserProfile|EmailInvite;
export type SearchResult = UserProfile|UserModel|EmailInvite;
export type InviteResult = {
userId: string;

View File

@@ -70,6 +70,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
},
searchListPadding: {
paddingVertical: 8,
flex: 1,
},
searchListShadow: {
shadowColor: '#000',
@@ -85,6 +86,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
searchListFlatList: {
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
paddingHorizontal: 16,
},
selectedItems: {
display: 'flex',
@@ -222,12 +224,12 @@ export default function Selection({
style.push(styles.searchListFlatList);
if (searchResults.length) {
if (searchResults.length || (term && !loading)) {
style.push(styles.searchListBorder, styles.searchListPadding);
}
return style;
}, [searchResults, styles]);
}, [searchResults, styles, Boolean(term && !loading)]);
const renderNoResults = useCallback(() => {
if (!term || loading) {
@@ -235,20 +237,18 @@ export default function Selection({
}
return (
<View style={[styles.searchListBorder, styles.searchListPadding]}>
<TextItem
text={term}
type={TextItemType.SEARCH_NO_RESULTS}
testID='invite.search_list_no_results'
/>
</View>
<TextItem
text={term}
type={TextItemType.SEARCH_NO_RESULTS}
testID='invite.search_list_no_results'
/>
);
}, [term, loading]);
const renderItem = useCallback(({item}: ListRenderItemInfo<SearchResult>) => {
const key = keyExtractor(item);
return (
return typeof item === 'string' ? (
<TouchableWithFeedback
key={key}
index={key}
@@ -257,19 +257,18 @@ export default function Selection({
type='native'
testID={`invite.search_list_item.${key}`}
>
{typeof item === 'string' ? (
<TextItem
text={item}
type={TextItemType.SEARCH_INVITE}
testID='invite.search_list_text_item'
/>
) : (
<UserItem
user={item}
testID='invite.search_list_user_item'
/>
)}
<TextItem
text={item}
type={TextItemType.SEARCH_INVITE}
testID='invite.search_list_text_item'
/>
</TouchableWithFeedback>
) : (
<UserItem
user={item}
testID='invite.search_list_user_item'
onUserPress={onSelectItem}
/>
);
}, [searchResults, onSelectItem]);

View File

@@ -46,11 +46,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
flexDirection: 'column',
paddingVertical: 12,
},
user: {
paddingTop: 0,
paddingBottom: 0,
height: 'auto',
},
reason: {
paddingLeft: 56,
paddingRight: 20,
@@ -135,7 +130,6 @@ export default function SummaryReport({
) : (
<UserItem
user={item}
containerStyle={styles.user}
testID={`${testID}.user_item`}
/>
)}

View File

@@ -13,7 +13,6 @@ import {typography} from '@utils/typography';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
item: {
paddingHorizontal: 20,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
@@ -21,7 +20,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
search: {
height: 40,
paddingVertical: 8,
paddingHorizontal: 16,
},
itemText: {
display: 'flex',

View File

@@ -10,7 +10,7 @@ import {observeTutorialWatched} from '@queries/app/global';
import {observeCurrentChannel} from '@queries/servers/channel';
import {observeCanManageChannelMembers, observePermissionForChannel} from '@queries/servers/role';
import {observeCurrentChannelId, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
import {observeCurrentUser, observeTeammateNameDisplay} from '@queries/servers/user';
import {observeCurrentUser} from '@queries/servers/user';
import ManageChannelMembers from './manage_channel_members';
@@ -31,7 +31,6 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
currentUserId: observeCurrentUserId(database),
currentTeamId: observeCurrentTeamId(database),
canManageAndRemoveMembers,
teammateNameDisplay: observeTeammateNameDisplay(database),
tutorialWatched: observeTutorialWatched(Tutorial.PROFILE_LONG_PRESS),
canChangeMemberRoles,
};

View File

@@ -30,7 +30,6 @@ type Props = {
componentId: AvailableScreens;
currentTeamId: string;
currentUserId: string;
teammateNameDisplay: string;
tutorialWatched: boolean;
}
@@ -69,7 +68,6 @@ export default function ManageChannelMembers({
componentId,
currentTeamId,
currentUserId,
teammateNameDisplay,
tutorialWatched,
}: Props) {
const serverUrl = useServerUrl();
@@ -271,7 +269,6 @@ export default function ManageChannelMembers({
selectedIds={EMPTY_IDS}
showManageMode={canManageAndRemoveMembers && isManageMode}
showNoResults={!loading}
teammateNameDisplay={teammateNameDisplay}
term={term}
testID='manage_members.user_list'
tutorialWatched={tutorialWatched}

View File

@@ -3,7 +3,7 @@
import React from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, StyleSheet, TouchableOpacity} from 'react-native';
import {Keyboard} from 'react-native';
import UserItem from '@components/user_item';
import {Screens} from '@constants';
@@ -15,20 +15,9 @@ import type UserModel from '@typings/database/models/servers/user';
type Props = {
channelId: string;
location: string;
user?: UserModel;
user: UserModel;
}
const style = StyleSheet.create({
container: {
marginBottom: 8,
paddingLeft: 0,
flexDirection: 'row',
},
picture: {
marginLeft: 0,
},
});
const Reactor = ({channelId, location, user}: Props) => {
const intl = useIntl();
const theme = useTheme();
@@ -46,14 +35,11 @@ const Reactor = ({channelId, location, user}: Props) => {
};
return (
<TouchableOpacity onPress={openUserProfile}>
<UserItem
containerStyle={style.container}
pictureContainerStyle={style.picture}
user={user}
testID='reactions.reactor_item'
/>
</TouchableOpacity>
<UserItem
user={user}
testID='reactions.reactor_item'
onUserPress={openUserProfile}
/>
);
};

View File

@@ -74,7 +74,6 @@ const UserProfileCustomStatus = ({customStatus}: Props) => {
<CustomStatusEmoji
customStatus={customStatus}
emojiSize={24}
testID={'user_profile.custom_status_emoji'}
style={styles.emoji}
/>
}

6
app/utils/strings.ts Normal file
View File

@@ -0,0 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export function nonBreakingString(s: string) {
return s.replace(' ', '\xa0');
}

View File

@@ -5,13 +5,18 @@ import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji
import type {StyleProp, TextStyle} from 'react-native';
import type {ImageStyle} from 'react-native-fast-image';
// The intersection of the image styles and text styles
type ImageStyleUniques = Omit<ImageStyle, keyof(TextStyle)>
export type EmojiCommonStyle = Omit<ImageStyle, keyof(ImageStyleUniques)>
export type EmojiProps = {
emojiName: string;
displayTextOnly?: boolean;
literal?: string;
size?: number;
textStyle?: StyleProp<TextStyle>;
customEmojiStyle?: StyleProp<ImageStyle>;
imageStyle?: StyleProp<ImageStyle>;
commonStyle?: StyleProp<EmojiCommonStyle>;
customEmojis: CustomEmojiModel[];
testID?: string;
}