Gekidou - Account Screen (#5708)

* Added DrawerItem component

* WIP Account Screen

* Added react-native-paper

* Added StatusLabel Component

* Extracted i18n

* TS fix DrawerItem component

* WIP Account Screen

* Added server name label under log out

* Updated translation

* WIP

* Fixes the Offline text style

* Added Metropolis fonts

* WIP

* Typo clean up

* WIP

* WIP

* WIP

* Added server display name

* Writing OpenSans properly

* WIP

* WIP

* Added OptionsModal

* Opening OptionsModal

* Added translation keys

* Writes status to local db

* Fix missing translation

* Fix OptionModal not dismissing

* Pushing status to server

* Refactored

* Added CustomStatusExpiry component

* Added sub components

* Added CustomLabel

* CustomStatus WIP

* Added Custom Status screen WIP

* WIP - unsetCustomStatus and CustomStatus constant

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* Retrieving RecentCustomStatuses from Preferences table

* WIP

* WIP

* WIP

* Added Clear After Modal

* WIP - Transations

* WIP

* Done with showing modal cst

* wip

* Clear After Modal - DONE

* fix

* Added missing API calls

* wip

* Causing screen refresh

* wip

* WIP

* WIP

* WIP

* Code clean up

* Added OOO alert box

* Refactored Options-Item

* Refactored OptionsModalList component

* Opening 'status' in BottomSheet instead of OptionsModal

* AddReaction screen - WIP

* Add Reaction screen - WIP

* Added EmojiPickerRow

* Added @components/emoji_picker - WIP

* Emoji Picker - WIP

* WIP

* WIP

* WIP

* SectionList - WIP

* Installed react-native-section_list_get_item_layout

* Adding API calls - WIP

* WIP

* Search Bar component - WIP

* WIP

* WIP

* WIP

* Rendering Emoticons now - have to tackle some fixmes

* Code clean up

* Code clean up - WIP

* Code clean up

* WIP

* Major clean up

* wip

* WIP

* Fix rendering issue with SectionIcons and SearchBar

* Tackled the CustomEmojiPage

* Code clean up

* WIP

* Done with loading User Profiles for Custom Emoji

* Code clean up

* Code Clean up

* Fix screen Account

* Added missing sql file for IOS Pod

* Updated Podfile.lock

* Using queryConfig instead of queryCommonSystemValues

* Fix - Custom status

* Fix - Custom Status - Error

* Fix - Clear Pass Status - WIP

* Fix - Custom Status Clear

* Need to fix CST clear

* WIP

* Status clear - working

* Using catchError operator

* remove unnecessary prop

* Status  BottomSheet now has colored indicators

* Added KeyboardTrackingView from 'react-native-keyboard-tracking-view'

* Code clean up

* WIP

* code clean up

* Added a safety check

* Fix - Display suggestions

* Code clean up based on PR Review

* Code clean up

* Code clean up

* Code clean up

* Corrections

* Fix tsc

* TS fix

* Removed unnecessary prop

* Fix SearchBar Ts

* Updated tests

* Delete search_bar.test.js.snap

* Merge branch 'gekidou' into gekidou_account_screen

* Revert "Merge branch 'gekidou' into gekidou_account_screen"

This reverts commit 5defc31321.

* Fix fonts

* Refactor home account screen

* fix theme provider

* refactor bottom sheet

* remove paper provider

* update drawer item snapshots

* Remove options modal screen

* remove react-native-ui-lib dependency

* Refactor & fix custom status & navigation (including tablet)

* Refactor emoji picker

Co-authored-by: Avinash Lingaloo <>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Avinash Lingaloo
2021-10-12 19:24:24 +04:00
committed by GitHub
parent 0807a20946
commit 7f91a6a78a
101 changed files with 5818 additions and 315 deletions

View File

@@ -4,8 +4,18 @@ exports[`components/custom_status/custom_status_emoji should match snapshot 1`]
<Text
testID="custom_status_emoji.calendar"
>
<Text>
<Text
style={
Array [
undefined,
Object {
"color": "#000",
"fontSize": 16,
},
]
}
>
📆
</Text>
</Text>
`;
@@ -14,8 +24,18 @@ exports[`components/custom_status/custom_status_emoji should match snapshot with
<Text
testID="custom_status_emoji.calendar"
>
<Text>
<Text
style={
Array [
undefined,
Object {
"color": "#000",
"fontSize": 34,
},
]
}
>
📆
</Text>
</Text>
`;

View File

@@ -5,6 +5,7 @@ import Database from '@nozbe/watermelondb/Database';
import React from 'react';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
import {CustomStatusDuration} from '@constants';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';

View File

@@ -13,7 +13,7 @@ interface ComponentProps {
testID?: string;
}
const CustomStatusEmoji = ({customStatus, emojiSize, style, testID}: ComponentProps) => {
const CustomStatusEmoji = ({customStatus, emojiSize = 16, style, testID}: ComponentProps) => {
const testIdPrefix = testID ? `${testID}.` : '';
return (
<Text
@@ -28,8 +28,4 @@ const CustomStatusEmoji = ({customStatus, emojiSize, style, testID}: ComponentPr
);
};
CustomStatusEmoji.defaultProps = {
emojiSize: 16,
};
export default CustomStatusEmoji;

View File

@@ -0,0 +1,141 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Q} from '@nozbe/watermelondb';
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import moment, {Moment} from 'moment-timezone';
import React from 'react';
import {Text, TextStyle} from 'react-native';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {getUserTimezone} from '@actions/local/timezone';
import FormattedDate from '@components/formatted_date';
import FormattedText from '@components/formatted_text';
import FormattedTime from '@components/formatted_time';
import {Preferences} from '@constants';
import {MM_TABLES} from '@constants/database';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {getCurrentMomentForTimezone} from '@utils/helpers';
import {makeStyleSheetFromTheme} from '@utils/theme';
import type {WithDatabaseArgs} from '@typings/database/database';
import type PreferenceModel from '@typings/database/models/servers/preference';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
currentUser: UserModel;
isMilitaryTime: boolean;
showPrefix?: boolean;
showTimeCompulsory?: boolean;
showToday?: boolean;
testID?: string;
textStyles?: TextStyle;
theme: Theme;
time: Date | Moment;
withinBrackets?: boolean;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
text: {
fontSize: 15,
color: theme.centerChannelColor,
},
};
});
const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCompulsory, showToday, testID = '', textStyles = {}, theme, time, withinBrackets}: Props) => {
const userTimezone = getUserTimezone(currentUser);
const timezone = userTimezone.useAutomaticTimezone ? userTimezone.automaticTimezone : userTimezone.manualTimezone;
const styles = getStyleSheet(theme);
const currentMomentTime = getCurrentMomentForTimezone(timezone);
const expiryMomentTime = timezone ? moment(time).tz(timezone) : moment(time);
const plusSixDaysEndTime = currentMomentTime.clone().add(6, 'days').endOf('day');
const tomorrowEndTime = currentMomentTime.clone().add(1, 'day').endOf('day');
const todayEndTime = currentMomentTime.clone().endOf('day');
const isCurrentYear = currentMomentTime.get('y') === expiryMomentTime.get('y');
let dateComponent;
if ((showToday && expiryMomentTime.isBefore(todayEndTime)) || expiryMomentTime.isSame(todayEndTime)) {
dateComponent = (
<FormattedText
id='custom_status.expiry_time.today'
defaultMessage='Today'
/>
);
} else if (expiryMomentTime.isAfter(todayEndTime) && expiryMomentTime.isSameOrBefore(tomorrowEndTime)) {
dateComponent = (
<FormattedText
id='custom_status.expiry_time.tomorrow'
defaultMessage='Tomorrow'
/>
);
} else if (expiryMomentTime.isAfter(tomorrowEndTime)) {
let format = 'dddd';
if (expiryMomentTime.isAfter(plusSixDaysEndTime) && isCurrentYear) {
format = 'MMM DD';
} else if (!isCurrentYear) {
format = 'MMM DD, YYYY';
}
dateComponent = (
<FormattedDate
format={format}
timezone={timezone}
value={expiryMomentTime.toDate()}
/>
);
}
const useTime = showTimeCompulsory || !(expiryMomentTime.isSame(todayEndTime) || expiryMomentTime.isAfter(tomorrowEndTime));
return (
<Text
testID={testID}
style={[styles.text, textStyles]}
>
{withinBrackets && '('}
{showPrefix && (
<FormattedText
id='custom_status.expiry.until'
defaultMessage='Until'
/>
)}
{showPrefix && ' '}
{dateComponent}
{useTime && dateComponent && (
<>
{' '}
<FormattedText
id='custom_status.expiry.at'
defaultMessage='at'
/>
{' '}
</>
)}
{useTime && (
<FormattedTime
isMilitaryTime={isMilitaryTime}
timezone={timezone || ''}
value={expiryMomentTime.toDate()}
/>
)}
{withinBrackets && ')'}
</Text>
);
};
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
isMilitaryTime: database.get<PreferenceModel>(MM_TABLES.SERVER.PREFERENCE).
query(
Q.where('category', Preferences.CATEGORY_DISPLAY_SETTINGS),
).observe().pipe(
switchMap(
(preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
),
),
}));
export default withDatabase(enhanced(CustomStatusExpiry));

View File

@@ -4,11 +4,10 @@
import React from 'react';
import {Text, TextStyle} from 'react-native';
import FormattedText from '@components/formatted_text';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
interface ComponentProps {
text: string | typeof FormattedText;
text: string | React.ReactNode;
theme: Theme;
textStyle?: TextStyle;
ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';

View File

@@ -0,0 +1,220 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DrawerItem should match snapshot 1`] = `
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
testID="test-id"
>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
>
<View
style={
Object {
"backgroundColor": "#ffffff",
"flexDirection": "row",
"minHeight": 50,
}
}
>
<View
style={
Object {
"alignItems": "center",
"height": 50,
"justifyContent": "center",
"marginLeft": 5,
"width": 45,
}
}
>
<Icon
name="icon-name"
style={
Array [
Object {
"color": "rgba(63,67,80,0.64)",
"fontSize": 24,
},
Object {
"color": "#d24b4e",
},
]
}
/>
</View>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"flex": 1,
"justifyContent": "center",
"paddingBottom": 14,
"paddingTop": 14,
}
}
>
<Text
style={
Array [
Object {
"color": "rgba(63,67,80,0.5)",
"fontSize": 17,
"includeFontPadding": false,
"textAlignVertical": "center",
},
Object {
"color": "#d24b4e",
},
Object {
"textAlign": "center",
"textAlignVertical": "center",
},
]
}
>
default message
</Text>
</View>
<View
style={
Object {
"backgroundColor": "rgba(63,67,80,0.2)",
"height": 1,
}
}
/>
</View>
</View>
</View>
</View>
`;
exports[`DrawerItem should match snapshot without separator and centered false 1`] = `
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
testID="test-id"
>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
>
<View
style={
Object {
"backgroundColor": "#ffffff",
"flexDirection": "row",
"minHeight": 50,
}
}
>
<View
style={
Object {
"alignItems": "center",
"height": 50,
"justifyContent": "center",
"marginLeft": 5,
"width": 45,
}
}
>
<Icon
name="icon-name"
style={
Array [
Object {
"color": "rgba(63,67,80,0.64)",
"fontSize": 24,
},
Object {
"color": "#d24b4e",
},
]
}
/>
</View>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"flex": 1,
"justifyContent": "center",
"paddingBottom": 14,
"paddingTop": 14,
}
}
>
<Text
style={
Array [
Object {
"color": "rgba(63,67,80,0.5)",
"fontSize": 17,
"includeFontPadding": false,
"textAlignVertical": "center",
},
Object {
"color": "#d24b4e",
},
Object {},
]
}
>
default message
</Text>
</View>
</View>
</View>
</View>
</View>
`;

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Preferences} from '@constants';
import {renderWithIntl} from '@test/intl-test-helper';
import DrawerItem from './';
describe('DrawerItem', () => {
const baseProps = {
onPress: () => null,
testID: 'test-id',
centered: true,
defaultMessage: 'default message',
i18nId: 'i18-id',
iconName: 'icon-name',
isDestructor: true,
separator: true,
theme: Preferences.THEMES.denim,
};
test('should match snapshot', () => {
const wrapper = renderWithIntl(<DrawerItem {...baseProps}/>);
expect(wrapper.toJSON()).toMatchSnapshot();
});
test('should match snapshot without separator and centered false', () => {
const props = {
...baseProps,
centered: false,
separator: false,
};
const wrapper = renderWithIntl(
<DrawerItem {...props}/>,
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,149 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ReactNode} from 'react';
import {Platform, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type DrawerItemProps = {
centered?: boolean;
defaultMessage?: string;
i18nId?: string;
iconName?: string;
isDestructor?: boolean;
labelComponent?: ReactNode;
leftComponent?: ReactNode;
onPress: () => void;
separator?: boolean;
testID: string;
theme: Theme;
};
const DrawerItem = (props: DrawerItemProps) => {
const {
centered,
defaultMessage = '',
i18nId,
iconName,
isDestructor = false,
labelComponent,
leftComponent,
onPress,
separator = true,
testID,
theme,
} = props;
const style = getStyleSheet(theme);
const destructor: any = {};
if (isDestructor) {
destructor.color = theme.errorTextColor;
}
let divider;
if (separator) {
divider = (<View style={style.divider}/>);
}
let icon;
if (leftComponent) {
icon = leftComponent;
} else if (iconName) {
icon = (
<CompassIcon
name={iconName}
style={[style.icon, destructor]}
/>
);
}
let label;
if (labelComponent) {
label = labelComponent;
} else if (i18nId) {
label = (
<FormattedText
id={i18nId}
defaultMessage={defaultMessage}
style={[
style.label,
destructor,
centered ? style.centerLabel : {},
]}
/>
);
}
return (
<TouchableWithFeedback
testID={testID}
onPress={onPress}
underlayColor={changeOpacity(theme.centerChannelColor, Platform.select({android: 0.1, ios: 0.3}) || 0.3)}
>
<View style={style.container}>
{icon && (
<View style={style.iconContainer}>
{icon}
</View>
)}
<View style={style.wrapper}>
<View style={style.labelContainer}>
{label}
</View>
{divider}
</View>
</View>
</TouchableWithFeedback>
);
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
backgroundColor: theme.centerChannelBg,
flexDirection: 'row',
minHeight: 50,
},
iconContainer: {
width: 45,
height: 50,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 5,
},
icon: {
color: changeOpacity(theme.centerChannelColor, 0.64),
fontSize: 24,
},
wrapper: {
flex: 1,
},
labelContainer: {
flex: 1,
justifyContent: 'center',
paddingTop: 14,
paddingBottom: 14,
},
centerLabel: {
textAlign: 'center',
textAlignVertical: 'center',
},
label: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 17,
textAlignVertical: 'center',
includeFontPadding: false,
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
height: 1,
},
};
});
export default DrawerItem;

View File

@@ -11,7 +11,6 @@ import {
StyleSheet,
Text,
TextStyle,
View,
} from 'react-native';
import FastImage, {ImageStyle} from 'react-native-fast-image';
import {of as of$} from 'rxjs';
@@ -102,7 +101,7 @@ const Emoji = (props: Props) => {
return (
<Text
style={[textStyle, {fontSize: size}]}
style={[textStyle, {fontSize: size, color: '#000'}]}
testID={testID}
>
{code}
@@ -118,15 +117,13 @@ const Emoji = (props: Props) => {
return null;
}
return (
<View style={Platform.select({ios: {flex: 1, justifyContent: 'center'}})}>
<FastImage
key={key}
source={image}
style={[customEmojiStyle, {width, height}]}
resizeMode={FastImage.resizeMode.contain}
testID={testID}
/>
</View>
<FastImage
key={key}
source={image}
style={[customEmojiStyle, {width, height}]}
resizeMode={FastImage.resizeMode.contain}
testID={testID}
/>
);
}
@@ -139,31 +136,27 @@ const Emoji = (props: Props) => {
const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null;
return (
<View style={Platform.select({ios: {flex: 1, justifyContent: 'center'}})}>
<FastImage
key={key}
style={[customEmojiStyle, {width, height}]}
source={{uri: imageUrl}}
resizeMode={FastImage.resizeMode.contain}
testID={testID}
/>
</View>
<FastImage
key={key}
style={[customEmojiStyle, {width, height}]}
source={{uri: imageUrl}}
resizeMode={FastImage.resizeMode.contain}
testID={testID}
/>
);
};
const withSystemIds = withObservables([], ({database}: WithDatabaseArgs) => ({
enableCustomEmoji: database.get(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG).pipe(
switchMap((config: SystemModel) => of$(config.value.EnableCustomEmoji)),
),
}));
const withCustomEmojis = withObservables(['emojiName'], ({database, emojiName}: WithDatabaseArgs & {emojiName: string}) => {
const hasEmojiBuiltIn = EmojiIndicesByAlias.has(emojiName);
const withCustomEmojis = withObservables(['enableCustomEmoji', 'emojiName'], ({enableCustomEmoji, database, emojiName}: WithDatabaseArgs & {enableCustomEmoji: string; emojiName: string}) => {
const displayTextOnly = enableCustomEmoji !== 'true';
const displayTextOnly = hasEmojiBuiltIn ? of$(false) : database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG).pipe(
switchMap((config) => of$(config.value.EnableCustomEmoji !== 'true')),
);
return {
displayTextOnly: of$(displayTextOnly),
customEmojis: database.get(MM_TABLES.SERVER.CUSTOM_EMOJI).query(Q.where('name', emojiName)).observe(),
displayTextOnly,
customEmojis: hasEmojiBuiltIn ? of$([]) : database.get(MM_TABLES.SERVER.CUSTOM_EMOJI).query(Q.where('name', emojiName)).observe(),
};
});
export default withDatabase(withSystemIds(withCustomEmojis(Emoji)));
export default withDatabase(withCustomEmojis(Emoji));

View File

@@ -0,0 +1,63 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {Text, TouchableOpacity, View} from 'react-native';
import Emoji from '@components/emoji';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type TouchableEmojiProps = {
name: string;
onEmojiPress: (emojiName: string) => void;
}
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
return {
container: {
height: 40,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
borderBottomWidth: 1,
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
overflow: 'hidden',
},
emojiContainer: {
marginRight: 5,
},
emoji: {
color: '#000',
},
emojiText: {
fontSize: 13,
color: theme.centerChannelColor,
},
};
});
const EmojiTouchable = ({name, onEmojiPress}: TouchableEmojiProps) => {
const theme = useTheme();
const style = getStyleSheetFromTheme(theme);
const onPress = useCallback(() => onEmojiPress(name), []);
return (
<TouchableOpacity
onPress={onPress}
style={style.container}
>
<View style={style.emojiContainer}>
<Emoji
emojiName={name}
textStyle={style.emoji}
size={20}
/>
</View>
<Text style={style.emojiText}>{`:${name}:`}</Text>
</TouchableOpacity>
);
};
export default memo(EmojiTouchable);

View File

@@ -0,0 +1,102 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Fuse from 'fuse.js';
import React, {useCallback, useMemo} from 'react';
import {FlatList} from 'react-native';
import {Emojis, EmojiIndicesByAlias} from '@utils/emoji';
import {compareEmojis, getSkin} from '@utils/emoji/helpers';
import EmojiItem from './emoji_item';
import NoResults from './no_results';
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
type Props = {
customEmojis: CustomEmojiModel[];
skinTone: string;
searchTerm: string;
onEmojiPress: (emojiName: string) => void;
};
const EmojiFiltered = ({customEmojis, skinTone, searchTerm, onEmojiPress}: Props) => {
const emojis = useMemo(() => {
const emoticons = new Set<string>();
for (const [key, index] of EmojiIndicesByAlias.entries()) {
const skin = getSkin(Emojis[index]);
if (!skin || skin === skinTone) {
emoticons.add(key);
}
}
for (const custom of customEmojis) {
emoticons.add(custom.name);
}
return Array.from(emoticons);
}, [skinTone, customEmojis]);
const fuse = useMemo(() => {
const options = {findAllMatches: true, ignoreLocation: true, includeMatches: true, shouldSort: false, includeScore: true};
return new Fuse(emojis, options);
}, [emojis]);
const data = useMemo(() => {
const searchTermLowerCase = searchTerm.toLowerCase();
if (!searchTerm) {
return [];
}
const sorter = (a: string, b: string) => {
return compareEmojis(a, b, searchTermLowerCase);
};
const fuzz = fuse.search(searchTermLowerCase);
if (fuzz) {
const results = fuzz.reduce((values, r) => {
const score = r?.score === undefined ? 1 : r.score;
const v = r?.matches?.[0]?.value;
if (score < 0.2 && v) {
values.push(v);
}
return values;
}, [] as string[]);
return results.sort(sorter);
}
return [];
}, [fuse, searchTerm]);
const keyExtractor = useCallback((item) => item, []);
const renderItem = useCallback(({item}) => {
return (
<EmojiItem
onEmojiPress={onEmojiPress}
name={item}
/>
);
}, []);
if (!data.length) {
return <NoResults searchTerm={searchTerm}/>;
}
return (
<FlatList
data={data}
initialNumToRender={30}
keyboardShouldPersistTaps='always'
keyExtractor={keyExtractor}
renderItem={renderItem}
removeClippedSubviews={false}
/>
);
};
export default EmojiFiltered;

View File

@@ -0,0 +1,90 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import {Text, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
searchTerm: string;
}
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
return {
flex: {
flex: 1,
},
flexCenter: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
notFoundIcon: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
width: 120,
height: 120,
borderRadius: 60,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
notFoundText: {
color: theme.centerChannelColor,
marginTop: 16,
},
notFoundText20: {
fontSize: 20,
fontWeight: '600',
},
notFoundText15: {
fontSize: 15,
},
};
});
const NoResults = ({searchTerm}: Props) => {
const theme = useTheme();
const intl = useIntl();
const styles = getStyleSheetFromTheme(theme);
const title = intl.formatMessage(
{
id: 'mobile.emoji_picker.search.not_found_title',
defaultMessage: 'No results found for "{searchTerm}"',
},
{
searchTerm,
},
);
const description = intl.formatMessage({
id: 'mobile.emoji_picker.search.not_found_description',
defaultMessage: 'Check the spelling or try another search.',
});
return (
<View style={[styles.flex, styles.flexCenter]}>
<View style={styles.flexCenter}>
<View style={styles.notFoundIcon}>
<CompassIcon
name='magnify'
size={72}
color={theme.buttonBg}
/>
</View>
<Text style={[styles.notFoundText, styles.notFoundText20]}>
{title}
</Text>
<Text style={[styles.notFoundText, styles.notFoundText15]}>
{description}
</Text>
</View>
</View>
);
};
export default NoResults;

