Merge branch 'develop' into update.enable-multiline-android-tablets

This commit is contained in:
Gerzon Z 2022-04-04 17:11:05 -04:00
commit c369baf63f
166 changed files with 2218 additions and 1887 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

View File

@ -1,45 +1,8 @@
import initStoryshots, { Stories2SnapsConverter } from '@storybook/addon-storyshots'; import initStoryshots, { Stories2SnapsConverter } from '@storybook/addon-storyshots';
import { render } from '@testing-library/react-native'; import { render } from '@testing-library/react-native';
jest.mock('rn-fetch-blob', () => ({
fs: {
dirs: {
DocumentDir: '/data/com.rocket.chat/documents',
DownloadDir: '/data/com.rocket.chat/downloads'
},
exists: jest.fn(() => null)
},
fetch: jest.fn(() => null),
config: jest.fn(() => null)
}));
jest.mock('react-native-file-viewer', () => ({
open: jest.fn(() => null)
}));
jest.mock('../app/lib/database', () => jest.fn(() => null));
global.Date.now = jest.fn(() => new Date('2019-10-10').getTime()); global.Date.now = jest.fn(() => new Date('2019-10-10').getTime());
jest.mock('react-native-mmkv-storage', () => {
return {
Loader: jest.fn().mockImplementation(() => {
return {
setProcessingMode: jest.fn().mockImplementation(() => {
return {
withEncryption: jest.fn().mockImplementation(() => {
return {
initialize: jest.fn()
};
})
};
})
};
}),
create: jest.fn(),
MODES: { MULTI_PROCESS: '' }
};
});
const converter = new Stories2SnapsConverter(); const converter = new Stories2SnapsConverter();
initStoryshots({ initStoryshots({

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.0" versionName "4.26.2"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
if (!isFoss) { if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]

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

@ -19,7 +19,7 @@ const mentions = {
mentionOtherColor: '#F3BE08' mentionOtherColor: '#F3BE08'
}; };
export const themes: any = { export const colors = {
light: { light: {
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
focusedBackground: '#ffffff', focusedBackground: '#ffffff',
@ -174,3 +174,5 @@ export const themes: any = {
...mentions ...mentions
} }
}; };
export const themes: any = colors;

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

@ -8,6 +8,7 @@ import { avatarURL } from '../../utils/avatar';
import { SubscriptionType } from '../../definitions/ISubscription'; import { SubscriptionType } from '../../definitions/ISubscription';
import Emoji from '../markdown/Emoji'; import Emoji from '../markdown/Emoji';
import { IAvatar } from './interfaces'; import { IAvatar } from './interfaces';
import { useTheme } from '../../theme';
const Avatar = React.memo( const Avatar = React.memo(
({ ({
@ -18,7 +19,6 @@ const Avatar = React.memo(
user, user,
onPress, onPress,
emoji, emoji,
theme,
getCustomEmoji, getCustomEmoji,
avatarETag, avatarETag,
isStatic, isStatic,
@ -34,6 +34,8 @@ const Avatar = React.memo(
return null; return null;
} }
const { theme } = useTheme();
const avatarStyle = { const avatarStyle = {
width: size, width: size,
height: size, height: size,
@ -44,7 +46,7 @@ const Avatar = React.memo(
if (emoji) { if (emoji) {
image = ( image = (
<Emoji <Emoji
theme={theme!} theme={theme}
baseUrl={server} baseUrl={server}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji isMessageContainsOnlyEmoji

View File

@ -5,13 +5,11 @@ import { Observable, Subscription } from 'rxjs';
import database from '../../lib/database'; import database from '../../lib/database';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import { TSubscriptionModel, TUserModel } from '../../definitions'; import { IApplicationState, TSubscriptionModel, TUserModel } from '../../definitions';
import Avatar from './Avatar'; import Avatar from './Avatar';
import { IAvatar } from './interfaces'; import { IAvatar } from './interfaces';
class AvatarContainer extends React.Component<IAvatar, any> { class AvatarContainer extends React.Component<IAvatar, any> {
private mounted: boolean;
private subscription?: Subscription; private subscription?: Subscription;
static defaultProps = { static defaultProps = {
@ -21,16 +19,11 @@ class AvatarContainer extends React.Component<IAvatar, any> {
constructor(props: IAvatar) { constructor(props: IAvatar) {
super(props); super(props);
this.mounted = false;
this.state = { avatarETag: '' }; this.state = { avatarETag: '' };
this.init(); this.init();
} }
componentDidMount() { componentDidUpdate(prevProps: IAvatar) {
this.mounted = true;
}
componentDidUpdate(prevProps: any) {
const { text, type } = this.props; const { text, type } = this.props;
if (prevProps.text !== text || prevProps.type !== type) { if (prevProps.text !== text || prevProps.type !== type) {
this.init(); this.init();
@ -88,12 +81,7 @@ class AvatarContainer extends React.Component<IAvatar, any> {
const observable = record.observe() as Observable<TSubscriptionModel | TUserModel>; const observable = record.observe() as Observable<TSubscriptionModel | TUserModel>;
this.subscription = observable.subscribe(r => { this.subscription = observable.subscribe(r => {
const { avatarETag } = r; const { avatarETag } = r;
if (this.mounted) {
this.setState({ avatarETag }); this.setState({ avatarETag });
} else {
// @ts-ignore
this.state.avatarETag = avatarETag;
}
}); });
} }
}; };
@ -105,12 +93,12 @@ class AvatarContainer extends React.Component<IAvatar, any> {
} }
} }
const mapStateToProps = (state: any) => ({ const mapStateToProps = (state: IApplicationState) => ({
user: getUserSelector(state), user: getUserSelector(state),
server: state.share.server.server || state.server.server, server: state.share.server.server || state.server.server,
serverVersion: state.share.server.version || state.server.version, serverVersion: state.share.server.version || state.server.version,
blockUnauthenticatedAccess: blockUnauthenticatedAccess:
state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess ?? (state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess as boolean) ??
state.settings.Accounts_AvatarBlockUnauthenticatedAccess ?? state.settings.Accounts_AvatarBlockUnauthenticatedAccess ??
true true
}); });

View File

@ -16,12 +16,11 @@ export interface IAvatar {
id?: string; id?: string;
token?: string; token?: string;
}; };
theme?: string;
onPress?: () => void; onPress?: () => void;
getCustomEmoji?: TGetCustomEmoji; getCustomEmoji?: TGetCustomEmoji;
avatarETag?: string; avatarETag?: string;
isStatic?: boolean | string; isStatic?: boolean | string;
rid?: string; rid?: string;
blockUnauthenticatedAccess?: boolean; blockUnauthenticatedAccess?: boolean;
serverVersion: string; serverVersion: string | null;
} }

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

@ -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

View File

@ -285,7 +285,7 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, ILoginServi
toValue: height, toValue: height,
duration: 300, duration: 300,
easing: Easing.inOut(Easing.quad), easing: Easing.inOut(Easing.quad),
useNativeDriver: true useNativeDriver: false
}).start(); }).start();
}; };

View File

@ -1,7 +1,7 @@
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 { useTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import shortnameToUnicode from '../../utils/shortnameToUnicode'; import shortnameToUnicode from '../../utils/shortnameToUnicode';
@ -10,26 +10,30 @@ 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: string;
} }
interface THeaderFooter { interface THeaderFooter {
onReaction: any; onReaction: TOnReaction;
theme: string; theme: string;
} }
@ -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) => {
const emojiModel = item as TFrequentlyUsedEmojiModel;
const emoji = (emojiModel.id ? emojiModel.content : item) as string;
return (
<Button <Button
testID={`message-actions-emoji-${item.content || item}`} testID={`message-actions-emoji-${emoji}`}
onPress={() => onReaction({ emoji: `:${item.content || item}:` })} onPress={() => onReaction({ emoji: `:${emoji}:` })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]} style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}> theme={theme}>
{item?.isCustom ? ( {emojiModel?.isCustom ? (
<CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} /> <CustomEmoji style={styles.customEmoji} emoji={emojiModel as IEmoji} baseUrl={server} />
) : ( ) : (
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${item.content || item}:`)}</Text> <Text style={styles.headerIcon}>{shortnameToUnicode(`:${emoji}:`)}</Text>
)} )}
</Button> </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

@ -1,5 +1,6 @@
import React, { forwardRef, useImperativeHandle } from 'react'; import React, { forwardRef, useImperativeHandle } from 'react';
import { Alert, Clipboard, Share } from 'react-native'; import { Alert, Share } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import moment from 'moment'; import moment from 'moment';
@ -12,33 +13,33 @@ 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(
@ -68,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 {
@ -87,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;
} }
@ -104,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');
} }
@ -113,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;
} }
@ -135,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');
} }
@ -144,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) {
@ -166,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;
@ -178,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
} }
@ -191,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);
if (permalink) {
Share.share({ message: 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);
@ -234,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);
} }
@ -255,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 {
@ -290,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);
@ -301,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);
@ -318,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) {
@ -462,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 { CustomIcon } from '../../../lib/Icons';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import MessageboxContext from '../Context'; import { CustomIcon } from '../../../lib/Icons';
import { useTheme } from '../../../theme';
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,30 @@
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 styles from '../styles';
import { themes } from '../../../constants/colors'; 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) => {
if (!showCommandPreview) { if (!showCommandPreview) {
return null; return null;
} }
const { theme } = useTheme();
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 +33,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 +43,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

@ -7,6 +7,7 @@ import EmojiPicker from '../EmojiPicker';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { IEmoji } from '../../definitions/IEmoji';
interface IMessageBoxEmojiKeyboard { interface IMessageBoxEmojiKeyboard {
theme: string; theme: string;
@ -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} theme={theme} />; return <CancelEditingButton onPress={editCancel} />;
} }
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} theme={theme} />; return <CancelEditingButton onPress={editCancel} />;
} }
if (isActionsEnabled) { if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />; return <ActionsButton onPress={showMessageBoxActions} />;
} }
return <View style={styles.buttonsWhitespace} />; return <View style={styles.buttonsWhitespace} />;
} });
);
export default LeftButtons; export default LeftButtons;

View File

@ -4,16 +4,18 @@ 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 '../../../constants/colors';
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) => {
const { theme } = useTheme();
return (
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.mentionItem, styles.mentionItem,
@ -29,5 +31,6 @@ const FixedMentionItem = ({ item, onPress, theme }: IMessageBoxFixedMentionItem)
</Text> </Text>
</TouchableOpacity> </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 styles from '../styles';
import sharedStyles from '../../../views/Styles';
import I18n from '../../../i18n';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import I18n from '../../../i18n';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
import { useTheme } from '../../../theme';
import sharedStyles from '../../../views/Styles';
import { MENTIONS_TRACKING_TYPE_CANNED } from '../constants';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import styles from '../styles';
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 Avatar from '../../Avatar';
import MessageboxContext from '../Context';
import FixedMentionItem from './FixedMentionItem';
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 { themes } from '../../../constants/colors';
import { IEmoji } from '../../../definitions/IEmoji'; import { IEmoji } from '../../../definitions/IEmoji';
import { useTheme } from '../../../theme';
import Avatar from '../../Avatar';
import { MENTIONS_TRACKING_TYPE_CANNED, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_EMOJIS } from '../constants';
import MessageboxContext from '../Context';
import styles from '../styles';
import FixedMentionItem from './FixedMentionItem';
import MentionEmoji from './MentionEmoji';
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

@ -6,29 +6,30 @@ 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 '../../../constants/colors';
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) => {
if (!trackingType) { if (!trackingType) {
return null; return null;
} }
const { theme } = useTheme();
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 +40,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

@ -47,23 +47,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 +106,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 +151,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',

View File

@ -8,6 +8,8 @@ import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
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,16 +51,15 @@ 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) => {
if (!replying) { if (!replying) {
return null; return null;
} }
const { theme } = useTheme();
const time = moment(message.ts).format(Message_TimeFormat); const time = moment(message.ts).format(Message_TimeFormat);
return ( return (
<View style={[styles.container, { backgroundColor: themes[theme].messageboxBackground }]}> <View style={[styles.container, { backgroundColor: themes[theme].messageboxBackground }]}>
@ -75,16 +76,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} theme={theme} />; return <SendButton onPress={submit} />;
} }
if (isActionsEnabled) { if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />; return <ActionsButton onPress={showMessageBoxActions} />;
} }
return <View style={styles.buttonsWhitespace} />; 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,13 @@
import React from 'react';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import React from 'react';
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 '../../../constants/colors';
interface IBaseButton { interface IBaseButton {
theme: string;
onPress(): void; onPress(): void;
testID: string; testID: string;
accessibilityLabel: string; accessibilityLabel: string;
@ -15,16 +15,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>) => {
const { theme } = useTheme();
return (
<BorderlessButton <BorderlessButton
onPress={onPress} {...props}
style={styles.actionButton} style={styles.actionButton}
testID={testID}
// @ts-ignore // @ts-ignore
accessibilityLabel={I18n.t(accessibilityLabel)} accessibilityLabel={i18n.t(accessibilityLabel)}
accessibilityTraits='button'> accessibilityTraits='button'>
<CustomIcon name={icon} size={24} color={color ?? themes[theme!].auxiliaryTintColor} /> <CustomIcon name={icon} size={24} color={color || themes[theme].auxiliaryTintColor} />
</BorderlessButton> </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 BaseButton from './BaseButton';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
import BaseButton from './BaseButton';
interface ISendButton { interface ISendButton {
theme: string;
onPress(): void; onPress(): void;
} }
const SendButton = React.memo(({ theme, onPress }: ISendButton) => ( const SendButton = ({ onPress }: ISendButton) => {
const { theme } = useTheme();
return (
<BaseButton <BaseButton
onPress={onPress} onPress={onPress}
testID='messagebox-send-message' testID='messagebox-send-message'
accessibilityLabel='Send_message' accessibilityLabel='Send_message'
icon='send-filled' icon='send-filled'
theme={theme}
color={themes[theme].tintColor} 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';
@ -50,7 +50,8 @@ 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';
if (isAndroid) { if (isAndroid) {
require('./EmojiKeyboard'); require('./EmojiKeyboard');
@ -63,18 +64,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;
@ -97,7 +98,6 @@ export interface IMessageBoxProps {
theme: string; theme: string;
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 +118,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 +132,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;
@ -713,7 +711,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 +756,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, value, attachments });
}; };
createDiscussion = () => { createDiscussion = () => {
@ -1058,7 +1057,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;
@ -1069,7 +1068,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

@ -8,7 +8,7 @@ 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 '../constants/colors';
import { withTheme } from '../theme'; import { 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: Function; onClose: () => void;
theme: string; theme: string;
} }
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,7 +142,9 @@ 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) => {
const { theme } = useTheme();
return (
<Modal <Modal
isVisible={isVisible} isVisible={isVisible}
onBackdropPress={onClose} onBackdropPress={onClose}
@ -153,8 +154,9 @@ const ReactionsModal = React.memo(
swipeDirection={['up', 'left', 'right', 'down']}> swipeDirection={['up', 'left', 'right', 'down']}>
<ModalContent onClose={onClose} theme={theme} {...props} /> <ModalContent onClose={onClose} theme={theme} {...props} />
</Modal> </Modal>
), );
(prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible && prevProps.theme === nextProps.theme },
(prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible
); );
ReactionsModal.displayName = 'ReactionsModal'; ReactionsModal.displayName = 'ReactionsModal';

View File

@ -6,8 +6,8 @@ import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { MarkdownPreview } from '../markdown'; import { MarkdownPreview } from '../markdown';
import RoomTypeIcon from '../RoomTypeIcon'; import RoomTypeIcon from '../RoomTypeIcon';
import { withTheme } from '../../theme';
import { TUserStatus } 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;
}; };
@ -77,7 +75,8 @@ interface IRoomHeader {
testID: string; testID: string;
} }
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,12 @@ const Header = React.memo(
prid, prid,
tmid, tmid,
onPress, onPress,
theme,
isGroupChat, isGroupChat,
teamMain, teamMain,
testID, testID,
usersTyping = [] usersTyping = []
}: IRoomHeader) => { }: IRoomHeader) => {
const { theme } = useTheme();
const portrait = height > width; const portrait = height > width;
let scale = 1; let scale = 1;
@ -153,7 +153,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 +168,18 @@ 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} />
)} )}
<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;

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 '../constants/colors';
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) => {
const { theme } = useTheme();
return (
<SafeAreaContext <SafeAreaContext
style={[styles.view, { backgroundColor: themes[theme!].auxiliaryBackground }, style]} style={[styles.view, { backgroundColor: themes[theme].auxiliaryBackground }, style]}
edges={vertical ? ['right', 'left'] : undefined} edges={vertical ? ['right', 'left'] : undefined}
testID={testID}
{...props}> {...props}>
{children} {children}
</SafeAreaContext> </SafeAreaContext>
)); );
});
export default withTheme(SafeAreaView); export default SafeAreaView;

View File

@ -47,11 +47,11 @@ const styles = StyleSheet.create({
interface ISearchBox extends TextInputProps { interface ISearchBox extends TextInputProps {
value?: string; value?: string;
hasCancel?: boolean; hasCancel?: boolean;
onCancelPress?: Function; onCancelPress?: () => void;
inputRef?: React.Ref<RNTextInput>; inputRef?: React.Ref<RNTextInput>;
} }
const CancelButton = ({ onCancelPress }: { onCancelPress?: Function }) => { const CancelButton = ({ onCancelPress }: { onCancelPress?: () => void }) => {
const { theme } = useTheme(); const { theme } = useTheme();
return ( return (
<Touchable onPress={onCancelPress} style={styles.cancel}> <Touchable onPress={onCancelPress} style={styles.cancel}>
@ -84,7 +84,7 @@ const SearchBox = ({ hasCancel, onCancelPress, inputRef, ...props }: ISearchBox)
{...props} {...props}
/> />
</View> </View>
{hasCancel ? <CancelButton onCancelPress={onCancelPress} /> : null} {hasCancel && onCancelPress ? <CancelButton onCancelPress={onCancelPress} /> : null}
</View> </View>
); );
}; };

View File

@ -4,8 +4,10 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import Button from '../Button'; import Button from '../Button';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { IActions } from './interfaces'; import { IActions } from './interfaces';
import { useTheme } from '../../theme';
export const Actions = ({ blockId, appId, elements, parser, theme }: IActions) => { export const Actions = ({ blockId, appId, elements, parser }: IActions) => {
const { theme } = useTheme();
const [showMoreVisible, setShowMoreVisible] = useState(() => elements && elements.length > 5); const [showMoreVisible, setShowMoreVisible] = useState(() => elements && elements.length > 5);
const renderedElements = showMoreVisible ? elements?.slice(0, 5) : elements; const renderedElements = showMoreVisible ? elements?.slice(0, 5) : elements;

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import DateTimePicker from '@react-native-community/datetimepicker'; import DateTimePicker, { Event } from '@react-native-community/datetimepicker';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import moment from 'moment'; import moment from 'moment';
@ -11,6 +11,7 @@ import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { isAndroid } from '../../utils/deviceInfo'; import { isAndroid } from '../../utils/deviceInfo';
import { useTheme } from '../../theme';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
import { IDatePicker } from './interfaces'; import { IDatePicker } from './interfaces';
@ -36,14 +37,17 @@ const styles = StyleSheet.create({
} }
}); });
export const DatePicker = ({ element, language, action, context, theme, loading, value, error }: IDatePicker) => { export const DatePicker = ({ element, language, action, context, loading, value, error }: IDatePicker) => {
const { theme } = useTheme();
const [show, onShow] = useState(false); const [show, onShow] = useState(false);
const initial_date = element?.initial_date; const initial_date = element?.initial_date;
const placeholder = element?.placeholder; const placeholder = element?.placeholder;
const [currentDate, onChangeDate] = useState(new Date(initial_date || value)); const [currentDate, onChangeDate] = useState(new Date(initial_date || value));
const onChange = ({ nativeEvent: { timestamp } }: any, date: any) => { // timestamp as number exists in Event
// @ts-ignore
const onChange = ({ nativeEvent: { timestamp } }: Event, date?: Date) => {
const newDate = date || new Date(timestamp); const newDate = date || new Date(timestamp);
onChangeDate(newDate); onChangeDate(newDate);
action({ value: moment(newDate).format('YYYY-MM-DD') }); action({ value: moment(newDate).format('YYYY-MM-DD') });
@ -52,7 +56,9 @@ export const DatePicker = ({ element, language, action, context, theme, loading,
} }
}; };
let button = <Button title={textParser([placeholder])} onPress={() => onShow(!show)} loading={loading} theme={theme} />; let button = placeholder ? (
<Button title={textParser([placeholder])} onPress={() => onShow(!show)} loading={loading} theme={theme} />
) : null;
if (context === BLOCK_CONTEXT.FORM) { if (context === BLOCK_CONTEXT.FORM) {
button = ( button = (

View File

@ -6,7 +6,7 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import ImageContainer from '../message/Image'; import ImageContainer from '../message/Image';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
import { IThumb, IImage, IElement } from './interfaces'; import { IThumb, IImage, IElement } from './interfaces';
import { TThemeMode } from '../../definitions/ITheme'; import { IAttachment } from '../../definitions';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
image: { image: {
@ -27,23 +27,22 @@ export const Thumb = ({ element, size = 88 }: IThumb) => (
<FastImage style={[{ width: size, height: size }, styles.image]} source={{ uri: element?.imageUrl }} /> <FastImage style={[{ width: size, height: size }, styles.image]} source={{ uri: element?.imageUrl }} />
); );
export const Media = ({ element, theme }: IImage) => { export const Media = ({ element }: IImage) => {
const showAttachment = (attachment: any) => Navigation.navigate('AttachmentView', { attachment }); const showAttachment = (attachment: IAttachment) => Navigation.navigate('AttachmentView', { attachment });
const imageUrl = element?.imageUrl ?? ''; const imageUrl = element?.imageUrl ?? '';
// @ts-ignore
// TODO: delete ts-ignore after refactor Markdown and ImageContainer return <ImageContainer file={{ image_url: imageUrl }} imageUrl={imageUrl} showAttachment={showAttachment} />;
return <ImageContainer file={{ image_url: imageUrl }} imageUrl={imageUrl} showAttachment={showAttachment} theme={theme} />;
}; };
const genericImage = (theme: TThemeMode, element: IElement, context?: number) => { const genericImage = (element: IElement, context?: number) => {
switch (context) { switch (context) {
case BLOCK_CONTEXT.SECTION: case BLOCK_CONTEXT.SECTION:
return <Thumb element={element} />; return <Thumb element={element} />;
case BLOCK_CONTEXT.CONTEXT: case BLOCK_CONTEXT.CONTEXT:
return <ThumbContext element={element} />; return <ThumbContext element={element} />;
default: default:
return <Media element={element} theme={theme} />; return <Media element={element} />;
} }
}; };
export const Image = ({ element, context, theme }: IImage) => genericImage(theme, element, context); export const Image = ({ element, context }: IImage) => genericImage(element, context);

View File

@ -7,26 +7,23 @@ import { themes } from '../../../constants/colors';
import { textParser } from '../utils'; import { textParser } from '../utils';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
import styles from './styles'; import styles from './styles';
import { IItemData } from '.';
interface IChip { interface IChip {
item: { item: IItemData;
value: string; onSelect: (item: IItemData) => void;
imageUrl: string;
text: string;
};
onSelect: Function;
style?: object; style?: object;
theme: string; theme: string;
} }
interface IChips { interface IChips {
items: []; items: IItemData[];
onSelect: Function; onSelect: (item: IItemData) => void;
style?: object; style?: object;
theme: string; theme: string;
} }
const keyExtractor = (item: any) => item.value.toString(); const keyExtractor = (item: IItemData) => item.value.toString();
const Chip = ({ item, onSelect, style, theme }: IChip) => ( const Chip = ({ item, onSelect, style, theme }: IChip) => (
<Touchable <Touchable

View File

@ -9,10 +9,10 @@ import styles from './styles';
interface IInput { interface IInput {
children?: JSX.Element; children?: JSX.Element;
onPress: Function; onPress: () => void;
theme: string; theme: string;
inputStyle?: object; inputStyle?: object;
disabled?: boolean | object; disabled?: boolean | null;
placeholder?: string; placeholder?: string;
loading?: boolean; loading?: boolean;
innerInputStyle?: object; innerInputStyle?: object;

View File

@ -8,34 +8,31 @@ import * as List from '../../List';
import { textParser } from '../utils'; import { textParser } from '../utils';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import styles from './styles'; import styles from './styles';
import { IItemData } from '.';
interface IItem { interface IItem {
item: { item: IItemData;
value: { name: string }; selected?: string;
text: { text: string };
imageUrl: string;
};
selected: any;
onSelect: Function; onSelect: Function;
theme: string; theme: string;
} }
interface IItems { interface IItems {
items: []; items: IItemData[];
selected: []; selected: string[];
onSelect: Function; onSelect: Function;
theme: string; theme: string;
} }
const keyExtractor = (item: any) => item.value.toString(); const keyExtractor = (item: IItemData) => item.value.toString();
// RectButton doesn't work on modal (Android) // RectButton doesn't work on modal (Android)
const Item = ({ item, selected, onSelect, theme }: IItem) => { const Item = ({ item, selected, onSelect, theme }: IItem) => {
const itemName = item.value.name || item.text.text.toLowerCase(); const itemName = item.value || item.text.text.toLowerCase();
return ( return (
<Touchable <Touchable
testID={`multi-select-item-${itemName}`} testID={`multi-select-item-${itemName}`}
key={item} key={itemName}
onPress={() => onSelect(item)} onPress={() => onSelect(item)}
style={[styles.item, { backgroundColor: themes[theme].backgroundColor }]}> style={[styles.item, { backgroundColor: themes[theme].backgroundColor }]}>
<> <>

View File

@ -1,5 +1,15 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Animated, Easing, KeyboardAvoidingView, Modal, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'; import {
Animated,
Easing,
KeyboardAvoidingView,
Modal,
StyleSheet,
Text,
TouchableWithoutFeedback,
View,
TextStyle
} from 'react-native';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import Button from '../../Button'; import Button from '../../Button';
@ -8,26 +18,31 @@ import { textParser } from '../utils';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
import { isIOS } from '../../../utils/deviceInfo'; import { isIOS } from '../../../utils/deviceInfo';
import { useTheme } from '../../../theme';
import { BlockContext, IText } from '../interfaces';
import Chips from './Chips'; import Chips from './Chips';
import Items from './Items'; import Items from './Items';
import Input from './Input'; import Input from './Input';
import styles from './styles'; import styles from './styles';
export interface IItemData {
value: string;
text: { text: string };
imageUrl?: string;
}
interface IMultiSelect { interface IMultiSelect {
options: any[]; options?: IItemData[];
onChange: Function; onChange: Function;
placeholder: { placeholder?: IText;
text: string; context?: BlockContext;
};
context?: number;
loading?: boolean; loading?: boolean;
multiselect?: boolean; multiselect?: boolean;
onSearch?: () => void; onSearch?: () => void;
onClose?: () => void; onClose?: () => void;
inputStyle?: object; inputStyle?: TextStyle;
value?: any[]; value?: any[];
disabled?: boolean | object; disabled?: boolean | null;
theme: string;
innerInputStyle?: object; innerInputStyle?: object;
} }
@ -54,9 +69,9 @@ export const MultiSelect = React.memo(
onClose = () => {}, onClose = () => {},
disabled, disabled,
inputStyle, inputStyle,
theme,
innerInputStyle innerInputStyle
}: IMultiSelect) => { }: IMultiSelect) => {
const { theme } = useTheme();
const [selected, select] = useState<any>(Array.isArray(values) ? values : []); const [selected, select] = useState<any>(Array.isArray(values) ? values : []);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [search, onSearchChange] = useState(''); const [search, onSearchChange] = useState('');
@ -95,7 +110,7 @@ export const MultiSelect = React.memo(
}).start(() => setShowContent(false)); }).start(() => setShowContent(false));
}; };
const onSelect = (item: any) => { const onSelect = (item: IItemData) => {
const { const {
value, value,
text: { text } text: { text }

View File

@ -6,6 +6,7 @@ import Touchable from 'react-native-platform-touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { useTheme } from '../../theme';
import { BUTTON_HIT_SLOP } from '../message/utils'; import { BUTTON_HIT_SLOP } from '../message/utils';
import * as List from '../List'; import * as List from '../List';
import { IOption, IOptions, IOverflow } from './interfaces'; import { IOption, IOptions, IOverflow } from './interfaces';
@ -43,9 +44,10 @@ const Options = ({ options, onOptionPress, parser, theme }: IOptions) => (
/> />
); );
const touchable: { [key: string]: any } = {}; const touchable: { [key: string]: Touchable | null } = {};
export const Overflow = ({ element, loading, action, parser, theme }: IOverflow) => { export const Overflow = ({ element, loading, action, parser }: IOverflow) => {
const { theme } = useTheme();
const options = element?.options || []; const options = element?.options || [];
const blockId = element?.blockId || ''; const blockId = element?.blockId || '';
const [show, onShow] = useState(false); const [show, onShow] = useState(false);
@ -58,7 +60,7 @@ export const Overflow = ({ element, loading, action, parser, theme }: IOverflow)
return ( return (
<> <>
<Touchable <Touchable
ref={(ref: any) => (touchable[blockId] = ref)} ref={ref => (touchable[blockId] = ref)}
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
onPress={() => onShow(!show)} onPress={() => onShow(!show)}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
@ -71,6 +73,7 @@ export const Overflow = ({ element, loading, action, parser, theme }: IOverflow)
</Touchable> </Touchable>
<Popover <Popover
isVisible={show} isVisible={show}
// fromView exists in Popover Component
/* @ts-ignore*/ /* @ts-ignore*/
fromView={touchable[blockId]} fromView={touchable[blockId]}
onRequestClose={() => onShow(false)}> onRequestClose={() => onShow(false)}>

View File

@ -4,6 +4,7 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { IAccessoryComponent, IFields, ISection } from './interfaces'; import { IAccessoryComponent, IFields, ISection } from './interfaces';
import { useTheme } from '../../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
content: { content: {
@ -37,10 +38,14 @@ const Fields = ({ fields, parser, theme }: IFields) => (
const accessoriesRight = ['image', 'overflow']; const accessoriesRight = ['image', 'overflow'];
export const Section = ({ blockId, appId, text, fields, accessory, parser, theme }: ISection) => ( export const Section = ({ blockId, appId, text, fields, accessory, parser }: ISection) => {
const { theme } = useTheme();
return (
<View style={[styles.content, accessory && accessoriesRight.includes(accessory.type) ? styles.row : styles.column]}> <View style={[styles.content, accessory && accessoriesRight.includes(accessory.type) ? styles.row : styles.column]}>
{text ? <View style={styles.text}>{parser.text(text)}</View> : null} {text ? <View style={styles.text}>{parser.text(text)}</View> : null}
{fields ? <Fields fields={fields} theme={theme} parser={parser} /> : null} {fields ? <Fields fields={fields} theme={theme} parser={parser} /> : null}
{accessory ? <Accessory element={{ blockId, appId, ...accessory }} parser={parser} /> : null} {accessory ? <Accessory element={{ blockId, appId, ...accessory }} parser={parser} /> : null}
</View> </View>
); );
};

View File

@ -8,6 +8,8 @@ import { CustomIcon } from '../../lib/Icons';
import { textParser } from './utils'; import { textParser } from './utils';
import { isAndroid, isIOS } from '../../utils/deviceInfo'; import { isAndroid, isIOS } from '../../utils/deviceInfo';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
import { useTheme } from '../../theme';
import { IText, Option } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
iosPadding: { iosPadding: {
@ -34,19 +36,16 @@ const styles = StyleSheet.create({
}); });
interface ISelect { interface ISelect {
options: { options?: Option[];
text: string; placeholder?: IText;
value: string;
}[];
placeholder: string;
onChange: Function; onChange: Function;
loading: boolean; loading: boolean;
disabled: boolean; disabled?: boolean;
value: []; value: [];
theme: string;
} }
export const Select = ({ options = [], placeholder, onChange, loading, disabled, value: initialValue, theme }: ISelect) => { export const Select = ({ options = [], placeholder, onChange, loading, disabled, value: initialValue }: ISelect) => {
const { theme } = useTheme();
const [selected, setSelected] = useState(!Array.isArray(initialValue) && initialValue); const [selected, setSelected] = useState(!Array.isArray(initialValue) && initialValue);
const items = options.map(option => ({ label: textParser([option.text]), value: option.value })); const items = options.map(option => ({ label: textParser([option.text]), value: option.value }));
const pickerStyle = { const pickerStyle = {
@ -80,6 +79,7 @@ export const Select = ({ options = [], placeholder, onChange, loading, disabled,
}} }}
Icon={Icon} Icon={Icon}
textInputProps={{ textInputProps={{
// style property was Omitted in lib, but can be used normally
// @ts-ignore // @ts-ignore
style: { ...styles.pickerText, color: selected ? themes[theme].titleText : themes[theme].auxiliaryText } style: { ...styles.pickerText, color: selected ? themes[theme].titleText : themes[theme].auxiliaryText }
}} }}

View File

@ -20,7 +20,7 @@ import { Input } from './Input';
import { DatePicker } from './DatePicker'; import { DatePicker } from './DatePicker';
import { Overflow } from './Overflow'; import { Overflow } from './Overflow';
import { ThemeContext } from '../../theme'; import { ThemeContext } from '../../theme';
import { BlockContext, IButton, IInputIndex, IParser, IText } from './interfaces'; import { BlockContext, IActions, IButton, IElement, IInputIndex, IParser, ISection, IText } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
input: { input: {
@ -78,35 +78,28 @@ class MessageParser extends UiKitParserMessage {
} }
divider() { divider() {
const { theme } = useContext(ThemeContext); return <Divider />;
// @ts-ignore
return <Divider theme={theme} />;
} }
section(args: any) { section(args: ISection) {
const { theme } = useContext(ThemeContext); return <Section {...args} parser={this.current} />;
return <Section {...args} theme={theme} parser={this} />;
} }
actions(args: any) { actions(args: IActions) {
const { theme } = useContext(ThemeContext); return <Actions {...args} parser={this.current} />;
return <Actions {...args} theme={theme} parser={this} />;
} }
overflow(element: any, context: any) { overflow(element: IElement, context: BlockContext) {
const [{ loading }, action]: any = useBlockContext(element, context); const [{ loading }, action] = useBlockContext(element, context);
const { theme }: any = useContext(ThemeContext); return <Overflow element={element} context={context} loading={loading} action={action} parser={this.current} />;
return <Overflow element={element} context={context} loading={loading} action={action} theme={theme} parser={this.current} />;
} }
datePicker(element: any, context: any) { datePicker(element: IElement, context: BlockContext) {
const [{ loading, value, error, language }, action]: any = useBlockContext(element, context); const [{ loading, value, error, language }, action] = useBlockContext(element, context);
const { theme }: any = useContext(ThemeContext);
return ( return (
<DatePicker <DatePicker
element={element} element={element}
language={language} language={language}
theme={theme}
value={value} value={value}
action={action} action={action}
context={context} context={context}
@ -116,9 +109,8 @@ class MessageParser extends UiKitParserMessage {
); );
} }
image(element: any, context: any) { image(element: IElement, context: BlockContext) {
const { theme }: any = useContext(ThemeContext); return <Image element={element} context={context} />;
return <Image element={element} theme={theme} context={context} />;
} }
context(args: any) { context(args: any) {
@ -126,24 +118,19 @@ class MessageParser extends UiKitParserMessage {
return <Context {...args} theme={theme} parser={this} />; return <Context {...args} theme={theme} parser={this} />;
} }
multiStaticSelect(element: any, context: any) { multiStaticSelect(element: IElement, context: BlockContext) {
const [{ loading, value }, action]: any = useBlockContext(element, context); const [{ loading, value }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext); return <MultiSelect {...element} value={value} onChange={action} context={context} loading={loading} multiselect />;
return (
<MultiSelect {...element} theme={theme} value={value} onChange={action} context={context} loading={loading} multiselect />
);
} }
staticSelect(element: any, context: any) { staticSelect(element: IElement, context: BlockContext) {
const [{ loading, value }, action]: any = useBlockContext(element, context); const [{ loading, value }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext); return <Select {...element} value={value} onChange={action} loading={loading} />;
return <Select {...element} theme={theme} value={value} onChange={action} loading={loading} />;
} }
selectInput(element: any, context: any) { selectInput(element: IElement, context: BlockContext) {
const [{ loading, value }, action]: any = useBlockContext(element, context); const [{ loading, value }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext); return <MultiSelect {...element} value={value} onChange={action} context={context} loading={loading} />;
return <MultiSelect {...element} theme={theme} value={value} onChange={action} context={context} loading={loading} />;
} }
} }
@ -160,8 +147,8 @@ class ModalParser extends UiKitParserModal {
} }
input({ element, blockId, appId, label, description, hint }: IInputIndex, context: number) { input({ element, blockId, appId, label, description, hint }: IInputIndex, context: number) {
const [{ error }]: any = useBlockContext({ ...element, appId, blockId }, context); const [{ error }] = useBlockContext({ ...element, appId, blockId }, context);
const { theme }: any = useContext(ThemeContext); const { theme } = useContext(ThemeContext);
return ( return (
<Input <Input
parser={this.current} parser={this.current}
@ -175,17 +162,15 @@ class ModalParser extends UiKitParserModal {
); );
} }
image(element: any, context: any) { image(element: IElement, context: BlockContext) {
const { theme }: any = useContext(ThemeContext); return <Image element={element} context={context} />;
return <Image element={element} theme={theme} context={context} />;
} }
plainInput(element: any, context: any) { plainInput(element: IElement, context: BlockContext) {
const [{ loading, value, error }, action]: any = useBlockContext(element, context); const [{ loading, value, error }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext); const { theme } = useContext(ThemeContext);
const { multiline, actionId, placeholder } = element; const { multiline, actionId, placeholder } = element;
return ( return (
// @ts-ignore
<TextInput <TextInput
key={actionId} key={actionId}
placeholder={plainText(placeholder)} placeholder={plainText(placeholder)}

View File

@ -1,5 +1,3 @@
import { TThemeMode } from '../../definitions/ITheme';
export enum ElementTypes { export enum ElementTypes {
IMAGE = 'image', IMAGE = 'image',
BUTTON = 'button', BUTTON = 'button',
@ -87,10 +85,11 @@ export interface IElement {
imageUrl?: string; imageUrl?: string;
appId?: string; appId?: string;
blockId?: string; blockId?: string;
multiline?: boolean;
} }
export interface IText { export interface IText {
type: ElementTypes; type?: ElementTypes;
text: string; text: string;
emoji?: boolean; emoji?: boolean;
} }
@ -98,12 +97,15 @@ export interface IText {
export interface Option { export interface Option {
text: IText; text: IText;
value: string; value: string;
imageUrl?: string;
} }
export interface IButton { export interface IButton {
type: ElementTypes; type: ElementTypes;
text: IText; text: IText;
actionId: string; actionId: string;
blockId: string;
appId: string;
value?: any; value?: any;
style?: any; style?: any;
} }
@ -177,7 +179,6 @@ export interface IParser {
} }
export interface IActions extends Block { export interface IActions extends Block {
parser?: IParser; parser?: IParser;
theme: TThemeMode;
} }
export interface IContext extends Block { export interface IContext extends Block {
@ -191,7 +192,6 @@ export interface IDatePicker extends Partial<Block> {
loading: boolean; loading: boolean;
value: string; value: string;
error: string; error: string;
theme: TThemeMode;
} }
export interface IInput extends Partial<Block> { export interface IInput extends Partial<Block> {
@ -199,7 +199,7 @@ export interface IInput extends Partial<Block> {
description: string; description: string;
error: string; error: string;
hint: string; hint: string;
theme: TThemeMode; theme: string;
} }
export interface IInputIndex { export interface IInputIndex {
@ -217,8 +217,7 @@ export interface IThumb {
} }
export interface IImage { export interface IImage {
element: IElement; element: IElement;
theme: TThemeMode; context?: BlockContext;
context?: number;
} }
// UiKit/Overflow // UiKit/Overflow
@ -226,14 +225,13 @@ export interface IOverflow extends Partial<Block> {
action: Function; action: Function;
loading: boolean; loading: boolean;
parser: IParser; parser: IParser;
theme: TThemeMode;
context: number; context: number;
} }
interface PropsOption { interface PropsOption {
onOptionPress: Function; onOptionPress: Function;
parser: IParser; parser: IParser;
theme: TThemeMode; theme: string;
} }
export interface IOptions extends PropsOption { export interface IOptions extends PropsOption {
options: Option[]; options: Option[];
@ -262,12 +260,11 @@ export interface ISection {
text?: IText; text?: IText;
accessory?: IAccessory; accessory?: IAccessory;
parser: IParser; parser: IParser;
theme: TThemeMode;
fields?: any[]; fields?: any[];
} }
export interface IFields { export interface IFields {
parser: IParser; parser: IParser;
theme: TThemeMode; theme: string;
fields: any[]; fields: any[];
} }

View File

@ -2,9 +2,9 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { BlockContext } from './interfaces'; import { BlockContext, IText } from './interfaces';
export const textParser = ([{ text }]: any) => text; export const textParser = ([{ text }]: IText[]) => text;
export const defaultContext: any = { export const defaultContext: any = {
action: (...args: any) => console.log(args), action: (...args: any) => console.log(args),
@ -27,7 +27,14 @@ type TFunctionReturn = (value: any) => Promise<void>;
type TReturn = [TObjectReturn, TFunctionReturn]; type TReturn = [TObjectReturn, TFunctionReturn];
export const useBlockContext = ({ blockId, actionId, appId, initialValue }: any, context: BlockContext): TReturn => { interface IUseBlockContext {
blockId?: string;
actionId: string;
appId?: string;
initialValue?: string;
}
export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUseBlockContext, context: BlockContext): TReturn => {
const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext); const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext);
const { value = initialValue } = values[actionId] || {}; const { value = initialValue } = values[actionId] || {};
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);

View File

@ -18,7 +18,7 @@ const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IH
const handlePress = () => { const handlePress = () => {
const index = channels?.findIndex(channel => channel.name === hashtag); const index = channels?.findIndex(channel => channel.name === hashtag);
if (index && navToRoomInfo) { if (typeof index !== 'undefined' && navToRoomInfo) {
const navParam = { const navParam = {
t: 'c', t: 'c',
rid: channels?.[index]._id rid: channels?.[index]._id

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Clipboard, Text } from 'react-native'; import { Text } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';

View File

@ -1,6 +1,6 @@
export interface IUserMention { export interface IUserMention {
_id: string; _id: string;
username: string; username?: string;
name?: string; name?: string;
type?: string; type?: string;
} }

View File

@ -1,6 +1,7 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { Text, Clipboard } from 'react-native'; import { Text } from 'react-native';
import { Link as LinkProps } from '@rocket.chat/message-parser'; import { Link as LinkProps } from '@rocket.chat/message-parser';
import Clipboard from '@react-native-clipboard/clipboard';
import styles from '../styles'; import styles from '../styles';
import I18n from '../../../i18n'; import I18n from '../../../i18n';

View File

@ -2,7 +2,7 @@ import React, { useContext } from 'react';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import { Text } from 'react-native'; import { Text } from 'react-native';
import { IMessageAttachments, IMessageAttachedActions } from './interfaces'; import { IMessageAttachments } from './interfaces';
import Image from './Image'; import Image from './Image';
import Audio from './Audio'; import Audio from './Audio';
import Video from './Video'; import Video from './Video';
@ -13,30 +13,49 @@ import MessageContext from './Context';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { IAttachment } from '../../definitions'; import { IAttachment } from '../../definitions';
import CollapsibleQuote from './Components/CollapsibleQuote'; import CollapsibleQuote from './Components/CollapsibleQuote';
import openLink from '../../utils/openLink';
import { themes } from '../../constants/colors';
const AttachedActions = ({ attachment }: IMessageAttachedActions) => { export type TElement = {
type: string;
msg?: string;
url?: string;
text: string;
};
const AttachedActions = ({ attachment }: { attachment: IAttachment }) => {
if (!attachment.actions) { if (!attachment.actions) {
return null; return null;
} }
const { onAnswerButtonPress } = useContext(MessageContext); const { onAnswerButtonPress } = useContext(MessageContext);
const { theme } = useTheme(); const { theme } = useTheme();
const attachedButtons = attachment.actions.map((element: { type: string; msg: string; text: string }) => { const attachedButtons = attachment.actions.map((element: TElement) => {
if (element.type === 'button') { const onPress = () => {
return <Button theme={theme} onPress={() => onAnswerButtonPress(element.msg)} title={element.text} />; if (element.msg) {
onAnswerButtonPress(element.msg);
} }
if (element.url) {
openLink(element.url);
}
};
if (element.type === 'button') {
return <Button theme={theme} onPress={onPress} title={element.text} />;
}
return null; return null;
}); });
return ( return (
<> <>
<Text style={styles.text}>{attachment.text}</Text> <Text style={[styles.text, { color: themes[theme].bodyText }]}>{attachment.text}</Text>
{attachedButtons} {attachedButtons}
</> </>
); );
}; };
const Attachments = React.memo( const Attachments: React.FC<IMessageAttachments> = React.memo(
// @ts-ignore
({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply }: IMessageAttachments) => { ({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply }: IMessageAttachments) => {
if (!attachments || attachments.length === 0) { if (!attachments || attachments.length === 0) {
return null; return null;
@ -44,7 +63,7 @@ const Attachments = React.memo(
const { theme } = useTheme(); const { theme } = useTheme();
return attachments.map((file: IAttachment, index: number) => { const attachmentsElements = attachments.map((file: IAttachment, index: number) => {
if (file && file.image_url) { if (file && file.image_url) {
return ( return (
<Image <Image
@ -54,7 +73,6 @@ const Attachments = React.memo(
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
style={style} style={style}
isReply={isReply} isReply={isReply}
theme={theme}
/> />
); );
} }
@ -74,7 +92,6 @@ const Attachments = React.memo(
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
style={style} style={style}
isReply={isReply} isReply={isReply}
theme={theme}
/> />
); );
} }
@ -90,6 +107,7 @@ const Attachments = React.memo(
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} />; return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} />;
}); });
return <>{attachmentsElements}</>;
}, },
(prevProps, nextProps) => dequal(prevProps.attachments, nextProps.attachments) (prevProps, nextProps) => dequal(prevProps.attachments, nextProps.attachments)
); );

View File

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native'; import { StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native';
import { Audio } from 'expo-av'; import { Audio, AVPlaybackStatus } from 'expo-av';
import Slider from '@react-native-community/slider'; import Slider from '@react-native-community/slider';
import moment from 'moment'; import moment from 'moment';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import { Sound } from 'expo-av/build/Audio/Sound';
import Touchable from './Touchable'; import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
@ -23,7 +24,7 @@ interface IButton {
paused: boolean; paused: boolean;
theme: string; theme: string;
disabled?: boolean; disabled?: boolean;
onPress: Function; onPress: () => void;
} }
interface IMessageAudioProps { interface IMessageAudioProps {
@ -108,7 +109,7 @@ Button.displayName = 'MessageAudioButton';
class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioState> { class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioState> {
static contextType = MessageContext; static contextType = MessageContext;
private sound: any; private sound: Sound;
constructor(props: IMessageAudioProps) { constructor(props: IMessageAudioProps) {
super(props); super(props);
@ -141,7 +142,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
this.setState({ loading: false }); this.setState({ loading: false });
} }
shouldComponentUpdate(nextProps: any, nextState: any) { shouldComponentUpdate(nextProps: IMessageAudioProps, nextState: IMessageAudioState) {
const { currentTime, duration, paused, loading } = this.state; const { currentTime, duration, paused, loading } = this.state;
const { file, theme } = this.props; const { file, theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
@ -182,7 +183,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
} }
} }
onPlaybackStatusUpdate = (status: any) => { onPlaybackStatusUpdate = (status: AVPlaybackStatus) => {
if (status) { if (status) {
this.onLoad(status); this.onLoad(status);
this.onProgress(status); this.onProgress(status);
@ -190,20 +191,25 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
} }
}; };
onLoad = (data: any) => { onLoad = (data: AVPlaybackStatus) => {
if (data.isLoaded && data.durationMillis) {
const duration = data.durationMillis / 1000; const duration = data.durationMillis / 1000;
this.setState({ duration: duration > 0 ? duration : 0 }); this.setState({ duration: duration > 0 ? duration : 0 });
}
}; };
onProgress = (data: any) => { onProgress = (data: AVPlaybackStatus) => {
if (data.isLoaded) {
const { duration } = this.state; const { duration } = this.state;
const currentTime = data.positionMillis / 1000; const currentTime = data.positionMillis / 1000;
if (currentTime <= duration) { if (currentTime <= duration) {
this.setState({ currentTime }); this.setState({ currentTime });
} }
}
}; };
onEnd = async (data: any) => { onEnd = async (data: AVPlaybackStatus) => {
if (data.isLoaded) {
if (data.didJustFinish) { if (data.didJustFinish) {
try { try {
await this.sound.stopAsync(); await this.sound.stopAsync();
@ -212,6 +218,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
// do nothing // do nothing
} }
} }
}
}; };
get duration() { get duration() {
@ -238,7 +245,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
} }
}; };
onValueChange = async (value: any) => { onValueChange = async (value: number) => {
try { try {
this.setState({ currentTime: value }); this.setState({ currentTime: value });
await this.sound.setPositionAsync(value * 1000); await this.sound.setPositionAsync(value * 1000);

View File

@ -8,7 +8,8 @@ const Blocks = React.memo(({ blocks, id: mid, rid, blockAction }: IMessageBlocks
const appId = blocks[0]?.appId || ''; const appId = blocks[0]?.appId || '';
return React.createElement( return React.createElement(
messageBlockWithContext({ messageBlockWithContext({
action: async ({ actionId, value, blockId }: any) => { action: async ({ actionId, value, blockId }: { actionId: string; value: string; blockId: string }) => {
if (blockAction) {
await blockAction({ await blockAction({
actionId, actionId,
appId, appId,
@ -17,6 +18,7 @@ const Blocks = React.memo(({ blocks, id: mid, rid, blockAction }: IMessageBlocks
rid, rid,
mid mid
}); });
}
}, },
appId, appId,
rid rid

View File

@ -9,10 +9,13 @@ import I18n from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import { IMessageBroadcast } from './interfaces'; import { IMessageBroadcast } from './interfaces';
import { useTheme } from '../../theme';
const Broadcast = React.memo(({ author, broadcast, theme }: IMessageBroadcast) => { const Broadcast = React.memo(({ author, broadcast }: IMessageBroadcast) => {
const { user, replyBroadcast } = useContext(MessageContext); const { user, replyBroadcast } = useContext(MessageContext);
const isOwn = author._id === user.id; const { theme } = useTheme();
const isOwn = author?._id === user.id;
if (broadcast && !isOwn) { if (broadcast && !isOwn) {
return ( return (
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>

View File

@ -8,8 +8,11 @@ import I18n from '../../i18n';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { IMessageCallButton } from './interfaces'; import { IMessageCallButton } from './interfaces';
import { useTheme } from '../../theme';
const CallButton = React.memo(({ theme, callJitsi }: IMessageCallButton) => ( const CallButton = React.memo(({ callJitsi }: IMessageCallButton) => {
const { theme } = useTheme();
return (
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<Touchable <Touchable
onPress={callJitsi} onPress={callJitsi}
@ -22,7 +25,8 @@ const CallButton = React.memo(({ theme, callJitsi }: IMessageCallButton) => (
</> </>
</Touchable> </Touchable>
</View> </View>
)); );
});
CallButton.displayName = 'CallButton'; CallButton.displayName = 'CallButton';

View File

@ -4,19 +4,6 @@ import React from 'react';
import MessageContext from '../../Context'; import MessageContext from '../../Context';
import CollapsibleQuote from '.'; import CollapsibleQuote from '.';
// For some reason a general mock didn't work, I have to do a search
jest.mock('react-native-mmkv-storage', () => ({
Loader: jest.fn().mockImplementation(() => ({
setProcessingMode: jest.fn().mockImplementation(() => ({
withEncryption: jest.fn().mockImplementation(() => ({
initialize: jest.fn()
}))
}))
})),
create: jest.fn(),
MODES: { MULTI_PROCESS: '' }
}));
const testAttachment = { const testAttachment = {
ts: '1970-01-01T00:00:00.000Z', ts: '1970-01-01T00:00:00.000Z',
title: 'Engineering (9 today)', title: 'Engineering (9 today)',

View File

@ -10,17 +10,19 @@ import { SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME, getInfoMessage } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import Encrypted from './Encrypted'; import Encrypted from './Encrypted';
import { E2E_MESSAGE_TYPE } from '../../lib/encryption/constants'; import { E2E_MESSAGE_TYPE } from '../../lib/constants';
import { IMessageContent } from './interfaces'; import { IMessageContent } from './interfaces';
import { useTheme } from '../../theme';
const Content = React.memo( const Content = React.memo(
(props: IMessageContent) => { (props: IMessageContent) => {
const { theme } = useTheme();
if (props.isInfo) { if (props.isInfo) {
// @ts-ignore // @ts-ignore
const infoMessage = getInfoMessage({ ...props }); const infoMessage = getInfoMessage({ ...props });
const renderMessageContent = ( const renderMessageContent = (
<Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]} accessibilityLabel={infoMessage}> <Text style={[styles.textInfo, { color: themes[theme].auxiliaryText }]} accessibilityLabel={infoMessage}>
{infoMessage} {infoMessage}
</Text> </Text>
); );
@ -36,14 +38,12 @@ const Content = React.memo(
return renderMessageContent; return renderMessageContent;
} }
const isPreview: any = props.tmid && !props.isThreadRoom; const isPreview = props.tmid && !props.isThreadRoom;
let content = null; let content = null;
if (props.isEncrypted) { if (props.isEncrypted) {
content = ( content = (
<Text <Text style={[styles.textInfo, { color: themes[theme].auxiliaryText }]} accessibilityLabel={I18n.t('Encrypted_message')}>
style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}
accessibilityLabel={I18n.t('Encrypted_message')}>
{I18n.t('Encrypted_message')} {I18n.t('Encrypted_message')}
</Text> </Text>
); );
@ -65,7 +65,7 @@ const Content = React.memo(
navToRoomInfo={props.navToRoomInfo} navToRoomInfo={props.navToRoomInfo}
tmid={props.tmid} tmid={props.tmid}
useRealName={props.useRealName} useRealName={props.useRealName}
theme={props.theme} theme={theme}
onLinkPress={onLinkPress} onLinkPress={onLinkPress}
/> />
); );
@ -76,13 +76,13 @@ const Content = React.memo(
content = ( content = (
<View style={styles.flex}> <View style={styles.flex}>
<View style={styles.contentContainer}>{content}</View> <View style={styles.contentContainer}>{content}</View>
<Encrypted type={props.type} theme={props.theme} /> <Encrypted type={props.type} />
</View> </View>
); );
} }
if (props.isIgnored) { if (props.isIgnored) {
content = <Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}>{I18n.t('Message_Ignored')}</Text>; content = <Text style={[styles.textInfo, { color: themes[theme].auxiliaryText }]}>{I18n.t('Message_Ignored')}</Text>;
} }
return <View style={props.isTemp && styles.temp}>{content}</View>; return <View style={props.isTemp && styles.temp}>{content}</View>;
@ -97,9 +97,6 @@ const Content = React.memo(
if (prevProps.type !== nextProps.type) { if (prevProps.type !== nextProps.type) {
return false; return false;
} }
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (prevProps.isEncrypted !== nextProps.isEncrypted) { if (prevProps.isEncrypted !== nextProps.isEncrypted) {
return false; return false;
} }

View File

@ -10,10 +10,12 @@ import { DISCUSSION } from './constants';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import { formatDateThreads } from '../../utils/room'; import { formatDateThreads } from '../../utils/room';
import { IMessageDiscussion } from './interfaces'; import { IMessage } from '../../definitions';
import { useTheme } from '../../theme';
const Discussion = React.memo( const Discussion = React.memo(
({ msg, dcount, dlm, theme }: IMessageDiscussion) => { ({ msg, dcount, dlm }: Pick<IMessage, 'msg' | 'dcount' | 'dlm'>) => {
const { theme } = useTheme();
let time; let time;
if (dlm) { if (dlm) {
time = formatDateThreads(dlm); time = formatDateThreads(dlm);
@ -50,9 +52,6 @@ const Discussion = React.memo(
if (prevProps.dlm !== nextProps.dlm) { if (prevProps.dlm !== nextProps.dlm) {
return false; return false;
} }
if (prevProps.theme !== nextProps.theme) {
return false;
}
return true; return true;
} }
); );

View File

@ -1,19 +1,16 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import Touchable from './Touchable'; import Touchable from './Touchable';
import { E2E_MESSAGE_TYPE } from '../../lib/encryption/constants'; import { E2E_MESSAGE_TYPE } from '../../lib/constants';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import MessageContext from './Context'; import MessageContext from './Context';
import styles from './styles'; import styles from './styles';
import { useTheme } from '../../theme';
interface IMessageEncrypted { const Encrypted = React.memo(({ type }: { type: string }) => {
type: string; const { theme } = useTheme();
theme: string;
}
const Encrypted = React.memo(({ type, theme }: IMessageEncrypted) => {
if (type !== E2E_MESSAGE_TYPE) { if (type !== E2E_MESSAGE_TYPE) {
return null; return null;
} }

View File

@ -13,32 +13,27 @@ import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IAttachment } from '../../definitions'; import { IAttachment } from '../../definitions';
import { useTheme } from '../../theme';
type TMessageButton = { interface IMessageButton {
children: JSX.Element; children: React.ReactElement;
disabled?: boolean; disabled?: boolean;
onPress: Function; onPress: () => void;
theme: string; theme: string;
}; }
type TMessageImage = {
img: string;
theme: string;
};
interface IMessageImage { interface IMessageImage {
file: IAttachment; file: IAttachment;
imageUrl?: string; imageUrl?: string;
showAttachment?: Function; showAttachment?: (file: IAttachment) => void;
style?: StyleProp<TextStyle>[]; style?: StyleProp<TextStyle>[];
isReply?: boolean; isReply?: boolean;
theme: string; getCustomEmoji?: TGetCustomEmoji;
getCustomEmoji: TGetCustomEmoji;
} }
const ImageProgress = createImageProgress(FastImage); const ImageProgress = createImageProgress(FastImage);
const Button = React.memo(({ children, onPress, disabled, theme }: TMessageButton) => ( const Button = React.memo(({ children, onPress, disabled, theme }: IMessageButton) => (
<Touchable <Touchable
disabled={disabled} disabled={disabled}
onPress={onPress} onPress={onPress}
@ -48,10 +43,10 @@ const Button = React.memo(({ children, onPress, disabled, theme }: TMessageButto
</Touchable> </Touchable>
)); ));
export const MessageImage = React.memo(({ img, theme }: TMessageImage) => ( export const MessageImage = React.memo(({ imgUri, theme }: { imgUri: string; theme: string }) => (
<ImageProgress <ImageProgress
style={[styles.image, { borderColor: themes[theme].borderColor }]} style={[styles.image, { borderColor: themes[theme].borderColor }]}
source={{ uri: encodeURI(img) }} source={{ uri: encodeURI(imgUri) }}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
indicator={Progress.Pie} indicator={Progress.Pie}
indicatorProps={{ indicatorProps={{
@ -61,9 +56,11 @@ export const MessageImage = React.memo(({ img, theme }: TMessageImage) => (
)); ));
const ImageContainer = React.memo( const ImageContainer = React.memo(
({ file, imageUrl, showAttachment, getCustomEmoji, style, isReply, theme }: IMessageImage) => { ({ file, imageUrl, showAttachment, getCustomEmoji, style, isReply }: IMessageImage) => {
const { theme } = useTheme();
const { baseUrl, user } = useContext(MessageContext); const { baseUrl, user } = useContext(MessageContext);
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl); const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
if (!img) { if (!img) {
return null; return null;
} }
@ -88,7 +85,7 @@ const ImageContainer = React.memo(
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}
/> />
<MessageImage img={img} theme={theme} /> <MessageImage imgUri={img} theme={theme} />
</View> </View>
</Button> </Button>
); );
@ -96,11 +93,11 @@ const ImageContainer = React.memo(
return ( return (
<Button disabled={isReply} theme={theme} onPress={onPress}> <Button disabled={isReply} theme={theme} onPress={onPress}>
<MessageImage img={img} theme={theme} /> <MessageImage imgUri={img} theme={theme} />
</Button> </Button>
); );
}, },
(prevProps, nextProps) => dequal(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme (prevProps, nextProps) => dequal(prevProps.file, nextProps.file)
); );
ImageContainer.displayName = 'MessageImageContainer'; ImageContainer.displayName = 'MessageImageContainer';

View File

@ -19,6 +19,7 @@ import ReadReceipt from './ReadReceipt';
import CallButton from './CallButton'; import CallButton from './CallButton';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { IMessage, IMessageInner, IMessageTouchable } from './interfaces'; import { IMessage, IMessageInner, IMessageTouchable } from './interfaces';
import { useTheme } from '../../theme';
const MessageInner = React.memo((props: IMessageInner) => { const MessageInner = React.memo((props: IMessageInner) => {
const { attachments } = props; const { attachments } = props;
@ -85,7 +86,6 @@ const Message = React.memo((props: IMessage) => {
<View style={[styles.container, props.style]}> <View style={[styles.container, props.style]}>
{thread} {thread}
<View style={styles.flex}> <View style={styles.flex}>
{/* @ts-ignore */}
<MessageAvatar small {...props} /> <MessageAvatar small {...props} />
<View style={[styles.messageContent, props.isHeader && styles.messageContentWithHeader]}> <View style={[styles.messageContent, props.isHeader && styles.messageContentWithHeader]}>
<Content {...props} /> <Content {...props} />
@ -98,12 +98,11 @@ const Message = React.memo((props: IMessage) => {
return ( return (
<View style={[styles.container, props.style]}> <View style={[styles.container, props.style]}>
<View style={styles.flex}> <View style={styles.flex}>
{/* @ts-ignore */}
<MessageAvatar {...props} /> <MessageAvatar {...props} />
<View style={[styles.messageContent, props.isHeader && styles.messageContentWithHeader]}> <View style={[styles.messageContent, props.isHeader && styles.messageContentWithHeader]}>
<MessageInner {...props} /> <MessageInner {...props} />
</View> </View>
<ReadReceipt isReadReceiptEnabled={props.isReadReceiptEnabled} unread={props.unread || false} theme={props.theme} /> <ReadReceipt isReadReceiptEnabled={props.isReadReceiptEnabled} unread={props.unread || false} />
</View> </View>
</View> </View>
); );
@ -119,12 +118,14 @@ const MessageTouchable = React.memo((props: IMessageTouchable & IMessage) => {
); );
} }
const { onPress, onLongPress } = useContext(MessageContext); const { onPress, onLongPress } = useContext(MessageContext);
const { theme } = useTheme();
return ( return (
<Touchable <Touchable
onLongPress={onLongPress} onLongPress={onLongPress}
onPress={onPress} onPress={onPress}
disabled={(props.isInfo && !props.isThreadReply) || props.archived || props.isTemp || props.type === 'jitsi_call_started'} disabled={(props.isInfo && !props.isThreadReply) || props.archived || props.isTemp || props.type === 'jitsi_call_started'}
style={{ backgroundColor: props.highlighted ? themes[props.theme].headerBackground : null }}> style={{ backgroundColor: props.highlighted ? themes[theme].headerBackground : null }}>
<View> <View>
<Message {...props} /> <Message {...props} />
</View> </View>

View File

@ -4,13 +4,13 @@ import Avatar from '../Avatar';
import styles from './styles'; import styles from './styles';
import MessageContext from './Context'; import MessageContext from './Context';
import { IMessageAvatar } from './interfaces'; import { IMessageAvatar } from './interfaces';
import { SubscriptionType } from '../../definitions';
const MessageAvatar = React.memo( const MessageAvatar = React.memo(({ isHeader, avatar, author, small, navToRoomInfo, emoji, getCustomEmoji }: IMessageAvatar) => {
({ isHeader, avatar, author, small, navToRoomInfo, emoji, getCustomEmoji, theme }: IMessageAvatar) => {
const { user } = useContext(MessageContext); const { user } = useContext(MessageContext);
if (isHeader && author) { if (isHeader && author) {
const navParam = { const navParam = {
t: 'd', t: SubscriptionType.DIRECT,
rid: author._id rid: author._id
}; };
return ( return (
@ -23,13 +23,11 @@ const MessageAvatar = React.memo(
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
avatar={avatar} avatar={avatar}
emoji={emoji} emoji={emoji}
theme={theme}
/> />
); );
} }
return null; return null;
} });
);
MessageAvatar.displayName = 'MessageAvatar'; MessageAvatar.displayName = 'MessageAvatar';

View File

@ -6,14 +6,12 @@ import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import { useTheme } from '../../theme';
interface IMessageError {
hasError: boolean;
theme: string;
}
const MessageError = React.memo( const MessageError = React.memo(
({ hasError, theme }: IMessageError) => { ({ hasError }: { hasError: boolean }) => {
const { theme } = useTheme();
if (!hasError) { if (!hasError) {
return null; return null;
} }
@ -24,7 +22,7 @@ const MessageError = React.memo(
</Touchable> </Touchable>
); );
}, },
(prevProps, nextProps) => prevProps.hasError === nextProps.hasError && prevProps.theme === nextProps.theme (prevProps, nextProps) => prevProps.hasError === nextProps.hasError
); );
MessageError.displayName = 'MessageError'; MessageError.displayName = 'MessageError';

View File

@ -7,30 +7,28 @@ import styles from './styles';
import Emoji from './Emoji'; import Emoji from './Emoji';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import MessageContext from './Context'; import MessageContext from './Context';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
interface IMessageAddReaction { interface IReaction {
theme: string; _id: string;
emoji: string;
usernames: string[];
} }
interface IMessageReaction { interface IMessageReaction {
reaction: { reaction: IReaction;
usernames: [];
emoji: object;
};
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
theme: string; theme: string;
} }
interface IMessageReactions { interface IMessageReactions {
reactions?: object[]; reactions?: IReaction[];
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
theme: string;
} }
const AddReaction = React.memo(({ theme }: IMessageAddReaction) => { const AddReaction = React.memo(({ theme }: { theme: string }) => {
const { reactionInit } = useContext(MessageContext); const { reactionInit } = useContext(MessageContext);
return ( return (
<Touchable <Touchable
@ -49,7 +47,7 @@ const AddReaction = React.memo(({ theme }: IMessageAddReaction) => {
const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReaction) => { const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReaction) => {
const { onReactionPress, onReactionLongPress, baseUrl, user } = useContext(MessageContext); const { onReactionPress, onReactionLongPress, baseUrl, user } = useContext(MessageContext);
const reacted = reaction.usernames.findIndex((item: IMessageReaction) => item === user.username) !== -1; const reacted = reaction.usernames.findIndex((item: string) => item === user.username) !== -1;
return ( return (
<Touchable <Touchable
onPress={() => onReactionPress(reaction.emoji)} onPress={() => onReactionPress(reaction.emoji)}
@ -76,13 +74,15 @@ const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReacti
); );
}); });
const Reactions = React.memo(({ reactions, getCustomEmoji, theme }: IMessageReactions) => { const Reactions = React.memo(({ reactions, getCustomEmoji }: IMessageReactions) => {
const { theme } = useTheme();
if (!Array.isArray(reactions) || reactions.length === 0) { if (!Array.isArray(reactions) || reactions.length === 0) {
return null; return null;
} }
return ( return (
<View style={styles.reactionsContainer}> <View style={styles.reactionsContainer}>
{reactions.map((reaction: any) => ( {reactions.map(reaction => (
<Reaction key={reaction.emoji} reaction={reaction} getCustomEmoji={getCustomEmoji} theme={theme} /> <Reaction key={reaction.emoji} reaction={reaction} getCustomEmoji={getCustomEmoji} theme={theme} />
))} ))}
<AddReaction theme={theme} /> <AddReaction theme={theme} />
@ -94,4 +94,4 @@ Reaction.displayName = 'MessageReaction';
Reactions.displayName = 'MessageReactions'; Reactions.displayName = 'MessageReactions';
AddReaction.displayName = 'MessageAddReaction'; AddReaction.displayName = 'MessageAddReaction';
export default withTheme(Reactions); export default Reactions;

View File

@ -3,14 +3,10 @@ import React from 'react';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import { useTheme } from '../../theme';
interface IMessageReadReceipt { const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }: { isReadReceiptEnabled?: boolean; unread: boolean }) => {
isReadReceiptEnabled: boolean; const { theme } = useTheme();
unread: boolean;
theme: string;
}
const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread, theme }: IMessageReadReceipt) => {
if (isReadReceiptEnabled && !unread && unread !== null) { if (isReadReceiptEnabled && !unread && unread !== null) {
return <CustomIcon name='check' color={themes[theme].tintColor} size={15} style={styles.readReceipt} />; return <CustomIcon name='check' color={themes[theme].tintColor} size={15} style={styles.readReceipt} />;
} }

View File

@ -7,15 +7,18 @@ import { themes } from '../../constants/colors';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { MarkdownPreview } from '../markdown'; import { MarkdownPreview } from '../markdown';
import { IMessageRepliedThread } from './interfaces'; import { IMessageRepliedThread } from './interfaces';
import { useTheme } from '../../theme';
const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted }: IMessageRepliedThread) => {
const { theme } = useTheme();
const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme }: IMessageRepliedThread) => {
if (!tmid || !isHeader) { if (!tmid || !isHeader) {
return null; return null;
} }
const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg); const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg);
const fetch = async () => { const fetch = async () => {
const threadName = await fetchThreadName(tmid, id); const threadName = fetchThreadName ? await fetchThreadName(tmid, id) : '';
setMsg(threadName); setMsg(threadName);
}; };

View File

@ -77,7 +77,7 @@ const styles = StyleSheet.create({
marginBottom: 4 marginBottom: 4
}, },
image: { image: {
// @ts-ignore // @ts-ignore TODO - check with the team, change this to undefined
width: null, width: null,
height: 200, height: 200,
flex: 1, flex: 1,
@ -93,24 +93,6 @@ const styles = StyleSheet.create({
} }
}); });
interface IMessageTitle {
attachment: IAttachment;
timeFormat?: string;
theme: string;
}
interface IMessageDescription {
attachment: IAttachment;
getCustomEmoji: TGetCustomEmoji;
theme: string;
}
interface IMessageFields {
attachment: IAttachment;
theme: string;
getCustomEmoji: TGetCustomEmoji;
}
interface IMessageReply { interface IMessageReply {
attachment: IAttachment; attachment: IAttachment;
timeFormat?: string; timeFormat?: string;
@ -118,7 +100,7 @@ interface IMessageReply {
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
} }
const Title = React.memo(({ attachment, timeFormat, theme }: IMessageTitle) => { const Title = React.memo(({ attachment, timeFormat, theme }: { attachment: IAttachment; timeFormat?: string; theme: string }) => {
const time = attachment.message_link && attachment.ts ? moment(attachment.ts).format(timeFormat) : null; const time = attachment.message_link && attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
return ( return (
<View style={styles.authorContainer}> <View style={styles.authorContainer}>
@ -132,7 +114,7 @@ const Title = React.memo(({ attachment, timeFormat, theme }: IMessageTitle) => {
}); });
const Description = React.memo( const Description = React.memo(
({ attachment, getCustomEmoji, theme }: IMessageDescription) => { ({ attachment, getCustomEmoji, theme }: { attachment: IAttachment; getCustomEmoji: TGetCustomEmoji; theme: string }) => {
const text = attachment.text || attachment.title; const text = attachment.text || attachment.title;
if (!text) { if (!text) {
return null; return null;
@ -164,7 +146,7 @@ const Description = React.memo(
); );
const UrlImage = React.memo( const UrlImage = React.memo(
({ image }: any) => { ({ image }: { image?: string }) => {
if (!image) { if (!image) {
return null; return null;
} }
@ -176,7 +158,7 @@ const UrlImage = React.memo(
); );
const Fields = React.memo( const Fields = React.memo(
({ attachment, theme, getCustomEmoji }: IMessageFields) => { ({ attachment, theme, getCustomEmoji }: { attachment: IAttachment; theme: string; getCustomEmoji: TGetCustomEmoji }) => {
if (!attachment.fields) { if (!attachment.fields) {
return null; return null;
} }
@ -206,12 +188,12 @@ const Fields = React.memo(
const Reply = React.memo( const Reply = React.memo(
({ attachment, timeFormat, index, getCustomEmoji }: IMessageReply) => { ({ attachment, timeFormat, index, getCustomEmoji }: IMessageReply) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { theme } = useTheme();
if (!attachment) { if (!attachment) {
return null; return null;
} }
const { theme } = useTheme();
const { baseUrl, user, jumpToMessage } = useContext(MessageContext); const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
const onPress = async () => { const onPress = async () => {

View File

@ -7,9 +7,12 @@ import MessageContext from './Context';
import ThreadDetails from '../ThreadDetails'; import ThreadDetails from '../ThreadDetails';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { IMessageThread } from './interfaces'; import { IMessageThread } from './interfaces';
import { useTheme } from '../../theme';
const Thread = React.memo( const Thread = React.memo(
({ msg, tcount, tlm, isThreadRoom, theme, id }: IMessageThread) => { ({ msg, tcount, tlm, isThreadRoom, id }: IMessageThread) => {
const { theme } = useTheme();
if (!tlm || isThreadRoom || tcount === 0) { if (!tlm || isThreadRoom || tcount === 0) {
return null; return null;
} }
@ -38,9 +41,6 @@ const Thread = React.memo(
if (prevProps.tcount !== nextProps.tcount) { if (prevProps.tcount !== nextProps.tcount) {
return false; return false;
} }
if (prevProps.theme !== nextProps.theme) {
return false;
}
return true; return true;
} }
); );

View File

@ -1,5 +1,6 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { Clipboard, StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import FastImage from '@rocket.chat/react-native-fast-image'; import FastImage from '@rocket.chat/react-native-fast-image';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
@ -7,11 +8,12 @@ import Touchable from './Touchable';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { useTheme, withTheme } from '../../theme';
import { LISTENER } from '../Toast'; import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import I18n from '../../i18n'; import I18n from '../../i18n';
import MessageContext from './Context'; import MessageContext from './Context';
import { IUrl } from '../../definitions';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -49,29 +51,6 @@ const styles = StyleSheet.create({
} }
}); });
interface IMessageUrlContent {
title: string;
description: string;
theme: string;
}
interface IMessageUrl {
url: {
ignoreParse: boolean;
url: string;
image: string;
title: string;
description: string;
};
index: number;
theme: string;
}
interface IMessageUrls {
urls?: any;
theme?: string;
}
const UrlImage = React.memo( const UrlImage = React.memo(
({ image }: { image: string }) => { ({ image }: { image: string }) => {
if (!image) { if (!image) {
@ -85,7 +64,7 @@ const UrlImage = React.memo(
); );
const UrlContent = React.memo( const UrlContent = React.memo(
({ title, description, theme }: IMessageUrlContent) => ( ({ title, description, theme }: { title: string; description: string; theme: string }) => (
<View style={styles.textContainer}> <View style={styles.textContainer}>
{title ? ( {title ? (
<Text style={[styles.title, { color: themes[theme].tintColor }]} numberOfLines={2}> <Text style={[styles.title, { color: themes[theme].tintColor }]} numberOfLines={2}>
@ -114,7 +93,7 @@ const UrlContent = React.memo(
); );
const Url = React.memo( const Url = React.memo(
({ url, index, theme }: IMessageUrl) => { ({ url, index, theme }: { url: IUrl; index: number; theme: string }) => {
if (!url || url?.ignoreParse) { if (!url || url?.ignoreParse) {
return null; return null;
} }
@ -151,14 +130,17 @@ const Url = React.memo(
); );
const Urls = React.memo( const Urls = React.memo(
({ urls, theme }: IMessageUrls) => { // TODO - didn't work - (React.ReactElement | null)[] | React.ReactElement | null
({ urls }: { urls?: IUrl[] }): any => {
const { theme } = useTheme();
if (!urls || urls.length === 0) { if (!urls || urls.length === 0) {
return null; return null;
} }
return urls.map((url: any, index: number) => <Url url={url} key={url.url} index={index} theme={theme!} />); return urls.map((url: IUrl, index: number) => <Url url={url} key={url.url} index={index} theme={theme} />);
}, },
(oldProps, newProps) => dequal(oldProps.urls, newProps.urls) && oldProps.theme === newProps.theme (oldProps, newProps) => dequal(oldProps.urls, newProps.urls)
); );
UrlImage.displayName = 'MessageUrlImage'; UrlImage.displayName = 'MessageUrlImage';

View File

@ -3,12 +3,14 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import moment from 'moment'; import moment from 'moment';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import MessageError from './MessageError'; import MessageError from './MessageError';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import messageStyles from './styles'; import messageStyles from './styles';
import MessageContext from './Context'; import MessageContext from './Context';
import { SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME } from './utils'; import { SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME } from './utils';
import { SubscriptionType } from '../../definitions';
import { IRoomInfoParam } from '../../views/SearchMessagesView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -49,15 +51,15 @@ interface IMessageUser {
alias?: string; alias?: string;
ts?: Date; ts?: Date;
timeFormat?: string; timeFormat?: string;
theme: string; navToRoomInfo?: (navParam: IRoomInfoParam) => void;
navToRoomInfo?: Function;
type: string; type: string;
} }
const User = React.memo( const User = React.memo(
({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, type, ...props }: IMessageUser) => { ({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, navToRoomInfo, type, ...props }: IMessageUser) => {
if (isHeader || hasError) { if (isHeader || hasError) {
const { user } = useContext(MessageContext); const { user } = useContext(MessageContext);
const { theme } = useTheme();
const username = (useRealName && author?.name) || author?.username; const username = (useRealName && author?.name) || author?.username;
const aliasUsername = alias ? ( const aliasUsername = alias ? (
<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text> <Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>
@ -65,8 +67,8 @@ const User = React.memo(
const time = moment(ts).format(timeFormat); const time = moment(ts).format(timeFormat);
const onUserPress = () => { const onUserPress = () => {
navToRoomInfo?.({ navToRoomInfo?.({
t: 'd', t: SubscriptionType.DIRECT,
rid: author?._id rid: author?._id || ''
}); });
}; };
const isDisabled = author?._id === user.id; const isDisabled = author?._id === user.id;
@ -83,7 +85,7 @@ const User = React.memo(
<Text <Text
style={[styles.usernameInfoMessage, { color: themes[theme].titleText }]} style={[styles.usernameInfoMessage, { color: themes[theme].titleText }]}
onPress={onUserPress} onPress={onUserPress}
// @ts-ignore // @ts-ignore // TODO - check this prop
disabled={isDisabled}> disabled={isDisabled}>
{textContent} {textContent}
</Text> </Text>
@ -98,7 +100,7 @@ const User = React.memo(
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<Text style={[messageStyles.time, { color: themes[theme].auxiliaryTintColor }]}>{time}</Text> <Text style={[messageStyles.time, { color: themes[theme].auxiliaryTintColor }]}>{time}</Text>
{hasError && <MessageError hasError={hasError} theme={theme} {...props} />} {hasError ? <MessageError hasError={hasError} {...props} /> : null}
</View> </View>
); );
} }
@ -108,4 +110,4 @@ const User = React.memo(
User.displayName = 'MessageUser'; User.displayName = 'MessageUser';
export default withTheme(User); export default User;

View File

@ -16,9 +16,10 @@ import I18n from '../../i18n';
import { IAttachment } from '../../definitions/IAttachment'; import { IAttachment } from '../../definitions/IAttachment';
import RCActivityIndicator from '../ActivityIndicator'; import RCActivityIndicator from '../ActivityIndicator';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { useTheme } from '../../theme';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])]; const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
const isTypeSupported = (type: any) => SUPPORTED_TYPES.indexOf(type) !== -1; const isTypeSupported = (type: string) => SUPPORTED_TYPES.indexOf(type) !== -1;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -33,23 +34,24 @@ const styles = StyleSheet.create({
interface IMessageVideo { interface IMessageVideo {
file: IAttachment; file: IAttachment;
showAttachment?: Function; showAttachment?: (file: IAttachment) => void;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
style?: StyleProp<TextStyle>[]; style?: StyleProp<TextStyle>[];
isReply?: boolean; isReply?: boolean;
theme: string;
} }
const Video = React.memo( const Video = React.memo(
({ file, showAttachment, getCustomEmoji, style, isReply, theme }: IMessageVideo) => { ({ file, showAttachment, getCustomEmoji, style, isReply }: IMessageVideo) => {
const { baseUrl, user } = useContext(MessageContext); const { baseUrl, user } = useContext(MessageContext);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { theme } = useTheme();
if (!baseUrl) { if (!baseUrl) {
return null; return null;
} }
const onPress = async () => { const onPress = async () => {
if (isTypeSupported(file.video_type) && showAttachment) { if (file.video_type && isTypeSupported(file.video_type) && showAttachment) {
return showAttachment(file); return showAttachment(file);
} }
@ -93,7 +95,7 @@ const Video = React.memo(
</> </>
); );
}, },
(prevProps, nextProps) => dequal(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme (prevProps, nextProps) => dequal(prevProps.file, nextProps.file)
); );
export default Video; export default Video;

View File

@ -1,16 +1,18 @@
import React from 'react'; import React from 'react';
import { Keyboard, ViewStyle } from 'react-native'; import { Keyboard, ViewStyle } from 'react-native';
import { Subscription } from 'rxjs';
import Message from './Message'; import Message from './Message';
import MessageContext from './Context'; import MessageContext from './Context';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils'; import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants';
import messagesStatus from '../../constants/messagesStatus'; import messagesStatus from '../../constants/messagesStatus';
import { withTheme } from '../../theme'; import { useTheme, withTheme } from '../../theme';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { TAnyMessageModel } from '../../definitions'; import { IAttachment, TAnyMessageModel } from '../../definitions';
import { IRoomInfoParam } from '../../views/SearchMessagesView';
interface IMessageContainerProps { interface IMessageContainerProps {
item: TAnyMessageModel; item: TAnyMessageModel;
@ -20,7 +22,7 @@ interface IMessageContainerProps {
token: string; token: string;
}; };
msg?: string; msg?: string;
rid?: string; rid: string;
timeFormat?: string; timeFormat?: string;
style?: ViewStyle; style?: ViewStyle;
archived?: boolean; archived?: boolean;
@ -37,44 +39,35 @@ interface IMessageContainerProps {
isIgnored?: boolean; isIgnored?: boolean;
highlighted?: boolean; highlighted?: boolean;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
onLongPress?: Function; onLongPress?: (item: TAnyMessageModel) => void;
onReactionPress?: Function; onReactionPress?: (emoji: string, id: string) => void;
onEncryptedPress?: Function; onEncryptedPress?: () => void;
onDiscussionPress?: Function; onDiscussionPress?: (item: TAnyMessageModel) => void;
onThreadPress?: Function; onThreadPress?: (item: TAnyMessageModel) => void;
errorActionsShow?: Function; errorActionsShow?: (item: TAnyMessageModel) => void;
replyBroadcast?: Function; replyBroadcast?: (item: TAnyMessageModel) => void;
reactionInit?: Function; reactionInit?: (item: TAnyMessageModel) => void;
fetchThreadName?: Function; fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>;
showAttachment?: Function; showAttachment: (file: IAttachment) => void;
onReactionLongPress?: Function; onReactionLongPress?: (item: TAnyMessageModel) => void;
navToRoomInfo?: Function; navToRoomInfo: (navParam: IRoomInfoParam) => void;
callJitsi?: Function; callJitsi?: () => void;
blockAction?: Function; blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void;
onAnswerButtonPress?: Function; onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void;
theme?: string;
threadBadgeColor?: string; threadBadgeColor?: string;
toggleFollowThread?: Function; toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>;
jumpToMessage?: Function; jumpToMessage?: (link: string) => void;
onPress?: Function; onPress?: () => void;
} }
class MessageContainer extends React.Component<IMessageContainerProps> { interface IMessageContainerState {
isManualUnignored: boolean;
}
class MessageContainer extends React.Component<IMessageContainerProps, IMessageContainerState> {
static defaultProps = { static defaultProps = {
getCustomEmoji: () => null, getCustomEmoji: () => null,
onLongPress: () => {}, onLongPress: () => {},
onReactionPress: () => {},
onEncryptedPress: () => {},
onDiscussionPress: () => {},
onThreadPress: () => {},
onAnswerButtonPress: () => {},
errorActionsShow: () => {},
replyBroadcast: () => {},
reactionInit: () => {},
fetchThreadName: () => {},
showAttachment: () => {},
onReactionLongPress: () => {},
navToRoomInfo: () => {},
callJitsi: () => {}, callJitsi: () => {},
blockAction: () => {}, blockAction: () => {},
archived: false, archived: false,
@ -85,7 +78,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
state = { isManualUnignored: false }; state = { isManualUnignored: false };
private subscription: any; private subscription?: Subscription;
componentDidMount() { componentDidMount() {
const { item } = this.props; const { item } = this.props;
@ -97,12 +90,9 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
} }
} }
shouldComponentUpdate(nextProps: any, nextState: any) { shouldComponentUpdate(nextProps: IMessageContainerProps, nextState: IMessageContainerState) {
const { isManualUnignored } = this.state; const { isManualUnignored } = this.state;
const { theme, threadBadgeColor, isIgnored, highlighted } = this.props; const { threadBadgeColor, isIgnored, highlighted } = this.props;
if (nextProps.theme !== theme) {
return true;
}
if (nextProps.highlighted !== highlighted) { if (nextProps.highlighted !== highlighted) {
return true; return true;
} }
@ -169,7 +159,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
} }
}; };
onReactionPress = (emoji: any) => { onReactionPress = (emoji: string) => {
const { onReactionPress, item } = this.props; const { onReactionPress, item } = this.props;
if (onReactionPress) { if (onReactionPress) {
onReactionPress(emoji, item.id); onReactionPress(emoji, item.id);
@ -228,7 +218,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
previousItem.u.username === item.u.username && previousItem.u.username === item.u.username &&
!(previousItem.groupable === false || item.groupable === false || broadcast === true) && !(previousItem.groupable === false || item.groupable === false || broadcast === true) &&
// @ts-ignore TODO: IMessage vs IMessageFromServer non-sense // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense
item.ts - previousItem.ts < Message_GroupingPeriod! * 1000 && item.ts - previousItem.ts < Message_GroupingPeriod * 1000 &&
previousItem.tmid === item.tmid previousItem.tmid === item.tmid
) { ) {
return false; return false;
@ -303,10 +293,11 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
}; };
onLinkPress = (link: string): void => { onLinkPress = (link: string): void => {
const { item, theme, jumpToMessage } = this.props; const { theme } = useTheme();
const isMessageLink = item?.attachments?.findIndex((att: any) => att?.message_link === link) !== -1; const { item, jumpToMessage } = this.props;
if (isMessageLink) { const isMessageLink = item?.attachments?.findIndex((att: IAttachment) => att?.message_link === link) !== -1;
return jumpToMessage!(link); if (isMessageLink && jumpToMessage) {
return jumpToMessage(link);
} }
openLink(link, theme); openLink(link, theme);
}; };
@ -332,7 +323,6 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
callJitsi, callJitsi,
blockAction, blockAction,
rid, rid,
theme,
threadBadgeColor, threadBadgeColor,
toggleFollowThread, toggleFollowThread,
jumpToMessage, jumpToMessage,
@ -371,8 +361,8 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
let message = msg; let message = msg;
// "autoTranslateRoom" and "autoTranslateLanguage" are properties from the subscription // "autoTranslateRoom" and "autoTranslateLanguage" are properties from the subscription
// "autoTranslateMessage" is a toggle between "View Original" and "Translate" state // "autoTranslateMessage" is a toggle between "View Original" and "Translate" state
if (autoTranslateRoom && autoTranslateMessage) { if (autoTranslateRoom && autoTranslateMessage && autoTranslateLanguage) {
message = getMessageTranslation(item, autoTranslateLanguage!) || message; message = getMessageTranslation(item, autoTranslateLanguage) || message;
} }
return ( return (
@ -396,14 +386,15 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
toggleFollowThread, toggleFollowThread,
replies replies
}}> }}>
{/* @ts-ignore*/}
<Message <Message
id={id} id={id}
msg={message} msg={message}
md={md} md={md}
rid={rid!} rid={rid}
author={u} author={u}
ts={ts} ts={ts}
type={t as any} type={t}
attachments={attachments} attachments={attachments}
blocks={blocks} blocks={blocks}
urls={urls} urls={urls}
@ -413,23 +404,20 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
emoji={emoji} emoji={emoji}
timeFormat={timeFormat} timeFormat={timeFormat}
style={style} style={style}
archived={archived!} archived={archived}
broadcast={broadcast!} broadcast={broadcast}
useRealName={useRealName} useRealName={useRealName}
isReadReceiptEnabled={isReadReceiptEnabled!} isReadReceiptEnabled={isReadReceiptEnabled}
unread={unread} unread={unread}
role={role} role={role}
drid={drid} drid={drid}
dcount={dcount} dcount={dcount}
// @ts-ignore
dlm={dlm} dlm={dlm}
tmid={tmid} tmid={tmid}
tcount={tcount} tcount={tcount}
// @ts-ignore
tlm={tlm} tlm={tlm}
tmsg={tmsg} tmsg={tmsg}
fetchThreadName={fetchThreadName!} fetchThreadName={fetchThreadName}
// @ts-ignore
mentions={mentions} mentions={mentions}
channels={channels} channels={channels}
isIgnored={this.isIgnored} isIgnored={this.isIgnored}
@ -442,13 +430,12 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
isTemp={this.isTemp} isTemp={this.isTemp}
isEncrypted={this.isEncrypted} isEncrypted={this.isEncrypted}
hasError={this.hasError} hasError={this.hasError}
showAttachment={showAttachment!} showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo!} navToRoomInfo={navToRoomInfo}
callJitsi={callJitsi!} callJitsi={callJitsi}
blockAction={blockAction!} blockAction={blockAction}
theme={theme as string} highlighted={highlighted}
highlighted={highlighted!}
/> />
</MessageContext.Provider> </MessageContext.Provider>
); );

View File

@ -1,64 +1,45 @@
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
import { StyleProp, TextStyle } from 'react-native'; import { StyleProp, TextStyle } from 'react-native';
import { ImageStyle } from '@rocket.chat/react-native-fast-image';
import { IUserChannel, IUserMention } from '../markdown/interfaces'; import { IUserChannel } from '../markdown/interfaces';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IAttachment } from '../../definitions'; import { IAttachment, IThread, IUrl, IUserMention, IUserMessage, MessageType, TAnyMessageModel } from '../../definitions';
import { IRoomInfoParam } from '../../views/SearchMessagesView';
export type TMessageType = 'discussion-created' | 'jitsi_call_started';
export interface IMessageAttachments { export interface IMessageAttachments {
attachments?: IAttachment[]; attachments?: IAttachment[];
timeFormat?: string; timeFormat?: string;
style?: StyleProp<TextStyle>[]; style?: StyleProp<TextStyle>[];
isReply?: boolean; isReply?: boolean;
showAttachment?: Function; showAttachment?: (file: IAttachment) => void;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
} }
export interface IMessageAttachedActions {
attachment: IAttachment;
}
export interface IMessageAvatar { export interface IMessageAvatar {
isHeader: boolean; isHeader: boolean;
avatar: string; avatar?: string;
emoji: string; emoji?: string;
author: { author?: IUserMessage;
username: string;
_id: string;
};
small?: boolean; small?: boolean;
navToRoomInfo: Function; navToRoomInfo: (navParam: IRoomInfoParam) => void;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
theme: string;
} }
export interface IMessageBlocks { export interface IMessageBlocks {
blocks: any; blocks: { appId?: string }[];
id: string; id: string;
rid: string; rid: string;
blockAction: Function; blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void;
} }
export interface IMessageBroadcast { export interface IMessageBroadcast {
author: { author?: IUserMessage;
_id: string; broadcast?: boolean;
};
broadcast: boolean;
theme: string;
} }
export interface IMessageCallButton { export interface IMessageCallButton {
theme: string; callJitsi?: () => void;
callJitsi: Function;
}
export interface IUser {
id: string;
username: string;
token: string;
name: string;
} }
export interface IMessageContent { export interface IMessageContent {
@ -69,40 +50,27 @@ export interface IMessageContent {
isThreadRoom: boolean; isThreadRoom: boolean;
msg?: string; msg?: string;
md?: MarkdownAST; md?: MarkdownAST;
theme: string;
isEdited: boolean; isEdited: boolean;
isEncrypted: boolean; isEncrypted: boolean;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
channels?: IUserChannel[]; channels?: IUserChannel[];
mentions?: IUserMention[]; mentions?: IUserMention[];
navToRoomInfo?: Function; navToRoomInfo: (navParam: IRoomInfoParam) => void;
useRealName?: boolean; useRealName?: boolean;
isIgnored: boolean; isIgnored: boolean;
type: string; type: string;
} }
export interface IMessageDiscussion {
msg?: string;
dcount?: number;
dlm?: Date;
theme: string;
}
export interface IMessageEmoji { export interface IMessageEmoji {
content: any; content: string;
baseUrl: string; baseUrl: string;
standardEmojiStyle: object; standardEmojiStyle: { fontSize: number };
customEmojiStyle: object; customEmojiStyle: StyleProp<ImageStyle>;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
} }
export interface IMessageThread { export interface IMessageThread extends Pick<IThread, 'msg' | 'tcount' | 'tlm' | 'id'> {
msg?: string;
tcount?: number | null;
theme: string;
tlm?: Date;
isThreadRoom: boolean; isThreadRoom: boolean;
id: string;
} }
export interface IMessageTouchable { export interface IMessageTouchable {
@ -110,40 +78,35 @@ export interface IMessageTouchable {
isInfo: boolean; isInfo: boolean;
isThreadReply: boolean; isThreadReply: boolean;
isTemp: boolean; isTemp: boolean;
archived: boolean; archived?: boolean;
highlighted: boolean; highlighted?: boolean;
theme: string; ts?: string | Date;
ts?: any; urls?: IUrl[];
urls?: any;
reactions?: any; reactions?: any;
alias?: any; alias?: string;
role?: any; role?: string;
drid?: any; drid?: string;
} }
export interface IMessageRepliedThread { export interface IMessageRepliedThread extends Pick<IThread, 'tmid' | 'tmsg' | 'id'> {
tmid?: string;
tmsg?: string;
id: string;
isHeader: boolean; isHeader: boolean;
theme: string; fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>;
fetchThreadName: Function;
isEncrypted: boolean; isEncrypted: boolean;
} }
export interface IMessageInner export interface IMessageInner
extends IMessageDiscussion, extends IMessageContent,
IMessageContent,
IMessageCallButton, IMessageCallButton,
IMessageBlocks, IMessageBlocks,
IMessageThread, IMessageThread,
IMessageAttachments, IMessageAttachments,
IMessageBroadcast { IMessageBroadcast {
type: TMessageType; type: MessageType;
blocks: []; blocks: [];
urls?: IUrl[];
} }
export interface IMessage extends IMessageRepliedThread, IMessageInner { export interface IMessage extends IMessageRepliedThread, IMessageInner, IMessageAvatar {
isThreadReply: boolean; isThreadReply: boolean;
isThreadSequential: boolean; isThreadSequential: boolean;
isInfo: boolean; isInfo: boolean;
@ -151,9 +114,11 @@ export interface IMessage extends IMessageRepliedThread, IMessageInner {
isHeader: boolean; isHeader: boolean;
hasError: boolean; hasError: boolean;
style: any; style: any;
onLongPress: Function; // style: ViewStyle;
isReadReceiptEnabled: boolean; onLongPress?: (item: TAnyMessageModel) => void;
isReadReceiptEnabled?: boolean;
unread?: boolean; unread?: boolean;
theme: string;
isIgnored: boolean; isIgnored: boolean;
dcount: number | undefined;
dlm: string | Date | undefined;
} }

View File

@ -2,7 +2,7 @@ import { IUser } from './IUser';
export interface IAttachment { export interface IAttachment {
ts?: string | Date; ts?: string | Date;
title: string; title?: string;
type?: string; type?: string;
description?: string; description?: string;
title_link?: string; title_link?: string;
@ -19,7 +19,7 @@ export interface IAttachment {
image_size?: number; image_size?: number;
author_name?: string; author_name?: string;
author_icon?: string; author_icon?: string;
actions?: []; actions?: { type: string; msg: string; text: string }[];
message_link?: string; message_link?: string;
text?: string; text?: string;
short?: boolean; short?: boolean;

View File

@ -1,8 +1,9 @@
// TODO: evaluate unification with IEmoji
export interface IEmoji { export interface IEmoji {
content: any; content?: string;
name: string; name?: string;
extension: any; extension?: string;
isCustom: boolean; isCustom?: boolean;
} }
export interface ICustomEmoji { export interface ICustomEmoji {

View File

@ -1,5 +1,6 @@
import Model from '@nozbe/watermelondb/Model'; import Model from '@nozbe/watermelondb/Model';
// TODO: evaluate unification with IEmoji
export interface IFrequentlyUsedEmoji { export interface IFrequentlyUsedEmoji {
content?: string; content?: string;
extension?: string; extension?: string;

View File

@ -6,7 +6,7 @@ import { IAttachment } from './IAttachment';
import { IReaction } from './IReaction'; import { IReaction } from './IReaction';
import { TThreadMessageModel } from './IThreadMessage'; import { TThreadMessageModel } from './IThreadMessage';
import { TThreadModel } from './IThread'; import { TThreadModel } from './IThread';
import { IUrlFromServer } from './IUrl'; import { IUrl, IUrlFromServer } from './IUrl';
export type MessageType = 'jitsi_call_started' | 'discussion-created' | 'e2e' | 'load_more' | 'rm' | 'uj' | MessageTypeLoad; export type MessageType = 'jitsi_call_started' | 'discussion-created' | 'e2e' | 'load_more' | 'rm' | 'uj' | MessageTypeLoad;
@ -75,7 +75,7 @@ export interface IMessageFromServer {
ts: string | Date; // wm date issue ts: string | Date; // wm date issue
u: IUserMessage; u: IUserMessage;
_updatedAt: string | Date; _updatedAt: string | Date;
urls?: IUrlFromServer[]; urls?: IUrl[];
mentions?: IUserMention[]; mentions?: IUserMention[];
channels?: IUserChannel[]; channels?: IUserChannel[];
md?: MarkdownAST; md?: MarkdownAST;
@ -111,7 +111,7 @@ export interface ILoadMoreMessage {
export interface IMessage extends IMessageFromServer { export interface IMessage extends IMessageFromServer {
id: string; id: string;
t?: MessageType; t: MessageType;
alias?: string; alias?: string;
parseUrls?: boolean; parseUrls?: boolean;
avatar?: string; avatar?: string;

View File

@ -106,6 +106,7 @@ export interface ISubscription {
} }
export type TSubscriptionModel = ISubscription & Model; export type TSubscriptionModel = ISubscription & Model;
export type TSubscription = TSubscriptionModel | ISubscription;
// https://github.com/RocketChat/Rocket.Chat/blob/a88a96fcadd925b678ff27ada37075e029f78b5e/definition/ISubscription.ts#L8 // https://github.com/RocketChat/Rocket.Chat/blob/a88a96fcadd925b678ff27ada37075e029f78b5e/definition/ISubscription.ts#L8
export interface IServerSubscription extends IRocketChatRecord { export interface IServerSubscription extends IRocketChatRecord {

View File

@ -45,5 +45,4 @@ export interface IUrl extends IUrlFromServer {
title: string; title: string;
description: string; description: string;
image: string; image: string;
url: string;
} }

View File

@ -2,6 +2,8 @@ import { RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { TColors } from '../theme';
export * from './IAttachment'; export * from './IAttachment';
export * from './INotification'; export * from './INotification';
export * from './IPreferences'; export * from './IPreferences';
@ -32,8 +34,10 @@ export interface IBaseScreen<T extends Record<string, object | undefined>, S ext
navigation: StackNavigationProp<T, S>; navigation: StackNavigationProp<T, S>;
route: RouteProp<T, S>; route: RouteProp<T, S>;
dispatch: Dispatch; dispatch: Dispatch;
theme: string;
isMasterDetail: boolean; isMasterDetail: boolean;
// TODO: remove after migrating all Class components
theme: string;
colors: TColors;
} }
export * from './redux'; export * from './redux';

View File

@ -49,6 +49,9 @@ export type UsersEndpoints = {
'users.resetAvatar': { 'users.resetAvatar': {
POST: (params: { userId: string }) => {}; POST: (params: { userId: string }) => {};
}; };
'users.removeOtherTokens': {
POST: (params: { userId: string }) => {};
};
'users.getPreferences': { 'users.getPreferences': {
GET: (params: { userId: IUser['_id'] }) => { GET: (params: { userId: IUser['_id'] }) => {
preferences: INotificationPreferences; preferences: INotificationPreferences;

View File

@ -7,7 +7,7 @@ import { TNavigationOptions } from './definitions/navigationTypes';
export interface IDimensionsContextProps { export interface IDimensionsContextProps {
width: number; width: number;
height: number; height: number;
scale?: number; scale: number;
fontScale: number; fontScale: number;
setDimensions?: ({ setDimensions?: ({
width, width,

View File

@ -9,6 +9,7 @@ import UnreadBadge from '../../../presentation/UnreadBadge';
import RocketChat from '../../../lib/rocketchat'; import RocketChat from '../../../lib/rocketchat';
import { changeLivechatStatus, isOmnichannelStatusAvailable } from '../lib'; import { changeLivechatStatus, isOmnichannelStatusAvailable } from '../lib';
import { IUser } from '../../../definitions/IUser'; import { IUser } from '../../../definitions/IUser';
import Touch from '../../../utils/touch';
interface IOmnichannelStatus { interface IOmnichannelStatus {
searching: boolean; searching: boolean;
@ -48,7 +49,9 @@ const OmnichannelStatus = memo(({ searching, goQueue, queueSize, inquiryEnabled,
right={() => ( right={() => (
<View style={styles.omnichannelRightContainer}> <View style={styles.omnichannelRightContainer}>
{inquiryEnabled ? <UnreadBadge style={styles.queueIcon} unread={queueSize} /> : null} {inquiryEnabled ? <UnreadBadge style={styles.queueIcon} unread={queueSize} /> : null}
<Touch theme={theme} onPress={toggleLivechat}>
<Switch value={status} trackColor={SWITCH_TRACK_COLOR} onValueChange={toggleLivechat} /> <Switch value={status} trackColor={SWITCH_TRACK_COLOR} onValueChange={toggleLivechat} />
</Touch>
</View> </View>
)} )}
/> />

View File

@ -3,7 +3,6 @@ declare module 'commonmark';
declare module 'commonmark-react-renderer'; declare module 'commonmark-react-renderer';
declare module 'remove-markdown'; declare module 'remove-markdown';
declare module 'react-native-image-progress'; declare module 'react-native-image-progress';
declare module 'react-native-platform-touchable';
declare module 'react-native-ui-lib/keyboard'; declare module 'react-native-ui-lib/keyboard';
declare module '@rocket.chat/ui-kit'; declare module '@rocket.chat/ui-kit';
declare module '@rocket.chat/sdk'; declare module '@rocket.chat/sdk';

View File

@ -14,7 +14,7 @@ import parseQuery from './lib/methods/helpers/parseQuery';
import { initializePushNotifications, onNotification } from './notifications/push'; import { initializePushNotifications, onNotification } from './notifications/push';
import store from './lib/createStore'; import store from './lib/createStore';
import { toggleAnalyticsEventsReport, toggleCrashErrorsReport } from './utils/log'; import { toggleAnalyticsEventsReport, toggleCrashErrorsReport } from './utils/log';
import { ThemeContext } from './theme'; import { ThemeContext, TSupportedThemes } from './theme';
import { DimensionsContext } from './dimensions'; import { DimensionsContext } from './dimensions';
import RocketChat from './lib/rocketchat'; import RocketChat from './lib/rocketchat';
import { MIN_WIDTH_MASTER_DETAIL_LAYOUT } from './constants/tablet'; import { MIN_WIDTH_MASTER_DETAIL_LAYOUT } from './constants/tablet';
@ -32,7 +32,7 @@ import { isFDroidBuild } from './constants/environment';
import { IThemePreference } from './definitions/ITheme'; import { IThemePreference } from './definitions/ITheme';
import { ICommand } from './definitions/ICommand'; import { ICommand } from './definitions/ICommand';
import { initStore } from './lib/auxStore'; import { initStore } from './lib/auxStore';
import { themes } from './constants/colors'; import { colors, themes } from './constants/colors';
RNScreens.enableScreens(); RNScreens.enableScreens();
initStore(store); initStore(store);
@ -45,7 +45,7 @@ interface IDimensions {
} }
interface IState { interface IState {
theme: string; theme: TSupportedThemes;
themePreferences: IThemePreference; themePreferences: IThemePreference;
width: number; width: number;
height: number; height: number;
@ -215,7 +215,8 @@ export default class Root extends React.Component<{}, IState> {
value={{ value={{
theme, theme,
themePreferences, themePreferences,
setTheme: this.setTheme setTheme: this.setTheme,
colors: colors[theme]
}}> }}>
<DimensionsContext.Provider <DimensionsContext.Provider
value={{ value={{

View File

@ -14,3 +14,8 @@ export const E2E_ROOM_TYPES: Record<string, string> = {
d: 'd', d: 'd',
p: 'p' p: 'p'
}; };
export const THEME_PREFERENCES_KEY = 'RC_THEME_PREFERENCES_KEY';
export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY';
export const ANALYTICS_EVENTS_KEY = 'RC_ANALYTICS_EVENTS_KEY';
export const MIN_ROCKETCHAT_VERSION = '0.70.0';

View File

@ -17,7 +17,7 @@ import {
E2E_PUBLIC_KEY, E2E_PUBLIC_KEY,
E2E_RANDOM_PASSWORD_KEY, E2E_RANDOM_PASSWORD_KEY,
E2E_STATUS E2E_STATUS
} from './constants'; } from '../constants';
import { joinVectorData, randomPassword, splitVectorData, toString, utf8ToBuffer } from './utils'; import { joinVectorData, randomPassword, splitVectorData, toString, utf8ToBuffer } from './utils';
import { EncryptionRoom } from './index'; import { EncryptionRoom } from './index';
import { IMessage, ISubscription, TMessageModel, TSubscriptionModel, TThreadMessageModel, TThreadModel } from '../../definitions'; import { IMessage, ISubscription, TMessageModel, TSubscriptionModel, TThreadMessageModel, TThreadModel } from '../../definitions';

View File

@ -9,7 +9,7 @@ import Deferred from '../../utils/deferred';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import database from '../database'; import database from '../database';
import log from '../../utils/log'; import log from '../../utils/log';
import { E2E_MESSAGE_TYPE, E2E_STATUS } from './constants'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../constants';
import { import {
b64ToBuffer, b64ToBuffer,
bufferToB64, bufferToB64,

View File

@ -1,18 +1,10 @@
import random from '../../utils/random'; import { ITriggerAction, IUserInteraction, ModalActions } from '../../containers/UIKit/interfaces';
import { TRocketChat } from '../../definitions/IRocketChat';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import fetch from '../../utils/fetch'; import fetch from '../../utils/fetch';
import random from '../../utils/random';
import Navigation from '../Navigation'; import Navigation from '../Navigation';
import sdk from '../rocketchat/services/sdk'; import sdk from '../rocketchat/services/sdk';
import {
ActionTypes,
ITriggerAction,
ITriggerBlockAction,
ITriggerCancel,
ITriggerSubmitView,
IUserInteraction,
ModalActions
} from '../../containers/UIKit/interfaces';
import { TRocketChat } from '../../definitions/IRocketChat';
const triggersId = new Map(); const triggersId = new Map();
@ -139,18 +131,3 @@ export function triggerAction(
return reject(); return reject();
}); });
} }
export default function triggerBlockAction(this: TRocketChat, options: ITriggerBlockAction) {
return triggerAction.call(this, { type: ActionTypes.ACTION, ...options });
}
export async function triggerSubmitView(this: TRocketChat, { viewId, ...options }: ITriggerSubmitView) {
const result = await triggerAction.call(this, { type: ActionTypes.SUBMIT, viewId, ...options });
if (!result || ModalActions.CLOSE === result) {
Navigation.back();
}
}
export function triggerCancel(this: TRocketChat, { view, ...options }: ITriggerCancel) {
return triggerAction.call(this, { type: ActionTypes.CLOSED, view, ...options });
}

View File

@ -1,4 +1,4 @@
import database from '../../database'; import database from '../database';
export default async function clearCache({ server }: { server: string }): Promise<void> { export default async function clearCache({ server }: { server: string }): Promise<void> {
try { try {

View File

@ -1,5 +1,5 @@
import { TSubscriptionModel } from '../../../definitions'; import { TSubscriptionModel } from '../../definitions';
import database from '../../database'; import database from '../database';
export default async function getRoom(rid: string): Promise<TSubscriptionModel> { export default async function getRoom(rid: string): Promise<TSubscriptionModel> {
try { try {

View File

@ -4,7 +4,7 @@ import { MessageTypeLoad } from '../../constants/messageTypeLoad';
import { IMessage, TMessageModel } from '../../definitions'; import { IMessage, TMessageModel } from '../../definitions';
import log from '../../utils/log'; import log from '../../utils/log';
import { getMessageById } from '../database/services/Message'; import { getMessageById } from '../database/services/Message';
import roomTypeToApiType, { RoomTypes } from '../rocketchat/methods/roomTypeToApiType'; import roomTypeToApiType, { RoomTypes } from './roomTypeToApiType';
import sdk from '../rocketchat/services/sdk'; import sdk from '../rocketchat/services/sdk';
import { generateLoadMoreId } from '../utils'; import { generateLoadMoreId } from '../utils';
import updateMessages from './updateMessages'; import updateMessages from './updateMessages';

View File

@ -8,7 +8,7 @@ import { MessageTypeLoad } from '../../constants/messageTypeLoad';
import { generateLoadMoreId } from '../utils'; import { generateLoadMoreId } from '../utils';
import updateMessages from './updateMessages'; import updateMessages from './updateMessages';
import { TMessageModel } from '../../definitions'; import { TMessageModel } from '../../definitions';
import RocketChat from '../rocketchat'; import sdk from '../rocketchat/services/sdk';
const COUNT = 50; const COUNT = 50;
@ -22,7 +22,7 @@ interface ILoadNextMessages {
export default function loadNextMessages(args: ILoadNextMessages): Promise<void> { export default function loadNextMessages(args: ILoadNextMessages): Promise<void> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const data = await RocketChat.methodCallWrapper('loadNextMessages', args.rid, args.ts, COUNT); const data = await sdk.methodCallWrapper('loadNextMessages', args.rid, args.ts, COUNT);
let messages = EJSON.fromJSONValue(data?.messages); let messages = EJSON.fromJSONValue(data?.messages);
messages = orderBy(messages, 'ts'); messages = orderBy(messages, 'ts');
if (messages?.length) { if (messages?.length) {

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