Merge 4.27.0 into single-server (#4138)

This commit is contained in:
Diego Mello 2022-04-28 15:44:40 -03:00 committed by GitHub
parent b3c37ede7e
commit 8f6100d450
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
460 changed files with 4434 additions and 3876 deletions

View File

@ -3,7 +3,7 @@ defaults: &defaults
macos: &macos macos: &macos
macos: macos:
xcode: "12.5.0" xcode: "13.3.0"
resource_class: large resource_class: large
bash-env: &bash-env bash-env: &bash-env
@ -339,7 +339,7 @@ jobs:
lint-testunit: lint-testunit:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/node:15 - image: cimg/node:16.14
resource_class: large resource_class: large
environment: environment:
CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6 CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6
@ -372,7 +372,7 @@ jobs:
android-build-experimental: android-build-experimental:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/android:api-29-node - image: cimg/android:2022.03.1-node
environment: environment:
<<: *android-env <<: *android-env
<<: *bash-env <<: *bash-env
@ -383,7 +383,7 @@ jobs:
android-build-official: android-build-official:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/android:api-29-node - image: cimg/android:2022.03.1-node
environment: environment:
<<: *android-env <<: *android-env
<<: *bash-env <<: *bash-env
@ -394,7 +394,7 @@ jobs:
android-internal-app-sharing-experimental: android-internal-app-sharing-experimental:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/android:api-28-node - image: cimg/android:2022.03.1-node
steps: steps:
- upload-to-internal-app-sharing - upload-to-internal-app-sharing
@ -402,7 +402,7 @@ jobs:
android-google-play-beta-experimental: android-google-play-beta-experimental:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/android:api-29-node - image: cimg/android:2022.03.1-node
steps: steps:
- upload-to-google-play-beta: - upload-to-google-play-beta:
@ -411,14 +411,14 @@ jobs:
android-google-play-production-experimental: android-google-play-production-experimental:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/android:api-29-node - image: cimg/android:2022.03.1-node
steps: steps:
- upload-to-google-play-production - upload-to-google-play-production
android-google-play-beta-official: android-google-play-beta-official:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/android:api-29-node - image: cimg/android:2022.03.1-node
steps: steps:
- upload-to-google-play-beta: - upload-to-google-play-beta:

View File

@ -17,7 +17,7 @@ module.exports = {
legacyDecorators: true legacyDecorators: true
} }
}, },
plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel', 'jest'], plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel', 'jest', 'react-hooks'],
env: { env: {
browser: true, browser: true,
commonjs: true, commonjs: true,
@ -148,7 +148,9 @@ module.exports = {
'no-async-promise-executor': [0], 'no-async-promise-executor': [0],
'max-classes-per-file': [0], 'max-classes-per-file': [0],
'no-multiple-empty-lines': [0], 'no-multiple-empty-lines': [0],
'no-sequences': 'off' 'no-sequences': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
}, },
globals: { globals: {
__DEV__: true __DEV__: true
@ -237,7 +239,9 @@ module.exports = {
} }
], ],
'new-cap': 'off', 'new-cap': 'off',
'lines-between-class-members': 'off' 'lines-between-class-members': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
}, },
globals: { globals: {
JSX: true JSX: true

View File

@ -144,7 +144,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "4.26.2" versionName "4.27.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
if (!isFoss) { if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
@ -277,14 +277,17 @@ android {
dependencies { dependencies {
addUnimodulesDependencies() addUnimodulesDependencies()
implementation project(':@react-native-community_viewpager') implementation project(':@react-native-community_viewpager')
playImplementation project(':reactnativenotifications') playImplementation project(':react-native-notifications')
playImplementation 'com.google.firebase:firebase-core:16.0.0'
playImplementation project(':@react-native-firebase_app') playImplementation project(':@react-native-firebase_app')
playImplementation project(':@react-native-firebase_analytics') playImplementation project(':@react-native-firebase_analytics')
playImplementation project(':@react-native-firebase_crashlytics') playImplementation project(':@react-native-firebase_crashlytics')
implementation(project(':react-native-jitsi-meet')) { // https://github.com/skrafft/react-native-jitsi-meet#side-note
exclude group: 'com.facebook.react',module:'react-native-svg'
}
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion //noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules implementation "com.facebook.react:react-native:+" // From node_modules
playImplementation "com.google.firebase:firebase-messaging:18.0.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni' exclude group:'com.facebook.fbjni'

View File

@ -3,7 +3,6 @@ package chat.rocket.reactnative;
import android.app.Application; import android.app.Application;
import com.facebook.react.ReactPackage; import com.facebook.react.ReactPackage;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -17,8 +16,7 @@ public class AdditionalModules {
return Arrays.<ReactPackage>asList( return Arrays.<ReactPackage>asList(
new ReactNativeFirebaseAnalyticsPackage(), new ReactNativeFirebaseAnalyticsPackage(),
new ReactNativeFirebaseAppPackage(), new ReactNativeFirebaseAppPackage(),
new ReactNativeFirebaseCrashlyticsPackage(), new ReactNativeFirebaseCrashlyticsPackage()
new RNNotificationsPackage(application)
); );
} }
} }

View File

@ -2,8 +2,6 @@ apply from: '../node_modules/react-native-unimodules/gradle.groovy'
includeUnimodulesProjects() includeUnimodulesProjects()
rootProject.name = 'RocketChatRN' rootProject.name = 'RocketChatRN'
include ':reactnativenotifications'
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android/app')
include ':@react-native-community_viewpager' include ':@react-native-community_viewpager'
project(':@react-native-community_viewpager').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/viewpager/android') project(':@react-native-community_viewpager').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/viewpager/android')
include ':@react-native-firebase_app' include ':@react-native-firebase_app'

View File

@ -1,10 +1,10 @@
import React from 'react'; import React, { useContext, memo, useEffect } from 'react';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack'; import { createStackNavigator } from '@react-navigation/stack';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SetUsernameStackParamList, StackParamList } from './definitions/navigationTypes'; import { SetUsernameStackParamList, StackParamList } from './definitions/navigationTypes';
import Navigation from './lib/Navigation'; import Navigation from './lib/navigation/appNavigation';
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation'; import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
import { RootEnum } from './definitions'; import { RootEnum } from './definitions';
// Stacks // Stacks
@ -27,21 +27,23 @@ const SetUsernameStack = () => (
// App // App
const Stack = createStackNavigator<StackParamList>(); const Stack = createStackNavigator<StackParamList>();
const App = React.memo(({ root, isMasterDetail }: { root: string; isMasterDetail: boolean }) => { const App = memo(({ root, isMasterDetail }: { root: string; isMasterDetail: boolean }) => {
const { theme } = useContext(ThemeContext);
useEffect(() => {
if (root) {
const state = Navigation.navigationRef.current?.getRootState();
const currentRouteName = getActiveRouteName(state);
Navigation.routeNameRef.current = currentRouteName;
setCurrentScreen(currentRouteName);
}
}, [root]);
if (!root) { if (!root) {
return null; return null;
} }
const { theme } = React.useContext(ThemeContext);
const navTheme = navigationTheme(theme); const navTheme = navigationTheme(theme);
React.useEffect(() => {
const state = Navigation.navigationRef.current?.getRootState();
const currentRouteName = getActiveRouteName(state);
Navigation.routeNameRef.current = currentRouteName;
setCurrentScreen(currentRouteName);
}, []);
return ( return (
<NavigationContainer <NavigationContainer
theme={navTheme} theme={navTheme}

View File

@ -25,6 +25,7 @@ interface ILoginFailure extends Action {
interface ILogout extends Action { interface ILogout extends Action {
forcedByServer: boolean; forcedByServer: boolean;
message: string;
} }
interface ISetUser extends Action { interface ISetUser extends Action {
@ -79,10 +80,11 @@ export function loginFailure(err: Record<string, any>): ILoginFailure {
}; };
} }
export function logout(forcedByServer = false): ILogout { export function logout(forcedByServer = false, message = ''): ILogout {
return { return {
type: types.LOGOUT, type: types.LOGOUT,
forcedByServer forcedByServer,
message
}; };
} }

View File

@ -1,8 +1,7 @@
import { Action } from 'redux'; import { Action } from 'redux';
import { MESSAGES } from './actionsTypes'; import { MESSAGES } from './actionsTypes';
import { IMessage } from '../definitions';
type IMessage = Record<string, string>;
interface IReplyBroadcast extends Action { interface IReplyBroadcast extends Action {
message: IMessage; message: IMessage;

View File

@ -7,7 +7,7 @@ import Animated, { Easing, Extrapolate, interpolateNode, Value } from 'react-nat
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet'; import ScrollBottomSheet from 'react-native-scroll-bottom-sheet';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { useDimensions, useOrientation } from '../../dimensions'; import { useDimensions, useOrientation } from '../../dimensions';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';

View File

@ -2,7 +2,7 @@ import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
export const Handle = React.memo(() => { export const Handle = React.memo(() => {

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Text, View } from 'react-native'; import { Text, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { Button } from './Button'; import { Button } from './Button';

View File

@ -2,7 +2,7 @@ import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react';
import ActionSheet from './ActionSheet'; import ActionSheet from './ActionSheet';
export type TActionSheetOptionsItem = { title: string; icon: string; onPress: () => void }; export type TActionSheetOptionsItem = { title: string; icon: string; onPress: () => void; danger?: boolean };
export type TActionSheetOptions = { export type TActionSheetOptions = {
options: TActionSheetOptionsItem[]; options: TActionSheetOptionsItem[];

View File

@ -2,7 +2,7 @@ import React from 'react';
import { ActivityIndicator, ActivityIndicatorProps, StyleSheet } from 'react-native'; import { ActivityIndicator, ActivityIndicatorProps, StyleSheet } from 'react-native';
import { useTheme } from '../theme'; import { useTheme } from '../theme';
import { themes } from '../constants/colors'; import { themes } from '../lib/constants';
interface IActivityIndicator extends ActivityIndicatorProps { interface IActivityIndicator extends ActivityIndicatorProps {
absolute?: boolean; absolute?: boolean;

View File

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { themes } from '../constants/colors'; import { themes } from '../lib/constants';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { getReadableVersion } from '../utils/deviceInfo'; import { getReadableVersion } from '../utils/deviceInfo';
import I18n from '../i18n'; import I18n from '../i18n';
import { TSupportedThemes } from '../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -20,7 +21,7 @@ const styles = StyleSheet.create({
} }
}); });
const AppVersion = React.memo(({ theme }: { theme: string }) => ( const AppVersion = React.memo(({ theme }: { theme: TSupportedThemes }) => (
<View style={styles.container}> <View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].auxiliaryText }]}> <Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>
{I18n.t('Version_no', { version: '' })} {I18n.t('Version_no', { version: '' })}

View File

@ -28,14 +28,15 @@ const Avatar = React.memo(
text, text,
size = 25, size = 25,
borderRadius = 4, borderRadius = 4,
type = SubscriptionType.DIRECT type = SubscriptionType.DIRECT,
externalProviderUrl
}: IAvatar) => { }: IAvatar) => {
const { theme } = useTheme();
if ((!text && !avatar && !emoji && !rid) || !server) { if ((!text && !avatar && !emoji && !rid) || !server) {
return null; return null;
} }
const { theme } = useTheme();
const avatarStyle = { const avatarStyle = {
width: size, width: size,
height: size, height: size,
@ -67,7 +68,8 @@ const Avatar = React.memo(
avatarETag, avatarETag,
serverVersion, serverVersion,
rid, rid,
blockUnauthenticatedAccess blockUnauthenticatedAccess,
externalProviderUrl
}); });
} }

View File

@ -32,7 +32,10 @@ class AvatarContainer extends React.Component<IAvatar, any> {
shouldComponentUpdate(nextProps: IAvatar, nextState: { avatarETag: string }) { shouldComponentUpdate(nextProps: IAvatar, nextState: { avatarETag: string }) {
const { avatarETag } = this.state; const { avatarETag } = this.state;
const { text, type } = this.props; const { text, type, size, externalProviderUrl } = this.props;
if (nextProps.externalProviderUrl !== externalProviderUrl) {
return true;
}
if (nextState.avatarETag !== avatarETag) { if (nextState.avatarETag !== avatarETag) {
return true; return true;
} }
@ -42,6 +45,10 @@ class AvatarContainer extends React.Component<IAvatar, any> {
if (nextProps.type !== type) { if (nextProps.type !== type) {
return true; return true;
} }
if (nextProps.size !== size) {
return true;
}
return false; return false;
} }
@ -100,6 +107,7 @@ const mapStateToProps = (state: IApplicationState) => ({
blockUnauthenticatedAccess: blockUnauthenticatedAccess:
(state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess as boolean) ?? (state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess as boolean) ??
state.settings.Accounts_AvatarBlockUnauthenticatedAccess ?? state.settings.Accounts_AvatarBlockUnauthenticatedAccess ??
true true,
externalProviderUrl: state.settings.Accounts_AvatarExternalProviderUrl as string
}); });
export default connect(mapStateToProps)(AvatarContainer); export default connect(mapStateToProps)(AvatarContainer);

View File

@ -23,4 +23,5 @@ export interface IAvatar {
rid?: string; rid?: string;
blockUnauthenticatedAccess?: boolean; blockUnauthenticatedAccess?: boolean;
serverVersion: string | null; serverVersion: string | null;
externalProviderUrl?: string;
} }

View File

@ -3,7 +3,7 @@ import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'reac
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
interface IBackgroundContainer { interface IBackgroundContainer {
text?: string; text?: string;

View File

@ -2,7 +2,8 @@ import React from 'react';
import { StyleSheet, Text } from 'react-native'; import { StyleSheet, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { themes } from '../../constants/colors'; import { TSupportedThemes } from '../../theme';
import { themes } from '../../lib/constants';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
@ -13,7 +14,7 @@ interface IButtonProps {
disabled: boolean; disabled: boolean;
backgroundColor: string; backgroundColor: string;
loading: boolean; loading: boolean;
theme: string; theme: TSupportedThemes;
color: string; color: string;
fontSize: any; fontSize: any;
style: any; style: any;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors'; import { themes } from '../lib/constants';
import { useTheme } from '../theme'; import { useTheme } from '../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -0,0 +1,74 @@
import React from 'react';
import { Text, View, ViewStyle } from 'react-native';
import Touch from '../../utils/touch';
import Avatar from '../Avatar';
import RoomTypeIcon from '../RoomTypeIcon';
import styles, { ROW_HEIGHT } from './styles';
import { themes } from '../../lib/constants';
import { TSupportedThemes, useTheme } from '../../theme';
export { ROW_HEIGHT };
interface IDirectoryItemLabel {
text?: string;
theme: TSupportedThemes;
}
interface IDirectoryItem {
title: string;
description: string;
avatar: string;
type: string;
onPress(): void;
testID: string;
style?: ViewStyle;
rightLabel?: string;
rid?: string;
teamMain?: boolean;
}
const DirectoryItemLabel = React.memo(({ text, theme }: IDirectoryItemLabel) => {
if (!text) {
return null;
}
return <Text style={[styles.directoryItemLabel, { color: themes[theme].auxiliaryText }]}>{text}</Text>;
});
const DirectoryItem = ({
title,
description,
avatar,
onPress,
testID,
style,
rightLabel,
type,
rid,
teamMain
}: IDirectoryItem): React.ReactElement => {
const { theme } = useTheme();
return (
<Touch onPress={onPress} style={{ backgroundColor: themes[theme].backgroundColor }} testID={testID} theme={theme}>
<View style={[styles.directoryItemContainer, styles.directoryItemButton, style]}>
<Avatar text={avatar} size={30} type={type} rid={rid} style={styles.directoryItemAvatar} />
<View style={styles.directoryItemTextContainer}>
<View style={styles.directoryItemTextTitle}>
<RoomTypeIcon type={type} teamMain={teamMain} />
<Text style={[styles.directoryItemName, { color: themes[theme].titleText }]} numberOfLines={1}>
{title}
</Text>
</View>
{description ? (
<Text style={[styles.directoryItemUsername, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
{description}
</Text>
) : null}
</View>
<DirectoryItemLabel text={rightLabel} theme={theme} />
</View>
</Touch>
);
};
export default DirectoryItem;

View File

@ -8,6 +8,7 @@ const CustomEmoji = React.memo(
<FastImage <FastImage
style={style} style={style}
source={{ source={{
// @ts-ignore
uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.content || emoji.name)}.${emoji.extension}`, uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.content || emoji.name)}.${emoji.extension}`,
priority: FastImage.priority.high priority: FastImage.priority.high
}} }}

View File

@ -56,6 +56,7 @@ class EmojiCategory extends React.Component<Partial<IEmojiCategory>> {
contentContainerStyle={{ marginHorizontal }} contentContainerStyle={{ marginHorizontal }}
// rerender FlatList in case of width changes // rerender FlatList in case of width changes
key={`emoji-category-${width}`} key={`emoji-category-${width}`}
// @ts-ignore
keyExtractor={item => (item && item.isCustom && item.content) || item} keyExtractor={item => (item && item.isCustom && item.content) || item}
data={emojis} data={emojis}
extraData={this.props} extraData={this.props}

View File

@ -2,14 +2,15 @@ import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native'; import { Text, TouchableOpacity, View } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { TSupportedThemes } from '../../theme';
interface ITabBarProps { interface ITabBarProps {
goToPage: Function; goToPage: Function;
activeTab: number; activeTab: number;
tabs: []; tabs: [];
tabEmojiStyle: object; tabEmojiStyle: object;
theme: string; theme: TSupportedThemes;
} }
export default class TabBar extends React.Component<Partial<ITabBarProps>> { export default class TabBar extends React.Component<Partial<ITabBarProps>> {

View File

@ -15,8 +15,8 @@ import { emojisByCategory } from '../../emojis';
import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import shortnameToUnicode from '../../utils/shortnameToUnicode'; import shortnameToUnicode from '../../utils/shortnameToUnicode';
import log from '../../utils/log'; import log from '../../utils/log';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { withTheme } from '../../theme'; import { TSupportedThemes, withTheme } from '../../theme';
import { IEmoji } from '../../definitions/IEmoji'; import { IEmoji } from '../../definitions/IEmoji';
const scrollProps = { const scrollProps = {
@ -30,7 +30,7 @@ interface IEmojiPickerProps {
baseUrl: string; baseUrl: string;
customEmojis?: any; customEmojis?: any;
style: object; style: object;
theme?: string; theme: TSupportedThemes;
onEmojiSelected?: ((emoji: any) => void) | ((keyboardId: any, params?: any) => void); onEmojiSelected?: ((emoji: any) => void) | ((keyboardId: any, params?: any) => void);
tabEmojiStyle?: object; tabEmojiStyle?: object;
} }
@ -109,6 +109,7 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
const freqEmojiCollection = db.get('frequently_used_emojis'); const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord: any; let freqEmojiRecord: any;
try { try {
// @ts-ignore
freqEmojiRecord = await freqEmojiCollection.find(emoji.content); freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
} catch (error) { } catch (error) {
// Do nothing // Do nothing
@ -185,7 +186,7 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />} renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />}
/* @ts-ignore*/ /* @ts-ignore*/
contentProps={scrollProps} contentProps={scrollProps}
style={{ backgroundColor: themes[theme!].focusedBackground }}> style={{ backgroundColor: themes[theme].focusedBackground }}>
{categories.tabs.map((tab, i) => {categories.tabs.map((tab, i) =>
i === 0 && frequentlyUsed.length === 0 i === 0 && frequentlyUsed.length === 0
? null // when no frequentlyUsed don't show the tab ? null // when no frequentlyUsed don't show the tab

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { ScrollView, ScrollViewProps, StyleSheet, View } from 'react-native'; import { ScrollView, ScrollViewProps, StyleSheet, View } from 'react-native';
import { themes } from '../constants/colors'; import { themes } from '../lib/constants';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import scrollPersistTaps from '../utils/scrollPersistTaps'; import scrollPersistTaps from '../utils/scrollPersistTaps';
import KeyboardView from '../presentation/KeyboardView'; import KeyboardView from './KeyboardView';
import { useTheme } from '../theme'; import { useTheme } from '../theme';
import StatusBar from './StatusBar'; import StatusBar from './StatusBar';
import AppVersion from './AppVersion'; import AppVersion from './AppVersion';

View File

@ -2,7 +2,7 @@ import React from 'react';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { themedHeader } from '../../utils/navigation'; import { themedHeader } from '../../utils/navigation';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../../utils/deviceInfo';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';

View File

@ -4,7 +4,7 @@ import Touchable from 'react-native-platform-touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
interface IHeaderButtonItem { interface IHeaderButtonItem {

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import UnreadBadge from '../../presentation/UnreadBadge'; import UnreadBadge from '../UnreadBadge';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
badgeContainer: { badgeContainer: {

View File

@ -8,11 +8,11 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Avatar from '../Avatar'; import Avatar from '../Avatar';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { ROW_HEIGHT } from '../../presentation/RoomItem'; import { ROW_HEIGHT } from '../RoomItem';
import { goRoom } from '../../utils/goRoom'; import { goRoom } from '../../utils/goRoom';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/navigation/appNavigation';
import { useOrientation } from '../../dimensions'; import { useOrientation } from '../../dimensions';
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions'; import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';

View File

@ -5,7 +5,7 @@ import { dequal } from 'dequal';
import NotifierComponent, { INotifierComponent } from './NotifierComponent'; import NotifierComponent, { INotifierComponent } from './NotifierComponent';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/navigation/appNavigation';
import { getActiveRoute } from '../../utils/navigation'; import { getActiveRoute } from '../../utils/navigation';
import { IApplicationState } from '../../definitions'; import { IApplicationState } from '../../definitions';
import { IRoom } from '../../reducers/room'; import { IRoom } from '../../reducers/room';

View File

@ -0,0 +1,24 @@
import React from 'react';
import { KeyboardAwareScrollView, KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view';
import scrollPersistTaps from '../utils/scrollPersistTaps';
interface IKeyboardViewProps extends KeyboardAwareScrollViewProps {
keyboardVerticalOffset?: number;
scrollEnabled?: boolean;
children: React.ReactElement[] | React.ReactElement;
}
const KeyboardView = ({ style, contentContainerStyle, scrollEnabled, keyboardVerticalOffset, children }: IKeyboardViewProps) => (
<KeyboardAwareScrollView
{...scrollPersistTaps}
style={style}
contentContainerStyle={contentContainerStyle}
scrollEnabled={scrollEnabled}
alwaysBounceVertical={false}
extraHeight={keyboardVerticalOffset}>
{children}
</KeyboardAwareScrollView>
);
export default KeyboardView;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { PADDING_HORIZONTAL } from './constants'; import { PADDING_HORIZONTAL } from './constants';

View File

@ -1,16 +1,17 @@
import React from 'react'; import React from 'react';
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { ICON_SIZE } from './constants'; import { ICON_SIZE } from './constants';
interface IListIcon { interface IListIcon {
name: string; name: string;
color?: string; color?: string | null;
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
testID?: string; testID?: string;
size?: number;
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -20,12 +21,12 @@ const styles = StyleSheet.create({
} }
}); });
const ListIcon = React.memo(({ name, color, style, testID }: IListIcon) => { const ListIcon = React.memo(({ name, color, style, testID, size }: IListIcon) => {
const { theme } = useTheme(); const { theme } = useTheme();
return ( return (
<View style={[styles.icon, style]}> <View style={[styles.icon, style]}>
<CustomIcon name={name} color={color ?? themes[theme].auxiliaryText} size={ICON_SIZE} testID={testID} /> <CustomIcon name={name} color={color ?? themes[theme].auxiliaryText} size={size ?? ICON_SIZE} testID={testID} />
</View> </View>
); );
}); });

View File

@ -2,7 +2,7 @@ import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { PADDING_HORIZONTAL } from './constants'; import { PADDING_HORIZONTAL } from './constants';
import I18n from '../../i18n'; import I18n from '../../i18n';

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { I18nManager, StyleSheet, Text, View } from 'react-native'; import { I18nManager, StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { useTheme } from '../../theme'; import { TSupportedThemes, useTheme } from '../../theme';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { Icon } from '.'; import { Icon } from '.';
import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants'; import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants';
@ -59,13 +59,15 @@ interface IListItemContent {
left?: () => JSX.Element | null; left?: () => JSX.Element | null;
right?: () => JSX.Element | null; right?: () => JSX.Element | null;
disabled?: boolean; disabled?: boolean;
theme: string; theme: TSupportedThemes;
testID?: string; testID?: string;
color?: string; color?: string;
translateTitle?: boolean; translateTitle?: boolean;
translateSubtitle?: boolean; translateSubtitle?: boolean;
showActionIndicator?: boolean; showActionIndicator?: boolean;
alert?: boolean; alert?: boolean;
heightContainer?: number;
styleTitle?: StyleProp<TextStyle>;
} }
const Content = React.memo( const Content = React.memo(
@ -81,17 +83,21 @@ const Content = React.memo(
translateTitle = true, translateTitle = true,
translateSubtitle = true, translateSubtitle = true,
showActionIndicator = false, showActionIndicator = false,
theme theme,
heightContainer,
styleTitle
}: IListItemContent) => { }: IListItemContent) => {
const { fontScale } = useDimensions(); const { fontScale } = useDimensions();
return ( return (
<View style={[styles.container, disabled && styles.disabled, { height: BASE_HEIGHT * fontScale }]} testID={testID}> <View
style={[styles.container, disabled && styles.disabled, { height: (heightContainer || BASE_HEIGHT) * fontScale }]}
testID={testID}>
{left ? <View style={styles.leftContainer}>{left()}</View> : null} {left ? <View style={styles.leftContainer}>{left()}</View> : null}
<View style={styles.textContainer}> <View style={styles.textContainer}>
<View style={styles.textAlertContainer}> <View style={styles.textAlertContainer}>
<Text style={[styles.title, { color: color || themes[theme].titleText }]} numberOfLines={1}> <Text style={[styles.title, styleTitle, { color: color || themes[theme].titleText }]} numberOfLines={1}>
{translateTitle ? I18n.t(title) : title} {translateTitle && title ? I18n.t(title) : title}
</Text> </Text>
{alert ? ( {alert ? (
<CustomIcon style={[styles.alertIcon, { color: themes[theme].dangerColor }]} size={ICON_SIZE} name='info' /> <CustomIcon style={[styles.alertIcon, { color: themes[theme].dangerColor }]} size={ICON_SIZE} name='info' />
@ -121,7 +127,7 @@ interface IListButtonPress extends IListItemButton {
interface IListItemButton { interface IListItemButton {
title?: string; title?: string;
disabled?: boolean; disabled?: boolean;
theme: string; theme: TSupportedThemes;
backgroundColor?: string; backgroundColor?: string;
underlayColor?: string; underlayColor?: string;
} }

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View, ViewStyle } from 'react-native'; import { StyleSheet, View, ViewStyle } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { Animated, Modal, StyleSheet, View } from 'react-native'; import { Animated, Modal, StyleSheet, View } from 'react-native';
import { withTheme } from '../theme'; import { TSupportedThemes, withTheme } from '../theme';
import { themes } from '../constants/colors'; import { themes } from '../lib/constants';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -19,7 +19,7 @@ const styles = StyleSheet.create({
interface ILoadingProps { interface ILoadingProps {
visible: boolean; visible: boolean;
theme?: string; theme?: TSupportedThemes;
} }
interface ILoadingState { interface ILoadingState {

View File

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import { Animated, Easing, Linking, StyleSheet, Text, View } from 'react-native'; import { Animated, Easing, Linking, StyleSheet, Text, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import * as AppleAuthentication from 'expo-apple-authentication'; import * as AppleAuthentication from 'expo-apple-authentication';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import { withTheme } from '../theme'; import { TSupportedThemes, withTheme } from '../theme';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../lib/constants';
import Button from './Button'; import Button from './Button';
import OrSeparator from './OrSeparator'; import OrSeparator from './OrSeparator';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
@ -100,7 +100,7 @@ interface ILoginServicesProps {
CAS_enabled: boolean; CAS_enabled: boolean;
CAS_login_url: string; CAS_login_url: string;
separator: boolean; separator: boolean;
theme: string; theme: TSupportedThemes;
} }
interface ILoginServicesState { interface ILoginServicesState {
@ -410,7 +410,7 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, ILoginServi
const { servicesHeight } = this.state; const { servicesHeight } = this.state;
const { services, separator } = this.props; const { services, separator } = this.props;
const { length } = Object.values(services); const { length } = Object.values(services);
const style = { const style: Animated.AnimatedProps<ViewStyle> = {
overflow: 'hidden', overflow: 'hidden',
height: servicesHeight height: servicesHeight
}; };

View File

@ -1,8 +1,8 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native'; import { FlatList, StyleSheet, Text, View } from 'react-native';
import { withTheme } from '../../theme'; import { TSupportedThemes, useTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import shortnameToUnicode from '../../utils/shortnameToUnicode'; import shortnameToUnicode from '../../utils/shortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
@ -10,27 +10,31 @@ import database from '../../lib/database';
import { Button } from '../ActionSheet'; import { Button } from '../ActionSheet';
import { useDimensions } from '../../dimensions'; import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { IEmoji } from '../../definitions/IEmoji';
import { TFrequentlyUsedEmojiModel } from '../../definitions/IFrequentlyUsedEmoji'; import { TFrequentlyUsedEmojiModel } from '../../definitions/IFrequentlyUsedEmoji';
import { TAnyMessageModel } from '../../definitions';
import { IEmoji } from '../../definitions/IEmoji';
interface IHeader { type TItem = TFrequentlyUsedEmojiModel | string;
handleReaction: Function;
export interface IHeader {
handleReaction: (emoji: TItem, message: TAnyMessageModel) => void;
server: string; server: string;
message: object; message: TAnyMessageModel;
isMasterDetail: boolean; isMasterDetail: boolean;
theme?: string;
} }
type TOnReaction = ({ emoji }: { emoji: TItem }) => void;
interface THeaderItem { interface THeaderItem {
item: IEmoji; item: TItem;
onReaction: Function; onReaction: TOnReaction;
server: string; server: string;
theme: string; theme: TSupportedThemes;
} }
interface THeaderFooter { interface THeaderFooter {
onReaction: any; onReaction: TOnReaction;
theme: string; theme: TSupportedThemes;
} }
export const HEADER_HEIGHT = 36; export const HEADER_HEIGHT = 36;
@ -62,25 +66,32 @@ const styles = StyleSheet.create({
} }
}); });
const keyExtractor = (item: any) => item?.id || item; const keyExtractor = (item: TItem) => {
const emojiModel = item as TFrequentlyUsedEmojiModel;
return (emojiModel.id ? emojiModel.content : item) as string;
};
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley']; const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];
const HeaderItem = React.memo(({ item, onReaction, server, theme }: THeaderItem) => ( const HeaderItem = ({ item, onReaction, server, theme }: THeaderItem) => {
<Button const emojiModel = item as TFrequentlyUsedEmojiModel;
testID={`message-actions-emoji-${item.content || item}`} const emoji = (emojiModel.id ? emojiModel.content : item) as string;
onPress={() => onReaction({ emoji: `:${item.content || item}:` })} return (
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]} <Button
theme={theme}> testID={`message-actions-emoji-${emoji}`}
{item?.isCustom ? ( onPress={() => onReaction({ emoji: `:${emoji}:` })}
<CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} /> style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
) : ( theme={theme}>
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${item.content || item}:`)}</Text> {emojiModel?.isCustom ? (
)} <CustomEmoji style={styles.customEmoji} emoji={emojiModel as IEmoji} baseUrl={server} />
</Button> ) : (
)); <Text style={styles.headerIcon}>{shortnameToUnicode(`:${emoji}:`)}</Text>
)}
</Button>
);
};
const HeaderFooter = React.memo(({ onReaction, theme }: THeaderFooter) => ( const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
<Button <Button
testID='add-reaction' testID='add-reaction'
onPress={onReaction} onPress={onReaction}
@ -88,17 +99,19 @@ const HeaderFooter = React.memo(({ onReaction, theme }: THeaderFooter) => (
theme={theme}> theme={theme}>
<CustomIcon name='reaction-add' size={24} color={themes[theme].bodyText} /> <CustomIcon name='reaction-add' size={24} color={themes[theme].bodyText} />
</Button> </Button>
)); );
const Header = React.memo(({ handleReaction, server, message, isMasterDetail, theme }: IHeader) => { const Header = React.memo(({ handleReaction, server, message, isMasterDetail }: IHeader) => {
const [items, setItems] = useState<(TFrequentlyUsedEmojiModel | string)[]>([]); const [items, setItems] = useState<TItem[]>([]);
const { width, height }: any = useDimensions(); const { width, height } = useDimensions();
const { theme } = useTheme();
// TODO: create custom hook to re-render based on screen size
const setEmojis = async () => { const setEmojis = async () => {
try { try {
const db = database.active; const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis'); const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojis: (TFrequentlyUsedEmojiModel | string)[] = await freqEmojiCollection.query().fetch(); let freqEmojis: TItem[] = await freqEmojiCollection.query().fetch();
const isLandscape = width > height; const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2; const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
@ -115,22 +128,21 @@ const Header = React.memo(({ handleReaction, server, message, isMasterDetail, th
setEmojis(); setEmojis();
}, []); }, []);
const onReaction = ({ emoji }: { emoji: IEmoji }) => handleReaction(emoji, message); const onReaction: TOnReaction = ({ emoji }) => handleReaction(emoji, message);
const renderItem = useCallback( const renderItem = ({ item }: { item: TItem }) => (
({ item }) => <HeaderItem item={item} onReaction={onReaction} server={server} theme={theme!} />, <HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />
[]
); );
const renderFooter = useCallback(() => <HeaderFooter onReaction={onReaction} theme={theme!} />, []); const renderFooter = () => <HeaderFooter onReaction={onReaction} theme={theme} />;
return ( return (
<View style={[styles.container, { backgroundColor: themes[theme!].focusedBackground }]}> <View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
<FlatList <FlatList
data={items} data={items}
renderItem={renderItem} renderItem={renderItem}
ListFooterComponent={renderFooter} ListFooterComponent={renderFooter}
style={{ backgroundColor: themes[theme!].focusedBackground }} style={{ backgroundColor: themes[theme].focusedBackground }}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
scrollEnabled={false} scrollEnabled={false}
@ -140,4 +152,4 @@ const Header = React.memo(({ handleReaction, server, message, isMasterDetail, th
); );
}); });
export default withTheme(Header); export default Header;

View File

@ -8,38 +8,38 @@ import RocketChat from '../../lib/rocketchat';
import database from '../../lib/database'; import database from '../../lib/database';
import I18n from '../../i18n'; import I18n from '../../i18n';
import log, { logEvent } from '../../utils/log'; import log, { logEvent } from '../../utils/log';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/navigation/appNavigation';
import { getMessageTranslation } from '../message/utils'; import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast'; import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import { showConfirmationAlert } from '../../utils/info'; import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet'; import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header'; import Header, { HEADER_HEIGHT, IHeader } from './Header';
import events from '../../utils/log/events'; import events from '../../utils/log/events';
import { ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions'; import { IApplicationState, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
export interface IMessageActions { export interface IMessageActions {
room: TSubscriptionModel; room: TSubscriptionModel;
tmid?: string; tmid?: string;
user: Pick<ILoggedUser, 'id'>; user: Pick<ILoggedUser, 'id'>;
editInit: Function; editInit: (message: TAnyMessageModel) => void;
reactionInit: Function; reactionInit: (message: TAnyMessageModel) => void;
onReactionPress: Function; onReactionPress: (shortname: string, messageId: string) => void;
replyInit: Function; replyInit: (message: TAnyMessageModel, mention: boolean) => void;
isMasterDetail: boolean; isMasterDetail: boolean;
isReadOnly: boolean; isReadOnly: boolean;
Message_AllowDeleting: boolean; Message_AllowDeleting?: boolean;
Message_AllowDeleting_BlockDeleteInMinutes: number; Message_AllowDeleting_BlockDeleteInMinutes?: number;
Message_AllowEditing: boolean; Message_AllowEditing?: boolean;
Message_AllowEditing_BlockEditInMinutes: number; Message_AllowEditing_BlockEditInMinutes?: number;
Message_AllowPinning: boolean; Message_AllowPinning?: boolean;
Message_AllowStarring: boolean; Message_AllowStarring?: boolean;
Message_Read_Receipt_Store_Users: boolean; Message_Read_Receipt_Store_Users?: boolean;
server: string; server: string;
editMessagePermission: []; editMessagePermission?: string[];
deleteMessagePermission: []; deleteMessagePermission?: string[];
forceDeleteMessagePermission: []; forceDeleteMessagePermission?: string[];
pinMessagePermission: []; pinMessagePermission?: string[];
} }
const MessageActions = React.memo( const MessageActions = React.memo(
@ -69,9 +69,14 @@ const MessageActions = React.memo(
pinMessagePermission pinMessagePermission
}: IMessageActions, }: IMessageActions,
ref ref
): any => { ) => {
let permissions: any = {}; let permissions = {
const { showActionSheet, hideActionSheet }: any = useActionSheet(); hasEditPermission: false,
hasDeletePermission: false,
hasForceDeletePermission: false,
hasPinPermission: false
};
const { showActionSheet, hideActionSheet } = useActionSheet();
const getPermissions = async () => { const getPermissions = async () => {
try { try {
@ -88,9 +93,9 @@ const MessageActions = React.memo(
} }
}; };
const isOwn = (message: any) => message.u && message.u._id === user.id; const isOwn = (message: TAnyMessageModel) => message.u && message.u._id === user.id;
const allowEdit = (message: any) => { const allowEdit = (message: TAnyMessageModel) => {
if (isReadOnly) { if (isReadOnly) {
return false; return false;
} }
@ -105,7 +110,7 @@ const MessageActions = React.memo(
if (message.ts != null) { if (message.ts != null) {
msgTs = moment(message.ts); msgTs = moment(message.ts);
} }
let currentTsDiff: any; let currentTsDiff = 0;
if (msgTs != null) { if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes'); currentTsDiff = moment().diff(msgTs, 'minutes');
} }
@ -114,7 +119,7 @@ const MessageActions = React.memo(
return true; return true;
}; };
const allowDelete = (message: any) => { const allowDelete = (message: TAnyMessageModel) => {
if (isReadOnly) { if (isReadOnly) {
return false; return false;
} }
@ -136,7 +141,7 @@ const MessageActions = React.memo(
if (message.ts != null) { if (message.ts != null) {
msgTs = moment(message.ts); msgTs = moment(message.ts);
} }
let currentTsDiff: any; let currentTsDiff = 0;
if (msgTs != null) { if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes'); currentTsDiff = moment().diff(msgTs, 'minutes');
} }
@ -145,19 +150,19 @@ const MessageActions = React.memo(
return true; return true;
}; };
const getPermalink = (message: any) => RocketChat.getPermalinkMessage(message); const getPermalink = (message: TAnyMessageModel) => RocketChat.getPermalinkMessage(message);
const handleReply = (message: any) => { const handleReply = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_REPLY); logEvent(events.ROOM_MSG_ACTION_REPLY);
replyInit(message, true); replyInit(message, true);
}; };
const handleEdit = (message: any) => { const handleEdit = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_EDIT); logEvent(events.ROOM_MSG_ACTION_EDIT);
editInit(message); editInit(message);
}; };
const handleCreateDiscussion = (message: any) => { const handleCreateDiscussion = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_DISCUSSION); logEvent(events.ROOM_MSG_ACTION_DISCUSSION);
const params = { message, channel: room, showCloseModal: true }; const params = { message, channel: room, showCloseModal: true };
if (isMasterDetail) { if (isMasterDetail) {
@ -167,7 +172,7 @@ const MessageActions = React.memo(
} }
}; };
const handleUnread = async (message: any) => { const handleUnread = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_UNREAD); logEvent(events.ROOM_MSG_ACTION_UNREAD);
const { id: messageId, ts } = message; const { id: messageId, ts } = message;
const { rid } = room; const { rid } = room;
@ -179,7 +184,7 @@ const MessageActions = React.memo(
const subRecord = await subCollection.find(rid); const subRecord = await subCollection.find(rid);
await db.write(async () => { await db.write(async () => {
try { try {
await subRecord.update(sub => (sub.lastOpen = ts)); await subRecord.update(sub => (sub.lastOpen = ts as Date)); // TODO: reevaluate IMessage
} catch { } catch {
// do nothing // do nothing
} }
@ -192,42 +197,44 @@ const MessageActions = React.memo(
} }
}; };
const handlePermalink = async (message: any) => { const handlePermalink = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_PERMALINK); logEvent(events.ROOM_MSG_ACTION_PERMALINK);
try { try {
const permalink: any = await getPermalink(message); const permalink = await getPermalink(message);
Clipboard.setString(permalink); Clipboard.setString(permalink ?? '');
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') }); EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
} catch { } catch {
logEvent(events.ROOM_MSG_ACTION_PERMALINK_F); logEvent(events.ROOM_MSG_ACTION_PERMALINK_F);
} }
}; };
const handleCopy = async (message: any) => { const handleCopy = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_COPY); logEvent(events.ROOM_MSG_ACTION_COPY);
await Clipboard.setString(message?.attachments?.[0]?.description || message.msg); await Clipboard.setString((message?.attachments?.[0]?.description || message.msg) ?? '');
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') }); EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
}; };
const handleShare = async (message: any) => { const handleShare = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_SHARE); logEvent(events.ROOM_MSG_ACTION_SHARE);
try { try {
const permalink: any = await getPermalink(message); const permalink = await getPermalink(message);
Share.share({ message: permalink }); if (permalink) {
Share.share({ message: permalink });
}
} catch { } catch {
logEvent(events.ROOM_MSG_ACTION_SHARE_F); logEvent(events.ROOM_MSG_ACTION_SHARE_F);
} }
}; };
const handleQuote = (message: any) => { const handleQuote = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_QUOTE); logEvent(events.ROOM_MSG_ACTION_QUOTE);
replyInit(message, false); replyInit(message, false);
}; };
const handleStar = async (message: any) => { const handleStar = async (message: TAnyMessageModel) => {
logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR); logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR);
try { try {
await RocketChat.toggleStarMessage(message.id, message.starred); await RocketChat.toggleStarMessage(message.id, message.starred as boolean); // TODO: reevaluate `message.starred` type on IMessage
EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') }); EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
} catch (e) { } catch (e) {
logEvent(events.ROOM_MSG_ACTION_STAR_F); logEvent(events.ROOM_MSG_ACTION_STAR_F);
@ -235,20 +242,21 @@ const MessageActions = React.memo(
} }
}; };
const handlePin = async (message: any) => { const handlePin = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_PIN); logEvent(events.ROOM_MSG_ACTION_PIN);
try { try {
await RocketChat.togglePinMessage(message.id, message.pinned); await RocketChat.togglePinMessage(message.id, message.pinned as boolean); // TODO: reevaluate `message.pinned` type on IMessage
} catch (e) { } catch (e) {
logEvent(events.ROOM_MSG_ACTION_PIN_F); logEvent(events.ROOM_MSG_ACTION_PIN_F);
log(e); log(e);
} }
}; };
const handleReaction = (shortname: any, message: any) => { const handleReaction: IHeader['handleReaction'] = (shortname, message) => {
logEvent(events.ROOM_MSG_ACTION_REACTION); logEvent(events.ROOM_MSG_ACTION_REACTION);
if (shortname) { if (shortname) {
onReactionPress(shortname, message.id); // TODO: evaluate unification with IEmoji
onReactionPress(shortname as any, message.id);
} else { } else {
reactionInit(message); reactionInit(message);
} }
@ -256,7 +264,7 @@ const MessageActions = React.memo(
hideActionSheet(); hideActionSheet();
}; };
const handleReadReceipt = (message: any) => { const handleReadReceipt = (message: TAnyMessageModel) => {
if (isMasterDetail) { if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'ReadReceiptsView', params: { messageId: message.id } }); Navigation.navigate('ModalStackNavigator', { screen: 'ReadReceiptsView', params: { messageId: message.id } });
} else { } else {
@ -291,7 +299,7 @@ const MessageActions = React.memo(
} }
}; };
const handleReport = async (message: any) => { const handleReport = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_REPORT); logEvent(events.ROOM_MSG_ACTION_REPORT);
try { try {
await RocketChat.reportMessage(message.id); await RocketChat.reportMessage(message.id);
@ -302,14 +310,14 @@ const MessageActions = React.memo(
} }
}; };
const handleDelete = (message: any) => { const handleDelete = (message: TAnyMessageModel) => {
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('You_will_not_be_able_to_recover_this_message'), message: I18n.t('You_will_not_be_able_to_recover_this_message'),
confirmationText: I18n.t('Delete'), confirmationText: I18n.t('Delete'),
onPress: async () => { onPress: async () => {
try { try {
logEvent(events.ROOM_MSG_ACTION_DELETE); logEvent(events.ROOM_MSG_ACTION_DELETE);
await RocketChat.deleteMessage(message.id, message.subscription.id); await RocketChat.deleteMessage(message.id, message.subscription ? message.subscription.id : '');
} catch (e) { } catch (e) {
logEvent(events.ROOM_MSG_ACTION_DELETE_F); logEvent(events.ROOM_MSG_ACTION_DELETE_F);
log(e); log(e);
@ -319,7 +327,7 @@ const MessageActions = React.memo(
}; };
const getOptions = (message: TAnyMessageModel) => { const getOptions = (message: TAnyMessageModel) => {
let options: any = []; let options: TActionSheetOptionsItem[] = [];
// Reply // Reply
if (!isReadOnly) { if (!isReadOnly) {
@ -463,16 +471,15 @@ const MessageActions = React.memo(
} }
) )
); );
const mapStateToProps = (state: IApplicationState) => ({
const mapStateToProps = (state: any) => ({
server: state.server.server, server: state.server.server,
Message_AllowDeleting: state.settings.Message_AllowDeleting, Message_AllowDeleting: state.settings.Message_AllowDeleting as boolean,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes, Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes as number,
Message_AllowEditing: state.settings.Message_AllowEditing, Message_AllowEditing: state.settings.Message_AllowEditing as boolean,
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes, Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes as number,
Message_AllowPinning: state.settings.Message_AllowPinning, Message_AllowPinning: state.settings.Message_AllowPinning as boolean,
Message_AllowStarring: state.settings.Message_AllowStarring, Message_AllowStarring: state.settings.Message_AllowStarring as boolean,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users, Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users as boolean,
isMasterDetail: state.app.isMasterDetail, isMasterDetail: state.app.isMasterDetail,
editMessagePermission: state.permissions['edit-message'], editMessagePermission: state.permissions['edit-message'],
deleteMessagePermission: state.permissions['delete-message'], deleteMessagePermission: state.permissions['delete-message'],

View File

@ -1,12 +1,13 @@
import FastImage from '@rocket.chat/react-native-fast-image';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { TouchableOpacity } from 'react-native'; import { TouchableOpacity } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
import styles from '../styles'; import { themes } from '../../../lib/constants';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
import { themes } from '../../../constants/colors'; import { useTheme } from '../../../theme';
import MessageboxContext from '../Context';
import ActivityIndicator from '../../ActivityIndicator'; import ActivityIndicator from '../../ActivityIndicator';
import MessageboxContext from '../Context';
import styles from '../styles';
interface IMessageBoxCommandsPreviewItem { interface IMessageBoxCommandsPreviewItem {
item: { item: {
@ -14,13 +15,13 @@ interface IMessageBoxCommandsPreviewItem {
id: string; id: string;
value: string; value: string;
}; };
theme?: string;
} }
const Item = ({ item, theme }: IMessageBoxCommandsPreviewItem) => { const Item = ({ item }: IMessageBoxCommandsPreviewItem) => {
const context = useContext(MessageboxContext); const context = useContext(MessageboxContext);
const { onPressCommandPreview } = context; const { onPressCommandPreview } = context;
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { theme } = useTheme();
return ( return (
<TouchableOpacity <TouchableOpacity
@ -37,7 +38,7 @@ const Item = ({ item, theme }: IMessageBoxCommandsPreviewItem) => {
{loading ? <ActivityIndicator /> : null} {loading ? <ActivityIndicator /> : null}
</FastImage> </FastImage>
) : ( ) : (
<CustomIcon name='attach' size={36} color={themes[theme!].actionTintColor} /> <CustomIcon name='attach' size={36} color={themes[theme].actionTintColor} />
)} )}
</TouchableOpacity> </TouchableOpacity>
); );

View File

@ -1,30 +1,32 @@
import { dequal } from 'dequal';
import React from 'react'; import React from 'react';
import { FlatList } from 'react-native'; import { FlatList } from 'react-native';
import { dequal } from 'dequal';
import Item from './Item'; import { themes } from '../../../lib/constants';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { withTheme } from '../../../theme';
import { IPreviewItem } from '../../../definitions'; import { IPreviewItem } from '../../../definitions';
import { useTheme } from '../../../theme';
import styles from '../styles';
import Item from './Item';
interface IMessageBoxCommandsPreview { interface IMessageBoxCommandsPreview {
commandPreview: IPreviewItem[]; commandPreview: IPreviewItem[];
showCommandPreview: boolean; showCommandPreview: boolean;
theme?: string;
} }
const CommandsPreview = React.memo( const CommandsPreview = React.memo(
({ theme, commandPreview, showCommandPreview }: IMessageBoxCommandsPreview) => { ({ commandPreview, showCommandPreview }: IMessageBoxCommandsPreview) => {
const { theme } = useTheme();
if (!showCommandPreview) { if (!showCommandPreview) {
return null; return null;
} }
return ( return (
<FlatList <FlatList
testID='commandbox-container' testID='commandbox-container'
style={[styles.mentionList, { backgroundColor: themes[theme!].messageboxBackground }]} style={[styles.mentionList, { backgroundColor: themes[theme].messageboxBackground }]}
data={commandPreview} data={commandPreview}
renderItem={({ item }) => <Item item={item} theme={theme} />} renderItem={({ item }) => <Item item={item} />}
keyExtractor={(item: any) => item.id} keyExtractor={(item: any) => item.id}
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
horizontal horizontal
@ -33,9 +35,6 @@ const CommandsPreview = React.memo(
); );
}, },
(prevProps, nextProps) => { (prevProps, nextProps) => {
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (prevProps.showCommandPreview !== nextProps.showCommandPreview) { if (prevProps.showCommandPreview !== nextProps.showCommandPreview) {
return false; return false;
} }
@ -46,4 +45,4 @@ const CommandsPreview = React.memo(
} }
); );
export default withTheme(CommandsPreview); export default CommandsPreview;

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
// @ts-ignore const MessageboxContext = React.createContext<any>(null);
const MessageboxContext = React.createContext<any>();
export default MessageboxContext; export default MessageboxContext;

View File

@ -2,14 +2,15 @@ import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { KeyboardRegistry } from 'react-native-ui-lib/keyboard'; import { KeyboardRegistry } from 'react-native-ui-lib/keyboard';
import { store } from '../../lib/auxStore'; import { store } from '../../lib/store/auxStore';
import EmojiPicker from '../EmojiPicker'; import EmojiPicker from '../EmojiPicker';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { withTheme } from '../../theme'; import { TSupportedThemes, withTheme } from '../../theme';
import { IEmoji } from '../../definitions/IEmoji';
interface IMessageBoxEmojiKeyboard { interface IMessageBoxEmojiKeyboard {
theme: string; theme: TSupportedThemes;
} }
export default class EmojiKeyboard extends React.PureComponent<IMessageBoxEmojiKeyboard, any> { export default class EmojiKeyboard extends React.PureComponent<IMessageBoxEmojiKeyboard, any> {
@ -21,7 +22,7 @@ export default class EmojiKeyboard extends React.PureComponent<IMessageBoxEmojiK
this.baseUrl = state.share.server.server || state.server.server; this.baseUrl = state.share.server.server || state.server.server;
} }
onEmojiSelected = (emoji: any) => { onEmojiSelected = (emoji: IEmoji) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji }); KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
}; };

View File

@ -3,7 +3,6 @@ import React from 'react';
import { CancelEditingButton, ToggleEmojiButton } from './buttons'; import { CancelEditingButton, ToggleEmojiButton } from './buttons';
interface IMessageBoxLeftButtons { interface IMessageBoxLeftButtons {
theme: string;
showEmojiKeyboard: boolean; showEmojiKeyboard: boolean;
openEmoji(): void; openEmoji(): void;
closeEmoji(): void; closeEmoji(): void;
@ -11,13 +10,11 @@ interface IMessageBoxLeftButtons {
editCancel(): void; editCancel(): void;
} }
const LeftButtons = React.memo( const LeftButtons = React.memo(({ showEmojiKeyboard, editing, editCancel, openEmoji, closeEmoji }: IMessageBoxLeftButtons) => {
({ theme, showEmojiKeyboard, editing, editCancel, openEmoji, closeEmoji }: IMessageBoxLeftButtons) => { if (editing) {
if (editing) { return <CancelEditingButton onPress={editCancel} />;
return <CancelEditingButton onPress={editCancel} theme={theme} />;
}
return <ToggleEmojiButton show={showEmojiKeyboard} open={openEmoji} close={closeEmoji} theme={theme} />;
} }
); return <ToggleEmojiButton show={showEmojiKeyboard} open={openEmoji} close={closeEmoji} />;
});
export default LeftButtons; export default LeftButtons;

View File

@ -5,23 +5,20 @@ import { ActionsButton, CancelEditingButton } from './buttons';
import styles from './styles'; import styles from './styles';
interface IMessageBoxLeftButtons { interface IMessageBoxLeftButtons {
theme: string;
showMessageBoxActions(): void; showMessageBoxActions(): void;
editing: boolean; editing: boolean;
editCancel(): void; editCancel(): void;
isActionsEnabled: boolean; isActionsEnabled: boolean;
} }
const LeftButtons = React.memo( const LeftButtons = React.memo(({ showMessageBoxActions, editing, editCancel, isActionsEnabled }: IMessageBoxLeftButtons) => {
({ theme, showMessageBoxActions, editing, editCancel, isActionsEnabled }: IMessageBoxLeftButtons) => { if (editing) {
if (editing) { return <CancelEditingButton onPress={editCancel} />;
return <CancelEditingButton onPress={editCancel} theme={theme} />;
}
if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
}
return <View style={styles.buttonsWhitespace} />;
} }
); if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} />;
}
return <View style={styles.buttonsWhitespace} />;
});
export default LeftButtons; export default LeftButtons;

View File

@ -3,31 +3,34 @@ import { Text, TouchableOpacity } from 'react-native';
import styles from '../styles'; import styles from '../styles';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
import { themes } from '../../../constants/colors'; import { themes } from '../../../lib/constants';
import { useTheme } from '../../../theme';
interface IMessageBoxFixedMentionItem { interface IMessageBoxFixedMentionItem {
item: { item: {
username: string; username: string;
}; };
onPress: Function; onPress: Function;
theme: string;
} }
const FixedMentionItem = ({ item, onPress, theme }: IMessageBoxFixedMentionItem) => ( const FixedMentionItem = ({ item, onPress }: IMessageBoxFixedMentionItem) => {
<TouchableOpacity const { theme } = useTheme();
style={[ return (
styles.mentionItem, <TouchableOpacity
{ style={[
backgroundColor: themes[theme].auxiliaryBackground, styles.mentionItem,
borderTopColor: themes[theme].separatorColor {
} backgroundColor: themes[theme].auxiliaryBackground,
]} borderTopColor: themes[theme].separatorColor
onPress={() => onPress(item)}> }
<Text style={[styles.fixedMentionAvatar, { color: themes[theme].titleText }]}>{item.username}</Text> ]}
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}> onPress={() => onPress(item)}>
{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')} <Text style={[styles.fixedMentionAvatar, { color: themes[theme].titleText }]}>{item.username}</Text>
</Text> <Text style={[styles.mentionText, { color: themes[theme].titleText }]}>
</TouchableOpacity> {item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}
); </Text>
</TouchableOpacity>
);
};
export default FixedMentionItem; export default FixedMentionItem;

View File

@ -1,12 +1,11 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { Text } from 'react-native'; import { Text } from 'react-native';
import PropTypes from 'prop-types';
import shortnameToUnicode from '../../../utils/shortnameToUnicode';
import styles from '../styles';
import MessageboxContext from '../Context';
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import { IEmoji } from '../../../definitions/IEmoji'; import { IEmoji } from '../../../definitions/IEmoji';
import shortnameToUnicode from '../../../utils/shortnameToUnicode';
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import MessageboxContext from '../Context';
import styles from '../styles';
interface IMessageBoxMentionEmoji { interface IMessageBoxMentionEmoji {
item: IEmoji; item: IEmoji;
@ -22,8 +21,4 @@ const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => {
return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>; return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>;
}; };
MentionEmoji.propTypes = {
item: PropTypes.object
};
export default MentionEmoji; export default MentionEmoji;

View File

@ -1,16 +1,23 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { View, Text, ActivityIndicator, TouchableOpacity } from 'react-native'; import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
import PropTypes from 'prop-types';
import { MENTIONS_TRACKING_TYPE_CANNED } from '../constants'; import { themes } from '../../../lib/constants';
import styles from '../styles';
import sharedStyles from '../../../views/Styles';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
import { themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
import { useTheme } from '../../../theme';
import sharedStyles from '../../../views/Styles';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import styles from '../styles';
import { MENTIONS_TRACKING_TYPE_CANNED } from '../constants';
const MentionHeaderList = ({ trackingType, hasMentions, theme, loading }) => { interface IMentionHeaderList {
trackingType: string;
hasMentions: boolean;
loading: boolean;
}
const MentionHeaderList = ({ trackingType, hasMentions, loading }: IMentionHeaderList) => {
const { theme } = useTheme();
const context = useContext(MessageboxContext); const context = useContext(MessageboxContext);
const { onPressNoMatchCanned } = context; const { onPressNoMatchCanned } = context;
@ -39,11 +46,4 @@ const MentionHeaderList = ({ trackingType, hasMentions, theme, loading }) => {
return null; return null;
}; };
MentionHeaderList.propTypes = {
trackingType: PropTypes.string,
hasMentions: PropTypes.bool,
theme: PropTypes.string,
loading: PropTypes.bool
};
export default MentionHeaderList; export default MentionHeaderList;

View File

@ -1,14 +1,15 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { Text, TouchableOpacity } from 'react-native'; import { Text, TouchableOpacity } from 'react-native';
import styles from '../styles'; import { themes } from '../../../lib/constants';
import { IEmoji } from '../../../definitions/IEmoji';
import { useTheme } from '../../../theme';
import Avatar from '../../Avatar'; import Avatar from '../../Avatar';
import { MENTIONS_TRACKING_TYPE_CANNED, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_EMOJIS } from '../constants';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import styles from '../styles';
import FixedMentionItem from './FixedMentionItem'; import FixedMentionItem from './FixedMentionItem';
import MentionEmoji from './MentionEmoji'; import MentionEmoji from './MentionEmoji';
import { MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_CANNED } from '../constants';
import { themes } from '../../../constants/colors';
import { IEmoji } from '../../../definitions/IEmoji';
interface IMessageBoxMentionItem { interface IMessageBoxMentionItem {
item: { item: {
@ -21,11 +22,48 @@ interface IMessageBoxMentionItem {
text: string; text: string;
} & IEmoji; } & IEmoji;
trackingType: string; trackingType: string;
theme: string;
} }
const MentionItem = ({ item, trackingType, theme }: IMessageBoxMentionItem) => { const MentionItemContent = React.memo(({ trackingType, item }: IMessageBoxMentionItem) => {
const { theme } = useTheme();
switch (trackingType) {
case MENTIONS_TRACKING_TYPE_EMOJIS:
return (
<>
<MentionEmoji item={item} />
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>:{item.name || item}:</Text>
</>
);
case MENTIONS_TRACKING_TYPE_COMMANDS:
return (
<>
<Text style={[styles.slash, { backgroundColor: themes[theme].borderColor, color: themes[theme].tintColor }]}>/</Text>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.id}</Text>
</>
);
case MENTIONS_TRACKING_TYPE_CANNED:
return (
<>
<Text style={[styles.cannedItem, { color: themes[theme].titleText }]}>!{item.shortcut}</Text>
<Text numberOfLines={1} style={[styles.cannedMentionText, { color: themes[theme].auxiliaryTintColor }]}>
{item.text}
</Text>
</>
);
default:
return (
<>
<Avatar style={styles.avatar} text={item.username || item.name} size={30} type={item.t} />
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.username || item.name || item}</Text>
</>
);
}
});
const MentionItem = ({ item, trackingType }: IMessageBoxMentionItem) => {
const context = useContext(MessageboxContext); const context = useContext(MessageboxContext);
const { theme } = useTheme();
const { onPressMention } = context; const { onPressMention } = context;
const defineTestID = (type: string) => { const defineTestID = (type: string) => {
@ -44,43 +82,7 @@ const MentionItem = ({ item, trackingType, theme }: IMessageBoxMentionItem) => {
const testID = defineTestID(trackingType); const testID = defineTestID(trackingType);
if (item.username === 'all' || item.username === 'here') { if (item.username === 'all' || item.username === 'here') {
return <FixedMentionItem item={item} onPress={onPressMention} theme={theme} />; return <FixedMentionItem item={item} onPress={onPressMention} />;
}
let content = (
<>
<Avatar style={styles.avatar} text={item.username || item.name} size={30} type={item.t} />
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.username || item.name || item}</Text>
</>
);
if (trackingType === MENTIONS_TRACKING_TYPE_EMOJIS) {
content = (
<>
<MentionEmoji item={item} />
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>:{item.name || item}:</Text>
</>
);
}
if (trackingType === MENTIONS_TRACKING_TYPE_COMMANDS) {
content = (
<>
<Text style={[styles.slash, { backgroundColor: themes[theme].borderColor, color: themes[theme].tintColor }]}>/</Text>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.id}</Text>
</>
);
}
if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) {
content = (
<>
<Text style={[styles.cannedItem, { color: themes[theme].titleText }]}>!{item.shortcut}</Text>
<Text numberOfLines={1} style={[styles.cannedMentionText, { color: themes[theme].auxiliaryTintColor }]}>
{item.text}
</Text>
</>
);
} }
return ( return (
@ -94,7 +96,7 @@ const MentionItem = ({ item, trackingType, theme }: IMessageBoxMentionItem) => {
]} ]}
onPress={() => onPressMention(item)} onPress={() => onPressMention(item)}
testID={testID}> testID={testID}>
{content} <MentionItemContent item={item} trackingType={trackingType} />
</TouchableOpacity> </TouchableOpacity>
); );
}; };

View File

@ -5,30 +5,33 @@ import { dequal } from 'dequal';
import MentionHeaderList from './MentionHeaderList'; import MentionHeaderList from './MentionHeaderList';
import styles from '../styles'; import styles from '../styles';
import MentionItem from './MentionItem'; import MentionItem from './MentionItem';
import { themes } from '../../../constants/colors'; import { themes } from '../../../lib/constants';
import { useTheme } from '../../../theme';
interface IMessageBoxMentions { interface IMessageBoxMentions {
mentions: any[]; mentions: any[];
trackingType: string; trackingType: string;
theme: string;
loading: boolean; loading: boolean;
} }
const Mentions = React.memo( const Mentions = React.memo(
({ mentions, trackingType, theme, loading }: IMessageBoxMentions) => { ({ mentions, trackingType, loading }: IMessageBoxMentions) => {
const { theme } = useTheme();
if (!trackingType) { if (!trackingType) {
return null; return null;
} }
return ( return (
<View testID='messagebox-container'> <View testID='messagebox-container'>
<FlatList <FlatList
style={[styles.mentionList, { backgroundColor: themes[theme].auxiliaryBackground }]} style={[styles.mentionList, { backgroundColor: themes[theme].auxiliaryBackground }]}
ListHeaderComponent={() => ( ListHeaderComponent={() => (
<MentionHeaderList trackingType={trackingType} hasMentions={mentions.length > 0} theme={theme} loading={loading} /> <MentionHeaderList trackingType={trackingType} hasMentions={mentions.length > 0} loading={loading} />
)} )}
data={mentions} data={mentions}
extraData={mentions} extraData={mentions}
renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} theme={theme} />} renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} />}
keyExtractor={item => item.rid || item.name || item.command || item.shortcut || item} keyExtractor={item => item.rid || item.name || item.command || item.shortcut || item}
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
/> />
@ -39,9 +42,6 @@ const Mentions = React.memo(
if (prevProps.loading !== nextProps.loading) { if (prevProps.loading !== nextProps.loading) {
return false; return false;
} }
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (prevProps.trackingType !== nextProps.trackingType) { if (prevProps.trackingType !== nextProps.trackingType) {
return false; return false;
} }

View File

@ -7,12 +7,13 @@ import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import styles from './styles'; import styles from './styles';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { events, logEvent } from '../../utils/log'; import { events, logEvent } from '../../utils/log';
import { TSupportedThemes } from '../../theme';
interface IMessageBoxRecordAudioProps { interface IMessageBoxRecordAudioProps {
theme: string; theme: TSupportedThemes;
permissionToUpload: boolean; permissionToUpload: boolean;
recordingCallback: Function; recordingCallback: Function;
onFinish: Function; onFinish: Function;
@ -47,23 +48,17 @@ const RECORDING_MODE = {
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
}; };
const formatTime = function (seconds: any) { const formatTime = function (time: number) {
let minutes: any = Math.floor(seconds / 60); const minutes = Math.floor(time / 60);
seconds %= 60; const seconds = time % 60;
if (minutes < 10) { const min = minutes < 10 ? `0${minutes}` : minutes;
minutes = `0${minutes}`; const sec = seconds < 10 ? `0${seconds}` : seconds;
} return `${min}:${sec}`;
if (seconds < 10) {
seconds = `0${seconds}`;
}
return `${minutes}:${seconds}`;
}; };
export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAudioProps, any> { export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAudioProps, any> {
private isRecorderBusy: boolean; private isRecorderBusy: boolean;
private recording!: Audio.Recording;
private recording: any;
private LastDuration: number; private LastDuration: number;
constructor(props: IMessageBoxRecordAudioProps) { constructor(props: IMessageBoxRecordAudioProps) {
@ -112,7 +107,7 @@ export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAu
return false; return false;
}; };
onRecordingStatusUpdate = (status: any) => { onRecordingStatusUpdate = (status: Audio.RecordingStatus) => {
this.setState({ this.setState({
isRecording: status.isRecording, isRecording: status.isRecording,
recordingDurationMillis: status.durationMillis recordingDurationMillis: status.durationMillis
@ -157,7 +152,7 @@ export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAu
await this.recording.stopAndUnloadAsync(); await this.recording.stopAndUnloadAsync();
const fileURI = this.recording.getURI(); const fileURI = this.recording.getURI();
const fileData = await getInfoAsync(fileURI); const fileData = await getInfoAsync(fileURI as string);
const fileInfo = { const fileInfo = {
name: `${Date.now()}.m4a`, name: `${Date.now()}.m4a`,
mime: 'audio/aac', mime: 'audio/aac',
@ -200,14 +195,10 @@ export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAu
} }
if (!isRecording && !isRecorderActive) { if (!isRecording && !isRecorderActive) {
return ( return (
<BorderlessButton <BorderlessButton onPress={this.startRecordingAudio} style={styles.actionButton} testID='messagebox-send-audio'>
onPress={this.startRecordingAudio} <View accessible accessibilityLabel={I18n.t('Send_audio_message')} accessibilityRole='button'>
style={styles.actionButton} <CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} />
testID='messagebox-send-audio' </View>
// @ts-ignore
accessibilityLabel={I18n.t('Send_audio_message')}
accessibilityTraits='button'>
<CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} />
</BorderlessButton> </BorderlessButton>
); );
} }
@ -216,23 +207,17 @@ export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAu
return ( return (
<View style={styles.recordingContent}> <View style={styles.recordingContent}>
<View style={styles.textArea}> <View style={styles.textArea}>
<BorderlessButton <BorderlessButton onPress={this.cancelRecordingAudio} style={styles.actionButton}>
onPress={this.cancelRecordingAudio} <View accessible accessibilityLabel={I18n.t('Cancel_recording')} accessibilityRole='button'>
// @ts-ignore <CustomIcon size={24} color={themes[theme].dangerColor} name='delete' />
accessibilityLabel={I18n.t('Cancel_recording')} </View>
accessibilityTraits='button'
style={styles.actionButton}>
<CustomIcon size={24} color={themes[theme].dangerColor} name='delete' />
</BorderlessButton> </BorderlessButton>
<Text style={[styles.recordingDurationText, { color: themes[theme].titleText }]}>{this.GetLastDuration}</Text> <Text style={[styles.recordingDurationText, { color: themes[theme].titleText }]}>{this.GetLastDuration}</Text>
</View> </View>
<BorderlessButton <BorderlessButton onPress={this.finishRecordingAudio} style={styles.actionButton}>
onPress={this.finishRecordingAudio} <View accessible accessibilityLabel={I18n.t('Finish_recording')} accessibilityRole='button'>
// @ts-ignore <CustomIcon size={24} color={themes[theme].tintColor} name='send-filled' />
accessibilityLabel={I18n.t('Finish_recording')} </View>
accessibilityTraits='button'
style={styles.actionButton}>
<CustomIcon size={24} color={themes[theme].tintColor} name='send-filled' />
</BorderlessButton> </BorderlessButton>
</View> </View>
); );
@ -241,24 +226,18 @@ export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAu
return ( return (
<View style={styles.recordingContent}> <View style={styles.recordingContent}>
<View style={styles.textArea}> <View style={styles.textArea}>
<BorderlessButton <BorderlessButton onPress={this.cancelRecordingAudio} style={styles.actionButton}>
onPress={this.cancelRecordingAudio} <View accessible accessibilityLabel={I18n.t('Cancel_recording')} accessibilityRole='button'>
// @ts-ignore <CustomIcon size={24} color={themes[theme].dangerColor} name='delete' />
accessibilityLabel={I18n.t('Cancel_recording')} </View>
accessibilityTraits='button'
style={styles.actionButton}>
<CustomIcon size={24} color={themes[theme].dangerColor} name='delete' />
</BorderlessButton> </BorderlessButton>
<Text style={[styles.recordingDurationText, { color: themes[theme].titleText }]}>{this.duration}</Text> <Text style={[styles.recordingDurationText, { color: themes[theme].titleText }]}>{this.duration}</Text>
<CustomIcon size={24} color={themes[theme].dangerColor} name='record' /> <CustomIcon size={24} color={themes[theme].dangerColor} name='record' />
</View> </View>
<BorderlessButton <BorderlessButton onPress={this.finishRecordingAudio} style={styles.actionButton}>
onPress={this.finishRecordingAudio} <View accessible accessibilityLabel={I18n.t('Finish_recording')} accessibilityRole='button'>
// @ts-ignore <CustomIcon size={24} color={themes[theme].tintColor} name='send-filled' />
accessibilityLabel={I18n.t('Finish_recording')} </View>
accessibilityTraits='button'
style={styles.actionButton}>
<CustomIcon size={24} color={themes[theme].tintColor} name='send-filled' />
</BorderlessButton> </BorderlessButton>
</View> </View>
); );

View File

@ -6,8 +6,10 @@ import { connect } from 'react-redux';
import { MarkdownPreview } from '../markdown'; import { MarkdownPreview } from '../markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { IMessage } from '../../definitions/IMessage'; import { IMessage } from '../../definitions/IMessage';
import { useTheme } from '../../theme';
import { IApplicationState } from '../../definitions';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -49,12 +51,13 @@ interface IMessageBoxReplyPreview {
baseUrl: string; baseUrl: string;
username: string; username: string;
getCustomEmoji: Function; getCustomEmoji: Function;
theme: string;
useRealName: boolean; useRealName: boolean;
} }
const ReplyPreview = React.memo( const ReplyPreview = React.memo(
({ message, Message_TimeFormat, replying, close, theme, useRealName }: IMessageBoxReplyPreview) => { ({ message, Message_TimeFormat, replying, close, useRealName }: IMessageBoxReplyPreview) => {
const { theme } = useTheme();
if (!replying) { if (!replying) {
return null; return null;
} }
@ -75,16 +78,14 @@ const ReplyPreview = React.memo(
</View> </View>
); );
}, },
(prevProps: any, nextProps: any) => (prevProps: IMessageBoxReplyPreview, nextProps: IMessageBoxReplyPreview) =>
prevProps.replying === nextProps.replying && prevProps.replying === nextProps.replying && prevProps.message.id === nextProps.message.id
prevProps.theme === nextProps.theme &&
prevProps.message.id === nextProps.message.id
); );
const mapStateToProps = (state: any) => ({ const mapStateToProps = (state: IApplicationState) => ({
Message_TimeFormat: state.settings.Message_TimeFormat, Message_TimeFormat: state.settings.Message_TimeFormat as string,
baseUrl: state.server.server, baseUrl: state.server.server,
useRealName: state.settings.UI_Use_Real_Name useRealName: state.settings.UI_Use_Real_Name as boolean
}); });
export default connect(mapStateToProps)(ReplyPreview); export default connect(mapStateToProps)(ReplyPreview);

View File

@ -5,24 +5,20 @@ import { ActionsButton, SendButton } from './buttons';
import styles from './styles'; import styles from './styles';
interface IMessageBoxRightButtons { interface IMessageBoxRightButtons {
theme: string;
showSend: boolean; showSend: boolean;
submit(): void; submit(): void;
showMessageBoxActions(): void; showMessageBoxActions(): void;
isActionsEnabled: boolean; isActionsEnabled: boolean;
} }
const RightButtons = React.memo( const RightButtons = React.memo(({ showSend, submit, showMessageBoxActions, isActionsEnabled }: IMessageBoxRightButtons) => {
({ theme, showSend, submit, showMessageBoxActions, isActionsEnabled }: IMessageBoxRightButtons) => { if (showSend) {
if (showSend) { return <SendButton onPress={submit} />;
return <SendButton onPress={submit} theme={theme} />;
}
if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
}
return <View style={styles.buttonsWhitespace} />;
} }
); if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} />;
}
return <View style={styles.buttonsWhitespace} />;
});
export default RightButtons; export default RightButtons;

View File

@ -3,16 +3,15 @@ import React from 'react';
import { SendButton } from './buttons'; import { SendButton } from './buttons';
interface IMessageBoxRightButtons { interface IMessageBoxRightButtons {
theme: string;
showSend: boolean; showSend: boolean;
submit(): void; submit(): void;
} }
const RightButtons = React.memo(({ theme, showSend, submit }: IMessageBoxRightButtons) => { const RightButtons = ({ showSend, submit }: IMessageBoxRightButtons) => {
if (showSend) { if (showSend) {
return <SendButton theme={theme} onPress={submit} />; return <SendButton onPress={submit} />;
} }
return null; return null;
}); };
export default RightButtons; export default RightButtons;

View File

@ -3,12 +3,11 @@ import React from 'react';
import BaseButton from './BaseButton'; import BaseButton from './BaseButton';
interface IActionsButton { interface IActionsButton {
theme: string;
onPress(): void; onPress(): void;
} }
const ActionsButton = React.memo(({ theme, onPress }: IActionsButton) => ( const ActionsButton = ({ onPress }: IActionsButton) => (
<BaseButton onPress={onPress} testID='messagebox-actions' accessibilityLabel='Message_actions' icon='add' theme={theme} /> <BaseButton onPress={onPress} testID='messagebox-actions' accessibilityLabel='Message_actions' icon='add' />
)); );
export default ActionsButton; export default ActionsButton;

View File

@ -1,13 +1,14 @@
import React from 'react';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import React from 'react';
import { View } from 'react-native';
import { themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons';
import styles from '../styles'; import styles from '../styles';
import I18n from '../../../i18n'; import i18n from '../../../i18n';
import { CustomIcon } from '../../../lib/Icons';
import { useTheme } from '../../../theme';
import { themes } from '../../../lib/constants';
interface IBaseButton { interface IBaseButton {
theme: string;
onPress(): void; onPress(): void;
testID: string; testID: string;
accessibilityLabel: string; accessibilityLabel: string;
@ -15,16 +16,18 @@ interface IBaseButton {
color: string; color: string;
} }
const BaseButton = React.memo(({ onPress, testID, accessibilityLabel, icon, theme, color }: Partial<IBaseButton>) => ( const BaseButton = ({ accessibilityLabel, icon, color, ...props }: Partial<IBaseButton>) => {
<BorderlessButton const { theme } = useTheme();
onPress={onPress} return (
style={styles.actionButton} <BorderlessButton {...props} style={styles.actionButton}>
testID={testID} <View
// @ts-ignore accessible
accessibilityLabel={I18n.t(accessibilityLabel)} accessibilityLabel={accessibilityLabel ? i18n.t(accessibilityLabel) : accessibilityLabel}
accessibilityTraits='button'> accessibilityRole='button'>
<CustomIcon name={icon} size={24} color={color ?? themes[theme!].auxiliaryTintColor} /> <CustomIcon name={icon} size={24} color={color || themes[theme].auxiliaryTintColor} />
</BorderlessButton> </View>
)); </BorderlessButton>
);
};
export default BaseButton; export default BaseButton;

View File

@ -3,18 +3,11 @@ import React from 'react';
import BaseButton from './BaseButton'; import BaseButton from './BaseButton';
interface ICancelEditingButton { interface ICancelEditingButton {
theme: string;
onPress(): void; onPress(): void;
} }
const CancelEditingButton = React.memo(({ theme, onPress }: ICancelEditingButton) => ( const CancelEditingButton = ({ onPress }: ICancelEditingButton) => (
<BaseButton <BaseButton onPress={onPress} testID='messagebox-cancel-editing' accessibilityLabel='Cancel_editing' icon='close' />
onPress={onPress} );
testID='messagebox-cancel-editing'
accessibilityLabel='Cancel_editing'
icon='close'
theme={theme}
/>
));
export default CancelEditingButton; export default CancelEditingButton;

View File

@ -1,22 +1,24 @@
import React from 'react'; import React from 'react';
import { themes } from '../../../lib/constants';
import { useTheme } from '../../../theme';
import BaseButton from './BaseButton'; import BaseButton from './BaseButton';
import { themes } from '../../../constants/colors';
interface ISendButton { interface ISendButton {
theme: string;
onPress(): void; onPress(): void;
} }
const SendButton = React.memo(({ theme, onPress }: ISendButton) => ( const SendButton = ({ onPress }: ISendButton) => {
<BaseButton const { theme } = useTheme();
onPress={onPress} return (
testID='messagebox-send-message' <BaseButton
accessibilityLabel='Send_message' onPress={onPress}
icon='send-filled' testID='messagebox-send-message'
theme={theme} accessibilityLabel='Send_message'
color={themes[theme].tintColor} icon='send-filled'
/> color={themes[theme].tintColor}
)); />
);
};
export default SendButton; export default SendButton;

View File

@ -3,33 +3,18 @@ import React from 'react';
import BaseButton from './BaseButton'; import BaseButton from './BaseButton';
interface IToggleEmojiButton { interface IToggleEmojiButton {
theme: string;
show: boolean; show: boolean;
open(): void; open(): void;
close(): void; close(): void;
} }
const ToggleEmojiButton = React.memo(({ theme, show, open, close }: IToggleEmojiButton) => { const ToggleEmojiButton = ({ show, open, close }: IToggleEmojiButton) => {
if (show) { if (show) {
return ( return (
<BaseButton <BaseButton onPress={close} testID='messagebox-close-emoji' accessibilityLabel='Close_emoji_selector' icon='keyboard' />
onPress={close}
testID='messagebox-close-emoji'
accessibilityLabel='Close_emoji_selector'
icon='keyboard'
theme={theme}
/>
); );
} }
return ( return <BaseButton onPress={open} testID='messagebox-open-emoji' accessibilityLabel='Open_emoji_selector' icon='emoji' />;
<BaseButton };
onPress={open}
testID='messagebox-open-emoji'
accessibilityLabel='Open_emoji_selector'
icon='emoji'
theme={theme}
/>
);
});
export default ToggleEmojiButton; export default ToggleEmojiButton;

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { Alert, Keyboard, NativeModules, Text, View } from 'react-native'; import { Alert, Keyboard, NativeModules, Text, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard'; import { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard';
import ImagePicker, { Image, ImageOrVideo } from 'react-native-image-crop-picker'; import ImagePicker, { Image, ImageOrVideo, Options } from 'react-native-image-crop-picker';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
@ -20,7 +20,7 @@ import RecordAudio from './RecordAudio';
import I18n from '../../i18n'; import I18n from '../../i18n';
import ReplyPreview from './ReplyPreview'; import ReplyPreview from './ReplyPreview';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
// @ts-ignore // @ts-ignore
// eslint-disable-next-line import/extensions,import/no-unresolved // eslint-disable-next-line import/extensions,import/no-unresolved
import LeftButtons from './LeftButtons'; import LeftButtons from './LeftButtons';
@ -44,13 +44,15 @@ import {
} from './constants'; } from './constants';
import CommandsPreview from './CommandsPreview'; import CommandsPreview from './CommandsPreview';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/navigation/appNavigation';
import { withActionSheet } from '../ActionSheet'; import { withActionSheet } from '../ActionSheet';
import { sanitizeLikeString } from '../../lib/database/utils'; import { sanitizeLikeString } from '../../lib/database/utils';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { IMessage } from '../../definitions/IMessage'; import { IMessage } from '../../definitions/IMessage';
import { forceJpgExtension } from './forceJpgExtension'; import { forceJpgExtension } from './forceJpgExtension';
import { IPreviewItem, IUser } from '../../definitions'; import { IBaseScreen, IPreviewItem, IUser, TSubscriptionModel, TThreadModel } from '../../definitions';
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
import { TSupportedThemes } from '../../theme';
if (isAndroid) { if (isAndroid) {
require('./EmojiKeyboard'); require('./EmojiKeyboard');
@ -63,18 +65,18 @@ const imagePickerConfig = {
forceJpg: true forceJpg: true
}; };
const libraryPickerConfig = { const libraryPickerConfig: Options = {
multiple: true, multiple: true,
compressVideoPreset: 'Passthrough', compressVideoPreset: 'Passthrough',
mediaType: 'any', mediaType: 'any',
forceJpg: true forceJpg: true
}; };
const videoPickerConfig = { const videoPickerConfig: Options = {
mediaType: 'video' mediaType: 'video'
}; };
export interface IMessageBoxProps { export interface IMessageBoxProps extends IBaseScreen<MasterDetailInsideStackParamList, any> {
rid: string; rid: string;
baseUrl: string; baseUrl: string;
message: IMessage; message: IMessage;
@ -94,10 +96,9 @@ export interface IMessageBoxProps {
editRequest: Function; editRequest: Function;
onSubmit: Function; onSubmit: Function;
typing: Function; typing: Function;
theme: string; theme: TSupportedThemes;
replyCancel(): void; replyCancel(): void;
showSend: boolean; showSend: boolean;
navigation: any;
children: JSX.Element; children: JSX.Element;
isMasterDetail: boolean; isMasterDetail: boolean;
showActionSheet: Function; showActionSheet: Function;
@ -118,7 +119,7 @@ interface IMessageBoxState {
commandPreview: IPreviewItem[]; commandPreview: IPreviewItem[];
showCommandPreview: boolean; showCommandPreview: boolean;
command: { command: {
appId?: any; appId?: string;
}; };
tshow: boolean; tshow: boolean;
mentionLoading: boolean; mentionLoading: boolean;
@ -132,17 +133,15 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
private focused: boolean; private focused: boolean;
private options: any; private imagePickerConfig: Options;
private imagePickerConfig: any; private libraryPickerConfig: Options;
private libraryPickerConfig: any; private videoPickerConfig: Options;
private videoPickerConfig: any; private room!: TSubscriptionModel;
private room: any; private thread!: TThreadModel;
private thread: any;
private unsubscribeFocus: any; private unsubscribeFocus: any;
@ -681,7 +680,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (result.success) { if (result.success) {
return true; return true;
} }
Alert.alert(I18n.t('Error_uploading'), I18n.t(result.error)); Alert.alert(I18n.t('Error_uploading'), result.error && I18n.isTranslated(result.error) ? I18n.t(result.error) : result.error);
return false; return false;
}; };
@ -713,7 +712,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
chooseFromLibrary = async () => { chooseFromLibrary = async () => {
logEvent(events.ROOM_BOX_ACTION_LIBRARY); logEvent(events.ROOM_BOX_ACTION_LIBRARY);
try { try {
let attachments = (await ImagePicker.openPicker(this.libraryPickerConfig)) as ImageOrVideo[]; // The type can be video or photo, however the lib understands that it is just one of them.
let attachments = (await ImagePicker.openPicker(this.libraryPickerConfig)) as unknown as ImageOrVideo[];
attachments = attachments.map(att => forceJpgExtension(att)); attachments = attachments.map(att => forceJpgExtension(att));
this.openShareView(attachments); this.openShareView(attachments);
} catch (e) { } catch (e) {
@ -757,12 +757,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
openShareView = (attachments: any) => { openShareView = (attachments: any) => {
const { message, replyCancel, replyWithMention } = this.props; const { message, replyCancel, replyWithMention } = this.props;
// Start a thread with an attachment // Start a thread with an attachment
let { thread } = this; let value: TThreadModel | IMessage = this.thread;
if (replyWithMention) { if (replyWithMention) {
thread = message; value = message;
replyCancel(); replyCancel();
} }
Navigation.navigate('ShareView', { room: this.room, thread, attachments }); Navigation.navigate('ShareView', { room: this.room, thread: value, attachments });
}; };
createDiscussion = () => { createDiscussion = () => {
@ -1060,7 +1060,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const commandsPreviewAndMentions = !recording ? ( const commandsPreviewAndMentions = !recording ? (
<> <>
<CommandsPreview commandPreview={commandPreview} showCommandPreview={showCommandPreview} /> <CommandsPreview commandPreview={commandPreview} showCommandPreview={showCommandPreview} />
<Mentions mentions={mentions} trackingType={trackingType} theme={theme} loading={mentionLoading} /> <Mentions mentions={mentions} trackingType={trackingType} loading={mentionLoading} />
</> </>
) : null; ) : null;
@ -1071,7 +1071,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
username={user.username} username={user.username}
replying={replying} replying={replying}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme}
/> />
) : null; ) : null;

View File

@ -3,7 +3,8 @@ import { StyleSheet, Text, View } from 'react-native';
import I18n from '../i18n'; import I18n from '../i18n';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../lib/constants';
import { TSupportedThemes } from '../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -24,7 +25,7 @@ const styles = StyleSheet.create({
}); });
interface IOrSeparator { interface IOrSeparator {
theme: string; theme: TSupportedThemes;
} }
const OrSeparator = React.memo(({ theme }: IOrSeparator) => { const OrSeparator = React.memo(({ theme }: IOrSeparator) => {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Text } from 'react-native'; import { Text } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../lib/constants';
import Touch from '../../../utils/touch'; import Touch from '../../../utils/touch';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
import { useTheme } from '../../../theme'; import { useTheme } from '../../../theme';

View File

@ -3,7 +3,7 @@ import { View } from 'react-native';
import range from 'lodash/range'; import range from 'lodash/range';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../lib/constants';
import { useTheme } from '../../../theme'; import { useTheme } from '../../../theme';
const SIZE_EMPTY = 12; const SIZE_EMPTY = 12;

View File

@ -3,7 +3,7 @@ import { View } from 'react-native';
import { Row } from 'react-native-easy-grid'; import { Row } from 'react-native-easy-grid';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../lib/constants';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
import { useTheme } from '../../../theme'; import { useTheme } from '../../../theme';

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Grid } from 'react-native-easy-grid'; import { Grid } from 'react-native-easy-grid';
import { themes } from '../../../constants/colors'; import { themes } from '../../../lib/constants';
import { resetAttempts } from '../../../utils/localAuthentication'; import { resetAttempts } from '../../../utils/localAuthentication';
import { TYPE } from '../constants'; import { TYPE } from '../constants';
import { getDiff, getLockedUntil } from '../utils'; import { getDiff, getLockedUntil } from '../utils';

View File

@ -3,7 +3,7 @@ import { Text, View } from 'react-native';
import { Row } from 'react-native-easy-grid'; import { Row } from 'react-native-easy-grid';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../lib/constants';
import { useTheme } from '../../../theme'; import { useTheme } from '../../../theme';
interface IPasscodeSubtitle { interface IPasscodeSubtitle {

View File

@ -3,7 +3,7 @@ import { Text, View } from 'react-native';
import { Row } from 'react-native-easy-grid'; import { Row } from 'react-native-easy-grid';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../lib/constants';
import { useTheme } from '../../../theme'; import { useTheme } from '../../../theme';
interface IPasscodeTitle { interface IPasscodeTitle {

View File

@ -9,8 +9,7 @@ import styles from './styles';
import Button from './Button'; import Button from './Button';
import Dots from './Dots'; import Dots from './Dots';
import { TYPE } from '../constants'; import { TYPE } from '../constants';
import { themes } from '../../../constants/colors'; import { PASSCODE_LENGTH, themes } from '../../../lib/constants';
import { PASSCODE_LENGTH } from '../../../constants/localAuthentication';
import { useTheme } from '../../../theme'; import { useTheme } from '../../../theme';
import LockIcon from './LockIcon'; import LockIcon from './LockIcon';
import Title from './Title'; import Title from './Title';
@ -20,7 +19,7 @@ interface IPasscodeBase {
type: string; type: string;
previousPasscode?: string; previousPasscode?: string;
title: string; title: string;
subtitle?: string; subtitle?: string | null;
showBiometry?: boolean; showBiometry?: boolean;
onEndProcess: Function; onEndProcess: Function;
onError?: Function; onError?: Function;

View File

@ -14,7 +14,7 @@ interface IPasscodeChoose {
const PasscodeChoose = ({ finishProcess, force = false }: IPasscodeChoose) => { const PasscodeChoose = ({ finishProcess, force = false }: IPasscodeChoose) => {
const chooseRef = useRef<IBase>(null); const chooseRef = useRef<IBase>(null);
const confirmRef = useRef<IBase>(null); const confirmRef = useRef<IBase>(null);
const [subtitle, setSubtitle] = useState(null); const [subtitle, setSubtitle] = useState<string | null>(null);
const [status, setStatus] = useState(TYPE.CHOOSE); const [status, setStatus] = useState(TYPE.CHOOSE);
const [previousPasscode, setPreviouPasscode] = useState(''); const [previousPasscode, setPreviouPasscode] = useState('');

View File

@ -7,10 +7,10 @@ import { sha256 } from 'js-sha256';
import Base, { IBase } from './Base'; import Base, { IBase } from './Base';
import Locked from './Base/Locked'; import Locked from './Base/Locked';
import { TYPE } from './constants'; import { TYPE } from './constants';
import { ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, MAX_ATTEMPTS, PASSCODE_KEY } from '../../constants/localAuthentication'; import { ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, MAX_ATTEMPTS, PASSCODE_KEY } from '../../lib/constants';
import { biometryAuth, resetAttempts } from '../../utils/localAuthentication'; import { biometryAuth, resetAttempts } from '../../utils/localAuthentication';
import { getDiff, getLockedUntil } from './utils'; import { getDiff, getLockedUntil } from './utils';
import { useUserPreferences } from '../../lib/userPreferences'; import { useUserPreferences } from '../../lib/methods/userPreferences';
import I18n from '../../i18n'; import I18n from '../../i18n';
interface IPasscodePasscodeEnter { interface IPasscodePasscodeEnter {

View File

@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import moment from 'moment'; import moment from 'moment';
import { LOCKED_OUT_TIMER_KEY, TIME_TO_LOCK } from '../../constants/localAuthentication'; import { LOCKED_OUT_TIMER_KEY, TIME_TO_LOCK } from '../../lib/constants';
export const getLockedUntil = async () => { export const getLockedUntil = async () => {
const t = await AsyncStorage.getItem(LOCKED_OUT_TIMER_KEY); const t = await AsyncStorage.getItem(LOCKED_OUT_TIMER_KEY);

View File

@ -7,8 +7,8 @@ import Emoji from './message/Emoji';
import I18n from '../i18n'; import I18n from '../i18n';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../lib/constants';
import { withTheme } from '../theme'; import { TSupportedThemes, useTheme, withTheme } from '../theme';
import { TGetCustomEmoji } from '../definitions/IEmoji'; import { TGetCustomEmoji } from '../definitions/IEmoji';
import { TMessageModel, ILoggedUser } from '../definitions'; import { TMessageModel, ILoggedUser } from '../definitions';
import SafeAreaView from './SafeAreaView'; import SafeAreaView from './SafeAreaView';
@ -61,38 +61,37 @@ const styles = StyleSheet.create({
const standardEmojiStyle = { fontSize: 20 }; const standardEmojiStyle = { fontSize: 20 };
const customEmojiStyle = { width: 20, height: 20 }; const customEmojiStyle = { width: 20, height: 20 };
interface IItem { interface ISharedFields {
user?: Pick<ILoggedUser, 'username'>;
baseUrl: string;
getCustomEmoji: TGetCustomEmoji;
}
interface IItem extends ISharedFields {
item: { item: {
usernames: any; usernames: string[];
emoji: string; emoji: string;
}; };
user?: Pick<ILoggedUser, 'username'>;
baseUrl?: string;
getCustomEmoji?: TGetCustomEmoji;
theme?: string;
} }
interface IModalContent { interface IModalContent extends ISharedFields {
message?: TMessageModel; message?: TMessageModel;
onClose: () => void; onClose: () => void;
theme: string; theme: TSupportedThemes;
} }
interface IReactionsModal { interface IReactionsModal extends ISharedFields {
message?: any; message?: TMessageModel;
user?: Pick<ILoggedUser, 'username'>;
isVisible: boolean; isVisible: boolean;
onClose(): void; onClose(): void;
baseUrl: string;
getCustomEmoji?: TGetCustomEmoji;
theme: string;
} }
const Item = React.memo(({ item, user, baseUrl, getCustomEmoji, theme }: IItem) => { const Item = React.memo(({ item, user, baseUrl, getCustomEmoji }: IItem) => {
const { theme } = useTheme();
const count = item.usernames.length; const count = item.usernames.length;
let usernames = item.usernames let usernames = item.usernames
.slice(0, 3) .slice(0, 3)
.map((username: any) => (username === user?.username ? I18n.t('you') : username)) .map((username: string) => (username === user?.username ? I18n.t('you') : username))
.join(', '); .join(', ');
if (count > 3) { if (count > 3) {
usernames = `${usernames} ${I18n.t('and_more')} ${count - 3}`; usernames = `${usernames} ${I18n.t('and_more')} ${count - 3}`;
@ -106,15 +105,15 @@ const Item = React.memo(({ item, user, baseUrl, getCustomEmoji, theme }: IItem)
content={item.emoji} content={item.emoji}
standardEmojiStyle={standardEmojiStyle} standardEmojiStyle={standardEmojiStyle}
customEmojiStyle={customEmojiStyle} customEmojiStyle={customEmojiStyle}
baseUrl={baseUrl!} baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji!} getCustomEmoji={getCustomEmoji}
/> />
</View> </View>
<View style={styles.peopleItemContainer}> <View style={styles.peopleItemContainer}>
<Text style={[styles.reactCount, { color: themes[theme!].buttonText }]}> <Text style={[styles.reactCount, { color: themes[theme].buttonText }]}>
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })} {count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
</Text> </Text>
<Text style={[styles.peopleReacted, { color: themes[theme!].buttonText }]}>{usernames}</Text> <Text style={[styles.peopleReacted, { color: themes[theme].buttonText }]}>{usernames}</Text>
</View> </View>
</View> </View>
); );
@ -143,18 +142,21 @@ const ModalContent = React.memo(({ message, onClose, ...props }: IModalContent)
}); });
const ReactionsModal = React.memo( const ReactionsModal = React.memo(
({ isVisible, onClose, theme, ...props }: IReactionsModal) => ( ({ isVisible, onClose, ...props }: IReactionsModal) => {
<Modal const { theme } = useTheme();
isVisible={isVisible} return (
onBackdropPress={onClose} <Modal
onBackButtonPress={onClose} isVisible={isVisible}
backdropOpacity={0.8} onBackdropPress={onClose}
onSwipeComplete={onClose} onBackButtonPress={onClose}
swipeDirection={['up', 'left', 'right', 'down']}> backdropOpacity={0.8}
<ModalContent onClose={onClose} theme={theme} {...props} /> onSwipeComplete={onClose}
</Modal> swipeDirection={['up', 'left', 'right', 'down']}>
), <ModalContent onClose={onClose} theme={theme} {...props} />
(prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible && prevProps.theme === nextProps.theme </Modal>
);
},
(prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible
); );
ReactionsModal.displayName = 'ReactionsModal'; ReactionsModal.displayName = 'ReactionsModal';

View File

@ -2,13 +2,15 @@
import React from 'react'; import React from 'react';
import { Dimensions, View } from 'react-native'; import { Dimensions, View } from 'react-native';
import { storiesOf } from '@storybook/react-native'; import { storiesOf } from '@storybook/react-native';
import { Provider } from 'react-redux';
import Header from '../Header'; import Header from '../Header';
import { longText } from '../../../storybook/utils'; import { longText } from '../../../storybook/utils';
import { ThemeContext } from '../../theme'; import { ThemeContext } from '../../theme';
import { store } from '../../../storybook/stories';
import RoomHeaderComponent from './RoomHeader'; import RoomHeaderComponent from './RoomHeader';
const stories = storiesOf('RoomHeader', module); const stories = storiesOf('RoomHeader', module).addDecorator(story => <Provider store={store}>{story()}</Provider>);
// TODO: refactor after react-navigation v6 // TODO: refactor after react-navigation v6
const HeaderExample = ({ title }) => ( const HeaderExample = ({ title }) => (

View File

@ -3,11 +3,11 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import I18n from '../../i18n'; import I18n from '../../i18n';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { MarkdownPreview } from '../markdown'; import { MarkdownPreview } from '../markdown';
import RoomTypeIcon from '../RoomTypeIcon'; import RoomTypeIcon from '../RoomTypeIcon';
import { withTheme } from '../../theme'; import { TUserStatus, IOmnichannelSource } from '../../definitions';
import { TUserStatus } from '../../definitions'; import { useTheme } from '../../theme';
const HIT_SLOP = { const HIT_SLOP = {
top: 5, top: 5,
@ -44,9 +44,8 @@ const styles = StyleSheet.create({
type TRoomHeaderSubTitle = { type TRoomHeaderSubTitle = {
usersTyping: []; usersTyping: [];
theme: string;
subtitle: string; subtitle: string;
renderFunc: any; renderFunc?: () => React.ReactElement;
scale: number; scale: number;
}; };
@ -55,7 +54,6 @@ type TRoomHeaderHeaderTitle = {
tmid: string; tmid: string;
prid: string; prid: string;
scale: number; scale: number;
theme: string;
testID: string; testID: string;
}; };
@ -69,15 +67,16 @@ interface IRoomHeader {
tmid: string; tmid: string;
teamMain: boolean; teamMain: boolean;
status: TUserStatus; status: TUserStatus;
theme?: string;
usersTyping: []; usersTyping: [];
isGroupChat: boolean; isGroupChat: boolean;
parentTitle: string; parentTitle: string;
onPress: () => void; onPress: () => void;
testID: string; testID: string;
sourceType?: IOmnichannelSource;
} }
const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, theme, scale }: TRoomHeaderSubTitle) => { const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, scale }: TRoomHeaderSubTitle) => {
const { theme } = useTheme();
const fontSize = getSubTitleSize(scale); const fontSize = getSubTitleSize(scale);
// typing // typing
if (usersTyping.length) { if (usersTyping.length) {
@ -108,7 +107,8 @@ const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, theme, scale }
return null; return null;
}); });
const HeaderTitle = React.memo(({ title, tmid, prid, scale, theme, testID }: TRoomHeaderHeaderTitle) => { const HeaderTitle = React.memo(({ title, tmid, prid, scale, testID }: TRoomHeaderHeaderTitle) => {
const { theme } = useTheme();
const titleStyle = { fontSize: TITLE_SIZE * scale, color: themes[theme].headerTitleColor }; const titleStyle = { fontSize: TITLE_SIZE * scale, color: themes[theme].headerTitleColor };
if (!tmid && !prid) { if (!tmid && !prid) {
return ( return (
@ -133,12 +133,13 @@ const Header = React.memo(
prid, prid,
tmid, tmid,
onPress, onPress,
theme,
isGroupChat, isGroupChat,
teamMain, teamMain,
testID, testID,
usersTyping = [] usersTyping = [],
sourceType
}: IRoomHeader) => { }: IRoomHeader) => {
const { theme } = useTheme();
const portrait = height > width; const portrait = height > width;
let scale = 1; let scale = 1;
@ -153,7 +154,7 @@ const Header = React.memo(
renderFunc = () => ( renderFunc = () => (
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} teamMain={teamMain} /> <RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} teamMain={teamMain} />
<Text style={[styles.subtitle, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}> <Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
{parentTitle} {parentTitle}
</Text> </Text>
</View> </View>
@ -168,25 +169,24 @@ const Header = React.memo(
accessibilityLabel={title} accessibilityLabel={title}
onPress={handleOnPress} onPress={handleOnPress}
style={styles.container} style={styles.container}
// @ts-ignore disabled={!!tmid}
disabled={tmid}
hitSlop={HIT_SLOP}> hitSlop={HIT_SLOP}>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
{tmid ? null : ( {tmid ? null : (
<RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} teamMain={teamMain} /> <RoomTypeIcon
type={prid ? 'discussion' : type}
isGroupChat={isGroupChat}
status={status}
teamMain={teamMain}
sourceType={sourceType}
/>
)} )}
<HeaderTitle title={title} tmid={tmid} prid={prid} scale={scale} theme={theme!} testID={testID} /> <HeaderTitle title={title} tmid={tmid} prid={prid} scale={scale} testID={testID} />
</View> </View>
<SubTitle <SubTitle usersTyping={tmid ? [] : usersTyping} subtitle={subtitle} renderFunc={renderFunc} scale={scale} />
usersTyping={tmid ? [] : usersTyping}
subtitle={subtitle}
theme={theme!}
renderFunc={renderFunc}
scale={scale}
/>
</TouchableOpacity> </TouchableOpacity>
); );
} }
); );
export default withTheme(Header); export default Header;

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@ import { dequal } from 'dequal';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { IApplicationState, TUserStatus } from '../../definitions'; import { IApplicationState, TUserStatus, IOmnichannelSource } from '../../definitions';
import { withDimensions } from '../../dimensions'; import { withDimensions } from '../../dimensions';
import I18n from '../../i18n'; import I18n from '../../i18n';
import RoomHeader from './RoomHeader'; import RoomHeader from './RoomHeader';
@ -27,12 +27,26 @@ interface IRoomHeaderContainerProps {
parentTitle: string; parentTitle: string;
isGroupChat: boolean; isGroupChat: boolean;
testID: string; testID: string;
sourceType?: IOmnichannelSource;
} }
class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> { class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> {
shouldComponentUpdate(nextProps: IRoomHeaderContainerProps) { shouldComponentUpdate(nextProps: IRoomHeaderContainerProps) {
const { type, title, subtitle, status, statusText, connecting, connected, onPress, usersTyping, width, height, teamMain } = const {
this.props; type,
title,
subtitle,
status,
statusText,
connecting,
connected,
onPress,
usersTyping,
width,
height,
teamMain,
sourceType
} = this.props;
if (nextProps.type !== type) { if (nextProps.type !== type) {
return true; return true;
} }
@ -63,6 +77,9 @@ class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> {
if (!dequal(nextProps.usersTyping, usersTyping)) { if (!dequal(nextProps.usersTyping, usersTyping)) {
return true; return true;
} }
if (!dequal(nextProps.sourceType, sourceType)) {
return true;
}
if (nextProps.onPress !== onPress) { if (nextProps.onPress !== onPress) {
return true; return true;
} }
@ -90,7 +107,8 @@ class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> {
height, height,
parentTitle, parentTitle,
isGroupChat, isGroupChat,
testID testID,
sourceType
} = this.props; } = this.props;
let subtitle; let subtitle;
@ -118,6 +136,7 @@ class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> {
isGroupChat={isGroupChat} isGroupChat={isGroupChat}
testID={testID} testID={testID}
onPress={onPress} onPress={onPress}
sourceType={sourceType}
/> />
); );
} }

View File

@ -4,34 +4,15 @@ import { RectButton } from 'react-native-gesture-handler';
import { isRTL } from '../../i18n'; import { isRTL } from '../../i18n';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { themes } from '../../constants/colors'; import { DisplayMode, themes } from '../../lib/constants';
import { DisplayMode } from '../../constants/constantDisplayMode';
import styles, { ACTION_WIDTH, LONG_SWIPE, ROW_HEIGHT_CONDENSED } from './styles'; import styles, { ACTION_WIDTH, LONG_SWIPE, ROW_HEIGHT_CONDENSED } from './styles';
import { ILeftActionsProps, IRightActionsProps } from './interfaces';
interface ILeftActions {
theme: string;
transX: any;
isRead: boolean;
width: number;
onToggleReadPress(): void;
displayMode: string;
}
interface IRightActions {
theme: string;
transX: any;
favorite: boolean;
width: number;
toggleFav(): void;
onHidePress(): void;
displayMode: string;
}
const reverse = new Animated.Value(isRTL() ? -1 : 1); const reverse = new Animated.Value(isRTL() ? -1 : 1);
const CONDENSED_ICON_SIZE = 24; const CONDENSED_ICON_SIZE = 24;
const EXPANDED_ICON_SIZE = 28; const EXPANDED_ICON_SIZE = 28;
export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleReadPress, displayMode }: ILeftActions) => { export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleReadPress, displayMode }: ILeftActionsProps) => {
const translateX = Animated.multiply( const translateX = Animated.multiply(
transX.interpolate({ transX.interpolate({
inputRange: [0, ACTION_WIDTH], inputRange: [0, ACTION_WIDTH],
@ -44,7 +25,7 @@ export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleR
const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null; const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null;
return ( return (
<View style={[styles.actionsContainer, styles.actionLeftContainer]} pointerEvents='box-none'> <View style={[styles.actionsContainer, styles.actionsLeftContainer]} pointerEvents='box-none'>
<Animated.View <Animated.View
style={[ style={[
styles.actionLeftButtonContainer, styles.actionLeftButtonContainer,
@ -71,7 +52,7 @@ export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleR
}); });
export const RightActions = React.memo( export const RightActions = React.memo(
({ transX, favorite, width, toggleFav, onHidePress, theme, displayMode }: IRightActions) => { ({ transX, favorite, width, toggleFav, onHidePress, theme, displayMode }: IRightActionsProps) => {
const translateXFav = Animated.multiply( const translateXFav = Animated.multiply(
transX.interpolate({ transX.interpolate({
inputRange: [-width / 2, -ACTION_WIDTH * 2, 0], inputRange: [-width / 2, -ACTION_WIDTH * 2, 0],

View File

@ -2,8 +2,8 @@ import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Avatar from '../../containers/Avatar'; import Avatar from '../Avatar';
import { DisplayMode } from '../../constants/constantDisplayMode'; import { DisplayMode } from '../../lib/constants';
import TypeIcon from './TypeIcon'; import TypeIcon from './TypeIcon';
import styles from './styles'; import styles from './styles';
@ -18,7 +18,8 @@ const IconOrAvatar = ({
teamMain, teamMain,
showLastMessage, showLastMessage,
theme, theme,
displayMode displayMode,
sourceType
}) => { }) => {
if (showAvatar) { if (showAvatar) {
return ( return (
@ -38,6 +39,7 @@ const IconOrAvatar = ({
teamMain={teamMain} teamMain={teamMain}
size={24} size={24}
style={{ marginRight: 12 }} style={{ marginRight: 12 }}
sourceType={sourceType}
/> />
</View> </View>
); );

View File

@ -3,28 +3,11 @@ import { dequal } from 'dequal';
import I18n from '../../i18n'; import I18n from '../../i18n';
import styles from './styles'; import styles from './styles';
import { MarkdownPreview } from '../../containers/markdown'; import { MarkdownPreview } from '../markdown';
import { themes } from '../../constants/colors'; import { E2E_MESSAGE_TYPE, E2E_STATUS, themes } from '../../lib/constants';
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants'; import { ILastMessageProps } from './interfaces';
interface ILastMessage { const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }: Partial<ILastMessageProps>) => {
theme: string;
lastMessage: {
u: any;
pinned: boolean;
t: string;
attachments: any;
msg: string;
e2e: string;
};
type: string;
showLastMessage: boolean;
username: string;
useRealName: boolean;
alert: boolean;
}
const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }: Partial<ILastMessage>) => {
if (!showLastMessage) { if (!showLastMessage) {
return ''; return '';
} }
@ -64,7 +47,7 @@ const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }
const arePropsEqual = (oldProps: any, newProps: any) => dequal(oldProps, newProps); const arePropsEqual = (oldProps: any, newProps: any) => dequal(oldProps, newProps);
const LastMessage = React.memo( const LastMessage = React.memo(
({ lastMessage, type, showLastMessage, username, alert, useRealName, theme }: ILastMessage) => ( ({ lastMessage, type, showLastMessage, username, alert, useRealName, theme }: ILastMessageProps) => (
<MarkdownPreview <MarkdownPreview
msg={formatMsg({ msg={formatMsg({
lastMessage, lastMessage,

View File

@ -11,57 +11,8 @@ import UpdatedAt from './UpdatedAt';
import Touchable from './Touchable'; import Touchable from './Touchable';
import Tag from './Tag'; import Tag from './Tag';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DisplayMode } from '../../constants/constantDisplayMode'; import { DisplayMode } from '../../lib/constants';
import { TUserStatus } from '../../definitions'; import { IRoomItemProps } from './interfaces';
interface IRoomItem {
rid: string;
type: string;
prid: string;
name: string;
avatar: string;
showLastMessage: boolean;
username: string;
avatarSize: number;
testID: string;
width: number;
status: TUserStatus;
useRealName: boolean;
theme: string;
isFocused: boolean;
isGroupChat: boolean;
isRead: boolean;
teamMain: boolean;
date: string;
accessibilityLabel: string;
lastMessage: {
u: any;
pinned: boolean;
t: string;
attachments: any;
msg: string;
e2e: string;
};
favorite: boolean;
alert: boolean;
hideUnreadStatus: boolean;
unread: number;
userMentions: number;
groupMentions: number;
tunread: [];
tunreadUser: [];
tunreadGroup: [];
swipeEnabled: boolean;
toggleFav(): void;
toggleRead(): void;
onPress(): void;
onLongPress(): void;
hideChannel(): void;
autoJoin: boolean;
size?: number;
showAvatar: boolean;
displayMode: string;
}
const RoomItem = ({ const RoomItem = ({
rid, rid,
@ -70,7 +21,6 @@ const RoomItem = ({
name, name,
avatar, avatar,
width, width,
avatarSize = 48,
username, username,
showLastMessage, showLastMessage,
status = 'offline', status = 'offline',
@ -101,8 +51,9 @@ const RoomItem = ({
teamMain, teamMain,
autoJoin, autoJoin,
showAvatar, showAvatar,
displayMode displayMode,
}: IRoomItem) => ( sourceType
}: IRoomItemProps) => (
<Touchable <Touchable
onPress={onPress} onPress={onPress}
onLongPress={onLongPress} onLongPress={onLongPress}
@ -122,7 +73,6 @@ const RoomItem = ({
<Wrapper <Wrapper
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
avatar={avatar} avatar={avatar}
avatarSize={avatarSize}
type={type} type={type}
theme={theme} theme={theme}
rid={rid} rid={rid}
@ -132,12 +82,20 @@ const RoomItem = ({
teamMain={teamMain} teamMain={teamMain}
displayMode={displayMode} displayMode={displayMode}
showAvatar={showAvatar} showAvatar={showAvatar}
showLastMessage={showLastMessage}> showLastMessage={showLastMessage}
sourceType={sourceType}>
{showLastMessage && displayMode === DisplayMode.Expanded ? ( {showLastMessage && displayMode === DisplayMode.Expanded ? (
<> <>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
{showAvatar ? ( {showAvatar ? (
<TypeIcon type={type} prid={prid} status={status} isGroupChat={isGroupChat} theme={theme} teamMain={teamMain} /> <TypeIcon
type={type}
prid={prid}
status={status}
isGroupChat={isGroupChat}
teamMain={teamMain}
sourceType={sourceType}
/>
) : null} ) : null}
<Title name={name} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} /> <Title name={name} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} />
{autoJoin ? <Tag testID='auto-join-tag' name={I18n.t('Auto-join')} /> : null} {autoJoin ? <Tag testID='auto-join-tag' name={I18n.t('Auto-join')} /> : null}
@ -170,10 +128,10 @@ const RoomItem = ({
prid={prid} prid={prid}
status={status} status={status}
isGroupChat={isGroupChat} isGroupChat={isGroupChat}
theme={theme}
teamMain={teamMain} teamMain={teamMain}
size={22} size={22}
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
sourceType={sourceType}
/> />
<Title name={name} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} /> <Title name={name} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} />
{autoJoin ? <Tag name={I18n.t('Auto-join')} /> : null} {autoJoin ? <Tag name={I18n.t('Auto-join')} /> : null}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Text, View } from 'react-native'; import { Text, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import styles from './styles'; import styles from './styles';
@ -11,7 +11,7 @@ interface ITag {
} }
const Tag = React.memo(({ name, testID }: ITag) => { const Tag = React.memo(({ name, testID }: ITag) => {
const { theme }: any = useTheme(); const { theme } = useTheme();
return ( return (
<View style={[styles.tagContainer, { backgroundColor: themes[theme].borderColor }]}> <View style={[styles.tagContainer, { backgroundColor: themes[theme].borderColor }]}>

View File

@ -2,16 +2,10 @@ import React from 'react';
import { Text } from 'react-native'; import { Text } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { ITitleProps } from './interfaces';
interface ITitle { const Title = React.memo(({ name, theme, hideUnreadStatus, alert }: ITitleProps) => (
name: string;
theme: string;
hideUnreadStatus: boolean;
alert: boolean;
}
const Title = React.memo(({ name, theme, hideUnreadStatus, alert }: ITitle) => (
<Text <Text
style={[styles.title, alert && !hideUnreadStatus && styles.alert, { color: themes[theme].titleText }]} style={[styles.title, alert && !hideUnreadStatus && styles.alert, { color: themes[theme].titleText }]}
ellipsizeMode='tail' ellipsizeMode='tail'

View File

@ -1,45 +1,28 @@
import React from 'react'; import React from 'react';
import { Animated } from 'react-native'; import { Animated } from 'react-native';
import { LongPressGestureHandler, PanGestureHandler, State } from 'react-native-gesture-handler'; import {
GestureEvent,
HandlerStateChangeEventPayload,
LongPressGestureHandler,
PanGestureHandler,
PanGestureHandlerEventPayload,
State
} from 'react-native-gesture-handler';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import { ACTION_WIDTH, LONG_SWIPE, SMALL_SWIPE } from './styles'; import { ACTION_WIDTH, LONG_SWIPE, SMALL_SWIPE } from './styles';
import { isRTL } from '../../i18n'; import { isRTL } from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { LeftActions, RightActions } from './Actions'; import { LeftActions, RightActions } from './Actions';
import { ITouchableProps } from './interfaces';
interface ITouchableProps {
children: JSX.Element;
type: string;
onPress(): void;
onLongPress(): void;
testID: string;
width: number;
favorite: boolean;
isRead: boolean;
rid: string;
toggleFav: Function;
toggleRead: Function;
hideChannel: Function;
theme: string;
isFocused: boolean;
swipeEnabled: boolean;
displayMode: string;
}
class Touchable extends React.Component<ITouchableProps, any> { class Touchable extends React.Component<ITouchableProps, any> {
private dragX: Animated.Value; private dragX: Animated.Value;
private rowOffSet: Animated.Value; private rowOffSet: Animated.Value;
private reverse: Animated.Value; private reverse: Animated.Value;
private transX: Animated.AnimatedAddition; private transX: Animated.AnimatedAddition;
private transXReverse: Animated.AnimatedMultiplication; private transXReverse: Animated.AnimatedMultiplication;
private _onGestureEvent: (event: GestureEvent<PanGestureHandlerEventPayload>) => void;
private _onGestureEvent: (...args: any[]) => void;
private _value: number; private _value: number;
constructor(props: ITouchableProps) { constructor(props: ITouchableProps) {
@ -56,19 +39,19 @@ class Touchable extends React.Component<ITouchableProps, any> {
this._value = 0; this._value = 0;
} }
_onHandlerStateChange = ({ nativeEvent }: any) => { _onHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload & PanGestureHandlerEventPayload }) => {
if (nativeEvent.oldState === State.ACTIVE) { if (nativeEvent.oldState === State.ACTIVE) {
this._handleRelease(nativeEvent); this._handleRelease(nativeEvent);
} }
}; };
onLongPressHandlerStateChange = ({ nativeEvent }: any) => { onLongPressHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
if (nativeEvent.state === State.ACTIVE) { if (nativeEvent.state === State.ACTIVE) {
this.onLongPress(); this.onLongPress();
} }
}; };
_handleRelease = (nativeEvent: any) => { _handleRelease = (nativeEvent: PanGestureHandlerEventPayload) => {
const { translationX } = nativeEvent; const { translationX } = nativeEvent;
const { rowState } = this.state; const { rowState } = this.state;
this._value += translationX; this._value += translationX;
@ -154,7 +137,7 @@ class Touchable extends React.Component<ITouchableProps, any> {
this._animateRow(toValue); this._animateRow(toValue);
}; };
_animateRow = (toValue: any) => { _animateRow = (toValue: number) => {
this.rowOffSet.setValue(this._value); this.rowOffSet.setValue(this._value);
this._value = toValue; this._value = toValue;
this.dragX.setValue(0); this.dragX.setValue(0);

View File

@ -0,0 +1,17 @@
import React from 'react';
import RoomTypeIcon from '../RoomTypeIcon';
import { ITypeIconProps } from './interfaces';
const TypeIcon = React.memo(({ type, prid, status, isGroupChat, teamMain, size, style }: ITypeIconProps) => (
<RoomTypeIcon
type={prid ? 'discussion' : type}
isGroupChat={isGroupChat}
status={status}
teamMain={teamMain}
size={size}
style={style}
/>
));
export default TypeIcon;

View File

@ -2,17 +2,11 @@ import React from 'react';
import { Text } from 'react-native'; import { Text } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../lib/constants';
import { capitalize } from '../../utils/room'; import { capitalize } from '../../utils/room';
import { IUpdatedAtProps } from './interfaces';
interface IUpdatedAt { const UpdatedAt = React.memo(({ date, theme, hideUnreadStatus, alert }: IUpdatedAtProps) => {
date: string;
theme: string;
hideUnreadStatus: boolean;
alert: boolean;
}
const UpdatedAt = React.memo(({ date, theme, hideUnreadStatus, alert }: IUpdatedAt) => {
if (!date) { if (!date) {
return null; return null;
} }

View File

@ -1,29 +1,12 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { themes } from '../../constants/colors'; import { DisplayMode, themes } from '../../lib/constants';
import { DisplayMode } from '../../constants/constantDisplayMode';
import IconOrAvatar from './IconOrAvatar'; import IconOrAvatar from './IconOrAvatar';
import { IWrapperProps } from './interfaces';
import styles from './styles'; import styles from './styles';
interface IWrapper { const Wrapper = ({ accessibilityLabel, theme, children, displayMode, ...props }: IWrapperProps): React.ReactElement => (
accessibilityLabel: string;
avatar: string;
avatarSize: number;
type: string;
theme: string;
rid: string;
children: JSX.Element;
displayMode: string;
prid: string;
showLastMessage: boolean;
status: string;
isGroupChat: boolean;
teamMain: boolean;
showAvatar: boolean;
}
const Wrapper = ({ accessibilityLabel, theme, children, displayMode, ...props }: IWrapper) => (
<View <View
style={[styles.container, displayMode === DisplayMode.Condensed && styles.containerCondensed]} style={[styles.container, displayMode === DisplayMode.Condensed && styles.containerCondensed]}
accessibilityLabel={accessibilityLabel}> accessibilityLabel={accessibilityLabel}>

View File

@ -5,36 +5,10 @@ import I18n from '../../i18n';
import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles'; import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles';
import { formatDate } from '../../utils/room'; import { formatDate } from '../../utils/room';
import RoomItem from './RoomItem'; import RoomItem from './RoomItem';
import { TUserStatus } from '../../definitions'; import { ISubscription, TUserStatus } from '../../definitions';
import { IRoomItemContainerProps } from './interfaces';
export { ROW_HEIGHT, ROW_HEIGHT_CONDENSED }; export { ROW_HEIGHT, ROW_HEIGHT_CONDENSED };
interface IRoomItemContainerProps {
item: any;
showLastMessage: boolean;
id: string;
onPress: Function;
onLongPress: Function;
username: string;
avatarSize: number;
width: number;
status: TUserStatus;
toggleFav(): void;
toggleRead(): void;
hideChannel(): void;
useRealName: boolean;
getUserPresence: Function;
connected: boolean;
theme: string;
isFocused: boolean;
getRoomTitle: Function;
getRoomAvatar: Function;
getIsGroupChat: Function;
getIsRead: Function;
swipeEnabled: boolean;
autoJoin: boolean;
showAvatar: boolean;
displayMode: string;
}
const attrs = [ const attrs = [
'width', 'width',
@ -50,12 +24,9 @@ const attrs = [
]; ];
class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> { class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> {
private mounted: boolean; private roomSubscription: ISubscription | undefined;
private roomSubscription: any;
static defaultProps: Partial<IRoomItemContainerProps> = { static defaultProps: Partial<IRoomItemContainerProps> = {
avatarSize: 48,
status: 'offline', status: 'offline',
getUserPresence: () => {}, getUserPresence: () => {},
getRoomTitle: () => 'title', getRoomTitle: () => 'title',
@ -67,24 +38,22 @@ class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> {
constructor(props: IRoomItemContainerProps) { constructor(props: IRoomItemContainerProps) {
super(props); super(props);
this.mounted = false;
this.init(); this.init();
} }
componentDidMount() { componentDidMount() {
this.mounted = true;
const { connected, getUserPresence, id } = this.props; const { connected, getUserPresence, id } = this.props;
if (connected && this.isDirect) { if (connected && this.isDirect) {
getUserPresence(id); getUserPresence(id);
} }
} }
shouldComponentUpdate(nextProps: any) { shouldComponentUpdate(nextProps: IRoomItemContainerProps) {
const { props }: any = this; const { props } = this;
return !attrs.every(key => props[key] === nextProps[key]); return !attrs.every(key => props[key] === nextProps[key]);
} }
componentDidUpdate(prevProps: any) { componentDidUpdate(prevProps: IRoomItemContainerProps) {
const { connected, getUserPresence, id } = this.props; const { connected, getUserPresence, id } = this.props;
if (prevProps.connected !== connected && connected && this.isDirect) { if (prevProps.connected !== connected && connected && this.isDirect) {
getUserPresence(id); getUserPresence(id);
@ -106,7 +75,7 @@ class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> {
const { const {
item: { t }, item: { t },
id id
}: any = this.props; } = this.props;
return t === 'd' && id && !this.isGroupChat; return t === 'd' && id && !this.isGroupChat;
} }
@ -144,7 +113,6 @@ class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> {
hideChannel, hideChannel,
theme, theme,
isFocused, isFocused,
avatarSize,
status, status,
showLastMessage, showLastMessage,
username, username,
@ -177,7 +145,6 @@ class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> {
} }
return ( return (
// @ts-ignore
<RoomItem <RoomItem
name={name} name={name}
avatar={avatar} avatar={avatar}
@ -197,7 +164,6 @@ class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> {
type={item.t} type={item.t}
theme={theme} theme={theme}
isFocused={isFocused} isFocused={isFocused}
size={avatarSize}
prid={item.prid} prid={item.prid}
status={status} status={status}
hideUnreadStatus={item.hideUnreadStatus} hideUnreadStatus={item.hideUnreadStatus}
@ -217,6 +183,7 @@ class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> {
autoJoin={autoJoin} autoJoin={autoJoin}
showAvatar={showAvatar} showAvatar={showAvatar}
displayMode={displayMode} displayMode={displayMode}
sourceType={item.source}
/> />
); );
} }

View File

@ -0,0 +1,166 @@
import React from 'react';
import { Animated } from 'react-native';
import { TSupportedThemes } from '../../theme';
import { TUserStatus, ILastMessage, SubscriptionType, IOmnichannelSource } from '../../definitions';
export interface ILeftActionsProps {
theme: TSupportedThemes;
transX: Animated.AnimatedAddition | Animated.AnimatedMultiplication;
isRead: boolean;
width: number;
onToggleReadPress(): void;
displayMode: string;
}
export interface IRightActionsProps {
theme: TSupportedThemes;
transX: Animated.AnimatedAddition | Animated.AnimatedMultiplication;
favorite: boolean;
width: number;
toggleFav(): void;
onHidePress(): void;
displayMode: string;
}
export interface ITitleProps {
name: string;
theme: TSupportedThemes;
hideUnreadStatus: boolean;
alert: boolean;
}
export interface IUpdatedAtProps {
date: string;
theme: TSupportedThemes;
hideUnreadStatus: boolean;
alert: boolean;
}
export interface IWrapperProps {
accessibilityLabel: string;
avatar: string;
type: string;
theme: TSupportedThemes;
rid: string;
children: React.ReactElement;
displayMode: string;
prid: string;
showLastMessage: boolean;
status: string;
isGroupChat: boolean;
teamMain: boolean;
showAvatar: boolean;
sourceType: IOmnichannelSource;
}
export interface ITypeIconProps {
type: string;
status: TUserStatus;
prid: string;
isGroupChat: boolean;
teamMain: boolean;
theme?: TSupportedThemes;
size?: number;
style?: object;
sourceType: IOmnichannelSource;
}
export interface IRoomItemContainerProps {
[key: string]: string | boolean | Function | number;
item: any;
showLastMessage: boolean;
id: string;
onPress: (item: any) => void;
onLongPress: (item: any) => Promise<void>;
username: string;
width: number;
status: TUserStatus;
toggleFav(): void;
toggleRead(): void;
hideChannel(): void;
useRealName: boolean;
getUserPresence: (uid: string) => void;
connected: boolean;
theme: TSupportedThemes;
isFocused: boolean;
getRoomTitle: (item: any) => string;
getRoomAvatar: (item: any) => string;
getIsGroupChat: (item: any) => boolean;
getIsRead: (item: any) => boolean;
swipeEnabled: boolean;
autoJoin: boolean;
showAvatar: boolean;
displayMode: string;
}
export interface IRoomItemProps {
rid: string;
type: SubscriptionType;
prid: string;
name: string;
avatar: string;
showLastMessage: boolean;
username: string;
testID: string;
width: number;
status: TUserStatus;
useRealName: boolean;
theme: TSupportedThemes;
isFocused: boolean;
isGroupChat: boolean;
isRead: boolean;
teamMain: boolean;
date: string;
accessibilityLabel: string;
lastMessage: ILastMessage;
favorite: boolean;
alert: boolean;
hideUnreadStatus: boolean;
unread: number;
userMentions: number;
groupMentions: number;
tunread: [];
tunreadUser: [];
tunreadGroup: [];
swipeEnabled: boolean;
toggleFav(): void;
toggleRead(): void;
onPress(): void;
onLongPress(): void;
hideChannel(): void;
autoJoin: boolean;
size?: number;
showAvatar: boolean;
displayMode: string;
sourceType: IOmnichannelSource;
}
export interface ILastMessageProps {
theme: TSupportedThemes;
lastMessage: ILastMessage;
type: SubscriptionType;
showLastMessage: boolean;
username: string;
useRealName: boolean;
alert: boolean;
}
export interface ITouchableProps {
children: JSX.Element;
type: string;
onPress(): void;
onLongPress(): void;
testID: string;
width: number;
favorite: boolean;
isRead: boolean;
rid: string;
toggleFav: Function;
toggleRead: Function;
hideChannel: Function;
theme: TSupportedThemes;
isFocused: boolean;
swipeEnabled: boolean;
displayMode: string;
}

View File

@ -8,7 +8,7 @@ export const ACTION_WIDTH = 80;
export const SMALL_SWIPE = ACTION_WIDTH / 2; export const SMALL_SWIPE = ACTION_WIDTH / 2;
export const LONG_SWIPE = ACTION_WIDTH * 3; export const LONG_SWIPE = ACTION_WIDTH * 3;
export default StyleSheet.create<any>({ export default StyleSheet.create({
flex: { flex: {
flex: 1 flex: 1
}, },

View File

@ -0,0 +1,51 @@
import React from 'react';
import { StyleProp, ViewStyle } from 'react-native';
import { SvgUri } from 'react-native-svg';
import { useSelector } from 'react-redux';
import { OmnichannelSourceType, IApplicationState, IOmnichannelSource } from '../../definitions';
import { STATUS_COLORS } from '../../lib/constants';
import { CustomIcon } from '../../lib/Icons';
const iconMap = {
widget: 'livechat-monochromatic',
email: 'mail',
sms: 'sms',
app: 'omnichannel',
api: 'omnichannel',
other: 'omnichannel'
};
interface IOmnichannelRoomIconProps {
size: number;
type: string;
style?: StyleProp<ViewStyle>;
status?: string;
sourceType?: IOmnichannelSource;
}
export const OmnichannelRoomIcon = ({ size, style, sourceType, status }: IOmnichannelRoomIconProps) => {
const baseUrl = useSelector((state: IApplicationState) => state.server?.server);
const connected = useSelector((state: IApplicationState) => state.meteor?.connected);
if (sourceType?.type === OmnichannelSourceType.APP && sourceType.id && sourceType.sidebarIcon && connected) {
return (
<SvgUri
height={size}
width={size}
color={STATUS_COLORS[status || 'offline']}
uri={`${baseUrl}/api/apps/public/${sourceType.id}/get-sidebar-icon?icon=${sourceType.sidebarIcon}`}
style={style}
/>
);
}
return (
<CustomIcon
name={iconMap[sourceType?.type || 'other']}
size={size}
style={style}
color={STATUS_COLORS[status || 'offline']}
/>
);
};

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { StyleSheet, ViewStyle } from 'react-native'; import { StyleSheet, ViewStyle } from 'react-native';
import { CustomIcon } from '../lib/Icons'; import { OmnichannelRoomIcon } from './OmnichannelRoomIcon';
import { STATUS_COLORS, themes } from '../constants/colors'; import { CustomIcon } from '../../lib/Icons';
import Status from './Status/Status'; import { STATUS_COLORS, themes } from '../../lib/constants';
import { withTheme } from '../theme'; import Status from '../Status/Status';
import { TUserStatus } from '../definitions'; import { useTheme } from '../../theme';
import { TUserStatus, IOmnichannelSource } from '../../definitions';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
icon: { icon: {
@ -14,21 +15,23 @@ const styles = StyleSheet.create({
}); });
interface IRoomTypeIcon { interface IRoomTypeIcon {
theme?: string;
type: string; type: string;
isGroupChat?: boolean; isGroupChat?: boolean;
teamMain?: boolean; teamMain?: boolean;
status?: TUserStatus; status?: TUserStatus;
size?: number; size?: number;
style?: ViewStyle; style?: ViewStyle;
sourceType?: IOmnichannelSource;
} }
const RoomTypeIcon = React.memo(({ type, isGroupChat, status, style, theme, teamMain, size = 16 }: IRoomTypeIcon) => { const RoomTypeIcon = React.memo(({ type, isGroupChat, status, style, teamMain, size = 16, sourceType }: IRoomTypeIcon) => {
const { theme } = useTheme();
if (!type) { if (!type) {
return null; return null;
} }
const color = themes[theme!].titleText; const color = themes[theme].titleText;
const iconStyle = [styles.icon, { color }, style]; const iconStyle = [styles.icon, { color }, style];
if (type === 'd' && !isGroupChat) { if (type === 'd' && !isGroupChat) {
@ -38,6 +41,10 @@ const RoomTypeIcon = React.memo(({ type, isGroupChat, status, style, theme, team
return <Status style={[iconStyle, { color: STATUS_COLORS[status] }]} size={size} status={status} />; return <Status style={[iconStyle, { color: STATUS_COLORS[status] }]} size={size} status={status} />;
} }
if (type === 'l') {
return <OmnichannelRoomIcon style={[styles.icon, style]} size={size} type={type} status={status} sourceType={sourceType} />;
}
// TODO: move this to a separate function // TODO: move this to a separate function
let icon = 'channel-private'; let icon = 'channel-private';
if (teamMain) { if (teamMain) {
@ -52,11 +59,9 @@ const RoomTypeIcon = React.memo(({ type, isGroupChat, status, style, theme, team
} else { } else {
icon = 'mention'; icon = 'mention';
} }
} else if (type === 'l') {
icon = 'omnichannel';
} }
return <CustomIcon name={icon} size={size} style={iconStyle} />; return <CustomIcon name={icon} size={size} style={iconStyle} />;
}); });
export default withTheme(RoomTypeIcon); export default RoomTypeIcon;

View File

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet, ViewProps } from 'react-native';
import { SafeAreaView as SafeAreaContext } from 'react-native-safe-area-context'; import { SafeAreaView as SafeAreaContext } from 'react-native-safe-area-context';
import { themes } from '../constants/colors'; import { themes } from '../lib/constants';
import { withTheme } from '../theme'; import { useTheme } from '../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
view: { view: {
@ -11,22 +11,24 @@ const styles = StyleSheet.create({
} }
}); });
interface ISafeAreaView { type SupportedChildren = React.ReactElement | React.ReactElement[] | null;
testID?: string; type TSafeAreaViewChildren = SupportedChildren | SupportedChildren[];
theme?: string;
interface ISafeAreaView extends ViewProps {
vertical?: boolean; vertical?: boolean;
style?: object; children: TSafeAreaViewChildren;
children: React.ReactNode;
} }
const SafeAreaView = React.memo(({ style, children, testID, theme, vertical = true, ...props }: ISafeAreaView) => ( const SafeAreaView = React.memo(({ style, children, vertical = true, ...props }: ISafeAreaView) => {
<SafeAreaContext const { theme } = useTheme();
style={[styles.view, { backgroundColor: themes[theme!].auxiliaryBackground }, style]} return (
edges={vertical ? ['right', 'left'] : undefined} <SafeAreaContext
testID={testID} style={[styles.view, { backgroundColor: themes[theme].auxiliaryBackground }, style]}
{...props}> edges={vertical ? ['right', 'left'] : undefined}
{children} {...props}>
</SafeAreaContext> {children}
)); </SafeAreaContext>
);
});
export default withTheme(SafeAreaView); export default SafeAreaView;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { StyleSheet, Text, TextInput as RNTextInput, TextInputProps, View } from 'react-native'; import { StyleSheet, Text, TextInput as RNTextInput, TextInputProps, View } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { themes } from '../constants/colors'; import { themes } from '../lib/constants';
import I18n from '../i18n'; import I18n from '../i18n';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import TextInput from '../presentation/TextInput'; import TextInput from '../presentation/TextInput';

View File

@ -4,7 +4,7 @@ import { StyleSheet, View } from 'react-native';
import I18n from '../i18n'; import I18n from '../i18n';
import { useTheme } from '../theme'; import { useTheme } from '../theme';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../lib/constants';
import TextInput from '../presentation/TextInput'; import TextInput from '../presentation/TextInput';
import { isIOS, isTablet } from '../utils/deviceInfo'; import { isIOS, isTablet } from '../utils/deviceInfo';
import { useOrientation } from '../dimensions'; import { useOrientation } from '../dimensions';

View File

@ -0,0 +1,64 @@
import React from 'react';
// @ts-ignore // TODO: Remove on react-native update
import { Pressable, Text, View } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
import { IServerInfo } from '../../definitions';
import Check from '../Check';
import styles, { ROW_HEIGHT } from './styles';
import { themes } from '../../lib/constants';
import { isIOS } from '../../utils/deviceInfo';
import { useTheme } from '../../theme';
export { ROW_HEIGHT };
interface IServerItem {
item: IServerInfo;
onPress(): void;
onLongPress?(): void;
hasCheck?: boolean;
}
const defaultLogo = require('../../static/images/logo.png');
const ServerItem = React.memo(({ item, onPress, onLongPress, hasCheck }: IServerItem) => {
const { theme } = useTheme();
return (
<Pressable
onPress={onPress}
onLongPress={() => onLongPress?.()}
testID={`rooms-list-header-server-${item.id}`}
android_ripple={{ color: themes[theme].bannerBackground }}
style={({ pressed }: { pressed: boolean }) => ({
backgroundColor: isIOS && pressed ? themes[theme].bannerBackground : themes[theme].backgroundColor
})}>
<View style={styles.serverItemContainer}>
{item.iconURL ? (
<FastImage
source={{
uri: item.iconURL,
priority: FastImage.priority.high
}}
// @ts-ignore TODO: Remove when updating FastImage
defaultSource={defaultLogo}
style={styles.serverIcon}
onError={() => console.log('err_loading_server_icon')}
/>
) : (
<FastImage source={defaultLogo} style={styles.serverIcon} />
)}
<View style={styles.serverTextContainer}>
<Text numberOfLines={1} style={[styles.serverName, { color: themes[theme].titleText }]}>
{item.name || item.id}
</Text>
<Text numberOfLines={1} style={[styles.serverUrl, { color: themes[theme].auxiliaryText }]}>
{item.id}
</Text>
</View>
{hasCheck ? <Check /> : null}
</View>
</Pressable>
);
});
export default ServerItem;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { StyleProp, TextStyle } from 'react-native'; import { StyleProp, TextStyle } from 'react-native';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { STATUS_COLORS } from '../../constants/colors'; import { STATUS_COLORS } from '../../lib/constants';
import { IStatus } from './definition'; import { IStatus } from './definition';
const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: Omit<IStatus, 'id'>) => { const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: Omit<IStatus, 'id'>) => {

Some files were not shown because too many files have changed in this diff Show More