[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
This commit is contained in:
Diego Mello 2018-09-11 13:32:52 -03:00 committed by GitHub
parent a2ec1e7279
commit 96d0b1fcbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
121 changed files with 18429 additions and 793 deletions

View File

@ -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] 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] 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] Rooms list layout ([#395][i395]) | ❌ |
| [NEW] Contextual bar layout ([#402][i402]) | ❌ |
| [NEW] Login/Register/Forgot Password layout ([#400][i400]) | ❌ |
| [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 | ✅ |

1
__mocks__/fileMock.js Normal file
View File

@ -0,0 +1 @@
module.exports = 'test-file-stub';

7
__mocks__/react-native-i18n.js vendored Normal file
View File

@ -0,0 +1,7 @@
// @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;

5
__mocks__/react-native-safari-view.js vendored Normal file
View File

@ -0,0 +1,5 @@
export default function() {
return {
show: () => {}
};
}

View File

@ -0,0 +1 @@
export default () => 'Video';

1
__mocks__/react-native-video.js vendored Normal file
View File

@ -0,0 +1 @@
export default () => 'Video';

View File

@ -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();
});

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

View File

@ -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');

View File

@ -1,6 +1,5 @@
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,

View File

@ -1,11 +1,7 @@
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,

View File

@ -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>);
}

View File

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

View File

@ -1,6 +1,9 @@
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
background: {
backgroundColor: '#fff'
},
container: {
flex: 1
},

View File

@ -1,22 +1,25 @@
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>
);
}
}

View File

@ -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>
);
}

View File

@ -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>
]
);

View File

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

View File

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

View File

