forked from Ivasoft/mattermost-mobile
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:
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
141
app/components/custom_status/custom_status_expiry.tsx
Normal file
141
app/components/custom_status/custom_status_expiry.tsx
Normal 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));
|
||||
@@ -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';
|
||||
|
||||
220
app/components/drawer_item/__snapshots__/index.test.tsx.snap
Normal file
220
app/components/drawer_item/__snapshots__/index.test.tsx.snap
Normal 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>
|
||||
`;
|
||||
42
app/components/drawer_item/index.test.tsx
Normal file
42
app/components/drawer_item/index.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
149
app/components/drawer_item/index.tsx
Normal file
149
app/components/drawer_item/index.tsx
Normal 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;
|
||||
@@ -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));
|
||||
|
||||
63
app/components/emoji_picker/filtered/emoji_item.tsx
Normal file
63
app/components/emoji_picker/filtered/emoji_item.tsx
Normal 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);
|
||||
102
app/components/emoji_picker/filtered/index.tsx
Normal file
102
app/components/emoji_picker/filtered/index.tsx
Normal 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;
|
||||
90
app/components/emoji_picker/filtered/no_results.tsx
Normal file
90
app/components/emoji_picker/filtered/no_results.tsx
Normal 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;
|
||||
172
app/components/emoji_picker/index.tsx
Normal file
172
app/components/emoji_picker/index.tsx
Normal 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));
|
||||
53
app/components/emoji_picker/sections/icons_bar/icon.tsx
Normal file
53
app/components/emoji_picker/sections/icons_bar/icon.tsx
Normal 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;
|
||||
75
app/components/emoji_picker/sections/icons_bar/index.tsx
Normal file
75
app/components/emoji_picker/sections/icons_bar/index.tsx
Normal 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;
|
||||
242
app/components/emoji_picker/sections/index.tsx
Normal file
242
app/components/emoji_picker/sections/index.tsx
Normal 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;
|
||||
30
app/components/emoji_picker/sections/section_footer.tsx
Normal file
30
app/components/emoji_picker/sections/section_footer.tsx
Normal 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);
|
||||
50
app/components/emoji_picker/sections/section_header.tsx
Normal file
50
app/components/emoji_picker/sections/section_header.tsx
Normal 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);
|
||||
35
app/components/emoji_picker/sections/touchable_emoji.tsx
Normal file
35
app/components/emoji_picker/sections/touchable_emoji.tsx
Normal 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);
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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}`}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
44
app/components/search_bar/components/clear_icon.tsx
Normal file
44
app/components/search_bar/components/clear_icon.tsx
Normal 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);
|
||||
53
app/components/search_bar/components/search_icon.tsx
Normal file
53
app/components/search_bar/components/search_icon.tsx
Normal 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;
|
||||
316
app/components/search_bar/index.tsx
Normal file
316
app/components/search_bar/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
125
app/components/search_bar/styles.ts
Normal file
125
app/components/search_bar/styles.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
66
app/components/status_label/index.tsx
Normal file
66
app/components/status_label/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {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;
|
||||
78
app/components/tablet_title/index.tsx
Normal file
78
app/components/tablet_title/index.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user