View File

@@ -0,0 +1,172 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Q} from '@nozbe/watermelondb';
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import React, {useCallback, useState} from 'react';
import {useIntl} from 'react-intl';
import {LayoutChangeEvent, Platform, View} from 'react-native';
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
import {of as of$} from 'rxjs';
import {catchError, switchMap} from 'rxjs/operators';
import {searchCustomEmojis} from '@actions/remote/custom_emoji';
import SearchBar from '@components/search_bar';
import {Preferences} from '@constants';
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
import {useServerUrl} from '@context/server_url';
import {useTheme} from '@context/theme';
import {debounce} from '@helpers/api/general';
import {safeParseJSON} from '@utils/helpers';
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
import EmojiFiltered from './filtered';
import EmojiSections from './sections';
import type {WithDatabaseArgs} from '@typings/database/database';
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
import type PreferenceModel from '@typings/database/models/servers/preference';
import type SystemModel from '@typings/database/models/servers/system';
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
const edges: Edge[] = ['bottom', 'left', 'right'];
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
flex: {
flex: 1,
},
container: {
flex: 1,
marginHorizontal: 12,
},
searchBar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
paddingVertical: 5,
...Platform.select({
ios: {
paddingLeft: 8,
},
}),
height: 50,
},
searchBarInput: {
backgroundColor: theme.centerChannelBg,
color: theme.centerChannelColor,
fontSize: 13,
},
}));
type Props = {
customEmojis: CustomEmojiModel[];
customEmojisEnabled: boolean;
onEmojiPress: (emoji: string) => void;
recentEmojis: string[];
skinTone: string;
testID?: string;
}
const EmojiPicker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis, skinTone, testID = ''}: Props) => {
const theme = useTheme();
const intl = useIntl();
const serverUrl = useServerUrl();
const [width, setWidth] = useState(0);
const [searchTerm, setSearchTerm] = useState<string|undefined>();
const styles = getStyleSheet(theme);
const onLayout = useCallback(({nativeEvent}: LayoutChangeEvent) => setWidth(nativeEvent.layout.width), []);
const onCancelSearch = useCallback(() => setSearchTerm(undefined), []);
const onChangeSearchTerm = useCallback((text) => {
setSearchTerm(text);
searchCustom(text);
}, []);
const searchCustom = debounce((text: string) => {
if (text && text.length > 1) {
searchCustomEmojis(serverUrl, text);
}
}, 500);
let EmojiList: React.ReactNode = null;
if (searchTerm) {
EmojiList = (
<EmojiFiltered
customEmojis={customEmojis}
skinTone={skinTone}
searchTerm={searchTerm}
onEmojiPress={onEmojiPress}
/>
);
} else {
EmojiList = (
<EmojiSections
customEmojis={customEmojis}
customEmojisEnabled={customEmojisEnabled}
onEmojiPress={onEmojiPress}
recentEmojis={recentEmojis}
skinTone={skinTone}
width={width}
/>
);
}
return (
<SafeAreaView
style={styles.flex}
edges={edges}
>
<View
style={styles.searchBar}
testID={testID}
>
<SearchBar
autoCapitalize='none'
backgroundColor='transparent'
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
inputHeight={33}
inputStyle={styles.searchBarInput}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onCancelButtonPress={onCancelSearch}
onChangeText={onChangeSearchTerm}
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
testID={`${testID}.search_bar`}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
titleCancelColor={theme.centerChannelColor}
value={searchTerm}
/>
</View>
<View
style={styles.container}
onLayout={onLayout}
>
{Boolean(width) &&
<>
{EmojiList}
</>
}
</View>
</SafeAreaView>
);
};
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
customEmojisEnabled: database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).
findAndObserve(SYSTEM_IDENTIFIERS.CONFIG).pipe(
switchMap((config) => of$(config.value.EnableCustomEmoji === 'true')),
),
customEmojis: database.get<CustomEmojiModel>(MM_TABLES.SERVER.CUSTOM_EMOJI).query().observe(),
recentEmojis: database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).
findAndObserve(SYSTEM_IDENTIFIERS.RECENT_REACTIONS).
pipe(
switchMap((recent) => of$(safeParseJSON(recent.value) as string[])),
catchError(() => of$([])),
),
skinTone: database.get<PreferenceModel>(MM_TABLES.SERVER.PREFERENCE).query(
Q.where('category', Preferences.CATEGORY_EMOJI),
Q.where('name', Preferences.EMOJI_SKINTONE),
).observe().pipe(
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
),
}));
export default withDatabase(enhanced(EmojiPicker));

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {TouchableOpacity} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
currentIndex: number;
icon: string;
index: number;
scrollToIndex: (index: number) => void;
theme: Theme;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
alignItems: 'center',
flex: 1,
height: 35,
justifyContent: 'center',
zIndex: 10,
},
icon: {
color: changeOpacity(theme.centerChannelColor, 0.4),
},
selected: {
color: theme.centerChannelColor,
},
}));
const SectionIcon = ({currentIndex, icon, index, scrollToIndex, theme}: Props) => {
const style = getStyleSheet(theme);
const onPress = useCallback(preventDoubleTap(() => scrollToIndex(index)), []);
return (
<TouchableOpacity
onPress={onPress}
style={style.container}
>
<CompassIcon
name={icon}
size={20}
style={[style.icon, currentIndex === index ? style.selected : undefined]}
/>
</TouchableOpacity>
);
};
export default SectionIcon;

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import SectionIcon from './icon';
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
bottom: 10,
height: 35,
position: 'absolute',
width: '100%',
},
background: {
backgroundColor: theme.centerChannelBg,
},
pane: {
flexDirection: 'row',
borderRadius: 10,
paddingHorizontal: 10,
width: '100%',
borderColor: changeOpacity(theme.centerChannelColor, 0.3),
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderWidth: 1,
justifyContent: 'space-between',
},
}));
export type SectionIconType = {
key: string;
icon: string;
}
type Props = {
currentIndex: number;
sections: SectionIconType[];
scrollToIndex: (index: number) => void;
}
const EmojiSectionBar = ({currentIndex, sections, scrollToIndex}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (
<KeyboardTrackingView
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
normalList={true}
style={styles.container}
>
<View style={styles.background}>
<View style={styles.pane}>
{sections.map((section, index) => (
<SectionIcon
currentIndex={currentIndex}
key={section.key}
icon={section.icon}
index={index}
scrollToIndex={scrollToIndex}
theme={theme}
/>
))}
</View>
</View>
</KeyboardTrackingView>
);
};
export default EmojiSectionBar;

