Unverified 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.
### Current priorities
1) Onboarding ([#392][i392])
2) Splash screen ([#399][i399])
3) Add empty chat background ([#398][i398])
4) Rooms list layout ([#395][i395])
5) Create channel layout ([#401][i401])
1) Open PDF and other file types ([#341][i341])
2) [NEW] Commands ([#405][i405])
3) Better message actions ([#329][i329])
4) [NEW] Login/Register/Forgot Password layout ([#400][i400])
### To do
| Task | Status |
|--------------------|-----|
| [NEW] Reply Preview ([#311][i311]) | ✅ |
| 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] Create channel layout ([#401][i401]) | ❌ |
| [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]) | ❌ |
| [Android] Add Fastlane ([#404][i404]) | ❌ |
| [Android] Adaptive icons ([#403][i403]) | ❌ |
| [NEW] Auto versioning app on Circle CI ([#393][i393]) | ❌ |
| [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]) | ❌ |
| Custom icons ([#210][i210]) | ❌ |
| Share Extension ([#69][i69]) | ❌ |
| Add components to Storybook ([#38][i38]) | ❌ |
| Upload files ([#2][i2]) | ❌ |
[i2]: https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/2
......@@ -124,10 +122,10 @@ Readme will guide you on how to config.
| Messages list: load more on scroll | ✅ |
| Messages list: receive new messages via subscription | ✅ |
| Subscriptions list | ✅ |
| Segmented subscriptions list: Favorites | |
| Segmented subscriptions list: Unreads | |
| Segmented subscriptions list: DMs | |
| Segmented subscriptions list: Channels | |
| Segmented subscriptions list: Favorites | |
| Segmented subscriptions list: Unreads | |
| Segmented subscriptions list: DMs | |
| Segmented subscriptions list: Channels | |
| Subscriptions list: update user status via subscription | ✅ |
| Numbers os messages unread in the Subscriptions list | ✅ |
| Status change | ✅ |
......@@ -205,7 +203,7 @@ Readme will guide you on how to config.
| Localized in Portuguese (pt-BR) | ❌ |
| Localized in Russian | ✅ |
| Localized in English | ✅ |
| Full name setting | |
| Full name setting | |
| Read only rooms | ✅ |
| Typing status | ✅ |
| 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';
import renderer from 'react-test-renderer';
const date = new Date(2017, 10, 10, 10);
jest.mock('react-native-img-cache', () => { return { CachedImage: 'View' } });
const onPress = () => {};
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', () => {
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', () => {
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', () => {
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', () => {
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', () => {
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 {
minSdkVersion 19
targetSdkVersion 27
versionCode VERSIONCODE as Integer
versionName "1.1.1"
versionName "1.2"
ndk {
abiFilters "armeabi-v7a", "x86"
}
......
......@@ -79,5 +79,9 @@ export default {
},
Store_Last_Message: {
type: 'valueAsBoolean'
},
UI_Use_Real_Name: {
type: 'valueAsBoolean'
}
};
export const settingsUpdatedAt = new Date('2018-09-10');
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StyleSheet, Text, View, ViewPropTypes } from 'react-native';
import FastImage from 'react-native-fast-image';
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
......@@ -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 {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
style: ViewPropTypes.style,
baseUrl: PropTypes.string,
text: PropTypes.string,
avatar: PropTypes.string,
size: PropTypes.number,
......
import React from 'react';
import { ViewPropTypes, Image } from 'react-native';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
@connect(state => ({
baseUrl: state.settings.Site_Url
}))
export default class CustomEmoji extends React.Component {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
......
......@@ -10,9 +10,9 @@ import scrollPersistTaps from '../../utils/scrollPersistTaps';
const emojisPerRow = Platform.OS === 'ios' ? 8 : 9;
const renderEmoji = (emoji, size) => {
const renderEmoji = (emoji, size, baseUrl) => {
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 (
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
......@@ -25,6 +25,7 @@ const renderEmoji = (emoji, size) => {
@responsive
export default class EmojiCategory extends React.Component {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
emojis: PropTypes.any,
window: PropTypes.any,
onEmojiSelected: PropTypes.func,
......@@ -44,6 +45,7 @@ export default class EmojiCategory extends React.Component {
}
renderItem(emoji, size) {
const { baseUrl } = this.props;
return (
<TouchableOpacity
activeOpacity={0.7}
......@@ -51,7 +53,7 @@ export default class EmojiCategory extends React.Component {
onPress={() => this.props.onEmojiSelected(emoji)}
testID={`reaction-picker-${ emoji.isCustom ? emoji.content : emoji }`}
>
{renderEmoji(emoji, size)}
{renderEmoji(emoji, size, baseUrl)}
</TouchableOpacity>);
}
......
......@@ -19,6 +19,7 @@ const scrollProps = {
export default class EmojiPicker extends Component {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
onEmojiSelected: PropTypes.func,
tabEmojiStyle: PropTypes.object,
emojisPerRow: PropTypes.number,
......@@ -110,6 +111,7 @@ export default class EmojiPicker extends Component {
style={styles.categoryContainer}
size={this.props.emojisPerRow}
width={this.props.width}
baseUrl={this.props.baseUrl}
/>
);
}
......@@ -123,12 +125,14 @@ export default class EmojiPicker extends Component {
<ScrollableTabView
renderTabBar={() => <TabBar tabEmojiStyle={this.props.tabEmojiStyle} />}
contentProps={scrollProps}
style={styles.background}
>
{
categories.tabs.map((tab, i) => (
<ScrollView
key={tab.category}
tabLabel={tab.tabLabel}
style={styles.background}
{...scrollProps}
>
{this.renderCategory(tab.category, i)}
......
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
background: {
backgroundColor: '#fff'
},
container: {
flex: 1
},
......
import React from 'react';
import { View } from 'react-native';
import { KeyboardRegistry } from 'react-native-keyboard-input';
import { Provider } from 'react-redux';
import store from '../../lib/createStore';
import EmojiPicker from '../EmojiPicker';
import styles from './styles';
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) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
}
render() {
return (
<Provider store={store}>
<View style={styles.emojiKeyboardContainer} testID='messagebox-keyboard-emoji'>
<EmojiPicker onEmojiSelected={emoji => this.onEmojiSelected(emoji)} />
</View>
</Provider>
<View style={styles.emojiKeyboardContainer} testID='messagebox-keyboard-emoji'>
<EmojiPicker onEmojiSelected={emoji => this.onEmojiSelected(emoji)} baseUrl={this.baseUrl} />
</View>
);
}
}
......
......@@ -9,15 +9,17 @@ import Markdown from '../message/Markdown';
const styles = StyleSheet.create({
container: {
flexDirection: 'row'
flexDirection: 'row',
marginTop: 10,
backgroundColor: '#fff'
},
messageContainer: {
flex: 1,
marginHorizontal: 15,
marginHorizontal: 10,
backgroundColor: '#F3F4F5',
paddingHorizontal: 15,
paddingVertical: 10,
borderRadius: 2
borderRadius: 4
},
header: {
flexDirection: 'row',
......@@ -35,18 +37,23 @@ const styles = StyleSheet.create({
marginLeft: 5
},
close: {
marginRight: 15
marginRight: 10
}
});
@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 {
static propTypes = {
message: PropTypes.object.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 = () => {
......@@ -54,7 +61,9 @@ export default class ReplyPreview extends Component {
}
render() {
const { message, Message_TimeFormat } = this.props;
const {
message, Message_TimeFormat, customEmojis, baseUrl, username
} = this.props;
const time = moment(message.ts).format(Message_TimeFormat);
return (
<View style={styles.container}>
......@@ -63,9 +72,9 @@ export default class ReplyPreview extends Component {
<Text style={styles.username}>{message.u.username}</Text>
<Text style={styles.time}>{time}</Text>
</View>
<Markdown msg={message.msg} />
<Markdown msg={message.msg} customEmojis={customEmojis} baseUrl={baseUrl} username={username} />
</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>
);
}
......
......@@ -530,6 +530,7 @@ export default class MessageBox extends React.PureComponent {
text={item.username || item.name}
size={30}
type={item.username ? 'd' : 'c'}
baseUrl={this.props.baseUrl}
/>,
<Text key='mention-item-name'>{ item.username || item.name }</Text>
]
......@@ -556,11 +557,13 @@ export default class MessageBox extends React.PureComponent {
};
renderReplyPreview = () => {
const { replyMessage, replying, closeReply } = this.props;
const {
replyMessage, replying, closeReply, username
} = this.props;
if (!replying) {
return null;
}
return <ReplyPreview key='reply-preview' message={replyMessage} close={closeReply} />;
return <ReplyPreview key='reply-preview' message={replyMessage} close={closeReply} username={username} />;
};
renderFilesActions = () => {
......@@ -584,29 +587,30 @@ export default class MessageBox extends React.PureComponent {
return (
[
this.renderMentions(),
this.renderReplyPreview(),
<View
key='messagebox'
style={[styles.textArea, this.props.editing && styles.editing]}
testID='messagebox'
>
{this.leftButtons}
<TextInput
ref={component => this.component = component}
style={styles.textBoxInput}
returnKeyType='default'
keyboardType='twitter'
blurOnSubmit={false}
placeholder={I18n.t('New_Message')}
onChangeText={text => this.onChangeText(text)}
value={this.state.text}
underlineColorAndroid='transparent'
defaultValue=''
multiline
placeholderTextColor='#9EA2A8'
testID='messagebox-input'
/>
{this.rightButtons}
<View style={styles.composer} key='messagebox'>
{this.renderReplyPreview()}
<View
style={[styles.textArea, this.props.editing && styles.editing]}
testID='messagebox'
>
{this.leftButtons}
<TextInput
ref={component => this.component = component}
style={styles.textBoxInput}
returnKeyType='default'
keyboardType='twitter'
blurOnSubmit={false}
placeholder={I18n.t('New_Message')}
onChangeText={text => this.onChangeText(text)}
value={this.state.text}
underlineColorAndroid='transparent'
defaultValue=''
multiline
placeholderTextColor='#9EA2A8'
testID='messagebox-input'
/>
{this.rightButtons}
</View>
</View>
]
);
......
......@@ -11,13 +11,17 @@ export default StyleSheet.create({
borderTopColor: '#D8D8D8',
zIndex: 2
},
composer: {
backgroundColor: '#fff',
flexDirection: 'column',
borderTopColor: '#e1e5e8',
borderTopWidth: 1
},
textArea: {
flexDirection: 'row',
alignItems: 'center',
flexGrow: 0,
backgroundColor: '#fff',
borderTopColor: '#ECECEC',
borderTopWidth: 1
backgroundColor: '#fff'
},
textBoxInput: {
textAlignVertical: 'center',
......
......@@ -85,7 +85,8 @@ const keyExtractor = item => item.id;
server: state.login.user && state.login.user.server,
status: state.login.user && state.login.user.status,
username: state.login.user && state.login.user.username
}
},
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}), dispatch => ({
selectServerRequest: server => dispatch(selectServerRequest(server)),
logout: () => dispatch(logout()),
......@@ -93,6 +94,7 @@ const keyExtractor = item => item.id;
}))
export default class Sidebar extends Component {
static propTypes = {
baseUrl: PropTypes.string,
navigator: PropTypes.object,
server: PropTypes.string.isRequired,
selectServerRequest: PropTypes.func.isRequired,
......@@ -323,7 +325,7 @@ export default class Sidebar extends Component {
)
render() {
const { user, server } = this.props;
const { user, server, baseUrl } = this.props;
if (!user) {
return null;
}
......@@ -341,6 +343,7 @@ export default class Sidebar extends Component {
text={user.username}
size={30}
style={styles.avatar}
baseUrl={baseUrl}
/>
<View style={styles.headerTextContainer}>
<View style={styles.headerUsername}>
......
import React from 'react';
import PropTypes from 'prop-types';
import { View, StyleSheet, TouchableOpacity, Text, Easing } from 'react-native';
import { View, StyleSheet, TouchableOpacity, Text, Easing, Image } from 'react-native';
import Video from 'react-native-video';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Slider from 'react-native-slider';
import { connect } from 'react-redux';
import moment from 'moment';
import Markdown from './Markdown';
const styles = StyleSheet.create({
audioContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
height: 50,
margin: 5,
backgroundColor: '#eee',
borderRadius: 6
height: 56,
backgroundColor: '#f7f8fa',
borderRadius: 4,