[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
42
README.md
|
@ -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 | ✅ |
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module.exports = 'test-file-stub';
|
|
@ -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;
|
|
@ -0,0 +1,5 @@
|
|||
export default function() {
|
||||
return {
|
||||
show: () => {}
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export default () => 'Video';
|
|
@ -0,0 +1 @@
|
|||
export default () => 'Video';
|
|
@ -13,29 +13,28 @@ import RoomItem from '../app/presentation/RoomItem';
|
|||
import renderer from 'react-test-renderer';
|
||||
|
||||
const date = new Date(2017, 10, 10, 10);
|
||||
|
||||
jest.mock('react-native-img-cache', () => { return { CachedImage: 'View' } });
|
||||
const onPress = () => {};
|
||||
|
||||
it('renders correctly', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem type="d" _updatedAt={date} name="name" /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" baseUrl="baseUrl" /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render unread', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem type="d" _updatedAt={date} name="name" unread={1} /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" unread={1} /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render unread +999', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem type="d" _updatedAt={date} name="name" unread={1000} /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" unread={1000} /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render no icon', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem type="X" _updatedAt={date} name="name" /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="X" _updatedAt={date} name="name" /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render private group', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem type="g" _updatedAt={date} name="private-group" /> </View></Provider>).toJSON()).toMatchSnapshot();
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="g" _updatedAt={date} name="private-group" /> </View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render channel', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem type="c" _updatedAt={date} name="general" /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="c" _updatedAt={date} name="general" /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -102,7 +102,7 @@ android {
|
|||
minSdkVersion 19
|
||||
targetSdkVersion 27
|
||||
versionCode VERSIONCODE as Integer
|
||||
versionName "1.1.1"
|
||||
versionName "1.2"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 1023 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 275 B |
After Width: | Height: | Size: 630 B |
After Width: | Height: | Size: 687 B |
After Width: | Height: | Size: 740 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 234 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 330 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 436 B |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 523 B |
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -10,9 +10,9 @@ import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
|||
|
||||
const emojisPerRow = Platform.OS === 'ios' ? 8 : 9;
|
||||
|
||||
const renderEmoji = (emoji, size) => {
|
||||
const renderEmoji = (emoji, size, baseUrl) => {
|
||||
if (emoji.isCustom) {
|
||||
return <CustomEmoji style={[styles.customCategoryEmoji, { height: size - 8, width: size - 8 }]} emoji={emoji} />;
|
||||
return <CustomEmoji style={[styles.customCategoryEmoji, { height: size - 8, width: size - 8 }]} emoji={emoji} baseUrl={baseUrl} />;
|
||||
}
|
||||
return (
|
||||
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
|
||||
|
@ -25,6 +25,7 @@ const renderEmoji = (emoji, size) => {
|
|||
@responsive
|
||||
export default class EmojiCategory extends React.Component {
|
||||
static propTypes = {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
emojis: PropTypes.any,
|
||||
window: PropTypes.any,
|
||||
onEmojiSelected: PropTypes.func,
|
||||
|
@ -44,6 +45,7 @@ export default class EmojiCategory extends React.Component {
|
|||
}
|
||||
|
||||
renderItem(emoji, size) {
|
||||
const { baseUrl } = this.props;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
|
@ -51,7 +53,7 @@ export default class EmojiCategory extends React.Component {
|
|||
onPress={() => this.props.onEmojiSelected(emoji)}
|
||||
testID={`reaction-picker-${ emoji.isCustom ? emoji.content : emoji }`}
|
||||
>
|
||||
{renderEmoji(emoji, size)}
|
||||
{renderEmoji(emoji, size, baseUrl)}
|
||||
</TouchableOpacity>);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ const scrollProps = {
|
|||
|
||||
export default class EmojiPicker extends Component {
|
||||
static propTypes = {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
onEmojiSelected: PropTypes.func,
|
||||
tabEmojiStyle: PropTypes.object,
|
||||
emojisPerRow: PropTypes.number,
|
||||
|
@ -110,6 +111,7 @@ export default class EmojiPicker extends Component {
|
|||
style={styles.categoryContainer}
|
||||
size={this.props.emojisPerRow}
|
||||
width={this.props.width}
|
||||
baseUrl={this.props.baseUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -123,12 +125,14 @@ export default class EmojiPicker extends Component {
|
|||
<ScrollableTabView
|
||||
renderTabBar={() => <TabBar tabEmojiStyle={this.props.tabEmojiStyle} />}
|
||||
contentProps={scrollProps}
|
||||
style={styles.background}
|
||||
>
|
||||
{
|
||||
categories.tabs.map((tab, i) => (
|
||||
<ScrollView
|
||||
key={tab.category}
|
||||
tabLabel={tab.tabLabel}
|
||||
style={styles.background}
|
||||
{...scrollProps}
|
||||
>
|
||||
{this.renderCategory(tab.category, i)}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export default StyleSheet.create({
|
||||
background: {
|
||||
backgroundColor: '#fff'
|
||||
},
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,15 +9,17 @@ import Markdown from '../message/Markdown';
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row'
|
||||
flexDirection: 'row',
|
||||
marginTop: 10,
|
||||
backgroundColor: '#fff'
|
||||
},
|
||||
messageContainer: {
|
||||
flex: 1,
|
||||
marginHorizontal: 15,
|
||||
marginHorizontal: 10,
|
||||
backgroundColor: '#F3F4F5',
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 2
|
||||
borderRadius: 4
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
|
@ -35,18 +37,23 @@ const styles = StyleSheet.create({
|
|||
marginLeft: 5
|
||||
},
|
||||
close: {
|
||||
marginRight: 15
|
||||
marginRight: 10
|
||||
}
|
||||
});
|
||||
|
||||
@connect(state => ({
|
||||
Message_TimeFormat: state.settings.Message_TimeFormat
|
||||
Message_TimeFormat: state.settings.Message_TimeFormat,
|
||||
customEmojis: state.customEmojis,
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
|
||||
}))
|
||||
export default class ReplyPreview extends Component {
|
||||
static propTypes = {
|
||||
message: PropTypes.object.isRequired,
|
||||
Message_TimeFormat: PropTypes.string.isRequired,
|
||||
close: PropTypes.func.isRequired
|
||||
close: PropTypes.func.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
close = () => {
|
||||
|
@ -54,7 +61,9 @@ export default class ReplyPreview extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { message, Message_TimeFormat } = this.props;
|
||||
const {
|
||||
message, Message_TimeFormat, customEmojis, baseUrl, username
|
||||
} = this.props;
|
||||
const time = moment(message.ts).format(Message_TimeFormat);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
@ -63,9 +72,9 @@ export default class ReplyPreview extends Component {
|
|||
<Text style={styles.username}>{message.u.username}</Text>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
<Markdown msg={message.msg} />
|
||||
<Markdown msg={message.msg} customEmojis={customEmojis} baseUrl={baseUrl} username={username} />
|
||||
</View>
|
||||
<Icon name='close' size={20} style={styles.close} onPress={this.close} />
|
||||
<Icon name='close' color='#9ea2a8' size={20} style={styles.close} onPress={this.close} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -530,6 +530,7 @@ export default class MessageBox extends React.PureComponent {
|
|||
text={item.username || item.name}
|
||||
size={30}
|
||||
type={item.username ? 'd' : 'c'}
|
||||
baseUrl={this.props.baseUrl}
|
||||
/>,
|
||||
<Text key='mention-item-name'>{ item.username || item.name }</Text>
|
||||
]
|
||||
|
@ -556,11 +557,13 @@ export default class MessageBox extends React.PureComponent {
|
|||
};
|
||||
|
||||
renderReplyPreview = () => {
|
||||
const { replyMessage, replying, closeReply } = this.props;
|
||||
const {
|
||||
replyMessage, replying, closeReply, username
|
||||
} = this.props;
|
||||
if (!replying) {
|
||||
return null;
|
||||
}
|
||||
return <ReplyPreview key='reply-preview' message={replyMessage} close={closeReply} />;
|
||||
return <ReplyPreview key='reply-preview' message={replyMessage} close={closeReply} username={username} />;
|
||||
};
|
||||
|
||||
renderFilesActions = () => {
|
||||
|
@ -584,29 +587,30 @@ export default class MessageBox extends React.PureComponent {
|
|||
return (
|
||||
[
|
||||
this.renderMentions(),
|
||||
this.renderReplyPreview(),
|
||||
<View
|
||||
key='messagebox'
|
||||
style={[styles.textArea, this.props.editing && styles.editing]}
|
||||
testID='messagebox'
|
||||
>
|
||||
{this.leftButtons}
|
||||
<TextInput
|
||||
ref={component => this.component = component}
|
||||
style={styles.textBoxInput}
|
||||
returnKeyType='default'
|
||||
keyboardType='twitter'
|
||||
blurOnSubmit={false}
|
||||
placeholder={I18n.t('New_Message')}
|
||||
onChangeText={text => this.onChangeText(text)}
|
||||
value={this.state.text}
|
||||
underlineColorAndroid='transparent'
|
||||
defaultValue=''
|
||||
multiline
|
||||
placeholderTextColor='#9EA2A8'
|
||||
testID='messagebox-input'
|
||||
/>
|
||||
{this.rightButtons}
|
||||
<View style={styles.composer} key='messagebox'>
|
||||
{this.renderReplyPreview()}
|
||||
<View
|
||||
style={[styles.textArea, this.props.editing && styles.editing]}
|
||||
testID='messagebox'
|
||||
>
|
||||
{this.leftButtons}
|
||||
<TextInput
|
||||
ref={component => this.component = component}
|
||||
style={styles.textBoxInput}
|
||||
returnKeyType='default'
|
||||
keyboardType='twitter'
|
||||
blurOnSubmit={false}
|
||||
placeholder={I18n.t('New_Message')}
|
||||
onChangeText={text => this.onChangeText(text)}
|
||||
value={this.state.text}
|
||||
underlineColorAndroid='transparent'
|
||||
defaultValue=''
|
||||
multiline
|
||||
placeholderTextColor='#9EA2A8'
|
||||
testID='messagebox-input'
|
||||
/>
|
||||
{this.rightButtons}
|
||||
</View>
|
||||
</View>
|
||||
]
|
||||
);
|
||||
|
|
|
@ -11,13 +11,17 @@ export default StyleSheet.create({
|
|||
borderTopColor: '#D8D8D8',
|
||||
zIndex: 2
|
||||
},
|
||||
composer: {
|
||||
backgroundColor: '#fff',
|
||||
flexDirection: 'column',
|
||||
borderTopColor: '#e1e5e8',
|
||||
borderTopWidth: 1
|
||||
},
|
||||
textArea: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexGrow: 0,
|
||||
backgroundColor: '#fff',
|
||||
borderTopColor: '#ECECEC',
|
||||
borderTopWidth: 1
|
||||
backgroundColor: '#fff'
|
||||
},
|
||||
textBoxInput: {
|
||||
textAlignVertical: 'center',
|
||||
|
|
|
@ -85,7 +85,8 @@ const keyExtractor = item => item.id;
|
|||
server: state.login.user && state.login.user.server,
|
||||
status: state.login.user && state.login.user.status,
|
||||
username: state.login.user && state.login.user.username
|
||||
}
|
||||
},
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
|
||||
}), dispatch => ({
|
||||
selectServerRequest: server => dispatch(selectServerRequest(server)),
|
||||
logout: () => dispatch(logout()),
|
||||
|
@ -93,6 +94,7 @@ const keyExtractor = item => item.id;
|
|||
}))
|
||||
export default class Sidebar extends Component {
|
||||
static propTypes = {
|
||||
baseUrl: PropTypes.string,
|
||||
navigator: PropTypes.object,
|
||||
server: PropTypes.string.isRequired,
|
||||
selectServerRequest: PropTypes.func.isRequired,
|
||||
|
@ -323,7 +325,7 @@ export default class Sidebar extends Component {
|
|||
)
|
||||
|
||||
render() {
|
||||
const { user, server } = this.props;
|
||||
const { user, server, baseUrl } = this.props;
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
@ -341,6 +343,7 @@ export default class Sidebar extends Component {
|
|||
text={user.username}
|
||||
size={30}
|
||||
style={styles.avatar}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<View style={styles.headerTextContainer}>
|
||||
<View style={styles.headerUsername}>
|
||||
|
|
|
@ -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} />
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
hashtag: node => (
|
||||
<Text key={node.key} onPress={() => alert(`Room #${ node.content }`)} style={styles.mention}>
|
||||
#{node.content}
|
||||
#{node.content}
|
||||
</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
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) }));
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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())));
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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={() => {}}
|
||||
/>
|
||||
)
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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={() => {}}
|
||||
/>
|
||||
)
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -46,7 +46,8 @@ export default StyleSheet.create({
|
|||
flexDirection: 'column'
|
||||
},
|
||||
loading: {
|
||||
flex: 1
|
||||
flex: 1,
|
||||
marginVertical: 15
|
||||
},
|
||||
imageBackground: {
|
||||
width: '100%',
|
||||
|
|
|
@ -7,7 +7,8 @@ import I18n from '../../../i18n';
|
|||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
button: {
|
||||
flexDirection: 'row'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 687 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.1 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 740 B |
After Width: | Height: | Size: 1.6 KiB |