Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/constants/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export const themes: any = {
previewTintColor: '#ffffff',
backdropOpacity: 0.3,
attachmentLoadingOpacity: 0.7,
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
...mentions
},
dark: {
Expand Down Expand Up @@ -114,6 +116,8 @@ export const themes: any = {
previewTintColor: '#ffffff',
backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3,
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
...mentions
},
black: {
Expand Down Expand Up @@ -162,6 +166,8 @@ export const themes: any = {
previewTintColor: '#ffffff',
backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3,
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
...mentions
}
};
5 changes: 5 additions & 0 deletions app/containers/message/Attachments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Reply from './Reply';
import Button from '../Button';
import styles from './styles';
import MessageContext from './Context';
import CollapsibleQuote from './Components/CollapsibleQuote';

const AttachedActions = ({ attachment, theme }: IMessageAttachedActions) => {
const { onAnswerButtonPress } = useContext(MessageContext);
Expand Down Expand Up @@ -51,6 +52,10 @@ const Attachments = React.memo(
if (file.actions && file.actions.length > 0) {
return <AttachedActions attachment={file} theme={theme} />;
}
if (file.title)
return (
<CollapsibleQuote key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} />
);

return (
<Reply
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { storiesOf } from '@storybook/react-native';
import React from 'react';
import { View } from 'react-native';

import MessageContext from '../../Context';
import CollapsibleQuote from '.';

const testAttachment = {
ts: '1970-01-01T00:00:00.000Z',
title: 'Engineering (9 today)',
fields: [
{
title: 'Out Today:\n',
value:
'Ricardo Mellu, 1 day, until Fri Mar 11\nLoma, 1 day, until Fri Mar 11\nAnitta, 3 hours\nDiego Carlitos, 19 days, until Fri Mar 11\nGabriel Vasconcelos, 5 days, until Fri Mar 11\nJorge Leite, 1 day, until Fri Mar 11\nKevin Aleman, 1 day, until Fri Mar 11\nPierre, 1 day, until Fri Mar 11\nTiago Evangelista Pinto, 1 day, until Fri Mar 11'
}
],
attachments: [],
collapsed: true
};

const stories = storiesOf('Message', module);

stories.add('Item', () => (
<View style={{ padding: 10 }}>
<MessageContext.Provider
value={{
onLongPress: () => {},
user: { username: 'Marcos' }
}}>
<CollapsibleQuote key={0} index={0} attachment={testAttachment} getCustomEmoji={() => {}} timeFormat='LT' />
</MessageContext.Provider>
</View>
));
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { fireEvent, render, within } from '@testing-library/react-native';
import React from 'react';

import MessageContext from '../../Context';
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 = {
ts: '1970-01-01T00:00:00.000Z',
title: 'Engineering (9 today)',
fields: [
{
title: 'Out Today:\n',
value:
'Ricardo Mellu, 1 day, until Fri Mar 11\nLoma, 1 day, until Fri Mar 11\nAnitta, 3 hours\nDiego Carlitos, 19 days, until Fri Mar 11\nGabriel Vasconcelos, 5 days, until Fri Mar 11\nJorge Leite, 1 day, until Fri Mar 11\nKevin Aleman, 1 day, until Fri Mar 11\nPierre, 1 day, until Fri Mar 11\nTiago Evangelista Pinto, 1 day, until Fri Mar 11'
}
],
attachments: [],
collapsed: true
};

const mockFn = jest.fn();

const Render = () => (
<MessageContext.Provider
value={{
onLongPress: () => {},
user: { username: 'Marcos' }
}}>
<CollapsibleQuote key={0} index={0} attachment={testAttachment} getCustomEmoji={mockFn} timeFormat='LT' />
</MessageContext.Provider>
);

const touchableTestID = `collapsibleQuoteTouchable-${testAttachment.title}`;

describe('CollapsibleQuote', () => {
test('rendered', async () => {
const { findByTestId } = render(<Render />);
const collapsibleQuoteTouchable = await findByTestId(touchableTestID);
expect(collapsibleQuoteTouchable).toBeTruthy();
});

test('title exists and is correct', async () => {
const { findByText } = render(<Render />);
const collapsibleQuoteTitle = await findByText(testAttachment.title);
expect(collapsibleQuoteTitle).toBeTruthy();
expect(collapsibleQuoteTitle.props.children).toEqual(testAttachment.title);
});

test('fields render title correctly', async () => {
const collapsibleQuote = render(<Render />);
const collapsibleQuoteTouchable = await collapsibleQuote.findByTestId(touchableTestID);
// open
fireEvent.press(collapsibleQuoteTouchable);
const open = within(collapsibleQuoteTouchable);
const fieldTitleOpen = open.getByTestId('collapsibleQuoteTouchableFieldTitle');
expect(fieldTitleOpen).toBeTruthy();
expect(fieldTitleOpen.props.children).toEqual(testAttachment.fields[0].title);
// close
fireEvent.press(collapsibleQuoteTouchable);
collapsibleQuote.rerender(<Render />);
const close = within(collapsibleQuoteTouchable);
const fieldTitleClosed = close.queryByTestId('collapsibleQuoteTouchableFieldTitle');
expect(fieldTitleClosed).toBeNull();
});

test('fields render fields correctly', async () => {
const collapsibleQuote = render(<Render />);
const collapsibleQuoteTouchable = await collapsibleQuote.findByTestId(touchableTestID);
// open
fireEvent.press(collapsibleQuoteTouchable);
const open = within(collapsibleQuoteTouchable);
const fieldValueOpen = open.getByLabelText(testAttachment.fields[0].value.split('\n')[0]);
expect(fieldValueOpen).toBeTruthy();
// close
fireEvent.press(collapsibleQuoteTouchable);
collapsibleQuote.rerender(<Render />);
const close = within(collapsibleQuoteTouchable);
const fieldValueClosed = close.queryByTestId(testAttachment.fields[0].value.split('\n')[0]);
expect(fieldValueClosed).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots Message Item 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"padding\\":10}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"testID\\":\\"collapsibleQuoteTouchable-Engineering (9 today)\\",\\"hitSlop\\":{\\"top\\":4,\\"right\\":4,\\"bottom\\":4,\\"left\\":4},\\"focusable\\":true,\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginTop\\":6,\\"borderWidth\\":1,\\"borderRadius\\":4,\\"minHeight\\":40,\\"backgroundColor\\":\\"#f3f4f5\\",\\"borderLeftColor\\":\\"#CBCED1\\",\\"borderTopColor\\":\\"#e1e5e8\\",\\"borderRightColor\\":\\"#e1e5e8\\",\\"borderBottomColor\\":\\"#e1e5e8\\",\\"borderLeftWidth\\":2,\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"borderRadius\\":4,\\"padding\\":8}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\"}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#6C727A\\"}]},\\"children\\":[\\"Engineering (9 today)\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"width\\":20,\\"height\\":20,\\"right\\":8,\\"top\\":8,\\"justifyContent\\":\\"center\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":22,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}]}"`;
185 changes: 185 additions & 0 deletions app/containers/message/Components/CollapsibleQuote/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { transparentize } from 'color2k';
import { dequal } from 'dequal';
import React, { useContext, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';

import { themes } from '../../../../constants/colors';
import { IAttachment } from '../../../../definitions/IAttachment';
import { TGetCustomEmoji } from '../../../../definitions/IEmoji';
import { CustomIcon } from '../../../../lib/Icons';
import { useTheme } from '../../../../theme';
import sharedStyles from '../../../../views/Styles';
import Markdown from '../../../markdown';
import MessageContext from '../../Context';
import Touchable from '../../Touchable';
import { BUTTON_HIT_SLOP } from '../../utils';

const styles = StyleSheet.create({
button: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 6,
borderWidth: 1,
borderRadius: 4,
minHeight: 40
},
attachmentContainer: {
flex: 1,
borderRadius: 4,
padding: 8
},
authorContainer: {
flexDirection: 'row'
},
fieldContainer: {
flexDirection: 'column',
paddingLeft: 10,
paddingTop: 10,
paddingBottom: 10
},
fieldTitle: {
fontSize: 15,
...sharedStyles.textBold
},
marginTop: {
marginTop: 4
},
marginBottom: {
marginBottom: 4
},
title: {
fontSize: 16,
...sharedStyles.textMedium
},
touchableContainer: {
flexDirection: 'row'
},
markdownFontSize: {
fontSize: 15
},
iconContainer: {
width: 20,
height: 20,
right: 8,
top: 8,
justifyContent: 'center',
alignItems: 'center'
}
});

interface IMessageFields {
attachment: IAttachment;
getCustomEmoji: TGetCustomEmoji;
}

interface IMessageReply {
attachment: IAttachment;
timeFormat?: string;
index: number;
getCustomEmoji: TGetCustomEmoji;
}

const Fields = React.memo(
({ attachment, getCustomEmoji }: IMessageFields) => {
if (!attachment.fields) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
const { theme } = useTheme();
return (
<>
{attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text testID='collapsibleQuoteTouchableFieldTitle' style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>
{field.title}
</Text>
<Markdown
msg={field?.value || ''}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
theme={theme}
style={[styles.markdownFontSize]}
/>
</View>
))}
</>
);
},
(prevProps, nextProps) => dequal(prevProps.attachment.fields, nextProps.attachment.fields)
);

const CollapsibleQuote = React.memo(
({ attachment, index, getCustomEmoji }: IMessageReply) => {
if (!attachment) {
return null;
}
const [collapsed, setCollapsed] = useState(attachment.collapsed);
const { theme } = useTheme();

const onPress = () => {
setCollapsed(!collapsed);
};

let {
borderColor,
chatComponentBackground: backgroundColor,
collapsibleQuoteBorder,
collapsibleChevron,
headerTintColor
} = themes[theme];

try {
if (attachment.color) {
backgroundColor = transparentize(attachment.color, 0.8);
borderColor = attachment.color;
collapsibleQuoteBorder = attachment.color;
collapsibleChevron = attachment.color;
headerTintColor = headerTintColor;
}
} catch (e) {
// fallback to default
}

return (
<>
<Touchable
testID={`collapsibleQuoteTouchable-${attachment.title}`}
onPress={onPress}
style={[
styles.button,
index > 0 && styles.marginTop,
attachment.description && styles.marginBottom,
{
backgroundColor,
borderLeftColor: collapsibleQuoteBorder,
borderTopColor: borderColor,
borderRightColor: borderColor,
borderBottomColor: borderColor,
borderLeftWidth: 2
}
]}
background={Touchable.Ripple(themes[theme].bannerBackground)}
hitSlop={BUTTON_HIT_SLOP}>
<View style={styles.touchableContainer}>
<View style={styles.attachmentContainer}>
<View style={styles.authorContainer}>
<Text style={[styles.title, { color: headerTintColor }]}>{attachment.title}</Text>
</View>
{!collapsed && <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} />}
</View>
<View style={styles.iconContainer}>
<CustomIcon name={!collapsed ? 'chevron-up' : 'chevron-down'} size={22} color={collapsibleChevron} />
</View>
</View>
</Touchable>
</>
);
},
(prevProps, nextProps) => dequal(prevProps.attachment, nextProps.attachment)
);

CollapsibleQuote.displayName = 'CollapsibleQuote';
Fields.displayName = 'CollapsibleQuoteFields';

export default CollapsibleQuote;
8 changes: 5 additions & 3 deletions app/definitions/IAttachment.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { IUser } from './IUser';

export interface IAttachment {
ts: string | Date;
ts?: string | Date;
title: string;
type: string;
description: string;
type?: string;
description?: string;
title_link?: string;
image_url?: string;
image_type?: string;
Expand All @@ -24,6 +24,8 @@ export interface IAttachment {
author_link?: string;
color?: string;
thumb_url?: string;
attachments?: any[];
collapsed?: boolean;
}

export interface IServerAttachment {
Expand Down
Loading