View File

@@ -0,0 +1,242 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {chunk} from 'lodash';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {NativeScrollEvent, NativeSyntheticEvent, SectionList, StyleSheet, View} from 'react-native';
import sectionListGetItemLayout from 'react-native-section-list-get-item-layout';
import {fetchCustomEmojis} from '@actions/remote/custom_emoji';
import {EMOJIS_PER_PAGE} from '@constants/emoji';
import {useServerUrl} from '@context/server_url';
import {CategoryNames, EmojiIndicesByCategory, CategoryTranslations, CategoryMessage} from '@utils/emoji';
import {fillEmoji} from '@utils/emoji/helpers';
import EmojiSectionBar, {SCROLLVIEW_NATIVE_ID, SectionIconType} from './icons_bar';
import SectionFooter from './section_footer';
import SectionHeader, {SECTION_HEADER_HEIGHT} from './section_header';
import TouchableEmoji from './touchable_emoji';
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
export const EMOJI_SIZE = 30;
export const EMOJI_GUTTER = 8;
const ICONS: Record<string, string> = {
recent: 'clock-outline',
'smileys-emotion': 'emoticon-happy-outline',
'people-body': 'eye-outline',
'animals-nature': 'leaf-outline',
'food-drink': 'food-apple',
'travel-places': 'airplane-variant',
activities: 'basketball',
objects: 'lightbulb-outline',
symbols: 'heart-outline',
flags: 'flag-outline',
custom: 'emoticon-custom-outline',
};
const categoryToI18n: Record<string, CategoryTranslation> = {};
const getItemLayout = sectionListGetItemLayout({
getItemHeight: () => (EMOJI_SIZE + (EMOJI_GUTTER * 2)),
getSectionHeaderHeight: () => SECTION_HEADER_HEIGHT,
});
const styles = StyleSheet.create(({
row: {
flexDirection: 'row',
marginBottom: EMOJI_GUTTER,
},
emoji: {
height: EMOJI_SIZE + EMOJI_GUTTER,
marginHorizontal: 7,
width: EMOJI_SIZE + EMOJI_GUTTER,
},
}));
type Props = {
customEmojis: CustomEmojiModel[];
customEmojisEnabled: boolean;
onEmojiPress: (emoji: string) => void;
recentEmojis: string[];
skinTone: string;
width: number;
}
CategoryNames.forEach((name: string) => {
if (CategoryTranslations.has(name) && CategoryMessage.has(name)) {
categoryToI18n[name] = {
id: CategoryTranslations.get(name)!,
defaultMessage: CategoryMessage.get(name)!,
icon: ICONS[name],
};
}
});
const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis, skinTone, width}: Props) => {
const serverUrl = useServerUrl();
const list = useRef<SectionList<EmojiSection>>();
const [sectionIndex, setSectionIndex] = useState(0);
const [customEmojiPage, setCustomEmojiPage] = useState(0);
const [fetchingCustomEmojis, setFetchingCustomEmojis] = useState(false);
const [loadedAllCustomEmojis, setLoadedAllCustomEmojis] = useState(false);
const sections: EmojiSection[] = useMemo(() => {
if (!width) {
return [];
}
const chunkSize = Math.floor(width / (EMOJI_SIZE + EMOJI_GUTTER));
return CategoryNames.map((category) => {
const emojiIndices = EmojiIndicesByCategory.get(skinTone)?.get(category);
let data: EmojiAlias[][];
switch (category) {
case 'custom': {
const builtInCustom = emojiIndices.map(fillEmoji);
// eslint-disable-next-line max-nested-callbacks
const custom = customEmojisEnabled ? customEmojis.map((ce) => ({
aliases: [],
name: ce.name,
short_name: '',
})) : [];
data = chunk<EmojiAlias>(builtInCustom.concat(custom), chunkSize);
break;
}
case 'recent':
// eslint-disable-next-line max-nested-callbacks
data = chunk<EmojiAlias>(recentEmojis.map((emoji) => ({
aliases: [],
name: emoji,
short_name: '',
})), chunkSize);
break;
default:
data = chunk(emojiIndices.map(fillEmoji), chunkSize);
break;
}
return {
...categoryToI18n[category],
data,
key: category,
};
}).filter((s: EmojiSection) => s.data.length);
}, [skinTone, customEmojis, customEmojisEnabled, width]);
const sectionIcons: SectionIconType[] = useMemo(() => {
return sections.map((s) => ({
key: s.key,
icon: s.icon,
}));
}, [sections]);
const emojiSectionsByOffset = useMemo(() => {
let lastOffset = 0;
return sections.map((s) => {
const start = lastOffset;
const nextOffset = s.data.length * (EMOJI_SIZE + (EMOJI_GUTTER * 2));
lastOffset += nextOffset;
return start;
});
}, [sections]);
const onLoadMoreCustomEmojis = useCallback(async () => {
if (!customEmojisEnabled || fetchingCustomEmojis || loadedAllCustomEmojis) {
return;
}
setFetchingCustomEmojis(true);
const {data, error} = await fetchCustomEmojis(serverUrl, customEmojiPage, EMOJIS_PER_PAGE);
if (data?.length) {
setCustomEmojiPage(customEmojiPage + 1);
} else if (!error && (data && data.length < EMOJIS_PER_PAGE)) {
setLoadedAllCustomEmojis(true);
}
setFetchingCustomEmojis(false);
}, [customEmojiPage, customEmojisEnabled, loadedAllCustomEmojis, fetchingCustomEmojis]);
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
const {contentOffset} = e.nativeEvent;
let nextIndex = emojiSectionsByOffset.findIndex(
(offset) => contentOffset.y <= offset,
);
if (nextIndex === -1) {
nextIndex = emojiSectionsByOffset.length - 1;
} else if (nextIndex !== 0) {
nextIndex -= 1;
}
if (nextIndex !== sectionIndex) {
setSectionIndex(nextIndex);
}
}, [emojiSectionsByOffset, sectionIndex]);
const scrollToIndex = (index: number) => {
list.current?.scrollToLocation({sectionIndex: index, itemIndex: 0, animated: false, viewOffset: 0});
setSectionIndex(index);
};
const renderSectionHeader = useCallback(({section}) => {
return (
<SectionHeader section={section as EmojiSection}/>
);
}, []);
const renderFooter = useMemo(() => {
return fetchingCustomEmojis ? <SectionFooter/> : null;
}, [fetchingCustomEmojis]);
const renderItem = useCallback(({item}) => {
return (
<View style={styles.row}>
{item.map((emoji: EmojiAlias) => {
return (
<TouchableEmoji
key={emoji.name}
name={emoji.name}
onEmojiPress={onEmojiPress}
size={EMOJI_SIZE}
style={styles.emoji}
/>
);
})}
</View>
);
}, []);
return (
<>
<SectionList
getItemLayout={getItemLayout}
initialNumToRender={20}
keyboardDismissMode='interactive'
keyboardShouldPersistTaps='always'
ListFooterComponent={renderFooter}
maxToRenderPerBatch={20}
nativeID={SCROLLVIEW_NATIVE_ID}
onEndReached={onLoadMoreCustomEmojis}
onEndReachedThreshold={2}
onScroll={onScroll}
// @ts-expect-error ref
ref={list}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
sections={sections}
contentContainerStyle={{paddingBottom: 50}}
windowSize={100}
/>
<EmojiSectionBar
currentIndex={sectionIndex}
scrollToIndex={scrollToIndex}
sections={sectionIcons}
/>
</>
);
};
export default EmojiSections;

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import {ActivityIndicator, View} from 'react-native';
import {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme} from '@utils/theme';
const Footer = () => {
const theme = useTheme();
const styles = getStyleSheetFromTheme(theme);
return (
<View style={styles.loading}>
<ActivityIndicator color={theme.centerChannelColor}/>
</View>
);
};
const getStyleSheetFromTheme = makeStyleSheetFromTheme(() => {
return {
loading: {
flex: 1,
alignItems: 'center',
},
};
});
export default memo(Footer);

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import {View} from 'react-native';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
section: EmojiSection;
}
export const SECTION_HEADER_HEIGHT = 28;
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
return {
sectionTitleContainer: {
height: SECTION_HEADER_HEIGHT,
justifyContent: 'center',
backgroundColor: theme.centerChannelBg,
},
sectionTitle: {
color: changeOpacity(theme.centerChannelColor, 0.2),
fontSize: 15,
fontWeight: '700',
},
};
});
const SectionHeader = ({section}: Props) => {
const theme = useTheme();
const styles = getStyleSheetFromTheme(theme);
return (
<View
style={styles.sectionTitleContainer}
key={section.id}
>
<FormattedText
style={styles.sectionTitle}
id={section.id}
defaultMessage={section.icon}
/>
</View>
);
};
export default memo(SectionHeader);

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {StyleProp, ViewStyle} from 'react-native';
import Emoji from '@components/emoji';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {preventDoubleTap} from '@utils/tap';
type Props = {
name: string;
onEmojiPress: (emoji: string) => void;
size?: number;
style: StyleProp<ViewStyle>;
}
const TouchableEmoji = ({name, onEmojiPress, size = 30, style}: Props) => {
const onPress = useCallback(preventDoubleTap(() => onEmojiPress(name)), []);
return (
<TouchableWithFeedback
onPress={onPress}
style={style}
type={'opacity'}
>
<Emoji
emojiName={name}
size={size}
/>
</TouchableWithFeedback>
);
};
export default React.memo(TouchableEmoji);

