Commit 96d0b1fc authored by Diego Mello's avatar Diego Mello Committed by GitHub

[NEW] Message layout (#426)

* message container/component

* Separator component

* Reply

* Url

* tests updated

* Minor changes

* Audio component

* Broadcast button

* Minor touches

* Reply preview

* Edited

* Minor bug fixes

* - Update roadmap
- Bump version to 1.2

* Onboarding styles fix
parent a2ec1e72
...@@ -50,37 +50,35 @@ Readme will guide you on how to config. ...@@ -50,37 +50,35 @@ Readme will guide you on how to config.
### Current priorities ### Current priorities
1) Onboarding ([#392][i392]) 1) Open PDF and other file types ([#341][i341])
2) Splash screen ([#399][i399]) 2) [NEW] Commands ([#405][i405])
3) Add empty chat background ([#398][i398]) 3) Better message actions ([#329][i329])
4) Rooms list layout ([#395][i395]) 4) [NEW] Login/Register/Forgot Password layout ([#400][i400])
5) Create channel layout ([#401][i401])
### To do ### To do
| Task | Status | | Task | Status |
|--------------------|-----| |--------------------|-----|
| [NEW] Reply Preview ([#311][i311]) | ✅ | | [NEW] Reply Preview ([#311][i311]) | ✅ |
| Image upload improvements ([#368][i368]) | ✅ | | Image upload improvements ([#368][i368]) | ✅ |
| [NEW] Onboarding ([#392][i392]) | WIP | | [NEW] Onboarding ([#392][i392]) | ✅ |
| [NEW] Create channel layout ([#401][i401]) | ✅ |
| [NEW] Splash screen ([#399][i399]) | ✅ |
| [NEW] Add empty chat background ([#398][i398]) | ✅ |
| [NEW] Message layout ([#397][i397]) | ✅ |
| [NEW] Rooms list layout ([#395][i395]) | ✅ |
| Add components to Storybook ([#38][i38]) | WIP |
| Open PDF and other file types ([#341][i341]) | WIP |
| Better message actions ([#329][i329]) | ❌ |
| [NEW] Settings layout ([#396][i396]) | ❌ |
| [NEW] Contextual bar layout ([#402][i402]) | ❌ | | [NEW] Contextual bar layout ([#402][i402]) | ❌ |
| [NEW] Create channel layout ([#401][i401]) | ❌ |
| [NEW] Login/Register/Forgot Password layout ([#400][i400]) | ❌ | | [NEW] Login/Register/Forgot Password layout ([#400][i400]) | ❌ |
| [NEW] Splash screen ([#399][i399]) | ❌ |
| [NEW] Add empty chat background ([#398][i398]) | ❌ |
| [NEW] Message layout ([#397][i397]) | ❌ |
| [NEW] Settings layout ([#396][i396]) | ❌ |
| [NEW] Rooms list layout ([#395][i395]) | ❌ |
| [NEW] Commands ([#405][i405]) | ❌ | | [NEW] Commands ([#405][i405]) | ❌ |
| [Android] Add Fastlane ([#404][i404]) | ❌ | | [Android] Add Fastlane ([#404][i404]) | ❌ |
| [Android] Adaptive icons ([#403][i403]) | ❌ |
| [NEW] Auto versioning app on Circle CI ([#393][i393]) | ❌ | | [NEW] Auto versioning app on Circle CI ([#393][i393]) | ❌ |
| [Android] Group notifications by room ([#391][i391]) | ❌ | | [Android] Group notifications by room ([#391][i391]) | ❌ |
| Open PDF and other file types ([#341][i341]) | ❌ |
| Better message actions ([#329][i329]) | ❌ |
| Integrate project with code push ([#233][i233]) | ❌ | | Integrate project with code push ([#233][i233]) | ❌ |
| Custom icons ([#210][i210]) | ❌ | | Custom icons ([#210][i210]) | ❌ |
| Share Extension ([#69][i69]) | ❌ | | Share Extension ([#69][i69]) | ❌ |
| Add components to Storybook ([#38][i38]) | ❌ |
| Upload files ([#2][i2]) | ❌ | | Upload files ([#2][i2]) | ❌ |
[i2]: https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/2 [i2]: https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/2
...@@ -124,10 +122,10 @@ Readme will guide you on how to config. ...@@ -124,10 +122,10 @@ Readme will guide you on how to config.
| Messages list: load more on scroll | ✅ | | Messages list: load more on scroll | ✅ |
| Messages list: receive new messages via subscription | ✅ | | Messages list: receive new messages via subscription | ✅ |
| Subscriptions list | ✅ | | Subscriptions list | ✅ |
| Segmented subscriptions list: Favorites | | | Segmented subscriptions list: Favorites | |
| Segmented subscriptions list: Unreads | | | Segmented subscriptions list: Unreads | |
| Segmented subscriptions list: DMs | | | Segmented subscriptions list: DMs | |
| Segmented subscriptions list: Channels | | | Segmented subscriptions list: Channels | |
| Subscriptions list: update user status via subscription | ✅ | | Subscriptions list: update user status via subscription | ✅ |
| Numbers os messages unread in the Subscriptions list | ✅ | | Numbers os messages unread in the Subscriptions list | ✅ |
| Status change | ✅ | | Status change | ✅ |
...@@ -205,7 +203,7 @@ Readme will guide you on how to config. ...@@ -205,7 +203,7 @@ Readme will guide you on how to config.
| Localized in Portuguese (pt-BR) | ❌ | | Localized in Portuguese (pt-BR) | ❌ |
| Localized in Russian | ✅ | | Localized in Russian | ✅ |
| Localized in English | ✅ | | Localized in English | ✅ |
| Full name setting | | | Full name setting | |
| Read only rooms | ✅ | | Read only rooms | ✅ |
| Typing status | ✅ | | Typing status | ✅ |
| Create channel/group | ✅ | | Create channel/group | ✅ |
......
module.exports = 'test-file-stub';
// @flow
/* eslint-disable */
import I18nJs from 'i18n-js';
I18nJs.locale = 'en'; // a locale from your available translations
export const getLanguages = (): Promise<string[]> => Promise.resolve(['en']);
export default I18nJs;
\ No newline at end of file
export default function() {
return {
show: () => {}
};
}
export default () => 'Video';
export default () => 'Video';
...@@ -13,29 +13,28 @@ import RoomItem from '../app/presentation/RoomItem'; ...@@ -13,29 +13,28 @@ import RoomItem from '../app/presentation/RoomItem';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
const date = new Date(2017, 10, 10, 10); const date = new Date(2017, 10, 10, 10);
const onPress = () => {};
jest.mock('react-native-img-cache', () => { return { CachedImage: 'View' } });
it('renders correctly', () => { it('renders correctly', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem type="d" _updatedAt={date} name="name" /></View></Provider>).toJSON()).toMatchSnapshot(); expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" baseUrl="baseUrl" /></View></Provider>).toJSON()).toMatchSnapshot();
}); });
it('render unread', () => { it('render unread', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem type="d" _updatedAt={date} name="name" unread={1} /></View></Provider>).toJSON()).toMatchSnapshot(); expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" unread={1} /></View></Provider>).toJSON()).toMatchSnapshot();
}); });
it('render unread +999', () => { it('render unread +999', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem type="d" _updatedAt={date} name="name" unread={1000} /></View></Provider>).toJSON()).toMatchSnapshot(); expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" unread={1000} /></View></Provider>).toJSON()).toMatchSnapshot();
}); });
it('render no icon', () => { it('render no icon', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem type="X" _updatedAt={date} name="name" /></View></Provider>).toJSON()).toMatchSnapshot(); expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="X" _updatedAt={date} name="name" /></View></Provider>).toJSON()).toMatchSnapshot();
}); });
it('render private group', () => { it('render private group', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem type="g" _updatedAt={date} name="private-group" /> </View></Provider>).toJSON()).toMatchSnapshot(); expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="g" _updatedAt={date} name="private-group" /> </View></Provider>).toJSON()).toMatchSnapshot();
}); });
it('render channel', () => { it('render channel', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem type="c" _updatedAt={date} name="general" /></View></Provider>).toJSON()).toMatchSnapshot(); expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="c" _updatedAt={date} name="general" /></View></Provider>).toJSON()).toMatchSnapshot();
}); });
...@@ -102,7 +102,7 @@ android { ...@@ -102,7 +102,7 @@ android {
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 27 targetSdkVersion 27
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "1.1.1" versionName "1.2"
ndk { ndk {
abiFilters "armeabi-v7a", "x86" abiFilters "armeabi-v7a", "x86"
} }
......
...@@ -79,5 +79,9 @@ export default { ...@@ -79,5 +79,9 @@ export default {
}, },
Store_Last_Message: { Store_Last_Message: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
},
UI_Use_Real_Name: {
type: 'valueAsBoolean'
} }
}; };
export const settingsUpdatedAt = new Date('2018-09-10');
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StyleSheet, Text, View, ViewPropTypes } from 'react-native'; import { StyleSheet, Text, View, ViewPropTypes } from 'react-native';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor'; import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
...@@ -19,13 +18,10 @@ const styles = StyleSheet.create({ ...@@ -19,13 +18,10 @@ const styles = StyleSheet.create({
} }
}); });
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class Avatar extends React.PureComponent { export default class Avatar extends React.PureComponent {
static propTypes = { static propTypes = {
baseUrl: PropTypes.string.isRequired,
style: ViewPropTypes.style, style: ViewPropTypes.style,
baseUrl: PropTypes.string,
text: PropTypes.string, text: PropTypes.string,
avatar: PropTypes.string, avatar: PropTypes.string,
size: PropTypes.number, size: PropTypes.number,
......
import React from 'react'; import React from 'react';
import { ViewPropTypes, Image } from 'react-native'; import { ViewPropTypes, Image } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux';
@connect(state => ({
baseUrl: state.settings.Site_Url
}))
export default class CustomEmoji extends React.Component { export default class CustomEmoji extends React.Component {
static propTypes = { static propTypes = {
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
......
...@@ -10,9 +10,9 @@ import scrollPersistTaps from '../../utils/scrollPersistTaps'; ...@@ -10,9 +10,9 @@ import scrollPersistTaps from '../../utils/scrollPersistTaps';
const emojisPerRow = Platform.OS === 'ios' ? 8 : 9; const emojisPerRow = Platform.OS === 'ios' ? 8 : 9;
const renderEmoji = (emoji, size) => { const renderEmoji = (emoji, size, baseUrl) => {
if (emoji.isCustom) { if (emoji.isCustom) {
return <CustomEmoji style={[styles.customCategoryEmoji, { height: size - 8, width: size - 8 }]} emoji={emoji} />; return <CustomEmoji style={[styles.customCategoryEmoji, { height: size - 8, width: size - 8 }]} emoji={emoji} baseUrl={baseUrl} />;
} }
return ( return (
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}> <Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
...@@ -25,6 +25,7 @@ const renderEmoji = (emoji, size) => { ...@@ -25,6 +25,7 @@ const renderEmoji = (emoji, size) => {
@responsive @responsive
export default class EmojiCategory extends React.Component { export default class EmojiCategory extends React.Component {
static propTypes = { static propTypes = {
baseUrl: PropTypes.string.isRequired,
emojis: PropTypes.any, emojis: PropTypes.any,
window: PropTypes.any, window: PropTypes.any,
onEmojiSelected: PropTypes.func, onEmojiSelected: PropTypes.func,
...@@ -44,6 +45,7 @@ export default class EmojiCategory extends React.Component { ...@@ -44,6 +45,7 @@ export default class EmojiCategory extends React.Component {
} }
renderItem(emoji, size) { renderItem(emoji, size) {
const { baseUrl } = this.props;
return ( return (
<TouchableOpacity <TouchableOpacity
activeOpacity={0.7} activeOpacity={0.7}
...@@ -51,7 +53,7 @@ export default class EmojiCategory extends React.Component { ...@@ -51,7 +53,7 @@ export default class EmojiCategory extends React.Component {
onPress={() => this.props.onEmojiSelected(emoji)} onPress={() => this.props.onEmojiSelected(emoji)}
testID={`reaction-picker-${ emoji.isCustom ? emoji.content : emoji }`} testID={`reaction-picker-${ emoji.isCustom ? emoji.content : emoji }`}
> >
{renderEmoji(emoji, size)} {renderEmoji(emoji, size, baseUrl)}
</TouchableOpacity>); </TouchableOpacity>);
} }
......
...@@ -19,6 +19,7 @@ const scrollProps = { ...@@ -19,6 +19,7 @@ const scrollProps = {
export default class EmojiPicker extends Component { export default class EmojiPicker extends Component {
static propTypes = { static propTypes = {
baseUrl: PropTypes.string.isRequired,
onEmojiSelected: PropTypes.func, onEmojiSelected: PropTypes.func,
tabEmojiStyle: PropTypes.object, tabEmojiStyle: PropTypes.object,
emojisPerRow: PropTypes.number, emojisPerRow: PropTypes.number,
...@@ -110,6 +111,7 @@ export default class EmojiPicker extends Component { ...@@ -110,6 +111,7 @@ export default class EmojiPicker extends Component {
style={styles.categoryContainer} style={styles.categoryContainer}
size={this.props.emojisPerRow} size={this.props.emojisPerRow}
width={this.props.width} width={this.props.width}
baseUrl={this.props.baseUrl}
/> />
); );
} }
...@@ -123,12 +125,14 @@ export default class EmojiPicker extends Component { ...@@ -123,12 +125,14 @@ export default class EmojiPicker extends Component {
<ScrollableTabView <ScrollableTabView
renderTabBar={() => <TabBar tabEmojiStyle={this.props.tabEmojiStyle} />} renderTabBar={() => <TabBar tabEmojiStyle={this.props.tabEmojiStyle} />}
contentProps={scrollProps} contentProps={scrollProps}
style={styles.background}
> >
{ {
categories.tabs.map((tab, i) => ( categories.tabs.map((tab, i) => (
<ScrollView <ScrollView
key={tab.category} key={tab.category}
tabLabel={tab.tabLabel} tabLabel={tab.tabLabel}
style={styles.background}
{...scrollProps} {...scrollProps}
> >
{this.renderCategory(tab.category, i)} {this.renderCategory(tab.category, i)}
......
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
export default StyleSheet.create({ export default StyleSheet.create({
background: {
backgroundColor: '#fff'
},
container: { container: {
flex: 1 flex: 1
}, },
......
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { KeyboardRegistry } from 'react-native-keyboard-input'; import { KeyboardRegistry } from 'react-native-keyboard-input';
import { Provider } from 'react-redux';
import store from '../../lib/createStore'; import store from '../../lib/createStore';
import EmojiPicker from '../EmojiPicker'; import EmojiPicker from '../EmojiPicker';
import styles from './styles'; import styles from './styles';
export default class EmojiKeyboard extends React.PureComponent { export default class EmojiKeyboard extends React.PureComponent {
constructor(props) {
super(props);
const state = store.getState();
this.baseUrl = state.settings.Site_Url || state.server ? state.server.server : '';
}
onEmojiSelected = (emoji) => { onEmojiSelected = (emoji) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji }); KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
} }
render() { render() {
return ( return (
<Provider store={store}> <View style={styles.emojiKeyboardContainer} testID='messagebox-keyboard-emoji'>
<View style={styles.emojiKeyboardContainer} testID='messagebox-keyboard-emoji'> <EmojiPicker onEmojiSelected={emoji => this.onEmojiSelected(emoji)} baseUrl={this.baseUrl} />
<EmojiPicker onEmojiSelected={emoji => this.onEmojiSelected(emoji)} /> </View>
</View>
</Provider>
); );
} }
} }
......
...@@ -9,15 +9,17 @@ import Markdown from '../message/Markdown'; ...@@ -9,15 +9,17 @@ import Markdown from '../message/Markdown';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flexDirection: 'row' flexDirection: 'row',
marginTop: 10,
backgroundColor: '#fff'
}, },
messageContainer: { messageContainer: {
flex: 1, flex: 1,
marginHorizontal: 15, marginHorizontal: 10,
backgroundColor: '#F3F4F5', backgroundColor: '#F3F4F5',
paddingHorizontal: 15, paddingHorizontal: 15,
paddingVertical: 10, paddingVertical: 10,
borderRadius: 2 borderRadius: 4
}, },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
...@@ -35,18 +37,23 @@ const styles = StyleSheet.create({ ...@@ -35,18 +37,23 @@ const styles = StyleSheet.create({
marginLeft: 5 marginLeft: 5
}, },
close: { close: {
marginRight: 15 marginRight: 10
} }
}); });
@connect(state => ({ @connect(state => ({
Message_TimeFormat: state.settings.Message_TimeFormat Message_TimeFormat: state.settings.Message_TimeFormat,
customEmojis: state.customEmojis,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
})) }))
export default class ReplyPreview extends Component { export default class ReplyPreview extends Component {
static propTypes = { static propTypes = {
message: PropTypes.object.isRequired, message: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired,
close: PropTypes.func.isRequired close: PropTypes.func.isRequired,
customEmojis: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
username: PropTypes.string.isRequired
} }
close = () => { close = () => {
...@@ -54,7 +61,9 @@ export default class ReplyPreview extends Component { ...@@ -54,7 +61,9 @@ export default class ReplyPreview extends Component {
} }
render() { render() {
const { message, Message_TimeFormat } = this.props; const {
message, Message_TimeFormat, customEmojis, baseUrl, username
} = this.props;
const time = moment(message.ts).format(Message_TimeFormat); const time = moment(message.ts).format(Message_TimeFormat);
return ( return (
<View style={styles.container}> <View style={styles.container}>
...@@ -63,9 +72,9 @@ export default class ReplyPreview extends Component { ...@@ -63,9 +72,9 @@ export default class ReplyPreview extends Component {
<Text style={styles.username}>{message.u.username}</Text> <Text style={styles.username}>{message.u.username}</Text>
<Text style={styles.time}>{time}</Text> <Text style={styles.time}>{time}</Text>
</View> </View>
<Markdown msg={message.msg} /> <Markdown msg={message.msg} customEmojis={customEmojis} baseUrl={baseUrl} username={username} />
</View> </View>
<Icon name='close' size={20} style={styles.close} onPress={this.close} /> <Icon name='close' color='#9ea2a8' size={20} style={styles.close} onPress={this.close} />
</View> </View>
); );
} }
......
...@@ -530,6 +530,7 @@ export default class MessageBox extends React.PureComponent { ...@@ -530,6 +530,7 @@ export default class MessageBox extends React.PureComponent {
text={item.username || item.name} text={item.username || item.name}
size={30} size={30}
type={item.username ? 'd' : 'c'} type={item.username ? 'd' : 'c'}
baseUrl={this.props.baseUrl}
/>, />,
<Text key='mention-item-name'>{ item.username || item.name }</Text> <Text key='mention-item-name'>{ item.username || item.name }</Text>
] ]
...@@ -556,11 +557,13 @@ export default class MessageBox extends React.PureComponent { ...@@ -556,11 +557,13 @@ export default class MessageBox extends React.PureComponent {
}; };
renderReplyPreview = () => { renderReplyPreview = () => {
const { replyMessage, replying, closeReply } = this.props; const {
replyMessage, replying, closeReply, username
} = this.props;
if (!replying) { if (!replying) {
return null; return null;
} }
return <ReplyPreview key='reply-preview' message={replyMessage} close={closeReply} />; return <ReplyPreview key='reply-preview' message={replyMessage} close={closeReply} username={username} />;
}; };
renderFilesActions = () => { renderFilesActions = () => {
...@@ -584,29 +587,30 @@ export default class MessageBox extends React.PureComponent { ...@@ -584,29 +587,30 @@ export default class MessageBox extends React.PureComponent {
return ( return (
[ [
this.renderMentions(), this.renderMentions(),
this.renderReplyPreview(), <View style={styles.composer} key='messagebox'>
<View {this.renderReplyPreview()}
key='messagebox' <View
style={[styles.textArea, this.props.editing && styles.editing]} style={[styles.textArea, this.props.editing && styles.editing]}
testID='messagebox' testID='messagebox'
> >
{this.leftButtons} {this.leftButtons}
<TextInput <TextInput
ref={component => this.component = component} ref={component => this.component = component}
style={styles.textBoxInput} style={styles.textBoxInput}
returnKeyType='default' returnKeyType='default'
keyboardType='twitter' keyboardType='twitter'
blurOnSubmit={false} blurOnSubmit={false}
placeholder={I18n.t('New_Message')} placeholder={I18n.t('New_Message')}
onChangeText={text => this.onChangeText(text)} onChangeText={text => this.onChangeText(text)}
value={this.state.text} value={this.state.text}
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
defaultValue='' defaultValue=''
multiline multiline
placeholderTextColor='#9EA2A8' placeholderTextColor='#9EA2A8'