@ -1,74 +1,55 @@
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,
marginBottom: 10
},
playPauseButton: {
width: 50,
width: 56,
alignItems: 'center',
backgroundColor: 'transparent',
borderRightColor: '#ccc',
borderRightWidth: 1
},
playPauseIcon: {
color: '#ccc',
backgroundColor: 'transparent'
},
progressContainer: {
playPauseImage: {
width: 30,
height: 30
},
slider: {
flex: 1,
justifyContent: 'center',
height: '100%',
marginHorizontal: 10
},
label: {
color: '#888',
fontSize: 10
},
currentTime: {
position: 'absolute',
left: 0,
bottom: 2
marginRight: 10
},
duration: {
position: 'absolute',
right: 0,
bottom: 2
marginRight: 16,
fontSize: 14,
fontWeight: '500',
color: '#54585e'
},
thumbStyle: {
width: 12,
height: 12
}
});
const formatTime = (t = 0, duration = 0) => {
const time = Math.min(
Math.max(t, 0),
duration
);
const formattedMinutes = Math.floor(time / 60).toFixed(0).padStart(2, 0);
const formattedSeconds = Math.floor(time % 60).toFixed(0).padStart(2, 0);
return `${ formattedMinutes }:${ formattedSeconds }`;
};
const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss');
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class Audio extends React.PureComponent {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired
user: PropTypes.object.isRequired,
customEmojis: PropTypes.object.isRequired
}
constructor(props) {
@ -90,7 +71,7 @@ export default class Audio extends React.PureComponent {
}
onProgress(data) {
if (data.currentTime < this.state.duration) {
if (data.currentTime <= this.state.duration) {
this.setState({ currentTime: data.currentTime });
}
}
@ -102,10 +83,6 @@ export default class Audio extends React.PureComponent {
});
}
getCurrentTime() {
return formatTime(this.state.currentTime, this.state.duration);
}
getDuration() {
return formatTime(this.state.duration);
}
@ -116,7 +93,10 @@ export default class Audio extends React.PureComponent {
render() {
const { uri, paused } = this.state;
const { description } = this.props.file;
const {
user, baseUrl, customEmojis, file
} = this.props;
const { description } = file;
return (
[
<View key='audio' style={styles.audioContainer}>
@ -136,29 +116,30 @@ export default class Audio extends React.PureComponent {
onPress={() => this.togglePlayPause()}
>
{
paused ? <Icon name='play-arrow' size={50} style={styles.playPauseIcon} />
: <Icon name='pause' size={47} style={styles.playPauseIcon} />
paused ?
<Image source={{ uri: 'play' }} style={styles.playPauseImage} /> :
<Image source={{ uri: 'pause' }} style={styles.playPauseImage} />
}
</TouchableOpacity>
<View style={styles.progressContainer}>
<Text style={[styles.label, styles.currentTime]}>{this.getCurrentTime()}</Text>
<Text style={[styles.label, styles.duration]}>{this.getDuration()}</Text>
<Slider
value={this.state.currentTime}
maximumValue={this.state.duration}
minimumValue={0}
animateTransitions
animationConfig={{
duration: 250,
easing: Easing.linear,
delay: 0
}}
thumbTintColor='#ccc'
onValueChange={value => this.setState({ currentTime: value })}
/>
</View>
<Slider
style={styles.slider}
value={this.state.currentTime}
maximumValue={this.state.duration}
minimumValue={0}
animateTransitions
animationConfig={{
duration: 250,
easing: Easing.linear,
delay: 0
}}
thumbTintColor='#1d74f5'
minimumTrackTintColor='#1d74f5'
onValueChange={value => this.setState({ currentTime: value })}
thumbStyle={styles.thumbStyle}
/>
<Text style={styles.duration}>{this.getDuration()}</Text>
</View>,
<Markdown key='description' msg={description} />
<Markdown key='description' msg={description} baseUrl={baseUrl} customEmojis={customEmojis} username={user.username} />
]
);
}

View File

@ -2,28 +2,28 @@ import React from 'react';
import { Text, ViewPropTypes } from 'react-native';
import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
import { connect } from 'react-redux';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
@connect(state => ({
customEmojis: state.customEmojis
}))
export default class Emoji extends React.PureComponent {
static propTypes = {
content: PropTypes.string,
content: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
standardEmojiStyle: Text.propTypes.style,
customEmojiStyle: ViewPropTypes.style,
customEmojis: PropTypes.object.isRequired
customEmojis: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
])
};
render() {
const {
content, standardEmojiStyle, customEmojiStyle, customEmojis
content, standardEmojiStyle, customEmojiStyle, customEmojis, baseUrl
} = this.props;
const parsedContent = content.replace(/^:|:$/g, '');
const emojiExtension = customEmojis[parsedContent];
if (emojiExtension) {
const emoji = { extension: emojiExtension, content: parsedContent };
return <CustomEmoji key={content} style={customEmojiStyle} emoji={emoji} />;
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
}
return <Text style={standardEmojiStyle}>{ emojify(`${ content }`, { output: 'unicode' }) }</Text>;
}

View File

@ -2,29 +2,30 @@ import PropTypes from 'prop-types';
import React from 'react';
import FastImage from 'react-native-fast-image';
import { TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import PhotoModal from './PhotoModal';
import Markdown from './Markdown';
import styles from './styles';
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class extends React.PureComponent {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
customEmojis: PropTypes.object
customEmojis: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
])
}
state = { modalVisible: false };
getDescription() {
const { file, customEmojis } = this.props;
const {
file, customEmojis, baseUrl, user
} = this.props;
if (file.description) {
return <Markdown msg={file.description} customEmojis={customEmojis} />;
return <Markdown msg={file.description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} />;
}
}
@ -47,13 +48,14 @@ export default class extends React.PureComponent {
<FastImage
style={styles.image}
source={{ uri: encodeURI(img) }}
resizeMode={FastImage.resizeMode.cover}
/>
{this.getDescription()}
</TouchableOpacity>,
<PhotoModal
key='modal'
title={this.props.file.title}
description={this.props.file.description}
title={file.title}
description={file.description}
image={img}
isVisible={this.state.modalVisible}
onClose={() => this.setState({ modalVisible: false })}

View File

@ -1,8 +1,7 @@
import React from 'react';
import { Text, Platform, Image } from 'react-native';
import { Text, Image } from 'react-native';
import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
import { connect } from 'react-redux';
import MarkdownRenderer, { PluginContainer } from 'react-native-markdown-renderer';
import MarkdownFlowdock from 'markdown-it-flowdock';
import styles from './styles';
@ -16,16 +15,13 @@ const formatText = text =>
(match, url, title) => `[${ title }](${ url })`
);
@connect(state => ({
customEmojis: state.customEmojis
}))
export default class Markdown extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.msg !== this.props.msg;
}
render() {
const {
msg, customEmojis, style, rules
msg, customEmojis, style, rules, baseUrl, username, edited
} = this.props;
if (!msg) {
return null;
@ -36,21 +32,38 @@ export default class Markdown extends React.Component {
return (
<MarkdownRenderer
rules={{
...Platform.OS === 'android' ? {} : {
paragraph: (node, children) => (
<Text key={node.key} style={styles.paragraph}>
{children}
</Text>
)
},
mention: node => (
<Text key={node.key} onPress={() => alert(`Username @${ node.content }`)} style={styles.mention}>
@{node.content}
paragraph: (node, children) => (
<Text key={node.key} style={styles.paragraph}>
{children}{edited ? <Text style={styles.edited}> (edited)</Text> : null}
</Text>
),
mention: (node) => {
const { content, key } = node;
let mentionStyle = styles.mention;
if (content === 'all' || content === 'here') {
mentionStyle = {
...mentionStyle,
...styles.mentionAll
};
} else if (content === username) {
mentionStyle = {
...mentionStyle,
...styles.mentionLoggedUser
};
}
return (
<Text
key={key}
onPress={() => alert(`Username ${ content }`)}
style={mentionStyle}
>
&nbsp;{content}&nbsp;
</Text>
);
},
hashtag: node => (
<Text key={node.key} onPress={() => alert(`Room #${ node.content }`)} style={styles.mention}>
#{node.content}
&nbsp;#{node.content}&nbsp;
</Text>
),
emoji: (node) => {
@ -59,7 +72,7 @@ export default class Markdown extends React.Component {
const emojiExtension = customEmojis[content];
if (emojiExtension) {
const emoji = { extension: emojiExtension, content };
return <CustomEmoji key={node.key} style={styles.customEmoji} emoji={emoji} />;
return <CustomEmoji key={node.key} baseUrl={baseUrl} style={styles.customEmoji} emoji={emoji} />;
}
return <Text key={node.key}>:{content}:</Text>;
}
@ -74,6 +87,11 @@ export default class Markdown extends React.Component {
}}
style={{
paragraph: styles.paragraph,
text: {
color: '#0C0D0F',
fontSize: 16,
letterSpacing: 0.1
},
codeInline: {
borderWidth: 1,
borderColor: '#CCCCCC',
@ -81,6 +99,9 @@ export default class Markdown extends React.Component {
padding: 2,
borderRadius: 4
},
link: {
color: '#1D74F5'
},
...style
}}
plugins={[
@ -95,7 +116,10 @@ export default class Markdown extends React.Component {
Markdown.propTypes = {
msg: PropTypes.string,
customEmojis: PropTypes.object,
username: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.object.isRequired,
style: PropTypes.any,
rules: PropTypes.object
rules: PropTypes.object,
edited: PropTypes.bool
};

View File

@ -0,0 +1,351 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { View, Text, TouchableOpacity, ViewPropTypes, Image as ImageRN } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment';
import { KeyboardUtils } from 'react-native-keyboard-input';
import Image from './Image';
import User from './User';
import Avatar from '../Avatar';
import Audio from './Audio';
import Video from './Video';
import Markdown from './Markdown';
import Url from './Url';
import Reply from './Reply';
import ReactionsModal from './ReactionsModal';
import Emoji from './Emoji';
import styles from './styles';
import Touch from '../../utils/touch';
import I18n from '../../i18n';
import messagesStatus from '../../constants/messagesStatus';
const SYSTEM_MESSAGES = [
'r',
'au',
'ru',
'ul',
'uj',
'rm',
'user-muted',
'user-unmuted',
'message_pinned',
'subscription-role-added',
'subscription-role-removed',
'room_changed_description',
'room_changed_announcement',
'room_changed_topic',
'room_changed_privacy'
];
const getInfoMessage = ({
type, role, msg, user
}) => {
const { username } = user;
if (type === 'rm') {
return I18n.t('Message_removed');
} else if (type === 'uj') {
return I18n.t('Has_joined_the_channel');
} else if (type === 'r') {
return I18n.t('Room_name_changed', { name: msg, userBy: username });
} else if (type === 'message_pinned') {
return I18n.t('Message_pinned');
} else if (type === 'ul') {
return I18n.t('Has_left_the_channel');
} else if (type === 'ru') {
return I18n.t('User_removed_by', { userRemoved: msg, userBy: username });
} else if (type === 'au') {
return I18n.t('User_added_by', { userAdded: msg, userBy: username });
} else if (type === 'user-muted') {
return I18n.t('User_muted_by', { userMuted: msg, userBy: username });
} else if (type === 'user-unmuted') {
return I18n.t('User_unmuted_by', { userUnmuted: msg, userBy: username });
} else if (type === 'subscription-role-added') {
return `${ msg } was set ${ role } by ${ username }`;
} else if (type === 'subscription-role-removed') {
return `${ msg } is no longer ${ role } by ${ username }`;
} else if (type === 'room_changed_description') {
return I18n.t('Room_changed_description', { description: msg, userBy: username });
} else if (type === 'room_changed_announcement') {
return I18n.t('Room_changed_announcement', { announcement: msg, userBy: username });
} else if (type === 'room_changed_topic') {
return I18n.t('Room_changed_topic', { topic: msg, userBy: username });
} else if (type === 'room_changed_privacy') {
return I18n.t('Room_changed_privacy', { type: msg, userBy: username });
}
return '';
};
export default class Message extends PureComponent {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.object.isRequired,
timeFormat: PropTypes.string.isRequired,
msg: PropTypes.string,
user: PropTypes.shape({
id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired
}),
author: PropTypes.shape({
_id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
name: PropTypes.string
}),
status: PropTypes.any,
reactions: PropTypes.any,
editing: PropTypes.bool,
style: ViewPropTypes.style,
archived: PropTypes.bool,
broadcast: PropTypes.bool,
reactionsModal: PropTypes.bool,
type: PropTypes.string,
header: PropTypes.bool,
avatar: PropTypes.string,
alias: PropTypes.string,
ts: PropTypes.instanceOf(Date),
edited: PropTypes.bool,
attachments: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
]),
urls: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
]),
useRealName: PropTypes.bool,
// methods
closeReactions: PropTypes.func,
onErrorPress: PropTypes.func,
onLongPress: PropTypes.func,
onReactionLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
replyBroadcast: PropTypes.func,
toggleReactionPicker: PropTypes.func
}
static defaultProps = {
archived: false,
broadcast: false,
attachments: [],
urls: [],
reactions: [],
onLongPress: () => {}
}
onPress = () => {
KeyboardUtils.dismiss();
}
isInfoMessage() {
return SYSTEM_MESSAGES.includes(this.props.type);
}
isOwn = () => this.props.author._id === this.props.user.id;
isDeleted() {
return this.props.type === 'rm';
}
isTemp() {
return this.props.status === messagesStatus.TEMP || this.props.status === messagesStatus.ERROR;
}
hasError() {
return this.props.status === messagesStatus.ERROR;
}
renderAvatar = () => {
const {
header, avatar, author, baseUrl
} = this.props;
if (header) {
return (
<Avatar
style={styles.avatar}
text={avatar ? '' : author.username}
size={36}
borderRadius={4}
avatar={avatar}
baseUrl={baseUrl}
/>
);
}
return null;
}
renderUsername = () => {
const {
header, timeFormat, author, alias, ts, useRealName
} = this.props;
if (header) {
return (
<User
onPress={this._onPress}
timeFormat={timeFormat}
username={(useRealName && author.name) || author.username}
alias={alias}
ts={ts}
temp={this.isTemp()}
/>
);
}
return null;
}
renderContent() {
if (this.isInfoMessage()) {
return <Text style={styles.textInfo}>{getInfoMessage({ ...this.props })}</Text>;
}
const {
customEmojis, msg, baseUrl, user, edited
} = this.props;
return <Markdown msg={msg} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} edited={edited} />;
}
renderAttachment() {
const { attachments, timeFormat } = this.props;
if (attachments.length === 0) {
return null;
}
return attachments.map((file, index) => {
const { user, baseUrl, customEmojis } = this.props;
if (file.image_url) {
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
}
if (file.audio_url) {
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
}
if (file.video_url) {
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
}
// eslint-disable-next-line react/no-array-index-key
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
});
}
renderUrl = () => {
const { urls } = this.props;
if (urls.length === 0) {
return null;
}
return urls.map((url, index) => (
<Url url={url} key={url.url} index={index} />
));
}
renderError = () => {
if (!this.hasError()) {
return null;
}
return <Icon name='error-outline' color='red' size={20} style={styles.errorIcon} onPress={this.props.onErrorPress} />;
}
renderReaction = (reaction) => {
const reacted = reaction.usernames.findIndex(item => item.value === this.props.user.username) !== -1;
const reactedContainerStyle = reacted && styles.reactedContainer;
return (
<TouchableOpacity
onPress={() => this.props.onReactionPress(reaction.emoji)}
onLongPress={this.props.onReactionLongPress}
key={reaction.emoji}
testID={`message-reaction-${ reaction.emoji }`}
>
<View style={[styles.reactionContainer, reactedContainerStyle]}>
<Emoji
content={reaction.emoji}
customEmojis={this.props.customEmojis}
standardEmojiStyle={styles.reactionEmoji}
customEmojiStyle={styles.reactionCustomEmoji}
baseUrl={this.props.baseUrl}
/>
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
</View>
</TouchableOpacity>
);
}
renderReactions() {
const { reactions } = this.props;
if (reactions.length === 0) {
return null;
}
return (
<View style={styles.reactionsContainer}>
{reactions.map(this.renderReaction)}
<TouchableOpacity
onPress={this.props.toggleReactionPicker}
key='message-add-reaction'
testID='message-add-reaction'
style={styles.reactionContainer}
>
<ImageRN source={{ uri: 'add_reaction' }} style={styles.addReaction} />
</TouchableOpacity>
</View>
);
}
renderBroadcastReply() {
if (this.props.broadcast && !this.isOwn()) {
return (
<Touch
onPress={this.props.replyBroadcast}
style={styles.broadcastButton}
>
<View style={styles.broadcastButtonContainer}>
<ImageRN source={{ uri: 'reply' }} style={styles.broadcastButtonIcon} />
<Text style={styles.broadcastButtonText}>Reply</Text>
</View>
</Touch>
);
}
return null;
}
render() {
const {
editing, style, header, archived, onLongPress, reactionsModal, closeReactions, msg, ts, reactions, author, user, timeFormat, customEmojis, baseUrl
} = this.props;
const accessibilityLabel = I18n.t('Message_accessibility', { user: author.username, time: moment(ts).format(timeFormat), message: msg });
return (
<Touch
onPress={this.onPress}
onLongPress={onLongPress}
disabled={this.isInfoMessage() || this.hasError() || archived}
accessibilityLabel={accessibilityLabel}
style={[styles.container, header && { marginBottom: 10 }]}
>
<View style={[styles.message, editing && styles.editing, style]}>
<View style={styles.flex}>
{this.renderError()}
{this.renderAvatar()}
<View style={[styles.messageContent, header && styles.hasHeader, this.isTemp() && styles.temp]}>
{this.renderUsername()}
{this.renderContent()}
{this.renderAttachment()}
{this.renderUrl()}
{this.renderReactions()}
{this.renderBroadcastReply()}
</View>
</View>
{reactionsModal ?
<ReactionsModal
isVisible={reactionsModal}
reactions={reactions}
user={user}
customEmojis={customEmojis}
baseUrl={baseUrl}
close={closeReactions}
/>
: null
}
</View>
</Touch>
);
}
}

View File

@ -3,7 +3,6 @@ import { View, Text, TouchableWithoutFeedback, FlatList, StyleSheet } from 'reac
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { connect } from 'react-redux';
import Emoji from './Emoji';
import I18n from '../../i18n';
@ -55,16 +54,17 @@ const styles = StyleSheet.create({
const standardEmojiStyle = { fontSize: 20 };
const customEmojiStyle = { width: 20, height: 20 };
@connect(state => ({
customEmojis: state.customEmojis
}))
export default class ReactionsModal extends React.PureComponent {
static propTypes = {
isVisible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
close: PropTypes.func.isRequired,
reactions: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
customEmojis: PropTypes.object.isRequired
baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
])
}
renderItem = (item) => {
const count = item.usernames.length;
@ -83,6 +83,7 @@ export default class ReactionsModal extends React.PureComponent {
standardEmojiStyle={standardEmojiStyle}
customEmojiStyle={customEmojiStyle}
customEmojis={this.props.customEmojis}
baseUrl={this.props.baseUrl}
/>
</View>
<View style={styles.peopleItemContainer}>
@ -97,22 +98,22 @@ export default class ReactionsModal extends React.PureComponent {
render() {
const {
isVisible, onClose, reactions
isVisible, close, reactions
} = this.props;
return (
<Modal
isVisible={isVisible}
onBackdropPress={onClose}
onBackButtonPress={onClose}
onBackdropPress={close}
onBackButtonPress={close}
backdropOpacity={0.9}
>
<TouchableWithoutFeedback onPress={onClose}>
<TouchableWithoutFeedback onPress={close}>
<View style={styles.titleContainer}>
<Icon
style={styles.closeButton}
name='close'
size={20}
onPress={onClose}
onPress={close}
/>
<Text style={styles.title}>{I18n.t('Reactions')}</Text>
</View>

View File

@ -1,39 +1,41 @@
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import moment from 'moment';
import Markdown from './Markdown';
import QuoteMark from './QuoteMark';
import Avatar from '../Avatar';
import openLink from '../../utils/openLink';
import Touch from '../../utils/touch';
const styles = StyleSheet.create({
button: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginTop: 2,
marginTop: 15,
alignSelf: 'flex-end'
},
attachmentContainer: {
flex: 1,
flexDirection: 'column'
borderRadius: 4,
flexDirection: 'column',
backgroundColor: '#f3f4f5',
padding: 15
},
authorContainer: {
flexDirection: 'row',
alignItems: 'center'
},
author: {
fontWeight: 'bold',
marginHorizontal: 5,
flex: 1
color: '#1d74f5',
fontSize: 18,
fontWeight: '500',
marginRight: 10
},
time: {
fontSize: 10,
fontSize: 14,
fontWeight: 'normal',
color: '#888',
color: '#9ea2a8',
marginLeft: 5
},
fieldsContainer: {
@ -47,6 +49,9 @@ const styles = StyleSheet.create({
},
fieldTitle: {
fontWeight: 'bold'
},
marginTop: {
marginTop: 4
}
});
@ -58,23 +63,13 @@ const onPress = (attachment) => {
openLink(attachment.title_link || attachment.author_link);
};
const Reply = ({ attachment, timeFormat }) => {
const Reply = ({
attachment, timeFormat, baseUrl, customEmojis, user, index
}) => {
if (!attachment) {
return null;
}
const renderAvatar = () => {
if (!attachment.author_icon && !attachment.author_name) {
return null;
}
return (
<Avatar
text={attachment.author_name}
size={16}
/>
);
};
const renderAuthor = () => (
attachment.author_name ? <Text style={styles.author}>{attachment.author_name}</Text> : null
);
@ -90,7 +85,6 @@ const Reply = ({ attachment, timeFormat }) => {
}
return (
<View style={styles.authorContainer}>
{renderAvatar()}
{renderAuthor()}
{renderTime()}
</View>
@ -98,7 +92,7 @@ const Reply = ({ attachment, timeFormat }) => {
};
const renderText = () => (
attachment.text ? <Markdown msg={attachment.text} /> : null
attachment.text ? <Markdown msg={attachment.text} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} /> : null
);
const renderFields = () => {
@ -119,28 +113,26 @@ const Reply = ({ attachment, timeFormat }) => {
};
return (
<TouchableOpacity
<Touch
onPress={() => onPress(attachment)}
style={styles.button}
style={[styles.button, index > 0 && styles.marginTop]}
>
<QuoteMark color={attachment.color} />
<View style={styles.attachmentContainer}>
{renderTitle()}
{renderText()}
{renderFields()}
{attachment.attachments ?
attachment.attachments
.map(attach => <Reply key={attach.text} attachment={attach} timeFormat={timeFormat} />)
: null
}
</View>
</TouchableOpacity>
</Touch>
);
};
Reply.propTypes = {
attachment: PropTypes.object.isRequired,
timeFormat: PropTypes.string.isRequired
timeFormat: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
index: PropTypes.number
};
export default Reply;

View File

@ -1,67 +1,82 @@
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Image } from 'react-native';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import QuoteMark from './QuoteMark';
import openLink from '../../utils/openLink';
import Touch from '../../utils/touch';
const styles = StyleSheet.create({
button: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginVertical: 2
marginTop: 10
},
image: {
height: 80,
width: 80,
resizeMode: 'cover',
borderRadius: 6
container: {
flex: 1,
flexDirection: 'column',
borderRadius: 4,
backgroundColor: '#F3F4F5'
},
textContainer: {
flex: 1,
height: '100%',
flexDirection: 'column',
padding: 4,
padding: 15,
justifyContent: 'flex-start',
alignItems: 'flex-start'
},
title: {
fontWeight: 'bold',
fontSize: 12
fontWeight: '500',
color: '#1D74F5',
fontSize: 16,
marginTop: 5
},
description: {
fontSize: 12
marginTop: 5,
fontSize: 16,
color: '#0C0D0F'
},
url: {
fontSize: 15,
fontWeight: '500',
color: '#9EA2A8'
},
marginTop: {
marginTop: 4
},
image: {
width: '100%',
height: 150,
borderTopLeftRadius: 4,
borderTopRightRadius: 4
}
});
const onPress = (url) => {
openLink(url);
};
const Url = ({ url }) => {
const Url = ({ url, index }) => {
if (!url) {
return null;
}
return (
<TouchableOpacity onPress={() => onPress(url.url)} style={styles.button}>
<QuoteMark />
{url.image ?
<Image
style={styles.image}
source={{ uri: encodeURI(url.image) }}
/>
: null
}
<View style={styles.textContainer}>
<Text style={styles.title}>{url.title}</Text>
<Text style={styles.description} numberOfLines={1}>{url.description}</Text>
<Touch onPress={() => onPress(url.url)} style={[styles.button, index > 0 && styles.marginTop]}>
<View style={styles.container}>
{/* <View style={{ backgroundColor: 'red', height: 150, borderTopLeftRadius: 5, borderTopRightRadius: 5 }}>
{url.image ? <FastImage source={{ uri: url.image }} /> : null}
</View> */}
{url.image ? <FastImage source={{ uri: url.image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} /> : null}
<View style={styles.textContainer}>
<Text style={styles.url} numberOfLines={1}>{url.url}</Text>
<Text style={styles.title} numberOfLines={2}>{url.title}</Text>
<Text style={styles.description} numberOfLines={2}>{url.description}</Text>
</View>
</View>
</TouchableOpacity>
</Touch>
);
};
Url.propTypes = {
url: PropTypes.object.isRequired
url: PropTypes.object.isRequired,
index: PropTypes.number
};
export default Url;

View File

@ -2,14 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, StyleSheet } from 'react-native';
import moment from 'moment';
import Icon from 'react-native-vector-icons/FontAwesome';
import Avatar from '../Avatar';
const styles = StyleSheet.create({
username: {
color: '#000',
fontWeight: '400',
fontSize: 14
color: '#0C0D0F',
fontWeight: '600',
fontSize: 16,
lineHeight: 22
},
usernameView: {
flexDirection: 'row',
@ -17,67 +16,50 @@ const styles = StyleSheet.create({
marginBottom: 2
},
alias: {
fontSize: 10,
color: '#888',
paddingLeft: 5
fontSize: 14,
color: '#9EA2A8',
paddingLeft: 6,
lineHeight: 16
},
time: {
fontSize: 10,
color: '#888',
paddingLeft: 5,
fontWeight: '400'
},
edited: {
marginLeft: 5,
flexDirection: 'row',
alignItems: 'center'
fontSize: 14,
color: '#9EA2A8',
paddingLeft: 10,
fontWeight: '300',
lineHeight: 16
}
});
export default class User extends React.PureComponent {
static propTypes = {
item: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
username: PropTypes.string,
alias: PropTypes.string,
ts: PropTypes.instanceOf(Date),
temp: PropTypes.bool,
onPress: PropTypes.func
}
renderEdited = (item) => {
if (!item.editedBy) {
return null;
}
return (
<View style={styles.edited}>
<Icon name='pencil-square-o' color='#888' size={10} />
<Avatar
style={{ marginLeft: 5 }}
text={item.editedBy.username}
size={20}
avatar={item.avatar}
/>
</View>
);
}
render() {
const { item } = this.props;
const {
username, alias, ts, temp
} = this.props;
const extraStyle = {};
if (item.temp) {
if (temp) {
extraStyle.opacity = 0.3;
}
const username = item.alias || item.u.username;
const aliasUsername = item.alias ? (<Text style={styles.alias}>@{item.u.username}</Text>) : null;
const time = moment(item.ts).format(this.props.Message_TimeFormat);
const aliasUsername = alias ? (<Text style={styles.alias}>@{username}</Text>) : null;
const time = moment(ts).format(this.props.timeFormat);
return (
<View style={styles.usernameView}>
<Text onPress={this.props.onPress} style={styles.username}>
{username}
{alias || username}
</Text>
{aliasUsername}
<Text style={styles.time}>{time}</Text>
{this.renderEdited(item)}
</View>
);
}

View File

@ -1,9 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, TouchableOpacity, Image, Platform } from 'react-native';
import { StyleSheet, TouchableOpacity, Image, Platform, View } from 'react-native';
import Modal from 'react-native-modal';
import VideoPlayer from 'react-native-video-controls';
import { connect } from 'react-redux';
import Markdown from './Markdown';
import openLink from '../../utils/openLink';
@ -11,31 +10,31 @@ const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(Platform.OS === 'io
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
const styles = StyleSheet.create({
container: {
button: {
flex: 1,
height: 100,
margin: 5
borderRadius: 4,
height: 150,
backgroundColor: '#1f2329',
marginBottom: 10,
alignItems: 'center',
justifyContent: 'center'
},
modal: {
margin: 0,
backgroundColor: '#000'
},
image: {
flex: 1,
width: null,
height: null,
resizeMode: 'contain'
width: 54,
height: 54
}
});
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class Video extends React.PureComponent {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired
user: PropTypes.object.isRequired,
customEmojis: PropTypes.object.isRequired
}
state = { isVisible: false };
@ -62,19 +61,21 @@ export default class Video extends React.PureComponent {
render() {
const { isVisible } = this.state;
const { description } = this.props.file;
const { baseUrl, user, customEmojis } = this.props;
return (
[
<TouchableOpacity
key='button'
style={styles.container}
onPress={() => this.open()}
>
<Image
source={require('../../static/images/logo.png')}
style={styles.image}
/>
<Markdown msg={description} />
</TouchableOpacity>,
<View key='button'>
<TouchableOpacity
style={styles.button}
onPress={() => this.open()}
>
<Image
source={{ uri: 'play_video' }}
style={styles.image}
/>
</TouchableOpacity>
<Markdown msg={description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} />
</View>,
<Modal
key='modal'
isVisible={isVisible}

View File

@ -1,120 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, TouchableOpacity, Vibration, ViewPropTypes } from 'react-native';
import { Vibration, ViewPropTypes } from 'react-native';
import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment';
import equal from 'deep-equal';
import { KeyboardUtils } from 'react-native-keyboard-input';
import Image from './Image';
import User from './User';
import Avatar from '../Avatar';
import Audio from './Audio';
import Video from './Video';
import Markdown from './Markdown';
import Url from './Url';
import Reply from './Reply';
import ReactionsModal from './ReactionsModal';
import Emoji from './Emoji';
import styles from './styles';
import { actionsShow, errorActionsShow, toggleReactionPicker, replyBroadcast } from '../../actions/messages';
import messagesStatus from '../../constants/messagesStatus';
import Touch from '../../utils/touch';
import I18n from '../../i18n';
const SYSTEM_MESSAGES = [
'r',
'au',
'ru',
'ul',
'uj',
'rm',
'user-muted',
'user-unmuted',
'message_pinned',
'subscription-role-added',
'subscription-role-removed',
'room_changed_description',
'room_changed_announcement',
'room_changed_topic',
'room_changed_privacy'
];
const getInfoMessage = ({
t, role, msg, u
}) => {
if (t === 'rm') {
return I18n.t('Message_removed');
} else if (t === 'uj') {
return I18n.t('Has_joined_the_channel');
} else if (t === 'r') {
return I18n.t('Room_name_changed', { name: msg, userBy: u.username });
} else if (t === 'message_pinned') {
return I18n.t('Message_pinned');
} else if (t === 'ul') {
return I18n.t('Has_left_the_channel');
} else if (t === 'ru') {
return I18n.t('User_removed_by', { userRemoved: msg, userBy: u.username });
} else if (t === 'au') {
return I18n.t('User_added_by', { userAdded: msg, userBy: u.username });
} else if (t === 'user-muted') {
return I18n.t('User_muted_by', { userMuted: msg, userBy: u.username });
} else if (t === 'user-unmuted') {
return I18n.t('User_unmuted_by', { userUnmuted: msg, userBy: u.username });
} else if (t === 'subscription-role-added') {
return `${ msg } was set ${ role } by ${ u.username }`;
} else if (t === 'subscription-role-removed') {
return `${ msg } is no longer ${ role } by ${ u.username }`;
} else if (t === 'room_changed_description') {
return I18n.t('Room_changed_description', { description: msg, userBy: u.username });
} else if (t === 'room_changed_announcement') {
return I18n.t('Room_changed_announcement', { announcement: msg, userBy: u.username });
} else if (t === 'room_changed_topic') {
return I18n.t('Room_changed_topic', { topic: msg, userBy: u.username });
} else if (t === 'room_changed_privacy') {
return I18n.t('Room_changed_privacy', { type: msg, userBy: u.username });
}
return '';
};
import Message from './Message';
import { errorActionsShow, toggleReactionPicker, replyBroadcast } from '../../actions/messages';
@connect(state => ({
message: state.messages.message,
editing: state.messages.editing,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
customEmojis: state.customEmojis,
editing: state.messages.editing,
Message_GroupingPeriod: state.settings.Message_GroupingPeriod,
Message_TimeFormat: state.settings.Message_TimeFormat,
Message_GroupingPeriod: state.settings.Message_GroupingPeriod
message: state.messages.message,
useRealName: state.settings.UI_Use_Real_Name
}), dispatch => ({
actionsShow: actionMessage => dispatch(actionsShow(actionMessage)),
errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)),
toggleReactionPicker: message => dispatch(toggleReactionPicker(message)),
replyBroadcast: message => dispatch(replyBroadcast(message))
replyBroadcast: message => dispatch(replyBroadcast(message)),
toggleReactionPicker: message => dispatch(toggleReactionPicker(message))
}))
export default class Message extends React.Component {
export default class MessageContainer extends React.Component {
static propTypes = {
status: PropTypes.any,
item: PropTypes.object.isRequired,
reactions: PropTypes.any.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
Message_GroupingPeriod: PropTypes.number.isRequired,
customTimeFormat: PropTypes.string,
message: PropTypes.object.isRequired,
user: PropTypes.shape({
id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired
}),
editing: PropTypes.bool,
errorActionsShow: PropTypes.func,
toggleReactionPicker: PropTypes.func,
replyBroadcast: PropTypes.func,
onReactionPress: PropTypes.func,
customTimeFormat: PropTypes.string,
style: ViewPropTypes.style,
onLongPress: PropTypes.func,
_updatedAt: PropTypes.instanceOf(Date),
status: PropTypes.number,
archived: PropTypes.bool,
broadcast: PropTypes.bool,
previousItem: PropTypes.object
previousItem: PropTypes.object,
_updatedAt: PropTypes.instanceOf(Date),
// redux
baseUrl: PropTypes.string,
customEmojis: PropTypes.object,
editing: PropTypes.bool,
Message_GroupingPeriod: PropTypes.number,
Message_TimeFormat: PropTypes.string,
message: PropTypes.object,
useRealName: PropTypes.bool,
// methods - props
onLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
// methods - redux
errorActionsShow: PropTypes.func,
replyBroadcast: PropTypes.func,
toggleReactionPicker: PropTypes.func
}
static defaultProps = {
@ -127,7 +63,7 @@ export default class Message extends React.Component {
constructor(props) {
super(props);
this.state = { reactionsModal: false };
this.onClose = this.onClose.bind(this);
this.closeReactions = this.closeReactions.bind(this);
}
shouldComponentUpdate(nextProps, nextState) {
@ -147,12 +83,12 @@ export default class Message extends React.Component {
if (this.props.broadcast !== nextProps.broadcast) {
return true;
}
if (this.props.editing !== nextProps.editing) {
return true;
}
return this.props._updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString();
}
onPress = () => {
KeyboardUtils.dismiss();
}
onLongPress = () => {
this.props.onLongPress(this.parseMessage());
@ -165,10 +101,9 @@ export default class Message extends React.Component {
onReactionPress = (emoji) => {
this.props.onReactionPress(emoji, this.props.item._id);
}
onClose() {
this.setState({ reactionsModal: false });
}
onReactionLongPress() {
onReactionLongPress = () => {
this.setState({ reactionsModal: true });
Vibration.vibrate(50);
}
@ -178,29 +113,12 @@ export default class Message extends React.Component {
return customTimeFormat || Message_TimeFormat;
}
parseMessage = () => JSON.parse(JSON.stringify(this.props.item));
isInfoMessage() {
return SYSTEM_MESSAGES.includes(this.props.item.t);
closeReactions = () => {
this.setState({ reactionsModal: false });
}
isOwn = () => this.props.item.u && this.props.item.u._id === this.props.user.id;
isDeleted() {
return this.props.item.t === 'rm';
}
isTemp() {
return this.props.item.status === messagesStatus.TEMP || this.props.item.status === messagesStatus.ERROR;
}
hasError() {
return this.props.item.status === messagesStatus.ERROR;
}
renderHeader = (username) => {
isHeader = () => {
const { item, previousItem } = this.props;
if (previousItem && (
(previousItem.ts.toDateString() === item.ts.toDateString()) &&
(previousItem.u.username === item.u.username) &&
@ -208,172 +126,61 @@ export default class Message extends React.Component {
(previousItem.status === item.status) &&
(item.ts - previousItem.ts < this.props.Message_GroupingPeriod * 1000)
)) {
return null;
return false;
}
return (
<View style={[styles.flex, { marginTop: 5 }]}>
<Avatar
style={styles.avatar}
text={item.avatar ? '' : username}
size={20}
avatar={item.avatar}
/>
<User
onPress={this._onPress}
item={item}
Message_TimeFormat={this.timeFormat}
/>
</View>
);
return true;
}
renderContent() {
if (this.isInfoMessage()) {
return <Text style={styles.textInfo}>{getInfoMessage(this.props.item)}</Text>;
}
const { item } = this.props;
return <Markdown msg={item.msg} />;
parseMessage = () => JSON.parse(JSON.stringify(this.props.item));
toggleReactionPicker = () => {
this.props.toggleReactionPicker(this.parseMessage());
}
renderAttachment() {
if (this.props.item.attachments.length === 0) {
return null;
}
return this.props.item.attachments.map((file) => {
const { user } = this.props;
if (file.image_url) {
return <Image file={file} user={user} />;
}
if (file.audio_url) {
return <Audio file={file} user={user} />;
}
if (file.video_url) {
return <Video file={file} user={user} />;
}
return <Reply attachment={file} timeFormat={this.timeFormat} />;
});
}
renderUrl = () => {
const { urls } = this.props.item;
if (urls.length === 0) {
return null;
}
return urls.map(url => (
<Url url={url} key={url.url} />
));
};
renderError = () => {
if (!this.hasError()) {
return null;
}
return (
<TouchableOpacity onPress={this.onErrorPress}>
<Icon name='error-outline' color='red' size={20} style={styles.errorIcon} />
</TouchableOpacity>
);
}
renderReaction = (reaction) => {
const reacted = reaction.usernames.findIndex(item => item.value === this.props.user.username) !== -1;
const reactedContainerStyle = reacted && styles.reactedContainer;
return (
<TouchableOpacity
onPress={() => this.onReactionPress(reaction.emoji)}
onLongPress={() => this.onReactionLongPress()}
key={reaction.emoji}
testID={`message-reaction-${ reaction.emoji }`}
>
<View style={[styles.reactionContainer, reactedContainerStyle]}>
<Emoji
content={reaction.emoji}
standardEmojiStyle={styles.reactionEmoji}
customEmojiStyle={styles.reactionCustomEmoji}
/>
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
</View>
</TouchableOpacity>
);
}
renderReactions() {
if (this.props.item.reactions.length === 0) {
return null;
}
return (
<View style={styles.reactionsContainer}>
{this.props.item.reactions.map(this.renderReaction)}
<TouchableOpacity
onPress={() => this.props.toggleReactionPicker(this.parseMessage())}
key='message-add-reaction'
testID='message-add-reaction'
style={[styles.reactionContainer, styles.addReactionContainer]}
>
<Icon name='insert-emoticon' color='#1D74F5' size={18} />
</TouchableOpacity>
</View>
);
}
renderBroadcastReply() {
if (!this.props.broadcast || this.isOwn()) {
return null;
}
return (
<TouchableOpacity
style={styles.broadcastButton}
onPress={() => this.props.replyBroadcast(this.parseMessage())}
>
<Text style={styles.broadcastButtonText}>Reply</Text>
</TouchableOpacity>
);
replyBroadcast = () => {
this.props.replyBroadcast(this.parseMessage());
}
render() {
const {
item, message, editing, style, archived
item, message, editing, user, style, archived, baseUrl, customEmojis, useRealName, broadcast
} = this.props;
const username = item.alias || item.u.username;
const {
msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy
} = item;
const isEditing = message._id === item._id && editing;
const accessibilityLabel = I18n.t('Message_accessibility', { user: username, time: moment(item.ts).format(this.timeFormat), message: this.props.item.msg });
return (
<Touch
onPress={this.onPress}
<Message
msg={msg}
author={u}
ts={ts}
type={t}
status={status}
attachments={attachments}
urls={urls}
reactions={reactions}
alias={alias}
editing={isEditing}
header={this.isHeader()}
avatar={avatar}
user={user}
edited={editedBy && !!editedBy.username}
timeFormat={this.timeFormat}
style={style}
archived={archived}
broadcast={broadcast}
baseUrl={baseUrl}
customEmojis={customEmojis}
reactionsModal={this.state.reactionsModal}
useRealName={useRealName}
closeReactions={this.closeReactions}
onErrorPress={this.onErrorPress}
onLongPress={this.onLongPress}
disabled={this.isInfoMessage() || this.hasError() || archived}
underlayColor='#FFFFFF'
activeOpacity={0.3}
accessibilityLabel={accessibilityLabel}
>
<View style={[styles.message, isEditing && styles.editing, style]}>
{this.renderHeader(username)}
<View style={styles.flex}>
{this.renderError()}
<View style={[styles.messageContent, this.isTemp() && styles.temp]}>
{this.renderContent()}
{this.renderAttachment()}
{this.renderUrl()}
{this.renderReactions()}
{this.renderBroadcastReply()}
</View>
</View>
{this.state.reactionsModal ?
<ReactionsModal
isVisible={this.state.reactionsModal}
onClose={this.onClose}
reactions={item.reactions}
user={this.props.user}
/>
: null
}
</View>
</Touch>
onReactionLongPress={this.onReactionLongPress}
onReactionPress={this.onReactionPress}
replyBroadcast={this.replyBroadcast}
toggleReactionPicker={this.toggleReactionPicker}
/>
);
}
}

View File

@ -1,31 +1,38 @@
import { StyleSheet, Platform } from 'react-native';
export default StyleSheet.create({
container: {
paddingVertical: 5
},
messageContent: {
flex: 1,
marginLeft: 30
marginLeft: 51
},
hasHeader: {
marginLeft: 15
},
flex: {
flexDirection: 'row',
flex: 1
},
message: {
paddingHorizontal: 12,
paddingVertical: 3,
paddingLeft: 10,
paddingRight: 15,
flexDirection: 'column',
transform: [{ scaleY: -1 }],
flex: 1
},
textInfo: {
fontStyle: 'italic',
color: '#a0a0a0'
color: '#a0a0a0',
fontSize: 16
},
editing: {
backgroundColor: '#fff5df'
},
customEmoji: {
width: 16,
height: 16
width: 20,
height: 20
},
temp: { opacity: 0.3 },
codeStyle: {
@ -48,57 +55,83 @@ export default StyleSheet.create({
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 4,
borderWidth: 1.5,
borderColor: '#e1e5e8',
marginRight: 10,
marginBottom: 10,
maxHeight: 28,
backgroundColor: '#E8F2FF'
},
addReactionContainer: {
paddingHorizontal: 15
height: 28,
minWidth: 46,
backgroundColor: '#FFF'
},
reactionCount: {
fontSize: 14,
marginLeft: 3,
marginRight: 8.5,
fontWeight: '600',
color: '#1D74F5'
},
reactionEmoji: {
fontSize: 14
fontSize: 13,
marginLeft: 7
},
reactionCustomEmoji: {
width: 20,
height: 20
width: 19,
height: 19,
marginLeft: 7
},
avatar: {
marginRight: 10
marginTop: 5
},
reactedContainer: {
borderWidth: 0,
backgroundColor: '#D1DAE6'
borderColor: '#1d74f580',
backgroundColor: '#e8f2ff'
},
addReaction: {
width: 17,
height: 17
},
errorIcon: {
padding: 10,
paddingRight: 12,
paddingLeft: 0
paddingLeft: 0,
alignSelf: 'center'
},
broadcastButton: {
borderColor: '#1d74f5',
borderWidth: 2,
borderRadius: 2,
paddingVertical: 10,
width: 100,
width: 107,
height: 44,
marginTop: 15
},
broadcastButtonContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: 6
backgroundColor: '#1d74f5',
borderRadius: 4
},
broadcastButtonIcon: {
width: 14,
height: 12,
marginRight: 11
},
broadcastButtonText: {
color: '#1d74f5'
color: '#fff',
fontSize: 14,
fontWeight: '500'
},
mention: {
color: '#13679a'
color: '#0072FE',
fontWeight: '500',
padding: 5,
backgroundColor: '#E8F2FF'
},
mentionLoggedUser: {
color: '#fff',
backgroundColor: '#1D74F5'
},
mentionAll: {
color: '#fff',
backgroundColor: '#FF5B5A'
},
paragraph: {
marginTop: 0,
@ -115,11 +148,17 @@ export default StyleSheet.create({
image: {
width: '100%',
maxWidth: 400,
height: 300
minHeight: 200,
borderRadius: 4,
marginBottom: 10
},
inlineImage: {
width: 300,
height: 300,
resizeMode: 'contain'
},
edited: {
fontSize: 14,
color: '#9EA2A8'
}
});

View File

@ -173,7 +173,6 @@ export default {
Leave_channel: 'Leave channel',
leave: 'leave',
Livechat: 'Livechat',
Loading_messages_ellipsis: 'Loading messages...',
Login: 'Login',
Logout: 'Logout',
Members: 'Members',

View File

@ -50,7 +50,7 @@ export default async function canOpenRoom({ rid, path }) {
try {
// eslint-disable-next-line
const data = await (this.ddp && this.ddp.status ? canOpenRoomDDP.call(this, { rid, type, name }) : canOpenRoomREST.call(this, { type, rid }));
const data = await (this.ddp && this.ddp.status && false ? canOpenRoomDDP.call(this, { rid, type, name }) : canOpenRoomREST.call(this, { type, rid }));
return data;
} catch (e) {
log('canOpenRoom', e);

View File

@ -39,7 +39,7 @@ export default async function() {
return new Promise(async(resolve, reject) => {
try {
// eslint-disable-next-line
const { subscriptions, rooms } = await (this.ddp.status ? getRoomDpp.apply(this) : getRoomRest.apply(this));
const { subscriptions, rooms } = await (this.ddp.status && false ? getRoomDpp.apply(this) : getRoomRest.apply(this));
const data = rooms.map(room => ({ room, sub: database.objects('subscriptions').filtered('rid == $0', room._id) }));

View File

@ -5,6 +5,7 @@ import reduxStore from '../createStore';
import database from '../realm';
import * as actions from '../../actions';
import log from '../../utils/log';
import { settingsUpdatedAt } from '../../constants/settings';
const getLastUpdate = () => {
const [setting] = database.objects('settings').sorted('_updatedAt', true);
@ -20,7 +21,8 @@ function updateServer(param) {
export default async function() {
try {
const lastUpdate = getLastUpdate();
const result = await (!lastUpdate ? this.ddp.call('public-settings/get') : this.ddp.call('public-settings/get', new Date(lastUpdate)));
const fetchNewSettings = lastUpdate < settingsUpdatedAt;
const result = await ((!lastUpdate || fetchNewSettings) ? this.ddp.call('public-settings/get') : this.ddp.call('public-settings/get', new Date(lastUpdate)));
const data = result.update || result || [];
const filteredSettings = this._prepareSettings(this._filterSettings(data));

View File

@ -155,9 +155,9 @@ const RocketChat = {
}
this.ddp = new Ddp(url, login);
// if (login) {
// protectedFunction(() => RocketChat.getRooms());
// }
if (login) {
protectedFunction(() => RocketChat.getRooms());
}
this.ddp.on('login', protectedFunction(() => reduxStore.dispatch(loginRequest())));

View File

@ -122,12 +122,14 @@ const renderNumber = (unread, userMentions) => {
const attrs = ['name', 'unread', 'userMentions', 'alert', 'showLastMessage', 'type'];
@connect(state => ({
username: state.login.user && state.login.user.username,
StoreLastMessage: state.settings.Store_Last_Message
StoreLastMessage: state.settings.Store_Last_Message,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class RoomItem extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
StoreLastMessage: PropTypes.bool,
_updatedAt: PropTypes.instanceOf(Date),
lastMessage: PropTypes.object,
@ -162,8 +164,10 @@ export default class RoomItem extends React.Component {
return attrs.some(key => nextProps[key] !== this.props[key]);
}
get avatar() {
const { type, name, avatarSize } = this.props;
return <Avatar text={name} size={avatarSize} type={type} style={{ marginHorizontal: 15 }} />;
const {
type, name, avatarSize, baseUrl
} = this.props;
return <Avatar text={name} size={avatarSize} type={type} baseUrl={baseUrl} style={{ marginHorizontal: 15 }} />;
}
get lastMessage() {

View File

@ -42,11 +42,11 @@ const styles = StyleSheet.create({
});
const UserItem = ({
name, username, onPress, testID, onLongPress, style, icon
name, username, onPress, testID, onLongPress, style, icon, baseUrl
}) => (
<Touch onPress={onPress} onLongPress={onLongPress} style={styles.button} testID={testID}>
<View style={[styles.container, style]}>
<Avatar text={username} size={30} type='d' style={styles.avatar} />
<Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} />
<View style={styles.textContainer}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.username}>@{username}</Text>
@ -59,6 +59,7 @@ const UserItem = ({
UserItem.propTypes = {
name: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired,
onLongPress: PropTypes.func,

View File

@ -6,7 +6,7 @@ import { BACKGROUND } from 'redux-enhancer-react-native-appstate';
import * as types from '../actions/actionsTypes';
// import { roomsSuccess, roomsFailure } from '../actions/rooms';
import { addUserTyping, removeUserTyping, setLastOpen } from '../actions/room';
import { messagesRequest, editCancel } from '../actions/messages';
import { messagesRequest, editCancel, replyCancel } from '../actions/messages';
import RocketChat from '../lib/rocketchat';
import database from '../lib/realm';
import log from '../utils/log';
@ -93,6 +93,7 @@ const watchRoomOpen = function* watchRoomOpen({ room }) {
cancel(thread);
sub.stop();
yield put(editCancel());
yield put(replyCancel());
// subscriptions.forEach((sub) => {
// sub.unsubscribe().catch(e => alert(e));

View File

@ -15,7 +15,7 @@ const Touch = ({ children, onPress, ...props }) => (
Touch.propTypes = {
children: PropTypes.node.isRequired,
onPress: PropTypes.func.isRequired
onPress: PropTypes.func
};
export default Touch;

View File

@ -16,7 +16,8 @@ import { showErrorAlert } from '../utils/info';
const styles = StyleSheet.create({
container: {
backgroundColor: '#f7f8fa'
backgroundColor: '#f7f8fa',
flex: 1
},
list: {
width: '100%',
@ -68,6 +69,7 @@ const styles = StyleSheet.create({
});
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
createChannel: state.createChannel,
users: state.selectedUsers.users
}), dispatch => ({
@ -78,6 +80,7 @@ const styles = StyleSheet.create({
export default class CreateChannelView extends LoggedView {
static propTypes = {
navigator: PropTypes.object,
baseUrl: PropTypes.string,
create: PropTypes.func.isRequired,
removeUser: PropTypes.func.isRequired,
createChannel: PropTypes.object.isRequired,
@ -218,6 +221,7 @@ export default class CreateChannelView extends LoggedView {
username={item.name}
onPress={() => this.removeUser(item)}
testID={`create-channel-view-item-${ item.name }`}
baseUrl={this.props.baseUrl}
/>
)
@ -241,7 +245,7 @@ export default class CreateChannelView extends LoggedView {
contentContainerStyle={[sharedStyles.container, styles.container]}
keyboardVerticalOffset={128}
>
<SafeAreaView testID='create-channel-view'>
<SafeAreaView testID='create-channel-view' style={styles.container}>
<ScrollView {...scrollPersistTaps}>
<View style={sharedStyles.separatorVertical}>
<TextInput

View File

@ -17,8 +17,7 @@ import I18n from '../../i18n';
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
},
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}
}), dispatch => ({
openMentionedMessages: (rid, limit) => dispatch(openMentionedMessages(rid, limit)),
closeMentionedMessages: () => dispatch(closeMentionedMessages())
@ -30,7 +29,6 @@ export default class MentionedMessagesView extends LoggedView {
messages: PropTypes.array,
ready: PropTypes.bool,
user: PropTypes.object,
baseUrl: PropTypes.string,
openMentionedMessages: PropTypes.func,
closeMentionedMessages: PropTypes.func
}
@ -87,9 +85,7 @@ export default class MentionedMessagesView extends LoggedView {
style={styles.message}
reactions={item.reactions}
user={this.props.user}
baseUrl={this.props.baseUrl}
customTimeFormat='MMMM Do YYYY, h:mm:ss a'
onLongPress={() => {}}
/>
)

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, StyleSheet, SafeAreaView, FlatList, Text, Platform, Image } from 'react-native';
import { connect } from 'react-redux';
import database from '../lib/realm';
import RocketChat from '../lib/rocketchat';
@ -40,8 +41,11 @@ const styles = StyleSheet.create({
}
});
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
/** @extends React.Component */
export default class SelectedUsersView extends LoggedView {
export default class NewMessageView extends LoggedView {
static navigatorButtons = {
leftButtons: [{
id: 'cancel',
@ -51,6 +55,7 @@ export default class SelectedUsersView extends LoggedView {
static propTypes = {
navigator: PropTypes.object,
baseUrl: PropTypes.string,
onPressItem: PropTypes.func.isRequired
};
@ -140,6 +145,7 @@ export default class SelectedUsersView extends LoggedView {
name={item.search ? item.name : item.fname}
username={item.search ? item.username : item.name}
onPress={() => this.onPressItem(item)}
baseUrl={this.props.baseUrl}
testID={`new-message-view-item-${ item.name }`}
style={style}
/>

View File

@ -32,10 +32,10 @@ const styles = StyleSheet.create({
},
input: {
color: '#9EA2A8',
fontSize: moderateScale(17),
paddingTop: scale(14),
paddingBottom: scale(14),
paddingHorizontal: scale(16)
fontSize: 17,
paddingTop: 14,
paddingBottom: 14,
paddingHorizontal: 16
}
});

View File

@ -23,7 +23,7 @@ export default StyleSheet.create({
alignSelf: 'center',
paddingHorizontal: scale(45),
marginTop: verticalScale(30),
marginBottom: verticalScale(50),
marginBottom: verticalScale(35),
maxHeight: verticalScale(250),
resizeMode: 'contain'
},
@ -48,13 +48,13 @@ export default StyleSheet.create({
marginTop: verticalScale(30)
},
buttonContainer: {
marginHorizontal: scale(15),
marginVertical: scale(5),
marginHorizontal: 15,
marginVertical: 5,
flexDirection: 'row',
height: verticalScale(60),
height: 60,
alignItems: 'center',
borderWidth: StyleSheet.hairlineWidth,
borderRadius: moderateScale(2)
borderWidth: 1,
borderRadius: 2
},
buttonCenter: {
flex: 1,
@ -62,13 +62,13 @@ export default StyleSheet.create({
justifyContent: 'center'
},
buttonTitle: {
fontSize: moderateScale(16),
fontSize: 16,
fontWeight: '600'
},
buttonSubtitle: {
color: '#9EA2A8',
fontSize: moderateScale(14),
height: moderateScale(18)
fontSize: 14,
height: 18
},
buttonIconContainer: {
width: 65,
@ -76,7 +76,7 @@ export default StyleSheet.create({
justifyContent: 'center'
},
buttonIcon: {
marginHorizontal: scale(10),
marginHorizontal: 10,
width: 20,
height: 20
},

View File

@ -23,8 +23,7 @@ const options = [I18n.t('Unpin'), I18n.t('Cancel')];
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
},
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}
}), dispatch => ({
openPinnedMessages: (rid, limit) => dispatch(openPinnedMessages(rid, limit)),
closePinnedMessages: () => dispatch(closePinnedMessages()),
@ -37,7 +36,6 @@ export default class PinnedMessagesView extends LoggedView {
messages: PropTypes.array,
ready: PropTypes.bool,
user: PropTypes.object,
baseUrl: PropTypes.string,
openPinnedMessages: PropTypes.func,
closePinnedMessages: PropTypes.func,
togglePinRequest: PropTypes.func
@ -113,7 +111,6 @@ export default class PinnedMessagesView extends LoggedView {
style={styles.message}
reactions={item.reactions}
user={this.props.user}
baseUrl={this.props.baseUrl}
customTimeFormat='MMMM Do YYYY, h:mm:ss a'
onLongPress={this.onLongPress}
/>

View File

@ -30,11 +30,13 @@ import Touch from '../../utils/touch';
customFields: state.login.user && state.login.user.customFields,
emails: state.login.user && state.login.user.emails
},
Accounts_CustomFields: state.settings.Accounts_CustomFields
Accounts_CustomFields: state.settings.Accounts_CustomFields,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
/** @extends React.Component */
export default class ProfileView extends LoggedView {
static propTypes = {
baseUrl: PropTypes.string,
navigator: PropTypes.object,
user: PropTypes.object,
Accounts_CustomFields: PropTypes.string
@ -279,7 +281,7 @@ export default class ProfileView extends LoggedView {
renderAvatarButtons = () => (
<View style={styles.avatarButtons}>
{this.renderAvatarButton({
child: <Avatar text={this.props.user.username} size={50} forceInitials />,
child: <Avatar text={this.props.user.username} size={50} baseUrl={this.props.baseUrl} forceInitials />,
onPress: () => this.resetAvatar(),
key: 'profile-view-reset-avatar'
})}
@ -298,7 +300,7 @@ export default class ProfileView extends LoggedView {
const { url, blob, contentType } = this.state.avatarSuggestions[service];
return this.renderAvatarButton({
key: `profile-view-avatar-${ service }`,
child: <Avatar avatar={url} size={50} />,
child: <Avatar avatar={url} size={50} baseUrl={this.props.baseUrl} />,
onPress: () => this.setAvatar({
url, data: blob, service, contentType
})
@ -381,6 +383,7 @@ export default class ProfileView extends LoggedView {
text={username}
avatar={this.state.avatar && this.state.avatar.url}
size={100}
baseUrl={this.props.baseUrl}
/>
</View>
<RCTextInput

View File

@ -23,13 +23,15 @@ const renderSeparator = () => <View style={styles.separator} />;
@connect(state => ({
userId: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username
username: state.login.user && state.login.user.username,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}), dispatch => ({
leaveRoom: rid => dispatch(leaveRoom(rid))
}))
/** @extends React.Component */
export default class RoomActionsView extends LoggedView {
static propTypes = {
baseUrl: PropTypes.string,
rid: PropTypes.string,
navigator: PropTypes.object,
userId: PropTypes.string,
@ -343,6 +345,7 @@ export default class RoomActionsView extends LoggedView {
size={50}
style={styles.avatar}
type={t}
baseUrl={this.props.baseUrl}
>
{t === 'd' ? <Status style={sharedStyles.status} id={member._id} /> : null }
</Avatar>,

View File

@ -86,7 +86,6 @@ export default class RoomFilesView extends LoggedView {
reactions={item.reactions}
user={this.props.user}
customTimeFormat='MMMM Do YYYY, h:mm:ss a'
onLongPress={() => {}}
/>
)

View File

@ -43,6 +43,7 @@ export default class RoomInfoView extends LoggedView {
navigator: PropTypes.object,
rid: PropTypes.string,
userId: PropTypes.string,
baseUrl: PropTypes.string,
activeUsers: PropTypes.object,
Message_TimeFormat: PropTypes.string,
roles: PropTypes.object
@ -192,6 +193,7 @@ export default class RoomInfoView extends LoggedView {
size={100}
style={styles.avatar}
type={room.t}
baseUrl={this.props.baseUrl}
>
{room.t === 'd' ? <Status style={[sharedStyles.status, styles.status]} id={roomUser._id} /> : null}
</Avatar>

View File

@ -12,13 +12,15 @@ const margin = Platform.OS === 'android' ? 40 : 20;
const tabEmojiStyle = { fontSize: 15 };
@connect(state => ({
showReactionPicker: state.messages.showReactionPicker
showReactionPicker: state.messages.showReactionPicker,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}), dispatch => ({
toggleReactionPicker: message => dispatch(toggleReactionPicker(message))
}))
@responsive
export default class ReactionPicker extends React.Component {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
window: PropTypes.any,
showReactionPicker: PropTypes.bool,
toggleReactionPicker: PropTypes.func,
@ -37,7 +39,7 @@ export default class ReactionPicker extends React.Component {
}
render() {
const { window: { width, height }, showReactionPicker } = this.props;
const { window: { width, height }, showReactionPicker, baseUrl } = this.props;
return (showReactionPicker ?
<Modal
@ -56,6 +58,7 @@ export default class ReactionPicker extends React.Component {
tabEmojiStyle={tabEmojiStyle}
width={Math.min(width, height) - margin}
onEmojiSelected={(emoji, shortname) => this.onEmojiSelected(emoji, shortname)}
baseUrl={baseUrl}
/>
</View>
</Modal> : null

View File

@ -9,51 +9,62 @@ const styles = StyleSheet.create({
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginVertical: 10
marginBottom: 25,
marginTop: 15,
transform: [{ scaleY: -1 }]
},
line: {
borderTopColor: '#eaeaea',
borderTopWidth: StyleSheet.hairlineWidth,
backgroundColor: '#9ea2a8',
height: 1,
flex: 1
},
text: {
color: '#444444',
fontSize: 11,
paddingHorizontal: 10,
transform: [{ scaleY: -1 }]
color: '#9ea2a8',
fontSize: 14,
fontWeight: '600'
},
unreadLine: {
borderTopColor: 'red'
backgroundColor: '#f5455c'
},
unreadText: {
color: 'red'
color: '#f5455c'
},
marginLeft: {
marginLeft: 10
},
marginRight: {
marginRight: 10
},
marginHorizontal: {
marginHorizontal: 10
}
});
const DateSeparator = ({ ts, unread }) => {
const date = ts ? moment(ts).format('MMMM DD, YYYY') : null;
const date = ts ? moment(ts).format('MMM DD, YYYY') : null;
if (ts && unread) {
return (
<View style={styles.container}>
<Text style={[styles.text, styles.unreadText]}>{date}</Text>
<View style={[styles.line, styles.unreadLine]} />
<Text style={[styles.text, styles.unreadText]}>{I18n.t('unread_messages')}</Text>
<Text style={[styles.text, styles.unreadText, styles.marginLeft]}>{date}</Text>
<View style={[styles.line, styles.unreadLine, styles.marginHorizontal]} />
<Text style={[styles.text, styles.unreadText, styles.marginRight]}>{I18n.t('unread_messages')}</Text>
</View>
);
}
if (ts) {
return (
<View style={styles.container}>
<View style={styles.line} />
<Text style={styles.text}>{date}</Text>
<View style={styles.line} />
<View style={[styles.line, styles.marginLeft]} />
<Text style={[styles.text, styles.marginHorizontal]}>{date}</Text>
<View style={[styles.line, styles.marginRight]} />
</View>
);
}
return (
<View style={styles.container}>
<View style={[styles.line, styles.unreadLine]} />
<Text style={[styles.text, styles.unreadText]}>{I18n.t('unread_messages')}</Text>
<View style={[styles.line, styles.unreadLine, styles.marginLeft]} />
<Text style={[styles.text, styles.unreadText, styles.marginHorizontal]}>{I18n.t('unread_messages')}</Text>
<View style={[styles.line, styles.unreadLine, styles.marginRight]} />
</View>
);
};

View File

@ -232,15 +232,15 @@ export default class RoomView extends LoggedView {
<Message
key={item._id}
item={item}
_updatedAt={item._updatedAt}
status={item.status}
reactions={JSON.parse(JSON.stringify(item.reactions))}
user={this.props.user}
onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress}
archived={this.state.room.archived}
broadcast={this.state.room.broadcast}
previousItem={previousItem}
_updatedAt={item._updatedAt}
onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress}
/>
);
@ -273,7 +273,7 @@ export default class RoomView extends LoggedView {
renderHeader = () => {
if (!this.state.end) {
return <Text style={styles.loadingMore}>{I18n.t('Loading_messages_ellipsis')}</Text>;
return <ActivityIndicator style={[styles.loading, { transform: [{ scaleY: -1 }] }]} />;
}
return null;
}

View File

@ -46,7 +46,8 @@ export default StyleSheet.create({
flexDirection: 'column'
},
loading: {
flex: 1
flex: 1,
marginVertical: 15
},
imageBackground: {
width: '100%',

View File

@ -7,7 +7,8 @@ import I18n from '../../../i18n';
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center'
alignItems: 'center',
justifyContent: 'center'
},
button: {
flexDirection: 'row'

View File

@ -43,7 +43,7 @@ if (Platform.OS === 'android') {
@connect(state => ({
userId: state.login.user && state.login.user.id,
server: state.server.server,
Site_Url: state.settings.Site_Url,
baseUrl: state.settings.baseUrl || state.server ? state.server.server : '',
searchText: state.rooms.searchText,
loadingServer: state.server.loading,
showServerDropdown: state.rooms.showServerDropdown,
@ -51,7 +51,8 @@ if (Platform.OS === 'android') {
sortBy: state.sortPreferences.sortBy,
groupByType: state.sortPreferences.groupByType,
showFavorites: state.sortPreferences.showFavorites,
showUnread: state.sortPreferences.showUnread
showUnread: state.sortPreferences.showUnread,
useRealName: state.settings.UI_Use_Real_Name
}), dispatch => ({
toggleSortDropdown: () => dispatch(toggleSortDropdown())
}))
@ -72,7 +73,7 @@ export default class RoomsListView extends LoggedView {
static propTypes = {
navigator: PropTypes.object,
userId: PropTypes.string,
Site_Url: PropTypes.string,
baseUrl: PropTypes.string,
server: PropTypes.string,
searchText: PropTypes.string,
loadingServer: PropTypes.bool,
@ -82,12 +83,14 @@ export default class RoomsListView extends LoggedView {
groupByType: PropTypes.bool,
showFavorites: PropTypes.bool,
showUnread: PropTypes.bool,
toggleSortDropdown: PropTypes.func
toggleSortDropdown: PropTypes.func,
useRealName: PropTypes.bool
}
constructor(props) {
super('RoomsListView', props);
this.data = [];
this.state = {
search: [],
loading: true,
@ -396,18 +399,19 @@ export default class RoomsListView extends LoggedView {
renderItem = ({ item }) => {
const id = item.rid.replace(this.props.userId, '').trim();
const { useRealName } = this.props;
return (<RoomItem
alert={item.alert}
unread={item.unread}
userMentions={item.userMentions}
favorite={item.f}
lastMessage={item.lastMessage}
name={item.name}
name={(useRealName && item.fname) || item.name}
_updatedAt={item.roomUpdatedAt}
key={item._id}
id={id}
type={item.t}
baseUrl={this.props.Site_Url}
baseUrl={this.props.baseUrl}
onPress={() => this._onPressItem(item)}
testID={`rooms-list-view-item-${ item.name }`}
height={ROW_HEIGHT}
@ -417,6 +421,15 @@ export default class RoomsListView extends LoggedView {
renderSeparator = () => <View style={styles.separator} />;
renderSection = (data, header) => {
if (header === 'Unread' && !this.props.showUnread) {
return null;
} else if (header === 'Favorites' && !this.props.showFavorites) {
return null;
} else if (['Channels', 'Direct_Messages', 'Private_Groups', 'Livechat'].includes(header) && !this.props.groupByType) {
return null;
} else if (header === 'Chats' && this.props.groupByType) {
return null;
}
if (data.length > 0) {
return (
<FlatList

View File

@ -97,9 +97,7 @@ export default class SearchMessagesView extends LoggedView {
style={styles.message}
reactions={item.reactions}
user={this.props.user}
baseUrl={this.props.baseUrl}
customTimeFormat='MMMM Do YYYY, h:mm:ss a'
onLongPress={() => {}}
onReactionPress={async(emoji) => {
try {
await RocketChat.setReaction(emoji, item._id);
@ -124,7 +122,7 @@ export default class SearchMessagesView extends LoggedView {
placeholder={I18n.t('Search_Messages')}
testID='search-message-view-input'
/>
<Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} />
<Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} username='' baseUrl='' customEmojis={{}} />
<View style={styles.divider} />
</View>
<FlatList

View File

@ -29,6 +29,7 @@ const styles = StyleSheet.create({
});
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
users: state.selectedUsers.users,
loading: state.selectedUsers.loading
}), dispatch => ({
@ -43,6 +44,7 @@ export default class SelectedUsersView extends LoggedView {
navigator: PropTypes.object,
rid: PropTypes.string,
nextAction: PropTypes.string.isRequired,
baseUrl: PropTypes.string,
addUser: PropTypes.func.isRequired,
removeUser: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
@ -185,6 +187,7 @@ export default class SelectedUsersView extends LoggedView {
username={item.name}
onPress={() => this._onPressSelectedItem(item)}
testID={`selected-user-${ item.name }`}
baseUrl={this.props.baseUrl}
style={{ paddingRight: 15 }}
/>
)
@ -211,6 +214,7 @@ export default class SelectedUsersView extends LoggedView {
onPress={() => this._onPressItem(item._id, item)}
testID={`select-users-view-item-${ item.name }`}
icon={this.isChecked(username) ? 'check' : null}
baseUrl={this.props.baseUrl}
style={style}
/>
);

View File

@ -17,8 +17,7 @@ import I18n from '../../i18n';
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
},
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}
}), dispatch => ({
openSnippetedMessages: (rid, limit) => dispatch(openSnippetedMessages(rid, limit)),
closeSnippetedMessages: () => dispatch(closeSnippetedMessages())
@ -30,7 +29,6 @@ export default class SnippetedMessagesView extends LoggedView {
messages: PropTypes.array,
ready: PropTypes.bool,
user: PropTypes.object,
baseUrl: PropTypes.string,
openSnippetedMessages: PropTypes.func,
closeSnippetedMessages: PropTypes.func
}
@ -87,9 +85,7 @@ export default class SnippetedMessagesView extends LoggedView {
style={styles.message}
reactions={item.reactions}
user={this.props.user}
baseUrl={this.props.baseUrl}
customTimeFormat='MMMM Do YYYY, h:mm:ss a'
onLongPress={() => {}}
/>
);

View File

@ -23,8 +23,7 @@ const options = [I18n.t('Unstar'), I18n.t('Cancel')];
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
},
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}
}), dispatch => ({
openStarredMessages: (rid, limit) => dispatch(openStarredMessages(rid, limit)),
closeStarredMessages: () => dispatch(closeStarredMessages()),
@ -37,7 +36,6 @@ export default class StarredMessagesView extends LoggedView {
messages: PropTypes.array,
ready: PropTypes.bool,
user: PropTypes.object,
baseUrl: PropTypes.string,
openStarredMessages: PropTypes.func,
closeStarredMessages: PropTypes.func,
toggleStarRequest: PropTypes.func
@ -113,7 +111,6 @@ export default class StarredMessagesView extends LoggedView {
style={styles.message}
reactions={item.reactions}
user={this.props.user}
baseUrl={this.props.baseUrl}
customTimeFormat='MMMM Do YYYY, h:mm:ss a'
onLongPress={this.onLongPress}
/>

View File

@ -16,7 +16,7 @@ describe('Rooms list screen', () => {
// });
it('should have room item', async() => {
await expect(element(by.id('rooms-list-view-item-general'))).toExist();
await expect(element(by.id('rooms-list-view-item-general')).atIndex(0)).toExist();
});
// Render - Header

View File

@ -88,8 +88,8 @@ describe('Create room screen', () => {
it('should navigate to create channel view', async() => {
await element(by.id('selected-users-view-submit')).tap();
await waitFor(element(by.id('create-channel-view'))).toBeVisible().withTimeout(5000);
await expect(element(by.id('create-channel-view'))).toBeVisible();
await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(5000);
await expect(element(by.id('create-channel-view'))).toExist();
});
})
@ -145,6 +145,7 @@ describe('Create room screen', () => {
await expect(element(by.text(`private${ data.random }`))).toBeVisible();
await tapBack(2);
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await element(by.id('rooms-list-view-search')).replaceText(`private${ data.random }`);
await waitFor(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible();
});

View File

@ -9,10 +9,11 @@ async function mockMessage(message) {
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText(`${ data.random }${ message }`);
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.text(`${ data.random }${ message }`))).toBeVisible().withTimeout(60000);
await waitFor(element(by.text(`${ data.random }${ message }`))).toExist().withTimeout(60000);
};
async function navigateToRoom() {
await element(by.id('rooms-list-view-search')).replaceText(`private${ data.random }`);
await waitFor(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible().withTimeout(60000);
await element(by.id(`rooms-list-view-item-private${ data.random }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
@ -92,8 +93,7 @@ describe('Room screen', () => {
describe('Messagebox', async() => {
it('should send message', async() => {
await mockMessage('message');
await waitFor(element(by.text(`${ data.random }message`))).toBeVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }message`))).toBeVisible();
await expect(element(by.text(`${ data.random }message`))).toExist();
});
it('should show/hide emoji keyboard', async() => {
@ -139,9 +139,9 @@ describe('Room screen', () => {
await element(by.id(`mention-item-${ data.user }`)).tap();
await expect(element(by.id('messagebox-input'))).toHaveText(`@${ data.user } `);
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText('test');
await element(by.id('messagebox-input')).typeText(`${ data.random }mention`);
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.text(`@${ data.user } test`))).toBeVisible().withTimeout(60000);
await waitFor(element(by.text(`@${ data.user } ${ data.random }mention`))).toBeVisible().withTimeout(60000);
});
it('should show and tap on room autocomplete', async() => {
@ -250,8 +250,8 @@ describe('Room screen', () => {
await element(by.text('Edit')).tap();
await element(by.id('messagebox-input')).typeText('ed');
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.text(`${ data.random }edited`))).toBeVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }edited`))).toBeVisible();
await waitFor(element(by.text(`${ data.random }edited (edited)`))).toBeVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }edited (edited)`))).toBeVisible();
});
it('should quote message', async() => {
@ -266,13 +266,15 @@ describe('Room screen', () => {
});
it('should pin message', async() => {
await element(by.text(`${ data.random }edited`)).longPress();
await waitFor(element(by.text(`${ data.random }edited (edited)`))).toBeVisible().whileElement(by.id('room-view-messages')).scroll(200, 'up');
await element(by.text(`${ data.random }edited (edited)`)).longPress();
await waitFor(element(by.text('Message actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Message actions'))).toBeVisible();
await element(by.text('Pin')).tap();
await waitFor(element(by.text('Message actions'))).toBeNotVisible().withTimeout(5000);
await waitFor(element(by.text(`${ data.random }edited`)).atIndex(1)).toBeVisible().withTimeout(60000);
await element(by.text(`${ data.random }edited`)).atIndex(0).longPress();
await waitFor(element(by.text(`${ data.random }edited (edited)`))).toBeVisible().whileElement(by.id('room-view-messages')).scroll(200, 'up');
await waitFor(element(by.text(`${ data.random }edited (edited)`)).atIndex(1)).toBeVisible().withTimeout(60000);
await element(by.text(`${ data.random }edited (edited)`)).atIndex(0).longPress();
await waitFor(element(by.text('Unpin'))).toBeVisible().withTimeout(2000);
await expect(element(by.text('Unpin'))).toBeVisible();
await element(by.text('Cancel')).tap();

View File

@ -14,7 +14,7 @@ async function navigateToRoomActions(type) {
} else {
room = `private${ data.random }`;
}
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(2000);
await element(by.id(`rooms-list-view-item-${ room }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000);
await element(by.id('room-view-header-actions')).tap();
@ -210,8 +210,9 @@ describe('Room actions screen', () => {
it('should show mentioned messages', async() => {
await element(by.id('room-actions-mentioned')).tap();
await waitFor(element(by.id('mentioned-messages-view'))).toExist().withTimeout(2000);
await waitFor(element(by.text(`@${ data.user } test`).withAncestor(by.id('mentioned-messages-view')))).toBeVisible().withTimeout(60000);
await expect(element(by.text(`@${ data.user } test`).withAncestor(by.id('mentioned-messages-view')))).toBeVisible();
await expect(element(by.id('mentioned-messages-view'))).toExist();
// await waitFor(element(by.text(` ${ data.random }mention`))).toBeVisible().withTimeout(60000);
// await expect(element(by.text(` ${ data.random }mention`))).toBeVisible();
await backToActions();
});
@ -233,14 +234,14 @@ describe('Room actions screen', () => {
await waitFor(element(by.id('room-actions-pinned'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await element(by.id('room-actions-pinned')).tap();
await waitFor(element(by.id('pinned-messages-view'))).toExist().withTimeout(2000);
await waitFor(element(by.text(`${ data.random }edited`).withAncestor(by.id('pinned-messages-view'))).atIndex(0)).toBeVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }edited`).withAncestor(by.id('pinned-messages-view')))).toBeVisible();
await element(by.text(`${ data.random }edited`).withAncestor(by.id('pinned-messages-view'))).longPress();
await waitFor(element(by.text(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view'))).atIndex(0)).toBeVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeVisible();
await element(by.text(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view'))).longPress();
await waitFor(element(by.text('Unpin'))).toBeVisible().withTimeout(2000);
await expect(element(by.text('Unpin'))).toBeVisible();
await element(by.text('Unpin')).tap();
await waitFor(element(by.text(`${ data.random }edited`).withAncestor(by.id('pinned-messages-view'))).atIndex(0)).toBeNotVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }edited`).withAncestor(by.id('pinned-messages-view')))).toBeNotVisible();
await waitFor(element(by.text(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view'))).atIndex(0)).toBeNotVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeNotVisible();
await backToActions();
});

View File

@ -12,6 +12,7 @@ async function navigateToRoomInfo(type) {
} else {
room = `private${ data.random }`;
}
await element(by.id('rooms-list-view-search')).replaceText(room);
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeVisible().withTimeout(2000);
await element(by.id(`rooms-list-view-item-${ room }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000);
@ -310,6 +311,7 @@ describe('Room info screen', () => {
await expect(element(by.text('Yes, delete it!'))).toBeVisible();
await element(by.text('Yes, delete it!')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await element(by.id('rooms-list-view-search')).replaceText('');
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible();
});

View File

@ -36,14 +36,14 @@ describe('Broadcast room', () => {
await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(2000);
await tapBack();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000);
await tapBack(2);
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist();
// await tapBack(2);
// await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
// await waitFor(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist().withTimeout(60000);
// await expect(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist();
});
it('should send message', async() => {
await element(by.id(`rooms-list-view-item-broadcast${ data.random }`)).tap();
// await element(by.id(`rooms-list-view-item-broadcast${ data.random }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText(`${ data.random }message`);

View File

@ -2,7 +2,7 @@ const random = require('./helpers/random');
const value = random(20);
const data = {
server: 'https://stable.rocket.chat',
alternateServer: 'https://open.rocket.chat',
alternateServer: 'https://unstable.rocket.chat',
user: `user${ value }`,
password: `password${ value }`,
alternateUser: 'detox',

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "pause.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "pause@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "pause@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "play.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "play@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "play@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

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