View File

@@ -5,7 +5,7 @@ import React, {useEffect} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, StyleProp, View, ViewStyle} from 'react-native';
import {fetchMissinProfilesByIds, fetchMissinProfilesByUsernames} from '@actions/remote/user';
import {fetchMissingProfilesByIds, fetchMissingProfilesByUsernames} from '@actions/remote/user';
import Markdown from '@components/markdown';
import SystemAvatar from '@components/system_avatar';
import SystemHeader from '@components/system_header';
@@ -70,11 +70,11 @@ const CombinedUserActivity = ({
const loadUserProfiles = () => {
if (allUserIds.length) {
fetchMissinProfilesByIds(serverUrl, allUserIds);
fetchMissingProfilesByIds(serverUrl, allUserIds);
}
if (allUsernames.length) {
fetchMissinProfilesByUsernames(serverUrl, allUsernames);
fetchMissingProfilesByUsernames(serverUrl, allUsernames);
}
};

View File

@@ -24,7 +24,7 @@ const getStyleSheet = (scale: number, th: Theme) => {
moreImagesText: {
color: theme.sidebarHeaderTextColor,
fontSize: Math.round(PixelRatio.roundToNearestPixel(24 * scale)),
fontFamily: 'Open Sans',
fontFamily: 'OpenSans',
textAlign: 'center',
},
};

View File

@@ -59,7 +59,7 @@ const Reaction = ({count, emojiName, highlight, onPress, onLongPress, theme}: Re
<Emoji
emojiName={emojiName}
size={20}
textStyle={{color: 'black', fontWeight: 'bold'}}
textStyle={{color: '#000'}}
customEmojiStyle={styles.customEmojiStyle}
testID={`reaction.emoji.${emojiName}`}
/>

View File

@@ -103,7 +103,7 @@ const Header = (props: HeaderProps) => {
theme={theme}
userId={post.userId}
/>
{showCustomStatusEmoji && customStatusExpired && Boolean(customStatus?.emoji) && (
{showCustomStatusEmoji && !customStatusExpired && Boolean(customStatus?.emoji) && (
<CustomStatusEmoji
customStatus={customStatus!}
style={style.customStatusEmoji}

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {Platform} from 'react-native';
import CompassIcon from '@components/compass_icon';
type ClearIconProps = {
deleteIconSizeAndroid: number;
onClear: () => void;
placeholderTextColor: string;
searchClearButtonTestID: string;
tintColorDelete: string;
titleCancelColor: string;
}
const ClearIcon = ({deleteIconSizeAndroid, onClear, placeholderTextColor, searchClearButtonTestID, tintColorDelete, titleCancelColor}: ClearIconProps) => {
const onPressClear = useCallback(() => onClear(), []);
if (Platform.OS === 'ios') {
return (
<CompassIcon
testID={searchClearButtonTestID}
name='close-circle'
size={18}
style={{color: tintColorDelete || 'grey'}}
onPress={onPressClear}
/>
);
}
return (
<CompassIcon
testID={searchClearButtonTestID}
name='close'
size={deleteIconSizeAndroid}
color={titleCancelColor || placeholderTextColor}
onPress={onPressClear}
/>
);
};
export default memo(ClearIcon);

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Platform, TouchableWithoutFeedback, ViewStyle} from 'react-native';
import CompassIcon from '@components/compass_icon';
type SearchIconProps = {
backArrowSize: number;
clearIconColorAndroid: string;
iOSStyle: ViewStyle;
onCancel: () => void;
searchCancelButtonTestID: string;
searchIconColor: string;
searchIconSize: number;
showArrow: boolean;
}
const SearchIcon = ({backArrowSize, clearIconColorAndroid, iOSStyle, onCancel, searchCancelButtonTestID, searchIconColor, searchIconSize, showArrow}: SearchIconProps) => {
if (Platform.OS === 'ios') {
return (
<CompassIcon
name='magnify'
size={24}
style={iOSStyle}
/>
);
}
if (showArrow) {
return (
<TouchableWithoutFeedback onPress={onCancel}>
<CompassIcon
testID={searchCancelButtonTestID}
name='arrow-left'
size={backArrowSize}
color={clearIconColorAndroid}
/>
</TouchableWithoutFeedback>
);
}
return (
<CompassIcon
name='magnify'
size={searchIconSize}
color={searchIconColor}
/>
);
};
export default SearchIcon;

View File

@@ -0,0 +1,316 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {IntlShape} from 'react-intl';
import {
Animated,
InteractionManager,
Keyboard,
StyleSheet,
View,
Platform,
ViewStyle,
ReturnKeyTypeOptions,
KeyboardTypeOptions,
NativeSyntheticEvent,
TextInputSelectionChangeEventData,
TextStyle,
} from 'react-native';
import {SearchBar} from 'react-native-elements';
import CompassIcon from '@components/compass_icon';
import ClearIcon from './components/clear_icon';
import SearchIcon from './components/search_icon';
import {getSearchStyles} from './styles';
const LEFT_COMPONENT_INITIAL_POSITION = Platform.OS === 'ios' ? 7 : 0;
type SearchProps = {
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters' | undefined;
autoFocus?: boolean;
backArrowSize?: number;
backgroundColor: string;
blurOnSubmit?: boolean;
cancelButtonStyle?: ViewStyle;
cancelTitle?: string;
containerHeight?: number;
containerStyle?: ViewStyle;
deleteIconSize?: number;
editable?: boolean;
inputHeight: number;
inputStyle?: TextStyle;
intl?: IntlShape;
keyboardAppearance?: 'default' | 'light' | 'dark' | undefined;
keyboardShouldPersist?: boolean;
keyboardType?: KeyboardTypeOptions | undefined;
leftComponent?: JSX.Element;
onBlur?: () => void;
onCancelButtonPress: (text?: string) => void;
onChangeText: (text: string) => void;
onFocus?: () => void;
onSearchButtonPress?: (value: string) => void;
onSelectionChange?: (e: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => void;
placeholder?: string;
placeholderTextColor?: string;
returnKeyType?: ReturnKeyTypeOptions | undefined;
searchBarRightMargin?: number;
searchIconSize?: number;
selectionColor?: string;
showArrow?: boolean;
showCancel?: boolean;
testID: string;
tintColorDelete: string;
tintColorSearch: string;
titleCancelColor: string;
value?: string;
};
type SearchState = {
leftComponentWidth: number;
}
export default class Search extends PureComponent<SearchProps, SearchState> {
static defaultProps = {
backArrowSize: 24,
blurOnSubmit: false,
containerHeight: 40,
deleteIconSize: 20,
editable: true,
keyboardShouldPersist: false,
keyboardType: 'default',
onBlur: () => true,
onSelectionChange: () => true,
placeholderTextColor: 'grey',
returnKeyType: 'search',
searchBarRightMargin: 0,
searchIconSize: 24,
showArrow: false,
showCancel: true,
value: '',
};
private readonly leftComponentAnimated: Animated.Value;
private readonly searchContainerAnimated: Animated.Value;
private searchContainerRef: any;
private inputKeywordRef: any;
private readonly searchStyle: any;
constructor(props: SearchProps | Readonly<SearchProps>) {
super(props);
this.state = {
leftComponentWidth: 0,
};
this.leftComponentAnimated = new Animated.Value(LEFT_COMPONENT_INITIAL_POSITION);
this.searchContainerAnimated = new Animated.Value(0);
const {backgroundColor, cancelButtonStyle, containerHeight, inputHeight, inputStyle, placeholderTextColor, searchBarRightMargin, tintColorDelete, tintColorSearch, titleCancelColor} = props;
this.searchStyle = getSearchStyles(backgroundColor, cancelButtonStyle, containerHeight!, inputHeight, inputStyle, placeholderTextColor!, searchBarRightMargin!, tintColorDelete, tintColorSearch, titleCancelColor);
}
setSearchContainerRef = (ref: any) => {
this.searchContainerRef = ref;
};
setInputKeywordRef = (ref: any) => {
this.inputKeywordRef = ref;
};
blur = () => {
this.inputKeywordRef.blur();
};
focus = () => {
this.inputKeywordRef.focus();
};
onBlur = async () => {
this.props?.onBlur?.();
if (this.props.leftComponent) {
await this.collapseAnimation();
}
};
onLeftComponentLayout = (event: { nativeEvent: { layout: { width: any } } }) => {
const leftComponentWidth = event.nativeEvent.layout.width;
this.setState({leftComponentWidth});
};
onSearch = async () => {
const {keyboardShouldPersist, onSearchButtonPress, value} = this.props;
if (!keyboardShouldPersist) {
await Keyboard.dismiss();
}
if (value) {
onSearchButtonPress?.(value);
}
};
onChangeText = (text: string) => {
const {onChangeText} = this.props;
if (onChangeText) {
onChangeText(text);
}
};
onFocus = () => {
const {leftComponent, onFocus} = this.props;
InteractionManager.runAfterInteractions(async () => {
onFocus?.();
if (leftComponent) {
await this.expandAnimation();
}
});
};
onClear = () => {
this.focus();
this.props.onChangeText('');
};
onCancel = () => {
const {onCancelButtonPress} = this.props;
Keyboard.dismiss();
InteractionManager.runAfterInteractions(() => {
return onCancelButtonPress?.();
});
};
onSelectionChange = (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
const {onSelectionChange} = this.props;
onSelectionChange?.(event);
};
expandAnimation = () => {
return new Promise((resolve) => {
Animated.parallel([
Animated.timing(this.leftComponentAnimated, {toValue: -115, duration: 200} as Animated.TimingAnimationConfig),
Animated.timing(this.searchContainerAnimated, {toValue: this.state.leftComponentWidth * -1, duration: 200} as Animated.TimingAnimationConfig),
]).start(resolve);
});
};
collapseAnimation = () => {
return new Promise((resolve) => {
Animated.parallel([
Animated.timing(this.leftComponentAnimated, {toValue: LEFT_COMPONENT_INITIAL_POSITION, duration: 200} as Animated.TimingAnimationConfig),
Animated.timing(this.searchContainerAnimated, {toValue: 0, duration: 200} as Animated.TimingAnimationConfig),
]).start(resolve);
});
};
render() {
const {autoCapitalize, autoFocus, backArrowSize, blurOnSubmit, cancelTitle, deleteIconSize, editable, intl, keyboardAppearance, keyboardType, leftComponent, placeholder, placeholderTextColor, returnKeyType, searchIconSize, selectionColor, showArrow, showCancel, testID, tintColorDelete, tintColorSearch, titleCancelColor, value} = this.props;
const searchClearButtonTestID = `${testID}.search.clear.button`;
const searchCancelButtonTestID = `${testID}.search.cancel.button`;
const searchInputTestID = `${testID}.search.input`;
const {cancelButtonPropStyle, containerStyle, inputContainerStyle, inputTextStyle, searchBarStyle, styles} = this.searchStyle;
return (
<View
testID={testID}
style={[searchBarStyle.container, this.props.containerStyle]}
>
{leftComponent && (
<Animated.View
style={[styles.leftComponent, {left: this.leftComponentAnimated}]}
onLayout={this.onLeftComponentLayout}
>
{leftComponent}
</Animated.View>
)}
<Animated.View
style={[
styles.fullWidth,
searchBarStyle.searchBarWrapper,
{marginLeft: this.searchContainerAnimated},
]}
>
<SearchBar
testID={searchInputTestID}
autoCapitalize={autoCapitalize}
autoCorrect={false}
autoFocus={autoFocus}
blurOnSubmit={blurOnSubmit}
cancelButtonProps={cancelButtonPropStyle}
cancelButtonTitle={cancelTitle || intl?.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}) || ''}
cancelIcon={
// Making sure the icon won't change depending on whether the input is in focus on Android devices
Platform.OS === 'android' && (
<CompassIcon
testID={searchCancelButtonTestID}
name='arrow-left'
size={25}
color={searchBarStyle.clearIconColorAndroid}
onPress={this.onCancel}
/>
)
}
// @ts-expect-error: The clearIcon can also accept a ReactElement
clearIcon={
<ClearIcon
deleteIconSizeAndroid={deleteIconSize!}
onClear={this.onClear}
placeholderTextColor={placeholderTextColor!}
searchClearButtonTestID={searchClearButtonTestID}
tintColorDelete={tintColorDelete}
titleCancelColor={titleCancelColor}
/>
}
containerStyle={containerStyle}
disableFullscreenUI={true}
editable={editable}
enablesReturnKeyAutomatically={true}
inputContainerStyle={inputContainerStyle}
inputStyle={inputTextStyle}
keyboardAppearance={keyboardAppearance}
keyboardType={keyboardType}
leftIconContainerStyle={styles.leftIcon}
placeholder={placeholder || intl?.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'}) || ''}
placeholderTextColor={placeholderTextColor}
platform={Platform.OS as 'ios' | 'android'}
onBlur={this.onBlur}
onCancel={this.onCancel}
// @ts-expect-error: The TS definition for this SearchBar is messed up
onChangeText={this.onChangeText}
onClear={this.onClear}
onFocus={this.onFocus}
onSelectionChange={this.onSelectionChange}
onSubmitEditing={this.onSearch}
// @ts-expect-error: The searchIcon can also accept a ReactElement
searchIcon={
<SearchIcon
searchIconColor={tintColorSearch || placeholderTextColor!}
searchIconSize={searchIconSize!}
clearIconColorAndroid={titleCancelColor || placeholderTextColor!}
backArrowSize={backArrowSize!}
searchCancelButtonTestID={searchCancelButtonTestID}
onCancel={this.onCancel}
showArrow={showArrow!}
iOSStyle={StyleSheet.flatten([styles.fullWidth, searchBarStyle.searchIcon])}
/>
}
selectionColor={selectionColor}
showCancel={showCancel!}
ref={this.setInputKeywordRef}
returnKeyType={returnKeyType}
underlineColorAndroid='transparent'
value={value!}
/>
</Animated.View>
</View>
);
}
}

View File

@@ -0,0 +1,125 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform, StyleSheet, TextStyle, ViewStyle} from 'react-native';
export const getSearchBarStyle = (backgroundColor: string, cancelButtonStyle: ViewStyle | undefined, containerHeight: number, inputHeight: number, inputStyle: TextStyle | undefined, placeholderTextColor: string, searchBarRightMargin: number, tintColorDelete: string, tintColorSearch: string, titleCancelColor: string) => ({
cancelButtonText: {
...cancelButtonStyle,
color: titleCancelColor,
},
container: {
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
height: containerHeight,
overflow: 'hidden',
},
clearIconColorIos: tintColorDelete || 'grey',
clearIconColorAndroid: titleCancelColor || placeholderTextColor,
inputStyle: {
...inputStyle,
backgroundColor: 'transparent',
height: inputHeight,
},
inputContainer: {
backgroundColor: inputStyle?.backgroundColor,
height: inputHeight,
},
searchBarWrapper: {
marginRight: searchBarRightMargin,
height: Platform.select({
ios: inputHeight || containerHeight - 10,
android: inputHeight,
}),
},
searchBarContainer: {
backgroundColor,
},
searchIcon: {
color: tintColorSearch || placeholderTextColor,
top: 8,
},
searchIconColor: tintColorSearch || placeholderTextColor,
});
export const getStyles = () => StyleSheet.create({
defaultColor: {
color: 'grey',
},
fullWidth: {
flex: 1,
},
inputContainer: {
borderRadius: Platform.select({
ios: 2,
android: 0,
}),
},
inputMargin: {
marginLeft: 4,
paddingTop: 0,
marginTop: Platform.select({
ios: 0,
android: 8,
}),
},
leftIcon: {
marginLeft: 4,
width: 30,
},
searchContainer: {
paddingTop: 0,
paddingBottom: 0,
},
text: {
fontSize: Platform.select({
ios: 14,
android: 15,
}),
color: '#fff',
},
leftComponent: {
position: 'relative',
marginLeft: 2,
},
});
export const getSearchStyles = (backgroundColor: string, cancelButtonStyle: ViewStyle | undefined, containerHeight: number, inputHeight: number, inputStyle: TextStyle | undefined, placeholderTextColor: string, searchBarRightMargin: number, tintColorDelete: string, tintColorSearch: string, titleCancelColor: string) => {
const searchBarStyle = getSearchBarStyle(backgroundColor, cancelButtonStyle, containerHeight, inputHeight, inputStyle, placeholderTextColor, searchBarRightMargin, tintColorDelete, tintColorSearch, titleCancelColor);
const styles = getStyles();
const inputTextStyle = {
...styles.text,
...styles.inputMargin,
...searchBarStyle.inputStyle,
};
const inputContainerStyle = {
...styles.inputContainer,
...searchBarStyle.inputContainer,
};
const containerStyle = {
...styles.searchContainer,
...styles.fullWidth,
...searchBarStyle.searchBarContainer,
};
const cancelButtonPropStyle = {
buttonTextStyle: {
...styles.text,
...searchBarStyle.cancelButtonText,
},
};
return {
cancelButtonPropStyle,
containerStyle,
inputContainerStyle,
inputTextStyle,
searchBarStyle,
styles,
};
};

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {StyleProp, Text, View, ViewStyle} from 'react-native';
import {StyleProp, Text, TextStyle, View, ViewStyle} from 'react-native';
import FastImage, {ImageStyle, Source} from 'react-native-fast-image';
import CompassIcon from '@components/compass_icon';
@@ -15,7 +15,9 @@ import {isValidUrl} from '@utils/url';
type SlideUpPanelProps = {
destructive?: boolean;
icon?: string | Source;
imageStyles?: StyleProp<TextStyle>;
onPress: () => void;
textStyles?: TextStyle;
testID?: string;
text: string;
}
@@ -32,14 +34,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: '#D0021B',
},
row: {
flex: 1,
width: '100%',
flexDirection: 'row',
},
iconContainer: {
alignItems: 'center',
height: 50,
justifyContent: 'center',
width: 60,
marginRight: 10,
},
noIconContainer: {
height: 50,
@@ -61,15 +62,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
opacity: 0.9,
letterSpacing: -0.45,
},
footer: {
marginHorizontal: 17,
borderBottomWidth: 0.5,
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
},
};
});
const SlideUpPanelItem = ({destructive, icon, onPress, testID, text}: SlideUpPanelProps) => {
const SlideUpPanelItem = ({destructive, icon, imageStyles, onPress, testID, text, textStyles}: SlideUpPanelProps) => {
const theme = useTheme();
const handleOnPress = useCallback(preventDoubleTap(onPress, 500), []);
const style = getStyleSheet(theme);
@@ -77,7 +73,7 @@ const SlideUpPanelItem = ({destructive, icon, onPress, testID, text}: SlideUpPan
let image;
let iconStyle: StyleProp<ViewStyle> = [style.iconContainer];
if (icon) {
const imageStyle: StyleProp<ImageStyle> = [style.icon];
const imageStyle: StyleProp<ImageStyle> = [style.icon, imageStyles];
if (destructive) {
imageStyle.push(style.destructive);
}
@@ -105,27 +101,22 @@ const SlideUpPanelItem = ({destructive, icon, onPress, testID, text}: SlideUpPan
}
return (
<View
testID={testID}
<TouchableWithFeedback
onPress={handleOnPress}
style={style.container}
testID={testID}
type='native'
underlayColor={changeOpacity(theme.centerChannelColor, 0.5)}
>
<TouchableWithFeedback
onPress={handleOnPress}
style={style.row}
type='native'
underlayColor={changeOpacity(theme.centerChannelColor, 0.5)}
>
<View style={style.row}>
{Boolean(image) &&
<View style={iconStyle}>{image}</View>
}
<View style={style.textContainer}>
<Text style={[style.text, destructive ? style.destructive : null]}>{text}</Text>
</View>
<View style={style.row}>
{Boolean(image) &&
<View style={iconStyle}>{image}</View>
}
<View style={style.textContainer}>
<Text style={[style.text, destructive ? style.destructive : null, textStyles]}>{text}</Text>
</View>
</TouchableWithFeedback>
<View style={style.footer}/>
</View>
</View>
</TouchableWithFeedback>
);
};

View File

@@ -0,0 +1,66 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {TextStyle} from 'react-native';
import FormattedText from '@components/formatted_text';
import {General} from '@constants';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type StatusLabelProps = {
status?: string;
labelStyle?: TextStyle;
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
label: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 17,
textAlignVertical: 'center',
includeFontPadding: false,
},
};
});
const StatusLabel = ({status = General.OFFLINE, labelStyle}: StatusLabelProps) => {
const theme = useTheme();
const style = getStyleSheet(theme);
let i18nId = t('status_dropdown.set_offline');
let defaultMessage = 'Offline';
switch (status) {
case General.AWAY:
i18nId = t('status_dropdown.set_away');
defaultMessage = 'Away';
break;
case General.DND:
i18nId = t('status_dropdown.set_dnd');
defaultMessage = 'Do Not Disturb';
break;
case General.ONLINE:
i18nId = t('status_dropdown.set_online');
defaultMessage = 'Online';
break;
}
if (status === General.OUT_OF_OFFICE) {
i18nId = t('status_dropdown.set_ooo');
defaultMessage = 'Out Of Office';
}
return (
<FormattedText
id={i18nId}
defaultMessage={defaultMessage}
style={[style.label, labelStyle]}
testID={`user_status.label.${status}`}
/>
);
};
export default StatusLabel;

View File

@@ -0,0 +1,78 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Platform, Text, View} from 'react-native';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
action?: string;
onPress: () => void;
title: string;
testID: string;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
actionContainer: {
alignItems: 'flex-end',
justifyContent: 'center',
marginRight: 20,
},
action: {
color: theme.buttonBg,
fontFamily: 'OpenSans-Semibold',
fontSize: 16,
lineHeight: 24,
},
container: {
backgroundColor: theme.centerChannelBg,
borderBottomWidth: 1,
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.08),
flexDirection: 'row',
height: 34,
width: '100%',
},
titleContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
title: {
color: theme.centerChannelColor,
fontFamily: 'OpenSans-Semibold',
fontSize: 18,
lineHeight: 24,
},
}));
const TabletTitle = ({action, onPress, testID, title}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (
<>
<View style={styles.container}>
<View style={styles.titleContainer}>
<Text style={styles.title}>{title}</Text>
</View>
{Boolean(action) &&
<View style={styles.actionContainer}>
<TouchableWithFeedback
onPress={onPress}
type={Platform.select({android: 'native', ios: 'opacity'})}
testID={testID}
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
>
<Text style={styles.action}>{action}</Text>
</TouchableWithFeedback>
</View>
}
</View>
</>
);
};
export default TabletTitle;

View File

@@ -4,16 +4,15 @@
/* eslint-disable new-cap */
import React, {memo} from 'react';
import {TouchableOpacity, TouchableWithoutFeedback, View, StyleProp, ViewStyle} from 'react-native';
import {Touchable, TouchableOpacity, TouchableWithoutFeedback, View, StyleProp, ViewStyle} from 'react-native';
import {TouchableNativeFeedback} from 'react-native-gesture-handler';
type TouchableProps = {
type TouchableProps = Touchable & {
testID: string;
children: React.ReactNode | React.ReactNode[];
underlayColor: string;
type: 'native' | 'opacity' | 'none';
style?: StyleProp<ViewStyle>;
[x: string]: any;
}
const TouchableWithFeedbackAndroid = ({testID, children, underlayColor, type = 'native', ...props}: TouchableProps) => {
@@ -23,7 +22,7 @@ const TouchableWithFeedbackAndroid = ({testID, children, underlayColor, type = '
<TouchableNativeFeedback
testID={testID}
{...props}
style={[props.style, {flex: undefined, flexDirection: undefined, width: '100%', height: '100%'}]}
style={[props.style]}
background={TouchableNativeFeedback.Ripple(underlayColor || '#fff', false)}
>
<View>

View File

@@ -2,14 +2,13 @@
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import {PanResponder, TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback, View} from 'react-native';
import {PanResponder, Touchable, TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback, View} from 'react-native';
type TouchableProps = {
type TouchableProps = Touchable & {
cancelTouchOnPanning: boolean;
children: React.ReactNode | React.ReactNode[];
testID: string;
type: 'native' | 'opacity' | 'none';
[x: string]: any;
}
const TouchableWithFeedbackIOS = ({testID, children, type = 'native', cancelTouchOnPanning, ...props}: TouchableProps) => {