Merge branch 'develop' into new.delete-server

# Conflicts:
#	app/lib/rocketchat.js
This commit is contained in:
Diego Mello 2020-05-04 16:52:26 -03:00
commit 533933373e
138 changed files with 13533 additions and 13336 deletions

View File

@ -0,0 +1,2 @@
export class Rocketchat {}
export const settings = {};

14
__mocks__/expo-av.js Normal file
View File

@ -0,0 +1,14 @@
export class Sound {
loadAsync = () => {};
playAsync = () => {};
pauseAsync = () => {};
stopAsync = () => {};
setOnPlaybackStatusUpdate = () => {};
setPositionAsync = () => {};
}
export const Audio = { Sound };

File diff suppressed because it is too large Load Diff

View File

@ -138,7 +138,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "4.6.0" versionName "4.7.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
} }

View File

@ -31,7 +31,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_SEARCH_HEADER', 'OPEN_SEARCH_HEADER',
'CLOSE_SEARCH_HEADER' 'CLOSE_SEARCH_HEADER'
]); ]);
export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'DELETE', 'REMOVED', 'USER_TYPING']); export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'USER_TYPING']);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']); export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']); export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);

View File

@ -1,5 +1,19 @@
import * as types from './actionsTypes'; import * as types from './actionsTypes';
export function subscribeRoom(rid) {
return {
type: types.ROOM.SUBSCRIBE,
rid
};
}
export function unsubscribeRoom(rid) {
return {
type: types.ROOM.UNSUBSCRIBE,
rid
};
}
export function leaveRoom(rid, t) { export function leaveRoom(rid, t) {
return { return {
type: types.ROOM.LEAVE, type: types.ROOM.LEAVE,

View File

@ -100,7 +100,7 @@ export const themes = {
infoText: '#6d6d72', infoText: '#6d6d72',
tintColor: '#1e9bfe', tintColor: '#1e9bfe',
auxiliaryTintColor: '#cdcdcd', auxiliaryTintColor: '#cdcdcd',
actionTintColor: '#1ea1fe', actionTintColor: '#1e9bfe',
separatorColor: '#272728', separatorColor: '#272728',
navbarBackground: '#0d0d0d', navbarBackground: '#0d0d0d',
headerBorder: '#323232', headerBorder: '#323232',

View File

@ -125,6 +125,9 @@ export default {
uniqueID: { uniqueID: {
type: 'valueAsString' type: 'valueAsString'
}, },
UI_Allow_room_names_with_special_chars: {
type: 'valueAsBoolean'
},
UI_Use_Real_Name: { UI_Use_Real_Name: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },

View File

@ -2,12 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View } from 'react-native'; import { View } from 'react-native';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk'; import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import Touch from '../utils/touch';
import { avatarURL } from '../utils/avatar'; import { avatarURL } from '../utils/avatar';
const Avatar = React.memo(({ const Avatar = React.memo(({
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress, theme text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress
}) => { }) => {
const avatarStyle = { const avatarStyle = {
width: size, width: size,
@ -36,9 +37,9 @@ const Avatar = React.memo(({
if (onPress) { if (onPress) {
image = ( image = (
<Touch onPress={onPress} theme={theme}> <Touchable onPress={onPress}>
{image} {image}
</Touch> </Touchable>
); );
} }
@ -61,7 +62,6 @@ Avatar.propTypes = {
children: PropTypes.object, children: PropTypes.object,
userId: PropTypes.string, userId: PropTypes.string,
token: PropTypes.string, token: PropTypes.string,
theme: PropTypes.string,
onPress: PropTypes.func onPress: PropTypes.func
}; };

View File

@ -13,9 +13,10 @@ const styles = StyleSheet.create({
} }
}); });
const Check = React.memo(({ theme }) => <CustomIcon style={styles.icon} color={themes[theme].tintColor} size={22} name='check' />); const Check = React.memo(({ theme, style }) => <CustomIcon style={[styles.icon, style]} color={themes[theme].tintColor} size={22} name='check' />);
Check.propTypes = { Check.propTypes = {
style: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
}; };

View File

@ -52,7 +52,7 @@ const Button = React.memo(({
onPress, ...props onPress, ...props
}) => ( }) => (
<Touch <Touch
onPress={onPress} onPress={() => onPress(props.title)}
style={{ backgroundColor: themes[props.theme].backgroundColor }} style={{ backgroundColor: themes[props.theme].backgroundColor }}
enabled={!props.disabled} enabled={!props.disabled}
theme={props.theme} theme={props.theme}
@ -89,6 +89,7 @@ Content.propTypes = {
}; };
Button.propTypes = { Button.propTypes = {
title: PropTypes.string,
onPress: PropTypes.func, onPress: PropTypes.func,
disabled: PropTypes.bool, disabled: PropTypes.bool,
theme: PropTypes.string theme: PropTypes.string

View File

@ -64,7 +64,7 @@ const MentionItem = ({
content = ( content = (
<> <>
<Text style={[styles.slash, { backgroundColor: themes[theme].borderColor, color: themes[theme].tintColor }]}>/</Text> <Text style={[styles.slash, { backgroundColor: themes[theme].borderColor, color: themes[theme].tintColor }]}>/</Text>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.command}</Text> <Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.id}</Text>
</> </>
); );
} }

View File

@ -59,7 +59,8 @@ export default class extends React.PureComponent {
SampleRate: 22050, SampleRate: 22050,
Channels: 1, Channels: 1,
AudioQuality: 'Low', AudioQuality: 'Low',
AudioEncoding: 'aac' AudioEncoding: 'aac',
OutputFormat: 'aac_adts'
}); });
AudioRecorder.onProgress = (data) => { AudioRecorder.onProgress = (data) => {

View File

@ -48,6 +48,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
if (!_.isEmpty(data)) { if (!_.isEmpty(data)) {
setCode('');
setVisible(true); setVisible(true);
} else { } else {
setVisible(false); setVisible(false);
@ -94,7 +95,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
<View style={styles.container}> <View style={styles.container}>
<View style={[styles.content, split && [sharedStyles.modal, sharedStyles.modalFormSheet], { backgroundColor: themes[theme].backgroundColor }]}> <View style={[styles.content, split && [sharedStyles.modal, sharedStyles.modalFormSheet], { backgroundColor: themes[theme].backgroundColor }]}>
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text> <Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text>
<Text style={[styles.subtitle, { color }]}>{I18n.t(method?.text)}</Text> {method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
<TextInput <TextInput
value={code} value={code}
theme={theme} theme={theme}

View File

@ -15,11 +15,12 @@ export default StyleSheet.create({
}, },
title: { title: {
fontSize: 14, fontSize: 14,
paddingBottom: 8,
...sharedStyles.textBold ...sharedStyles.textBold
}, },
subtitle: { subtitle: {
fontSize: 14, fontSize: 14,
paddingVertical: 8, paddingBottom: 8,
...sharedStyles.textRegular, ...sharedStyles.textRegular,
...sharedStyles.textAlignCenter ...sharedStyles.textAlignCenter
}, },

View File

@ -43,14 +43,14 @@ export const MultiSelect = React.memo(({
inputStyle, inputStyle,
theme theme
}) => { }) => {
const [selected, select] = useState(values || []); const [selected, select] = useState(Array.isArray(values) ? values : []);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [search, onSearchChange] = useState(''); const [search, onSearchChange] = useState('');
const [currentValue, setCurrentValue] = useState(''); const [currentValue, setCurrentValue] = useState('');
const [showContent, setShowContent] = useState(false); const [showContent, setShowContent] = useState(false);
useEffect(() => { useEffect(() => {
if (values) { if (Array.isArray(values)) {
select(values); select(values);
} }
}, [values]); }, [values]);

View File

@ -8,7 +8,7 @@ import Video from './Video';
import Reply from './Reply'; import Reply from './Reply';
const Attachments = React.memo(({ const Attachments = React.memo(({
attachments, timeFormat, user, baseUrl, showAttachment, getCustomEmoji, theme attachments, timeFormat, showAttachment, getCustomEmoji, theme
}) => { }) => {
if (!attachments || attachments.length === 0) { if (!attachments || attachments.length === 0) {
return null; return null;
@ -16,25 +16,23 @@ const Attachments = React.memo(({
return attachments.map((file, index) => { return attachments.map((file, index) => {
if (file.image_url) { if (file.image_url) {
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />; return <Image key={file.image_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
} }
if (file.audio_url) { if (file.audio_url) {
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />; return <Audio key={file.audio_url} file={file} getCustomEmoji={getCustomEmoji} theme={theme} />;
} }
if (file.video_url) { if (file.video_url) {
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />; return <Video key={file.video_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
} }
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />; return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} theme={theme} />;
}); });
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme); }, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme);
Attachments.propTypes = { Attachments.propTypes = {
attachments: PropTypes.array, attachments: PropTypes.array,
timeFormat: PropTypes.string, timeFormat: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string,
showAttachment: PropTypes.func, showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string

View File

@ -3,18 +3,30 @@ import PropTypes from 'prop-types';
import { import {
View, StyleSheet, Text, Easing, Dimensions View, StyleSheet, Text, Easing, Dimensions
} from 'react-native'; } from 'react-native';
import Video from 'react-native-video'; import { Audio } from 'expo-av';
import Slider from '@react-native-community/slider'; import Slider from '@react-native-community/slider';
import moment from 'moment'; import moment from 'moment';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { isAndroid, isIOS } from '../../utils/deviceInfo'; import { isAndroid, isIOS } from '../../utils/deviceInfo';
import { withSplit } from '../../split'; import { withSplit } from '../../split';
import MessageContext from './Context';
import ActivityIndicator from '../ActivityIndicator';
const mode = {
allowsRecordingIOS: false,
playsInSilentModeIOS: true,
staysActiveInBackground: false,
shouldDuckAndroid: true,
playThroughEarpieceAndroid: false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
audioContainer: { audioContainer: {
@ -31,6 +43,9 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
backgroundColor: 'transparent' backgroundColor: 'transparent'
}, },
audioLoading: {
marginHorizontal: 8
},
slider: { slider: {
flex: 1 flex: 1
}, },
@ -51,29 +66,36 @@ const sliderAnimationConfig = {
delay: 0 delay: 0
}; };
const Button = React.memo(({ paused, onPress, theme }) => ( const Button = React.memo(({
loading, paused, onPress, theme
}) => (
<Touchable <Touchable
style={styles.playPauseButton} style={styles.playPauseButton}
onPress={onPress} onPress={onPress}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()} background={Touchable.SelectableBackgroundBorderless()}
> >
<CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} /> {
loading
? <ActivityIndicator style={[styles.playPauseButton, styles.audioLoading]} theme={theme} />
: <CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} />
}
</Touchable> </Touchable>
)); ));
Button.propTypes = { Button.propTypes = {
loading: PropTypes.bool,
paused: PropTypes.bool, paused: PropTypes.bool,
theme: PropTypes.string, theme: PropTypes.string,
onPress: PropTypes.func onPress: PropTypes.func
}; };
Button.displayName = 'MessageAudioButton'; Button.displayName = 'MessageAudioButton';
class Audio extends React.Component { class MessageAudio extends React.Component {
static contextType = MessageContext;
static propTypes = { static propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool, split: PropTypes.bool,
getCustomEmoji: PropTypes.func getCustomEmoji: PropTypes.func
@ -81,18 +103,34 @@ class Audio extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const { baseUrl, file, user } = props;
this.state = { this.state = {
loading: false,
currentTime: 0, currentTime: 0,
duration: 0, duration: 0,
paused: true, paused: true
uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
}; };
this.sound = new Audio.Sound();
this.sound.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate);
}
async componentDidMount() {
const { file } = this.props;
const { baseUrl, user } = this.context;
this.setState({ loading: true });
try {
await Audio.setAudioModeAsync(mode);
await this.sound.loadAsync({ uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }` });
} catch {
// Do nothing
}
this.setState({ loading: false });
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { const {
currentTime, duration, paused, uri currentTime, duration, paused, loading
} = this.state; } = this.state;
const { file, split, theme } = this.props; const { file, split, theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
@ -107,58 +145,99 @@ class Audio extends React.Component {
if (nextState.paused !== paused) { if (nextState.paused !== paused) {
return true; return true;
} }
if (nextState.uri !== uri) {
return true;
}
if (!equal(nextProps.file, file)) { if (!equal(nextProps.file, file)) {
return true; return true;
} }
if (nextProps.split !== split) { if (nextProps.split !== split) {
return true; return true;
} }
if (nextState.loading !== loading) {
return true;
}
return false; return false;
} }
async componentWillUnmount() {
try {
await this.sound.stopAsync();
} catch {
// Do nothing
}
}
onPlaybackStatusUpdate = (status) => {
if (status) {
this.onLoad(status);
this.onProgress(status);
this.onEnd(status);
}
}
onLoad = (data) => { onLoad = (data) => {
this.setState({ duration: data.duration > 0 ? data.duration : 0 }); const duration = data.durationMillis / 1000;
this.setState({ duration: duration > 0 ? duration : 0 });
} }
onProgress = (data) => { onProgress = (data) => {
const { duration } = this.state; const { duration } = this.state;
if (data.currentTime <= duration) { const currentTime = data.positionMillis / 1000;
this.setState({ currentTime: data.currentTime }); if (currentTime <= duration) {
this.setState({ currentTime });
} }
} }
onEnd = () => { onEnd = async(data) => {
if (data.didJustFinish) {
try {
await this.sound.stopAsync();
this.setState({ paused: true, currentTime: 0 }); this.setState({ paused: true, currentTime: 0 });
requestAnimationFrame(() => { } catch {
this.player.seek(0); // do nothing
}); }
}
} }
get duration() { get duration() {
const { duration } = this.state; const { currentTime, duration } = this.state;
return formatTime(duration); return formatTime(currentTime || duration);
} }
setRef = ref => this.player = ref;
togglePlayPause = () => { togglePlayPause = () => {
const { paused } = this.state; const { paused } = this.state;
this.setState({ paused: !paused }); this.setState({ paused: !paused }, this.playPause);
} }
onValueChange = value => this.setState({ currentTime: value }); playPause = async() => {
const { paused } = this.state;
try {
if (paused) {
await this.sound.pauseAsync();
} else {
await this.sound.playAsync();
}
} catch {
// Do nothing
}
}
onValueChange = async(value) => {
try {
this.setState({ currentTime: value });
await this.sound.setPositionAsync(value * 1000);
} catch {
// Do nothing
}
}
render() { render() {
const { const {
uri, paused, currentTime, duration loading, paused, currentTime, duration
} = this.state; } = this.state;
const { const {
user, baseUrl, file, getCustomEmoji, split, theme file, getCustomEmoji, split, theme
} = this.props; } = this.props;
const { description } = file; const { description } = file;
const { baseUrl, user } = this.context;
if (!baseUrl) { if (!baseUrl) {
return null; return null;
@ -173,17 +252,7 @@ class Audio extends React.Component {
split && sharedStyles.tabletContent split && sharedStyles.tabletContent
]} ]}
> >
<Video <Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
ref={this.setRef}
source={{ uri }}
onLoad={this.onLoad}
onProgress={this.onProgress}
onEnd={this.onEnd}
paused={paused}
repeat={false}
ignoreSilentSwitch='ignore'
/>
<Button paused={paused} onPress={this.togglePlayPause} theme={theme} />
<Slider <Slider
style={styles.slider} style={styles.slider}
value={currentTime} value={currentTime}
@ -205,4 +274,4 @@ class Audio extends React.Component {
} }
} }
export default withSplit(Audio); export default withSplit(MessageAudio);

View File

@ -6,8 +6,7 @@ const Blocks = React.memo(({
blocks, id: mid, rid, blockAction blocks, id: mid, rid, blockAction
}) => { }) => {
if (blocks && blocks.length > 0) { if (blocks && blocks.length > 0) {
const [, secondBlock] = blocks; const appId = blocks[0]?.appId || '';
const { appId = '' } = secondBlock;
return React.createElement( return React.createElement(
messageBlockWithContext({ messageBlockWithContext({
action: async({ actionId, value, blockId }) => { action: async({ actionId, value, blockId }) => {

View File

@ -1,17 +1,19 @@
import React from 'react'; import React, { useContext } from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Broadcast = React.memo(({ const Broadcast = React.memo(({
author, user, broadcast, replyBroadcast, theme author, broadcast, theme
}) => { }) => {
const { user, replyBroadcast } = useContext(MessageContext);
const isOwn = author._id === user.id; const isOwn = author._id === user.id;
if (broadcast && !isOwn) { if (broadcast && !isOwn) {
return ( return (
@ -36,10 +38,8 @@ const Broadcast = React.memo(({
Broadcast.propTypes = { Broadcast.propTypes = {
author: PropTypes.object, author: PropTypes.object,
user: PropTypes.object,
broadcast: PropTypes.bool, broadcast: PropTypes.bool,
theme: PropTypes.string, theme: PropTypes.string
replyBroadcast: PropTypes.func
}; };
Broadcast.displayName = 'MessageBroadcast'; Broadcast.displayName = 'MessageBroadcast';

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { formatLastMessage, BUTTON_HIT_SLOP } from './utils'; import { formatLastMessage, BUTTON_HIT_SLOP } from './utils';
import styles from './styles'; import styles from './styles';
import I18n from '../../i18n'; import I18n from '../../i18n';

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useContext } from 'react';
import { Text, View } from 'react-native'; import { Text, View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import equal from 'deep-equal'; import equal from 'deep-equal';
@ -8,6 +8,7 @@ import styles from './styles';
import Markdown from '../markdown'; import Markdown from '../markdown';
import { getInfoMessage } from './utils'; import { getInfoMessage } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Content = React.memo((props) => { const Content = React.memo((props) => {
if (props.isInfo) { if (props.isInfo) {
@ -26,12 +27,13 @@ const Content = React.memo((props) => {
if (props.tmid && !props.msg) { if (props.tmid && !props.msg) {
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>; content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
} else { } else {
const { baseUrl, user } = useContext(MessageContext);
content = ( content = (
<Markdown <Markdown
msg={props.msg} msg={props.msg}
baseUrl={props.baseUrl} baseUrl={baseUrl}
getCustomEmoji={props.getCustomEmoji} getCustomEmoji={props.getCustomEmoji}
username={props.user.username} username={user.username}
isEdited={props.isEdited} isEdited={props.isEdited}
numberOfLines={(props.tmid && !props.isThreadRoom) ? 1 : 0} numberOfLines={(props.tmid && !props.isThreadRoom) ? 1 : 0}
preview={props.tmid && !props.isThreadRoom} preview={props.tmid && !props.isThreadRoom}
@ -77,8 +79,6 @@ Content.propTypes = {
msg: PropTypes.string, msg: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
isEdited: PropTypes.bool, isEdited: PropTypes.bool,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),

View File

@ -0,0 +1,4 @@
import React from 'react';
const MessageContext = React.createContext();
export default MessageContext;

View File

@ -1,20 +1,22 @@
import React from 'react'; import React, { useContext } from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils'; import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
import styles from './styles'; import styles from './styles';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { DISCUSSION } from './constants'; import { DISCUSSION } from './constants';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Discussion = React.memo(({ const Discussion = React.memo(({
msg, dcount, dlm, onDiscussionPress, theme msg, dcount, dlm, theme
}) => { }) => {
const time = formatLastMessage(dlm); const time = formatLastMessage(dlm);
const buttonText = formatMessageCount(dcount, DISCUSSION); const buttonText = formatMessageCount(dcount, DISCUSSION);
const { onDiscussionPress } = useContext(MessageContext);
return ( return (
<> <>
<Text style={[styles.startedDiscussion, { color: themes[theme].auxiliaryText }]}>{I18n.t('Started_discussion')}</Text> <Text style={[styles.startedDiscussion, { color: themes[theme].auxiliaryText }]}>{I18n.t('Started_discussion')}</Text>
@ -55,8 +57,7 @@ Discussion.propTypes = {
msg: PropTypes.string, msg: PropTypes.string,
dcount: PropTypes.number, dcount: PropTypes.number,
dlm: PropTypes.string, dlm: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string
onDiscussionPress: PropTypes.func
}; };
Discussion.displayName = 'MessageDiscussion'; Discussion.displayName = 'MessageDiscussion';

View File

@ -6,7 +6,7 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
const Emoji = React.memo(({ const Emoji = React.memo(({
content, standardEmojiStyle, customEmojiStyle, baseUrl, getCustomEmoji content, baseUrl, standardEmojiStyle, customEmojiStyle, getCustomEmoji
}) => { }) => {
const parsedContent = content.replace(/^:|:$/g, ''); const parsedContent = content.replace(/^:|:$/g, '');
const emoji = getCustomEmoji(parsedContent); const emoji = getCustomEmoji(parsedContent);
@ -18,9 +18,9 @@ const Emoji = React.memo(({
Emoji.propTypes = { Emoji.propTypes = {
content: PropTypes.string, content: PropTypes.string,
baseUrl: PropTypes.string,
standardEmojiStyle: PropTypes.object, standardEmojiStyle: PropTypes.object,
customEmojiStyle: PropTypes.object, customEmojiStyle: PropTypes.object,
baseUrl: PropTypes.string,
getCustomEmoji: PropTypes.func getCustomEmoji: PropTypes.func
}; };
Emoji.displayName = 'MessageEmoji'; Emoji.displayName = 'MessageEmoji';

View File

@ -1,18 +1,19 @@
import React from 'react'; import React, { useContext } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import { createImageProgress } from 'react-native-image-progress'; import { createImageProgress } from 'react-native-image-progress';
import * as Progress from 'react-native-progress'; import * as Progress from 'react-native-progress';
import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import styles from './styles'; import styles from './styles';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { withSplit } from '../../split'; import { withSplit } from '../../split';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import MessageContext from './Context';
const ImageProgress = createImageProgress(FastImage); const ImageProgress = createImageProgress(FastImage);
@ -41,8 +42,9 @@ export const MessageImage = React.memo(({ img, theme }) => (
)); ));
const ImageContainer = React.memo(({ const ImageContainer = React.memo(({
file, imageUrl, baseUrl, user, showAttachment, getCustomEmoji, split, theme file, imageUrl, showAttachment, getCustomEmoji, split, theme
}) => { }) => {
const { baseUrl, user } = useContext(MessageContext);
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl); const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
if (!img) { if (!img) {
return null; return null;
@ -71,8 +73,6 @@ const ImageContainer = React.memo(({
ImageContainer.propTypes = { ImageContainer.propTypes = {
file: PropTypes.object, file: PropTypes.object,
imageUrl: PropTypes.string, imageUrl: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
showAttachment: PropTypes.func, showAttachment: PropTypes.func,
theme: PropTypes.string, theme: PropTypes.string,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,

View File

@ -1,8 +1,10 @@
import React from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View } from 'react-native'; import { View } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import MessageContext from './Context';
import User from './User'; import User from './User';
import styles from './styles'; import styles from './styles';
import RepliedThread from './RepliedThread'; import RepliedThread from './RepliedThread';
@ -111,10 +113,11 @@ const MessageTouchable = React.memo((props) => {
</View> </View>
); );
} }
const { onPress, onLongPress } = useContext(MessageContext);
return ( return (
<Touchable <Touchable
onLongPress={props.onLongPress} onLongPress={onLongPress}
onPress={props.onPress} onPress={onPress}
disabled={props.isInfo || props.archived || props.isTemp} disabled={props.isInfo || props.archived || props.isTemp}
> >
<View> <View>
@ -129,9 +132,7 @@ MessageTouchable.propTypes = {
hasError: PropTypes.bool, hasError: PropTypes.bool,
isInfo: PropTypes.bool, isInfo: PropTypes.bool,
isTemp: PropTypes.bool, isTemp: PropTypes.bool,
archived: PropTypes.bool, archived: PropTypes.bool
onLongPress: PropTypes.func,
onPress: PropTypes.func
}; };
Message.propTypes = { Message.propTypes = {
@ -143,7 +144,6 @@ Message.propTypes = {
hasError: PropTypes.bool, hasError: PropTypes.bool,
style: PropTypes.any, style: PropTypes.any,
onLongPress: PropTypes.func, onLongPress: PropTypes.func,
onPress: PropTypes.func,
isReadReceiptEnabled: PropTypes.bool, isReadReceiptEnabled: PropTypes.bool,
unread: PropTypes.bool, unread: PropTypes.bool,
theme: PropTypes.string theme: PropTypes.string

View File

@ -1,34 +1,31 @@
import React from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TouchableOpacity } from 'react-native';
import Avatar from '../Avatar'; import Avatar from '../Avatar';
import styles from './styles'; import styles from './styles';
import MessageContext from './Context';
const MessageAvatar = React.memo(({ const MessageAvatar = React.memo(({
isHeader, avatar, author, baseUrl, user, small, navToRoomInfo isHeader, avatar, author, small, navToRoomInfo
}) => { }) => {
const { baseUrl, user } = useContext(MessageContext);
if (isHeader && author) { if (isHeader && author) {
const navParam = { const navParam = {
t: 'd', t: 'd',
rid: author._id rid: author._id
}; };
return ( return (
<TouchableOpacity
onPress={() => navToRoomInfo(navParam)}
disabled={author._id === user.id}
>
<Avatar <Avatar
style={small ? styles.avatarSmall : styles.avatar} style={small ? styles.avatarSmall : styles.avatar}
text={avatar ? '' : author.username} text={avatar ? '' : author.username}
size={small ? 20 : 36} size={small ? 20 : 36}
borderRadius={small ? 2 : 4} borderRadius={small ? 2 : 4}
onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)}
avatar={avatar} avatar={avatar}
baseUrl={baseUrl} baseUrl={baseUrl}
userId={user.id} userId={user.id}
token={user.token} token={user.token}
/> />
</TouchableOpacity>
); );
} }
return null; return null;
@ -38,8 +35,6 @@ MessageAvatar.propTypes = {
isHeader: PropTypes.bool, isHeader: PropTypes.bool,
avatar: PropTypes.string, avatar: PropTypes.string,
author: PropTypes.obj, author: PropTypes.obj,
baseUrl: PropTypes.string,
user: PropTypes.obj,
small: PropTypes.bool, small: PropTypes.bool,
navToRoomInfo: PropTypes.func navToRoomInfo: PropTypes.func
}; };

View File

@ -1,16 +1,18 @@
import React from 'react'; import React, { useContext } from 'react';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context';
const MessageError = React.memo(({ hasError, onErrorPress, theme }) => { const MessageError = React.memo(({ hasError, theme }) => {
if (!hasError) { if (!hasError) {
return null; return null;
} }
const { onErrorPress } = useContext(MessageContext);
return ( return (
<Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}> <Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}>
<CustomIcon name='warning' color={themes[theme].dangerColor} size={18} /> <CustomIcon name='warning' color={themes[theme].dangerColor} size={18} />
@ -20,7 +22,6 @@ const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
MessageError.propTypes = { MessageError.propTypes = {
hasError: PropTypes.bool, hasError: PropTypes.bool,
onErrorPress: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
MessageError.displayName = 'MessageError'; MessageError.displayName = 'MessageError';

View File

@ -1,16 +1,19 @@
import React from 'react'; import React, { useContext } from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import Emoji from './Emoji'; import Emoji from './Emoji';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import MessageContext from './Context';
const AddReaction = React.memo(({ reactionInit, theme }) => ( const AddReaction = React.memo(({ theme }) => {
const { reactionInit } = useContext(MessageContext);
return (
<Touchable <Touchable
onPress={reactionInit} onPress={reactionInit}
key='message-add-reaction' key='message-add-reaction'
@ -23,11 +26,15 @@ const AddReaction = React.memo(({ reactionInit, theme }) => (
<CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} /> <CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} />
</View> </View>
</Touchable> </Touchable>
)); );
});
const Reaction = React.memo(({ const Reaction = React.memo(({
reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji, theme reaction, getCustomEmoji, theme
}) => { }) => {
const {
onReactionPress, onReactionLongPress, baseUrl, user
} = useContext(MessageContext);
const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1; const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1;
return ( return (
<Touchable <Touchable
@ -54,7 +61,7 @@ const Reaction = React.memo(({
}); });
const Reactions = React.memo(({ const Reactions = React.memo(({
reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji, theme reactions, getCustomEmoji, theme
}) => { }) => {
if (!Array.isArray(reactions) || reactions.length === 0) { if (!Array.isArray(reactions) || reactions.length === 0) {
return null; return null;
@ -65,25 +72,17 @@ const Reactions = React.memo(({
<Reaction <Reaction
key={reaction.emoji} key={reaction.emoji}
reaction={reaction} reaction={reaction}
user={user}
baseUrl={baseUrl}
onReactionLongPress={onReactionLongPress}
onReactionPress={onReactionPress}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}
/> />
))} ))}
<AddReaction reactionInit={reactionInit} theme={theme} /> <AddReaction theme={theme} />
</View> </View>
); );
}); });
Reaction.propTypes = { Reaction.propTypes = {
reaction: PropTypes.object, reaction: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
@ -91,18 +90,12 @@ Reaction.displayName = 'MessageReaction';
Reactions.propTypes = { Reactions.propTypes = {
reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
reactionInit: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
Reactions.displayName = 'MessageReactions'; Reactions.displayName = 'MessageReactions';
AddReaction.propTypes = { AddReaction.propTypes = {
reactionInit: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
AddReaction.displayName = 'MessageAddReaction'; AddReaction.displayName = 'MessageAddReaction';

View File

@ -1,15 +1,16 @@
import React from 'react'; import React, { useContext } from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal'; import isEqual from 'deep-equal';
import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withSplit } from '../../split'; import { withSplit } from '../../split';
import MessageContext from './Context';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -79,12 +80,13 @@ const Title = React.memo(({ attachment, timeFormat, theme }) => {
}); });
const Description = React.memo(({ const Description = React.memo(({
attachment, baseUrl, user, getCustomEmoji, theme attachment, getCustomEmoji, theme
}) => { }) => {
const text = attachment.text || attachment.title; const text = attachment.text || attachment.title;
if (!text) { if (!text) {
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext);
return ( return (
<Markdown <Markdown
msg={text} msg={text}
@ -124,11 +126,12 @@ const Fields = React.memo(({ attachment, theme }) => {
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme); }, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme);
const Reply = React.memo(({ const Reply = React.memo(({
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, split, theme attachment, timeFormat, index, getCustomEmoji, split, theme
}) => { }) => {
if (!attachment) { if (!attachment) {
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext);
const onPress = () => { const onPress = () => {
let url = attachment.title_link || attachment.author_link; let url = attachment.title_link || attachment.author_link;
@ -160,8 +163,6 @@ const Reply = React.memo(({
<Description <Description
attachment={attachment} attachment={attachment}
timeFormat={timeFormat} timeFormat={timeFormat}
baseUrl={baseUrl}
user={user}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}
/> />
@ -174,8 +175,6 @@ const Reply = React.memo(({
Reply.propTypes = { Reply.propTypes = {
attachment: PropTypes.object, attachment: PropTypes.object,
timeFormat: PropTypes.string, timeFormat: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
index: PropTypes.number, index: PropTypes.number,
theme: PropTypes.string, theme: PropTypes.string,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
@ -192,8 +191,6 @@ Title.displayName = 'MessageReplyTitle';
Description.propTypes = { Description.propTypes = {
attachment: PropTypes.object, attachment: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };

View File

@ -0,0 +1,25 @@
import React, { useContext } from 'react';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import MessageContext from './Context';
const RCTouchable = React.memo(({ children, ...props }) => {
const { onLongPress } = useContext(MessageContext);
return (
<Touchable
onLongPress={onLongPress}
{...props}
>
{children}
</Touchable>
);
});
RCTouchable.propTypes = {
children: PropTypes.node
};
RCTouchable.Ripple = (...args) => Touchable.Ripple(...args);
RCTouchable.SelectableBackgroundBorderless = () => Touchable.SelectableBackgroundBorderless();
export default RCTouchable;

View File

@ -1,12 +1,12 @@
import React from 'react'; import React, { useContext } from 'react';
import { import {
View, Text, StyleSheet, Clipboard View, Text, StyleSheet, Clipboard
} from 'react-native'; } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import Touchable from './Touchable';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -15,6 +15,7 @@ import { withSplit } from '../../split';
import { LISTENER } from '../Toast'; import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import I18n from '../../i18n'; import I18n from '../../i18n';
import MessageContext from './Context';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -52,10 +53,11 @@ const styles = StyleSheet.create({
} }
}); });
const UrlImage = React.memo(({ image, user, baseUrl }) => { const UrlImage = React.memo(({ image }) => {
if (!image) { if (!image) {
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext);
image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`; image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`;
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />; return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
}, (prevProps, nextProps) => prevProps.image === nextProps.image); }, (prevProps, nextProps) => prevProps.image === nextProps.image);
@ -79,7 +81,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
}); });
const Url = React.memo(({ const Url = React.memo(({
url, index, user, baseUrl, split, theme url, index, split, theme
}) => { }) => {
if (!url) { if (!url) {
return null; return null;
@ -109,7 +111,7 @@ const Url = React.memo(({
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
> >
<> <>
<UrlImage image={url.image} user={user} baseUrl={baseUrl} /> <UrlImage image={url.image} />
<UrlContent title={url.title} description={url.description} theme={theme} /> <UrlContent title={url.title} description={url.description} theme={theme} />
</> </>
</Touchable> </Touchable>
@ -117,21 +119,19 @@ const Url = React.memo(({
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme); }, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
const Urls = React.memo(({ const Urls = React.memo(({
urls, user, baseUrl, split, theme urls, split, theme
}) => { }) => {
if (!urls || urls.length === 0) { if (!urls || urls.length === 0) {
return null; return null;
} }
return urls.map((url, index) => ( return urls.map((url, index) => (
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} split={split} theme={theme} /> <Url url={url} key={url.url} index={index} split={split} theme={theme} />
)); ));
}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.split === newProps.split && oldProps.theme === newProps.theme); }, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
UrlImage.propTypes = { UrlImage.propTypes = {
image: PropTypes.string, image: PropTypes.string
user: PropTypes.object,
baseUrl: PropTypes.string
}; };
UrlImage.displayName = 'MessageUrlImage'; UrlImage.displayName = 'MessageUrlImage';
@ -145,8 +145,6 @@ UrlContent.displayName = 'MessageUrlContent';
Url.propTypes = { Url.propTypes = {
url: PropTypes.object.isRequired, url: PropTypes.object.isRequired,
index: PropTypes.number, index: PropTypes.number,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool split: PropTypes.bool
}; };
@ -154,8 +152,6 @@ Url.displayName = 'MessageUrl';
Urls.propTypes = { Urls.propTypes = {
urls: PropTypes.array, urls: PropTypes.array,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool split: PropTypes.bool
}; };

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
View, Text, StyleSheet, TouchableOpacity View, Text, StyleSheet, TouchableOpacity
@ -11,6 +11,7 @@ import { withTheme } from '../../theme';
import MessageError from './MessageError'; import MessageError from './MessageError';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import messageStyles from './styles'; import messageStyles from './styles';
import MessageContext from './Context';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -35,13 +36,14 @@ const styles = StyleSheet.create({
}); });
const User = React.memo(({ const User = React.memo(({
isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, user, ...props isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, ...props
}) => { }) => {
if (isHeader || hasError) { if (isHeader || hasError) {
const navParam = { const navParam = {
t: 'd', t: 'd',
rid: author._id rid: author._id
}; };
const { user } = useContext(MessageContext);
const username = (useRealName && author.name) || author.username; const username = (useRealName && author.name) || author.username;
const aliasUsername = alias ? (<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>) : null; const aliasUsername = alias ? (<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>) : null;
const time = moment(ts).format(timeFormat); const time = moment(ts).format(timeFormat);
@ -49,15 +51,14 @@ const User = React.memo(({
return ( return (
<View style={styles.container}> <View style={styles.container}>
<TouchableOpacity <TouchableOpacity
style={styles.titleContainer}
onPress={() => navToRoomInfo(navParam)} onPress={() => navToRoomInfo(navParam)}
disabled={author._id === user.id} disabled={author._id === user.id}
> >
<View style={styles.titleContainer}>
<Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}> <Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}>
{alias || username} {alias || username}
{aliasUsername} {aliasUsername}
</Text> </Text>
</View>
</TouchableOpacity> </TouchableOpacity>
<Text style={[messageStyles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text> <Text style={[messageStyles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
{ hasError && <MessageError hasError={hasError} theme={theme} {...props} /> } { hasError && <MessageError hasError={hasError} theme={theme} {...props} /> }
@ -76,7 +77,6 @@ User.propTypes = {
ts: PropTypes.instanceOf(Date), ts: PropTypes.instanceOf(Date),
timeFormat: PropTypes.string, timeFormat: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
user: PropTypes.obj,
navToRoomInfo: PropTypes.func navToRoomInfo: PropTypes.func
}; };
User.displayName = 'MessageUser'; User.displayName = 'MessageUser';

View File

@ -1,9 +1,9 @@
import React from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal'; import isEqual from 'deep-equal';
import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../../utils/deviceInfo';
@ -11,6 +11,7 @@ import { CustomIcon } from '../../lib/Icons';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import MessageContext from './Context';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])]; const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1; const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
@ -27,12 +28,12 @@ const styles = StyleSheet.create({
}); });
const Video = React.memo(({ const Video = React.memo(({
file, baseUrl, user, showAttachment, getCustomEmoji, theme file, showAttachment, getCustomEmoji, theme
}) => { }) => {
const { baseUrl, user } = useContext(MessageContext);
if (!baseUrl) { if (!baseUrl) {
return null; return null;
} }
const onPress = () => { const onPress = () => {
if (isTypeSupported(file.video_type)) { if (isTypeSupported(file.video_type)) {
return showAttachment(file); return showAttachment(file);
@ -61,8 +62,6 @@ const Video = React.memo(({
Video.propTypes = { Video.propTypes = {
file: PropTypes.object, file: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
showAttachment: PropTypes.func, showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { KeyboardUtils } from 'react-native-keyboard-input'; import { KeyboardUtils } from 'react-native-keyboard-input';
import Message from './Message'; import Message from './Message';
import MessageContext from './Context';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils'; import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
import messagesStatus from '../../constants/messagesStatus'; import messagesStatus from '../../constants/messagesStatus';
@ -240,6 +241,20 @@ class MessageContainer extends React.Component {
} }
return ( return (
<MessageContext.Provider
value={{
user,
baseUrl,
onPress: this.onPress,
onLongPress: this.onLongPress,
reactionInit: this.reactionInit,
onErrorPress: this.onErrorPress,
replyBroadcast: this.replyBroadcast,
onReactionPress: this.onReactionPress,
onDiscussionPress: this.onDiscussionPress,
onReactionLongPress: this.onReactionLongPress
}}
>
<Message <Message
id={id} id={id}
msg={message} msg={message}
@ -253,13 +268,11 @@ class MessageContainer extends React.Component {
reactions={reactions} reactions={reactions}
alias={alias} alias={alias}
avatar={avatar} avatar={avatar}
user={user}
timeFormat={timeFormat} timeFormat={timeFormat}
customThreadTimeFormat={customThreadTimeFormat} customThreadTimeFormat={customThreadTimeFormat}
style={style} style={style}
archived={archived} archived={archived}
broadcast={broadcast} broadcast={broadcast}
baseUrl={baseUrl}
useRealName={useRealName} useRealName={useRealName}
isReadReceiptEnabled={isReadReceiptEnabled} isReadReceiptEnabled={isReadReceiptEnabled}
unread={unread} unread={unread}
@ -282,14 +295,6 @@ class MessageContainer extends React.Component {
isInfo={this.isInfo} isInfo={this.isInfo}
isTemp={this.isTemp} isTemp={this.isTemp}
hasError={this.hasError} hasError={this.hasError}
onErrorPress={this.onErrorPress}
onPress={this.onPress}
onLongPress={this.onLongPress}
onReactionLongPress={this.onReactionLongPress}
onReactionPress={this.onReactionPress}
replyBroadcast={this.replyBroadcast}
reactionInit={this.reactionInit}
onDiscussionPress={this.onDiscussionPress}
showAttachment={showAttachment} showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo} navToRoomInfo={navToRoomInfo}
@ -297,6 +302,7 @@ class MessageContainer extends React.Component {
blockAction={blockAction} blockAction={blockAction}
theme={theme} theme={theme}
/> />
</MessageContext.Provider>
); );
} }
} }

View File

@ -18,6 +18,7 @@ export default {
'error-email-domain-blacklisted': 'The email domain is blacklisted', 'error-email-domain-blacklisted': 'The email domain is blacklisted',
'error-email-send-failed': 'Error trying to send email: {{message}}', 'error-email-send-failed': 'Error trying to send email: {{message}}',
'error-save-image': 'Error while saving image', 'error-save-image': 'Error while saving image',
'error-save-video': 'Error while saving video',
'error-field-unavailable': '{{field}} is already in use :(', 'error-field-unavailable': '{{field}} is already in use :(',
'error-file-too-large': 'File is too large', 'error-file-too-large': 'File is too large',
'error-importer-not-defined': 'The importer was not defined correctly, it is missing the Import class.', 'error-importer-not-defined': 'The importer was not defined correctly, it is missing the Import class.',
@ -414,6 +415,7 @@ export default {
Select_Server: 'Select Server', Select_Server: 'Select Server',
Select_Users: 'Select Users', Select_Users: 'Select Users',
Select_a_Channel: 'Select a Channel', Select_a_Channel: 'Select a Channel',
Select_an_option: 'Select an option',
Send: 'Send', Send: 'Send',
Send_audio_message: 'Send audio message', Send_audio_message: 'Send audio message',
Send_crash_report: 'Send crash report', Send_crash_report: 'Send crash report',

View File

@ -377,6 +377,7 @@ export default {
Select_Server: 'Selecionar Servidor', Select_Server: 'Selecionar Servidor',
Select_Users: 'Selecionar Usuários', Select_Users: 'Selecionar Usuários',
Select_a_Channel: 'Selecione um canal', Select_a_Channel: 'Selecione um canal',
Select_an_option: 'Selecione uma opção',
Send: 'Enviar', Send: 'Enviar',
Send_audio_message: 'Enviar mensagem de áudio', Send_audio_message: 'Enviar mensagem de áudio',
Send_message: 'Enviar mensagem', Send_message: 'Enviar mensagem',

View File

@ -166,6 +166,9 @@ const ChatsStack = createStackNavigator({
NotificationPrefView: { NotificationPrefView: {
getScreen: () => require('./views/NotificationPreferencesView').default getScreen: () => require('./views/NotificationPreferencesView').default
}, },
PickerView: {
getScreen: () => require('./views/PickerView').default
},
...RoomRoutes ...RoomRoutes
}, { }, {
defaultNavigationOptions: defaultHeader, defaultNavigationOptions: defaultHeader,
@ -448,6 +451,9 @@ const RoomActionsStack = createStackNavigator({
}, },
AttachmentView: { AttachmentView: {
getScreen: () => require('./views/AttachmentView').default getScreen: () => require('./views/AttachmentView').default
},
PickerView: {
getScreen: () => require('./views/PickerView').default
} }
}, { }, {
defaultNavigationOptions: defaultHeader, defaultNavigationOptions: defaultHeader,
@ -508,7 +514,7 @@ class CustomModalStack extends React.Component {
const pageSheetViews = ['AttachmentView']; const pageSheetViews = ['AttachmentView'];
const pageSheet = pageSheetViews.includes(getActiveRouteName(navigation.state)); const pageSheet = pageSheetViews.includes(getActiveRouteName(navigation.state));
const androidProps = isAndroid && { const androidProps = isAndroid && !pageSheet && {
style: { marginBottom: 0 } style: { marginBottom: 0 }
}; };
@ -518,7 +524,7 @@ class CustomModalStack extends React.Component {
</View> </View>
); );
if (isAndroid) { if (isAndroid && !pageSheet) {
content = ( content = (
<ScrollView overScrollMode='never'> <ScrollView overScrollMode='never'>
{content} {content}

View File

@ -109,7 +109,8 @@ class DB {
Message, Message,
Thread, Thread,
ThreadMessage, ThreadMessage,
Upload Upload,
Permission
], ],
actionsEnabled: true actionsEnabled: true
}); });

View File

@ -61,8 +61,15 @@ export default async function canOpenRoom({ rid, path }) {
if (rid) { if (rid) {
try { try {
await subsCollection.find(rid); const room = await subsCollection.find(rid);
return { rid }; return {
rid,
t: room.t,
name: room.name,
fname: room.fname,
prid: room.prid,
uids: room.uids
};
} catch (e) { } catch (e) {
// Do nothing // Do nothing
} }

View File

@ -13,6 +13,22 @@ import fetch from '../../utils/fetch';
const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize']; const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize'];
// these settings are used only on onboarding process
const loginSettings = [
'API_Gitlab_URL',
'CAS_enabled',
'CAS_login_url',
'Accounts_EmailVerification',
'Accounts_ManuallyApproveNewUsers',
'Accounts_ShowFormLogin',
'Site_Url',
'Accounts_RegistrationForm',
'Accounts_RegistrationForm_LinkReplacementText',
'Accounts_EmailOrUsernamePlaceholder',
'Accounts_PasswordPlaceholder',
'Accounts_PasswordReset'
];
const serverInfoUpdate = async(serverInfo, iconSetting) => { const serverInfoUpdate = async(serverInfo, iconSetting) => {
const serversDB = database.servers; const serversDB = database.servers;
const serverId = reduxStore.getState().server.server; const serverId = reduxStore.getState().server.server;
@ -54,7 +70,7 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
export async function getLoginSettings({ server }) { export async function getLoginSettings({ server }) {
try { try {
const settingsParams = JSON.stringify(['Accounts_ShowFormLogin', 'Accounts_RegistrationForm']); const settingsParams = JSON.stringify(loginSettings);
const result = await fetch(`${ server }/api/v1/settings.public?query={"_id":{"$in":${ settingsParams }}}`).then(response => response.json()); const result = await fetch(`${ server }/api/v1/settings.public?query={"_id":{"$in":${ settingsParams }}}`).then(response => response.json());
if (result.success && result.settings.length) { if (result.success && result.settings.length) {
@ -84,7 +100,7 @@ export async function setSettings() {
export default async function() { export default async function() {
try { try {
const db = database.active; const db = database.active;
const settingsParams = JSON.stringify(Object.keys(settings)); const settingsParams = JSON.stringify(Object.keys(settings).filter(key => !loginSettings.includes(key)));
// RC 0.60.0 // RC 0.60.0
const result = await fetch(`${ this.sdk.client.host }/api/v1/settings.public?query={"_id":{"$in":${ settingsParams }}}`).then(response => response.json()); const result = await fetch(`${ this.sdk.client.host }/api/v1/settings.public?query={"_id":{"$in":${ settingsParams }}}`).then(response => response.json());

View File

@ -3,12 +3,13 @@ import semver from 'semver';
import reduxStore from '../createStore'; import reduxStore from '../createStore';
import { setActiveUsers } from '../../actions/activeUsers'; import { setActiveUsers } from '../../actions/activeUsers';
import { setUser } from '../../actions/login';
export function subscribeUsersPresence() { export function subscribeUsersPresence() {
const serverVersion = reduxStore.getState().server.version; const serverVersion = reduxStore.getState().server.version;
// if server is lower than 1.1.0 // if server is lower than 1.1.0
if (serverVersion && semver.lt(semver.coerce(serverVersion), '1.1.0')) { if (serverVersion && semver.lt(serverVersion, '1.1.0')) {
if (this.activeUsersSubTimeout) { if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout); clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false; this.activeUsersSubTimeout = false;
@ -25,35 +26,43 @@ let ids = [];
export default async function getUsersPresence() { export default async function getUsersPresence() {
const serverVersion = reduxStore.getState().server.version; const serverVersion = reduxStore.getState().server.version;
const { user: loggedUser } = reduxStore.getState().login;
// if server is greather than or equal 1.1.0 // if server is greather than or equal 1.1.0
if (serverVersion && !semver.lt(semver.coerce(serverVersion), '1.1.0')) { if (serverVersion && semver.gte(serverVersion, '1.1.0')) {
let params = {}; let params = {};
// if server is greather than or equal 3.0.0 // if server is greather than or equal 3.0.0
if (serverVersion && !semver.lt(semver.coerce(serverVersion), '3.0.0')) { if (serverVersion && semver.gte(serverVersion, '3.0.0')) {
// if not have any id // if not have any id
if (!ids.length) { if (!ids.length) {
return; return;
} }
// Request userPresence on demand // Request userPresence on demand
params = { ids: ids.join(',') }; params = { ids: ids.join(',') };
ids = [];
} }
try {
// RC 1.1.0 // RC 1.1.0
const result = await this.sdk.get('users.presence', params); const result = await this.sdk.get('users.presence', params);
if (result.success) { if (result.success) {
const activeUsers = result.users.reduce((ret, item) => { const activeUsers = result.users.reduce((ret, item) => {
ret[item._id] = { const { _id, status, statusText } = item;
status: item.status,
statusText: item.statusText if (loggedUser && loggedUser.id === _id) {
}; reduxStore.dispatch(setUser({ status, statusText }));
}
ret[_id] = { status, statusText };
return ret; return ret;
}, {}); }, {});
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {
reduxStore.dispatch(setActiveUsers(activeUsers)); reduxStore.dispatch(setActiveUsers(activeUsers));
}); });
ids = [];
}
} catch {
// do nothing
} }
} }
} }

View File

@ -0,0 +1,78 @@
import { Q } from '@nozbe/watermelondb';
import database from '../../database';
export default async(subscriptions = [], rooms = []) => {
try {
const db = database.active;
const subCollection = db.collections.get('subscriptions');
const roomIds = rooms.filter(r => !subscriptions.find(s => s.rid === r._id)).map(r => r._id);
let existingSubs = await subCollection.query(Q.where('rid', Q.oneOf(roomIds))).fetch();
existingSubs = existingSubs.map(s => ({
_id: s._id,
f: s.f,
t: s.t,
ts: s.ts,
ls: s.ls,
name: s.name,
fname: s.fname,
rid: s.rid,
open: s.open,
alert: s.alert,
unread: s.unread,
userMentions: s.userMentions,
roomUpdatedAt: s.roomUpdatedAt,
ro: s.ro,
lastOpen: s.lastOpen,
description: s.description,
announcement: s.announcement,
topic: s.topic,
blocked: s.blocked,
blocker: s.blocker,
reactWhenReadOnly: s.reactWhenReadOnly,
archived: s.archived,
joinCodeRequired: s.joinCodeRequired,
muted: s.muted,
broadcast: s.broadcast,
prid: s.prid,
draftMessage: s.draftMessage,
lastThreadSync: s.lastThreadSync,
jitsiTimeout: s.jitsiTimeout,
autoTranslate: s.autoTranslate,
autoTranslateLanguage: s.autoTranslateLanguage,
lastMessage: s.lastMessage,
usernames: s.usernames,
uids: s.uids
}));
subscriptions = subscriptions.concat(existingSubs);
const subsIds = subscriptions.filter(s => !rooms.find(r => s.rid === r._id)).map(s => s._id);
let existingRooms = await subCollection.query(Q.where('id', Q.oneOf(subsIds))).fetch();
existingRooms = existingRooms.map(r => ({
_updatedAt: r._updatedAt,
lastMessage: r.lastMessage,
description: r.description,
topic: r.topic,
announcement: r.announcement,
reactWhenReadOnly: r.reactWhenReadOnly,
archived: r.archived,
joinCodeRequired: r.joinCodeRequired,
jitsiTimeout: r.jitsiTimeout,
usernames: r.usernames,
uids: r.uids,
ro: r.ro,
broadcast: r.broadcast,
muted: r.muted,
sysMes: r.sysMes
}));
rooms = rooms.concat(existingRooms);
} catch {
// do nothing
}
return {
subscriptions,
rooms
};
};

View File

@ -1,6 +1,7 @@
import EJSON from 'ejson'; import EJSON from 'ejson';
import normalizeMessage from './normalizeMessage'; import normalizeMessage from './normalizeMessage';
import findSubscriptionsRooms from './findSubscriptionsRooms';
// TODO: delete and update // TODO: delete and update
export const merge = (subscription, room) => { export const merge = (subscription, room) => {
@ -46,11 +47,14 @@ export const merge = (subscription, room) => {
return subscription; return subscription;
}; };
export default (subscriptions = [], rooms = []) => { export default async(subscriptions = [], rooms = []) => {
if (subscriptions.update) { if (subscriptions.update) {
subscriptions = subscriptions.update; subscriptions = subscriptions.update;
rooms = rooms.update; rooms = rooms.update;
} }
({ subscriptions, rooms } = await findSubscriptionsRooms(subscriptions, rooms));
return { return {
subscriptions: subscriptions.map((s) => { subscriptions: subscriptions.map((s) => {
const index = rooms.findIndex(({ _id }) => _id === s.rid); const index = rooms.findIndex(({ _id }) => _id === s.rid);

View File

@ -96,6 +96,7 @@ export async function removeServer({ server }) {
export default async function logout({ server }) { export default async function logout({ server }) {
if (this.roomsSub) { if (this.roomsSub) {
this.roomsSub.stop(); this.roomsSub.stop();
this.roomsSub = null;
} }
if (this.activeUsersSubTimeout) { if (this.activeUsersSubTimeout) {

View File

@ -62,7 +62,7 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
formData.append('file', { formData.append('file', {
uri: fileInfo.path, uri: fileInfo.path,
type: fileInfo.type, type: fileInfo.type,
name: fileInfo.name || 'fileMessage' name: encodeURI(fileInfo.name) || 'fileMessage'
}); });
if (fileInfo.description) { if (fileInfo.description) {

View File

@ -10,6 +10,7 @@ import reduxStore from '../../createStore';
import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actions/usersTyping'; import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actions/usersTyping';
import debounce from '../../../utils/debounce'; import debounce from '../../../utils/debounce';
import RocketChat from '../../rocketchat'; import RocketChat from '../../rocketchat';
import { subscribeRoom, unsubscribeRoom } from '../../../actions/room';
const WINDOW_TIME = 1000; const WINDOW_TIME = 1000;
@ -38,6 +39,8 @@ export default class RoomSubscription {
if (!this.isAlive) { if (!this.isAlive) {
this.unsubscribe(); this.unsubscribe();
} }
reduxStore.dispatch(subscribeRoom(this.rid));
} }
unsubscribe = async() => { unsubscribe = async() => {
@ -51,14 +54,16 @@ export default class RoomSubscription {
// do nothing // do nothing
} }
} }
reduxStore.dispatch(clearUserTyping());
this.removeListener(this.connectedListener); this.removeListener(this.connectedListener);
this.removeListener(this.disconnectedListener); this.removeListener(this.disconnectedListener);
this.removeListener(this.notifyRoomListener); this.removeListener(this.notifyRoomListener);
this.removeListener(this.messageReceivedListener); this.removeListener(this.messageReceivedListener);
reduxStore.dispatch(clearUserTyping());
if (this.timer) { if (this.timer) {
clearTimeout(this.timer); clearTimeout(this.timer);
} }
reduxStore.dispatch(unsubscribeRoom(this.rid));
} }
removeListener = async(promise) => { removeListener = async(promise) => {
@ -73,6 +78,7 @@ export default class RoomSubscription {
}; };
handleConnection = () => { handleConnection = () => {
reduxStore.dispatch(clearUserTyping());
RocketChat.loadMissedMessages({ rid: this.rid }).catch(e => console.log(e)); RocketChat.loadMissedMessages({ rid: this.rid }).catch(e => console.log(e));
}; };
@ -154,22 +160,17 @@ export default class RoomSubscription {
const msgCollection = db.collections.get('messages'); const msgCollection = db.collections.get('messages');
const threadsCollection = db.collections.get('threads'); const threadsCollection = db.collections.get('threads');
const threadMessagesCollection = db.collections.get('thread_messages'); const threadMessagesCollection = db.collections.get('thread_messages');
let messageRecord;
let threadRecord;
let threadMessageRecord;
// Create or update message // Create or update message
try { try {
messageRecord = await msgCollection.find(message._id); const messageRecord = await msgCollection.find(message._id);
} catch (error) { if (!messageRecord._hasPendingUpdate) {
// Do nothing const update = messageRecord.prepareUpdate(protectedFunction((m) => {
}
if (messageRecord) {
const update = messageRecord.prepareUpdate((m) => {
Object.assign(m, message); Object.assign(m, message);
}); }));
this._messagesBatch[message._id] = update; this._messagesBatch[message._id] = update;
} else { }
} catch {
const create = msgCollection.prepareCreate(protectedFunction((m) => { const create = msgCollection.prepareCreate(protectedFunction((m) => {
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema); m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
m.subscription.id = this.rid; m.subscription.id = this.rid;
@ -181,17 +182,14 @@ export default class RoomSubscription {
// Create or update thread // Create or update thread
if (message.tlm) { if (message.tlm) {
try { try {
threadRecord = await threadsCollection.find(message._id); const threadRecord = await threadsCollection.find(message._id);
} catch (error) { if (!threadRecord._hasPendingUpdate) {
// Do nothing
}
if (threadRecord) {
const updateThread = threadRecord.prepareUpdate(protectedFunction((t) => { const updateThread = threadRecord.prepareUpdate(protectedFunction((t) => {
Object.assign(t, message); Object.assign(t, message);
})); }));
this._threadsBatch[message._id] = updateThread; this._threadsBatch[message._id] = updateThread;
} else { }
} catch {
const createThread = threadsCollection.prepareCreate(protectedFunction((t) => { const createThread = threadsCollection.prepareCreate(protectedFunction((t) => {
t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema); t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema);
t.subscription.id = this.rid; t.subscription.id = this.rid;
@ -204,19 +202,16 @@ export default class RoomSubscription {
// Create or update thread message // Create or update thread message
if (message.tmid) { if (message.tmid) {
try { try {
threadMessageRecord = await threadMessagesCollection.find(message._id); const threadMessageRecord = await threadMessagesCollection.find(message._id);
} catch (error) { if (!threadMessageRecord._hasPendingUpdate) {
// Do nothing
}
if (threadMessageRecord) {
const updateThreadMessage = threadMessageRecord.prepareUpdate(protectedFunction((tm) => { const updateThreadMessage = threadMessageRecord.prepareUpdate(protectedFunction((tm) => {
Object.assign(tm, message); Object.assign(tm, message);
tm.rid = message.tmid; tm.rid = message.tmid;
delete tm.tmid; delete tm.tmid;
})); }));
this._threadMessagesBatch[message._id] = updateThreadMessage; this._threadMessagesBatch[message._id] = updateThreadMessage;
} else { }
} catch {
const createThreadMessage = threadMessagesCollection.prepareCreate(protectedFunction((tm) => { const createThreadMessage = threadMessagesCollection.prepareCreate(protectedFunction((tm) => {
tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema); tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema);
Object.assign(tm, message); Object.assign(tm, message);

View File

@ -72,6 +72,7 @@ const createOrUpdateSubscription = async(subscription, room) => {
autoTranslate: s.autoTranslate, autoTranslate: s.autoTranslate,
autoTranslateLanguage: s.autoTranslateLanguage, autoTranslateLanguage: s.autoTranslateLanguage,
lastMessage: s.lastMessage, lastMessage: s.lastMessage,
roles: s.roles,
usernames: s.usernames, usernames: s.usernames,
uids: s.uids uids: s.uids
}; };
@ -140,7 +141,8 @@ const createOrUpdateSubscription = async(subscription, room) => {
} }
} }
if (tmp.lastMessage) { const { rooms } = store.getState().room;
if (tmp.lastMessage && !rooms.includes(tmp.rid)) {
const lastMessage = buildMessage(tmp.lastMessage); const lastMessage = buildMessage(tmp.lastMessage);
const messagesCollection = db.collections.get('messages'); const messagesCollection = db.collections.get('messages');
let messageRecord; let messageRecord;

View File

@ -62,14 +62,13 @@ const RocketChat = {
TOKEN_KEY, TOKEN_KEY,
callJitsi, callJitsi,
async subscribeRooms() { async subscribeRooms() {
if (this.roomsSub) { if (!this.roomsSub) {
this.roomsSub.stop();
}
try { try {
this.roomsSub = await subscribeRooms.call(this); this.roomsSub = await subscribeRooms.call(this);
} catch (e) { } catch (e) {
log(e); log(e);
} }
}
}, },
canOpenRoom, canOpenRoom,
createChannel({ createChannel({
@ -185,6 +184,7 @@ const RocketChat = {
if (this.roomsSub) { if (this.roomsSub) {
this.roomsSub.stop(); this.roomsSub.stop();
this.roomsSub = null;
} }
if (this.sdk) { if (this.sdk) {
@ -199,7 +199,7 @@ const RocketChat = {
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
this.getSettings(); this.getSettings();
this.sdk.connect() const sdkConnect = () => this.sdk.connect()
.then(() => { .then(() => {
if (user && user.token) { if (user && user.token) {
reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError)); reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError));
@ -210,10 +210,12 @@ const RocketChat = {
// when `connect` raises an error, we try again in 10 seconds // when `connect` raises an error, we try again in 10 seconds
this.connectTimeout = setTimeout(() => { this.connectTimeout = setTimeout(() => {
this.connect({ server, user }); sdkConnect();
}, 10000); }, 10000);
}); });
sdkConnect();
this.connectedListener = this.sdk.onStreamData('connected', () => { this.connectedListener = this.sdk.onStreamData('connected', () => {
reduxStore.dispatch(connectSuccess()); reduxStore.dispatch(connectSuccess());
}); });
@ -277,14 +279,11 @@ const RocketChat = {
user = { user = {
id: userRecord.id, id: userRecord.id,
token: userRecord.token, token: userRecord.token,
username: userRecord.username username: userRecord.username,
roles: userRecord.roles
}; };
} }
reduxStore.dispatch(shareSetUser({ reduxStore.dispatch(shareSetUser(user));
id: user.id,
token: user.token,
username: user.username
}));
await RocketChat.login({ resume: user.token }); await RocketChat.login({ resume: user.token });
} catch (e) { } catch (e) {
log(e); log(e);
@ -296,6 +295,8 @@ const RocketChat = {
this.shareSDK = null; this.shareSDK = null;
} }
database.share = null; database.share = null;
reduxStore.dispatch(shareSetUser(null));
}, },
updateJitsiTimeout(rid) { updateJitsiTimeout(rid) {
@ -326,8 +327,8 @@ const RocketChat = {
if (e.data?.error && (e.data.error === 'totp-required' || e.data.error === 'totp-invalid')) { if (e.data?.error && (e.data.error === 'totp-required' || e.data.error === 'totp-invalid')) {
const { details } = e.data; const { details } = e.data;
try { try {
await twoFactor({ method: details?.method, invalid: e.data.error === 'totp-invalid' }); const code = await twoFactor({ method: details?.method || 'totp', invalid: e.data.error === 'totp-invalid' });
return resolve(this.loginTOTP(params)); return resolve(this.loginTOTP({ ...params, code: code?.twoFactorCode }));
} catch { } catch {
// twoFactor was canceled // twoFactor was canceled
return reject(); return reject();
@ -497,12 +498,25 @@ const RocketChat = {
).fetch(); ).fetch();
if (filterUsers && !filterRooms) { if (filterUsers && !filterRooms) {
data = data.filter(item => item.t === 'd'); data = data.filter(item => item.t === 'd' && !RocketChat.isGroupChat(item));
} else if (!filterUsers && filterRooms) { } else if (!filterUsers && filterRooms) {
data = data.filter(item => item.t !== 'd'); data = data.filter(item => item.t !== 'd' || RocketChat.isGroupChat(item));
} }
data = data.slice(0, 7); data = data.slice(0, 7);
data = data.map((sub) => {
if (sub.t !== 'd') {
return ({
rid: sub.rid,
name: sub.name,
fname: sub.fname,
t: sub.t,
search: true
});
}
return sub;
});
const usernames = data.map(sub => sub.name); const usernames = data.map(sub => sub.name);
try { try {
if (data.length < 7) { if (data.length < 7) {
@ -553,11 +567,11 @@ const RocketChat = {
}, },
createGroupChat() { createGroupChat() {
let { users } = reduxStore.getState().selectedUsers; const { users } = reduxStore.getState().selectedUsers;
users = users.map(u => u.name); const usernames = users.map(u => u.name).join(',');
// RC 3.1.0 // RC 3.1.0
return this.methodCall('createDirectMessage', ...users); return this.post('im.create', { usernames });
}, },
createDiscussion({ createDiscussion({
@ -692,12 +706,9 @@ const RocketChat = {
setUserPresenceOnline() { setUserPresenceOnline() {
return this.methodCall('UserPresence:online'); return this.methodCall('UserPresence:online');
}, },
setUserPresenceDefaultStatus(status) { setUserStatus(status, message) {
return this.methodCall('UserPresence:setDefaultStatus', status);
},
setUserStatus(message) {
// RC 1.2.0 // RC 1.2.0
return this.post('users.setStatus', { message }); return this.post('users.setStatus', { status, message });
}, },
setReaction(emoji, messageId) { setReaction(emoji, messageId) {
// RC 0.62.2 // RC 0.62.2
@ -738,7 +749,9 @@ const RocketChat = {
return this.sdk.get('rooms.info', { roomId }); return this.sdk.get('rooms.info', { roomId });
}, },
getUidDirectMessage(room, userId) { getUidDirectMessage(room) {
const { id: userId } = reduxStore.getState().login.user;
// legacy method // legacy method
if (!room.uids && room.rid && room.t === 'd') { if (!room.uids && room.rid && room.t === 'd') {
return room.rid.replace(userId, '').trim(); return room.rid.replace(userId, '').trim();
@ -821,7 +834,7 @@ const RocketChat = {
methodCall(...args) { methodCall(...args) {
return new Promise(async(resolve, reject) => { return new Promise(async(resolve, reject) => {
try { try {
const result = await this.sdk.methodCall(...args, this.code); const result = await this.sdk.methodCall(...args, this.code || '');
return resolve(result); return resolve(result);
} catch (e) { } catch (e) {
if (e.error && (e.error === 'totp-required' || e.error === 'totp-invalid')) { if (e.error && (e.error === 'totp-required' || e.error === 'totp-invalid')) {
@ -875,7 +888,7 @@ const RocketChat = {
// get the room from database // get the room from database
const room = await subsCollection.find(rid); const room = await subsCollection.find(rid);
// get room roles // get room roles
roomRoles = room.roles; roomRoles = room.roles || [];
} catch (error) { } catch (error) {
console.log('hasPermission -> Room not found'); console.log('hasPermission -> Room not found');
return permissions.reduce((result, permission) => { return permissions.reduce((result, permission) => {
@ -886,8 +899,10 @@ const RocketChat = {
// get permissions from database // get permissions from database
try { try {
const permissionsFiltered = await permissionsCollection.query(Q.where('id', Q.oneOf(permissions))).fetch(); const permissionsFiltered = await permissionsCollection.query(Q.where('id', Q.oneOf(permissions))).fetch();
const shareUser = reduxStore.getState().share.user;
const loginUser = reduxStore.getState().login.user;
// get user roles on the server from redux // get user roles on the server from redux
const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || []; const userRoles = (shareUser.roles || loginUser.roles) || [];
// merge both roles // merge both roles
const mergedRoles = [...new Set([...roomRoles, ...userRoles])]; const mergedRoles = [...new Set([...roomRoles, ...userRoles])];
@ -1136,16 +1151,19 @@ const RocketChat = {
return this.methodCall('autoTranslate.translateMessage', message, targetLanguage); return this.methodCall('autoTranslate.translateMessage', message, targetLanguage);
}, },
getRoomTitle(room) { getRoomTitle(room) {
const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings; const { UI_Use_Real_Name: useRealName, UI_Allow_room_names_with_special_chars: allowSpecialChars } = reduxStore.getState().settings;
const { username } = reduxStore.getState().login.user; const { username } = reduxStore.getState().login.user;
if (RocketChat.isGroupChat(room) && !(room.name && room.name.length)) { if (RocketChat.isGroupChat(room) && !(room.name && room.name.length)) {
return room.usernames.filter(u => u !== username).sort((u1, u2) => u1.localeCompare(u2)).join(', '); return room.usernames.filter(u => u !== username).sort((u1, u2) => u1.localeCompare(u2)).join(', ');
} }
if (allowSpecialChars && room.t !== 'd') {
return room.fname || room.name;
}
return ((room.prid || useRealName) && room.fname) || room.name; return ((room.prid || useRealName) && room.fname) || room.name;
}, },
getRoomAvatar(room) { getRoomAvatar(room) {
if (RocketChat.isGroupChat(room)) { if (RocketChat.isGroupChat(room)) {
return room.uids.length + room.usernames.join(); return room.uids?.length + room.usernames?.join();
} }
return room.prid ? room.fname : room.name; return room.prid ? room.fname : room.name;
}, },

View File

@ -27,6 +27,7 @@ const attrs = [
'isRead', 'isRead',
'favorite', 'favorite',
'status', 'status',
'connected',
'theme' 'theme'
]; ];
@ -40,15 +41,15 @@ const arePropsEqual = (oldProps, newProps) => {
}; };
const RoomItem = React.memo(({ const RoomItem = React.memo(({
onPress, width, favorite, toggleFav, isRead, rid, toggleRead, hideChannel, testID, unread, userMentions, name, _updatedAt, alert, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, hideUnreadStatus, lastMessage, status, avatar, useRealName, getUserPresence, isGroupChat, theme onPress, width, favorite, toggleFav, isRead, rid, toggleRead, hideChannel, testID, unread, userMentions, name, _updatedAt, alert, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, hideUnreadStatus, lastMessage, status, avatar, useRealName, getUserPresence, isGroupChat, connected, theme
}) => { }) => {
useEffect(() => { useEffect(() => {
if (type === 'd') { if (connected && type === 'd' && id) {
getUserPresence(id); getUserPresence(id);
} }
}, []); }, [connected]);
const date = formatDate(_updatedAt); const date = lastMessage && formatDate(lastMessage.ts);
let accessibilityLabel = name; let accessibilityLabel = name;
if (unread === 1) { if (unread === 1) {
@ -197,6 +198,7 @@ RoomItem.propTypes = {
hideUnreadStatus: PropTypes.bool, hideUnreadStatus: PropTypes.bool,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
getUserPresence: PropTypes.func, getUserPresence: PropTypes.func,
connected: PropTypes.bool,
isGroupChat: PropTypes.bool, isGroupChat: PropTypes.bool,
theme: PropTypes.string theme: PropTypes.string
}; };
@ -208,6 +210,7 @@ RoomItem.defaultProps = {
}; };
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state, ownProps) => ({
connected: state.meteor.connected,
status: status:
state.meteor.connected && ownProps.type === 'd' state.meteor.connected && ownProps.type === 'd'
? state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status ? state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status

View File

@ -2,11 +2,23 @@ import { ROOM } from '../actions/actionsTypes';
const initialState = { const initialState = {
rid: null, rid: null,
isDeleting: false isDeleting: false,
rooms: []
}; };
export default function(state = initialState, action) { export default function(state = initialState, action) {
switch (action.type) { switch (action.type) {
case ROOM.SUBSCRIBE:
return {
...state,
rooms: [action.rid, ...state.rooms]
};
case ROOM.UNSUBSCRIBE:
return {
...state,
rooms: state.rooms
.filter(room => room.rid === action.rid)
};
case ROOM.LEAVE: case ROOM.LEAVE:
return { return {
...state, ...state,

View File

@ -28,7 +28,10 @@ const handleRequest = function* handleRequest({ data }) {
let sub; let sub;
if (data.group) { if (data.group) {
sub = yield call(createGroupChat); const result = yield call(createGroupChat);
if (result.success) {
({ room: sub } = result);
}
} else { } else {
sub = yield call(createChannel, data); sub = yield call(createChannel, data);
} }

View File

@ -30,15 +30,22 @@ const handleInviteLink = function* handleInviteLink({ params, requireLogin = fal
const navigate = function* navigate({ params }) { const navigate = function* navigate({ params }) {
yield put(appStart('inside')); yield put(appStart('inside'));
if (params.path) { if (params.path) {
const room = yield RocketChat.canOpenRoom(params);
const [type, name] = params.path.split('/'); const [type, name] = params.path.split('/');
if (type !== 'invite') {
const room = yield RocketChat.canOpenRoom(params);
if (room) { if (room) {
yield Navigation.navigate('RoomsListView'); yield Navigation.navigate('RoomsListView');
Navigation.navigate('RoomView', { name, t: roomTypes[type], ...room }); Navigation.navigate('RoomView', {
name,
t: roomTypes[type],
roomUserId: RocketChat.getUidDirectMessage(room),
...room
});
} }
} else { } else {
yield handleInviteLink({ params }); yield handleInviteLink({ params });
} }
}
}; };
const handleOpen = function* handleOpen({ params }) { const handleOpen = function* handleOpen({ params }) {

View File

@ -21,6 +21,7 @@ import database from '../lib/database';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
import { inviteLinksRequest } from '../actions/inviteLinks'; import { inviteLinksRequest } from '../actions/inviteLinks';
import { showErrorAlert } from '../utils/info'; import { showErrorAlert } from '../utils/info';
import { setActiveUsers } from '../actions/activeUsers';
const getServer = state => state.server.server; const getServer = state => state.server.server;
const loginWithPasswordCall = args => RocketChat.loginWithPassword(args); const loginWithPasswordCall = args => RocketChat.loginWithPassword(args);
@ -73,7 +74,7 @@ const registerPushToken = function* registerPushToken() {
const fetchUsersPresence = function* fetchUserPresence() { const fetchUsersPresence = function* fetchUserPresence() {
yield RocketChat.getUsersPresence(); yield RocketChat.getUsersPresence();
yield RocketChat.subscribeUsersPresence(); RocketChat.subscribeUsersPresence();
}; };
const handleLoginSuccess = function* handleLoginSuccess({ user }) { const handleLoginSuccess = function* handleLoginSuccess({ user }) {
@ -81,6 +82,8 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
const adding = yield select(state => state.server.adding); const adding = yield select(state => state.server.adding);
yield RNUserDefaults.set(RocketChat.TOKEN_KEY, user.token); yield RNUserDefaults.set(RocketChat.TOKEN_KEY, user.token);
RocketChat.getUserPresence(user.id);
const server = yield select(getServer); const server = yield select(getServer);
yield put(roomsRequest()); yield put(roomsRequest());
yield fork(fetchPermissions); yield fork(fetchPermissions);
@ -186,11 +189,16 @@ const handleLogout = function* handleLogout({ forcedByServer }) {
} }
}; };
const handleSetUser = function handleSetUser({ user }) { const handleSetUser = function* handleSetUser({ user }) {
if (user && user.language) { if (user && user.language) {
I18n.locale = user.language; I18n.locale = user.language;
moment.locale(toMomentLocale(user.language)); moment.locale(toMomentLocale(user.language));
} }
if (user && user.status) {
const userId = yield select(state => state.login.user.id);
yield put(setActiveUsers({ [userId]: user }));
}
}; };
const root = function* root() { const root = function* root() {

View File

@ -41,7 +41,7 @@ const handleRoomsRequest = function* handleRoomsRequest({ params }) {
({ roomsUpdatedAt } = serverRecord); ({ roomsUpdatedAt } = serverRecord);
} }
const [subscriptionsResult, roomsResult] = yield RocketChat.getRooms(roomsUpdatedAt); const [subscriptionsResult, roomsResult] = yield RocketChat.getRooms(roomsUpdatedAt);
const { subscriptions } = mergeSubscriptionsRooms(subscriptionsResult, roomsResult); const { subscriptions } = yield mergeSubscriptionsRooms(subscriptionsResult, roomsResult);
const db = database.active; const db = database.active;
const subCollection = db.collections.get('subscriptions'); const subCollection = db.collections.get('subscriptions');

View File

@ -12,8 +12,8 @@ import * as actions from '../actions';
import { import {
serverFailure, selectServerRequest, selectServerSuccess, selectServerFailure serverFailure, selectServerRequest, selectServerSuccess, selectServerFailure
} from '../actions/server'; } from '../actions/server';
import { setUser } from '../actions/login';
import { clearSettings } from '../actions/settings'; import { clearSettings } from '../actions/settings';
import { setUser } from '../actions/login';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/database'; import database from '../lib/database';
import log, { logServerVersion } from '../utils/log'; import log, { logServerVersion } from '../utils/log';
@ -38,7 +38,10 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
return; return;
} }
const validVersion = semver.coerce(serverInfo.version); let serverVersion = semver.valid(serverInfo.version);
if (!serverVersion) {
({ version: serverVersion } = semver.coerce(serverInfo.version));
}
const serversDB = database.servers; const serversDB = database.servers;
const serversCollection = serversDB.collections.get('servers'); const serversCollection = serversDB.collections.get('servers');
@ -46,12 +49,12 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
try { try {
const serverRecord = await serversCollection.find(server); const serverRecord = await serversCollection.find(server);
await serverRecord.update((record) => { await serverRecord.update((record) => {
record.version = validVersion; record.version = serverVersion;
}); });
} catch (e) { } catch (e) {
await serversCollection.create((record) => { await serversCollection.create((record) => {
record._raw = sanitizedRaw({ id: server }, serversCollection.schema); record._raw = sanitizedRaw({ id: server }, serversCollection.schema);
record.version = validVersion; record.version = serverVersion;
}); });
} }
}); });
@ -95,9 +98,8 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
const basicAuth = yield RNUserDefaults.get(`${ BASIC_AUTH_KEY }-${ server }`); const basicAuth = yield RNUserDefaults.get(`${ BASIC_AUTH_KEY }-${ server }`);
setBasicAuth(basicAuth); setBasicAuth(basicAuth);
yield put(clearSettings());
if (user) { if (user) {
yield put(clearSettings());
yield RocketChat.connect({ server, user, logoutOnError: true }); yield RocketChat.connect({ server, user, logoutOnError: true });
yield put(setUser(user)); yield put(setUser(user));
yield put(actions.appStart('inside')); yield put(actions.appStart('inside'));

24
app/utils/isReadOnly.js Normal file
View File

@ -0,0 +1,24 @@
import RocketChat from '../lib/rocketchat';
const canPost = async({ rid }) => {
try {
const permission = await RocketChat.hasPermission(['post-readonly'], rid);
return permission && permission['post-readonly'];
} catch {
// do nothing
}
return false;
};
const isMuted = (room, user) => room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username);
export const isReadOnly = async(room, user) => {
if (room.archived) {
return true;
}
const allowPost = await canPost(room);
if (allowPost) {
return false;
}
return (room && room.ro) || isMuted(room, user);
};

View File

@ -2,17 +2,6 @@ import moment from 'moment';
import I18n from '../i18n'; import I18n from '../i18n';
export const isOwner = room => room && room.roles && room.roles.length && !!room.roles.find(role => role === 'owner');
export const isMuted = (room, user) => room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username);
export const isReadOnly = (room, user) => {
if (isOwner(room)) {
return false;
}
return (room && room.ro) || isMuted(room, user);
};
export const isBlocked = (room) => { export const isBlocked = (room) => {
if (room) { if (room) {
const { t, blocked, blocker } = room; const { t, blocked, blocker } = room;

View File

@ -33,11 +33,11 @@ class AttachmentView extends React.Component {
const attachment = navigation.getParam('attachment'); const attachment = navigation.getParam('attachment');
const from = navigation.getParam('from'); const from = navigation.getParam('from');
const handleSave = navigation.getParam('handleSave', () => {}); const handleSave = navigation.getParam('handleSave', () => {});
const { title, video_url } = attachment; const { title } = attachment;
const options = { const options = {
title, title: decodeURI(title),
...themedHeader(theme), ...themedHeader(theme),
headerRight: !video_url ? <SaveButton testID='save-image' onPress={handleSave} /> : null headerRight: <SaveButton testID='save-image' onPress={handleSave} />
}; };
if (from !== 'MessagesView') { if (from !== 'MessagesView') {
options.gesturesEnabled = false; options.gesturesEnabled = false;
@ -84,8 +84,11 @@ class AttachmentView extends React.Component {
handleSave = async() => { handleSave = async() => {
const { attachment } = this.state; const { attachment } = this.state;
const { user, baseUrl } = this.props; const { user, baseUrl } = this.props;
const { image_url, image_type } = attachment; const {
const img = formatAttachmentUrl(image_url, user.id, user.token, baseUrl); image_url, image_type, video_url, video_type
} = attachment;
const url = image_url || video_url;
const mediaAttachment = formatAttachmentUrl(url, user.id, user.token, baseUrl);
if (isAndroid) { if (isAndroid) {
const rationale = { const rationale = {
@ -100,13 +103,13 @@ class AttachmentView extends React.Component {
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
const extension = `.${ mime.extension(image_type) || 'jpg' }`; const extension = image_url ? `.${ mime.extension(image_type) || 'jpg' }` : `.${ mime.extension(video_type) || 'mp4' }`;
const file = `${ FileSystem.documentDirectory + SHA256(image_url) + extension }`; const file = `${ FileSystem.documentDirectory + SHA256(url) + extension }`;
const { uri } = await FileSystem.downloadAsync(img, file); const { uri } = await FileSystem.downloadAsync(mediaAttachment, file);
await CameraRoll.save(uri, { album: 'Rocket.Chat' }); await CameraRoll.save(uri, { album: 'Rocket.Chat' });
EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') }); EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') });
} catch (e) { } catch (e) {
EventEmitter.emit(LISTENER, { message: I18n.t('error-save-image') }); EventEmitter.emit(LISTENER, { message: I18n.t(image_url ? 'error-save-image' : 'error-save-video') });
} }
this.setState({ loading: false }); this.setState({ loading: false });
}; };

View File

@ -79,11 +79,12 @@ class AuthenticationWebView extends React.PureComponent {
if (this.authType === 'saml' || this.authType === 'cas') { if (this.authType === 'saml' || this.authType === 'cas') {
const { navigation } = this.props; const { navigation } = this.props;
const ssoToken = navigation.getParam('ssoToken'); const ssoToken = navigation.getParam('ssoToken');
if (url.includes('ticket') || url.includes('validate') || url.includes('saml_idp_credentialToken')) { const parsedUrl = parse(url, true);
// ticket -> cas / validate & saml_idp_credentialToken -> saml
if (parsedUrl.pathname?.includes('validate') || parsedUrl.query?.ticket || parsedUrl.query?.saml_idp_credentialToken) {
let payload; let payload;
if (this.authType === 'saml') { if (this.authType === 'saml') {
const parsedUrl = parse(url, true); const token = parsedUrl.query?.saml_idp_credentialToken || ssoToken;
const token = (parsedUrl.query && parsedUrl.query.saml_idp_credentialToken) || ssoToken;
const credentialToken = { credentialToken: token }; const credentialToken = { credentialToken: token };
payload = { ...credentialToken, saml: true }; payload = { ...credentialToken, saml: true };
} else { } else {

View File

@ -125,10 +125,14 @@ class DirectoryView extends React.Component {
this.setState(({ showOptionsDropdown }) => ({ showOptionsDropdown: !showOptionsDropdown })); this.setState(({ showOptionsDropdown }) => ({ showOptionsDropdown: !showOptionsDropdown }));
} }
goRoom = async({ rid, name, t }) => { goRoom = async({
rid, name, t, search
}) => {
const { navigation } = this.props; const { navigation } = this.props;
await navigation.navigate('RoomsListView'); await navigation.navigate('RoomsListView');
navigation.navigate('RoomView', { rid, name, t }); navigation.navigate('RoomView', {
rid, name, t, search
});
} }
onPressItem = async(item) => { onPressItem = async(item) => {
@ -139,7 +143,9 @@ class DirectoryView extends React.Component {
this.goRoom({ rid: result.room._id, name: item.username, t: 'd' }); this.goRoom({ rid: result.room._id, name: item.username, t: 'd' });
} }
} else { } else {
this.goRoom({ rid: item._id, name: item.name, t: 'c' }); this.goRoom({
rid: item._id, name: item.name, t: 'c', search: true
});
} }
} }

View File

@ -67,7 +67,7 @@ class ModalBlockView extends React.Component {
return { return {
title: textParser([title]), title: textParser([title]),
...themedHeader(theme), ...themedHeader(theme),
headerLeft: ( headerLeft: close ? (
<CustomHeaderButtons> <CustomHeaderButtons>
<Item <Item
title={textParser([close.text])} title={textParser([close.text])}
@ -76,8 +76,8 @@ class ModalBlockView extends React.Component {
testID='close-modal-uikit' testID='close-modal-uikit'
/> />
</CustomHeaderButtons> </CustomHeaderButtons>
), ) : null,
headerRight: ( headerRight: submit ? (
<CustomHeaderButtons> <CustomHeaderButtons>
<Item <Item
title={textParser([submit.text])} title={textParser([submit.text])}
@ -86,7 +86,7 @@ class ModalBlockView extends React.Component {
testID='submit-modal-uikit' testID='submit-modal-uikit'
/> />
</CustomHeaderButtons> </CustomHeaderButtons>
) ) : null
}; };
} }
@ -136,7 +136,7 @@ class ModalBlockView extends React.Component {
const { navigation } = this.props; const { navigation } = this.props;
const oldData = prevProps.navigation.getParam('data', {}); const oldData = prevProps.navigation.getParam('data', {});
const newData = navigation.getParam('data', {}); const newData = navigation.getParam('data', {});
if (!isEqual(oldData, newData)) { if (oldData.viewId !== newData.viewId) {
navigation.push('ModalBlockView', { data: newData }); navigation.push('ModalBlockView', { data: newData });
} }
} }
@ -148,12 +148,14 @@ class ModalBlockView extends React.Component {
} }
handleUpdate = ({ type, ...data }) => { handleUpdate = ({ type, ...data }) => {
const { navigation } = this.props;
if ([MODAL_ACTIONS.ERRORS].includes(type)) { if ([MODAL_ACTIONS.ERRORS].includes(type)) {
const { errors } = data; const { errors } = data;
this.setState({ errors }); this.setState({ errors });
} else { } else {
this.setState({ data }); this.setState({ data });
} }
navigation.setParams({ data });
}; };
cancel = async({ closeModal }) => { cancel = async({ closeModal }) => {

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
Text, Keyboard, StyleSheet, TouchableOpacity, View, Alert Text, Keyboard, StyleSheet, TouchableOpacity, View, Alert, BackHandler
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system';
@ -105,6 +105,7 @@ class NewServerView extends React.Component {
certificate: null certificate: null
}; };
EventEmitter.addEventListener('NewServer', this.handleNewServerEvent); EventEmitter.addEventListener('NewServer', this.handleNewServerEvent);
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
} }
componentDidMount() { componentDidMount() {
@ -116,6 +117,16 @@ class NewServerView extends React.Component {
componentWillUnmount() { componentWillUnmount() {
EventEmitter.removeListener('NewServer', this.handleNewServerEvent); EventEmitter.removeListener('NewServer', this.handleNewServerEvent);
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
}
handleBackPress = () => {
const { navigation } = this.props;
if (navigation.isFocused() && this.previousServer) {
this.close();
return true;
}
return false;
} }
onChangeText = (text) => { onChangeText = (text) => {

View File

@ -3,9 +3,9 @@ import {
View, ScrollView, Switch, Text View, ScrollView, Switch, Text
} from 'react-native'; } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import RNPickerSelect from 'react-native-picker-select';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import database from '../../lib/database';
import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import ListItem from '../../containers/ListItem'; import ListItem from '../../containers/ListItem';
@ -15,9 +15,9 @@ import scrollPersistTaps from '../../utils/scrollPersistTaps';
import styles from './styles'; import styles from './styles';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation'; import { themedHeader } from '../../utils/navigation';
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
const SectionTitle = React.memo(({ title, theme }) => ( const SectionTitle = React.memo(({ title, theme }) => (
<Text <Text
@ -181,43 +181,52 @@ class NotificationPreferencesView extends React.Component {
} }
} }
onValueChangeSwitch = async(key, value) => { saveNotificationSettings = async(key, value, params) => {
const params = { const { room } = this.state;
[key]: value ? '1' : '0' const db = database.active;
};
await db.action(async() => {
await room.update(protectedFunction((r) => {
r[key] = value;
}));
});
try { try {
await RocketChat.saveNotificationSettings(this.rid, params); const result = await RocketChat.saveNotificationSettings(this.rid, params);
} catch (e) { if (result.success) {
log(e); return;
} }
} catch {
// do nothing
} }
onValueChangePicker = async(key, value) => { await db.action(async() => {
const params = { await room.update(protectedFunction((r) => {
[key]: value.toString() r[key] = room[key];
}; }));
try { });
await RocketChat.saveNotificationSettings(this.rid, params);
} catch (e) {
log(e);
}
} }
renderPicker = (key) => { onValueChangeSwitch = (key, value) => this.saveNotificationSettings(key, value, { [key]: value ? '1' : '0' });
onValueChangePicker = (key, value) => this.saveNotificationSettings(key, value, { [key]: value.toString() });
pickerSelection = (title, key) => {
const { room } = this.state;
const { navigation } = this.props;
navigation.navigate('PickerView', {
title,
data: OPTIONS[key],
value: room[key],
onChangeValue: value => this.onValueChangePicker(key, value)
});
}
renderPickerOption = (key) => {
const { room } = this.state; const { room } = this.state;
const { theme } = this.props; const { theme } = this.props;
return ( const text = room[key] ? OPTIONS[key].find(option => option.value === room[key]) : OPTIONS[key][0];
<RNPickerSelect return <Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}>{text?.label}</Text>;
testID={key}
style={{ viewContainer: styles.viewContainer }}
value={room[key]}
textInputProps={{ style: { ...styles.pickerText, color: themes[theme].actionTintColor } }}
useNativeAndroidPickerStyle={false}
placeholder={{}}
onValueChange={value => this.onValueChangePicker(key, value)}
items={OPTIONS[key]}
/>
);
} }
renderSwitch = (key) => { renderSwitch = (key) => {
@ -283,7 +292,8 @@ class NotificationPreferencesView extends React.Component {
<ListItem <ListItem
title={I18n.t('Alert')} title={I18n.t('Alert')}
testID='notification-preference-view-alert' testID='notification-preference-view-alert'
right={() => this.renderPicker('desktopNotifications')} onPress={title => this.pickerSelection(title, 'desktopNotifications')}
right={() => this.renderPickerOption('desktopNotifications')}
theme={theme} theme={theme}
/> />
<Separator theme={theme} /> <Separator theme={theme} />
@ -296,7 +306,8 @@ class NotificationPreferencesView extends React.Component {
<ListItem <ListItem
title={I18n.t('Alert')} title={I18n.t('Alert')}
testID='notification-preference-view-push-notification' testID='notification-preference-view-push-notification'
right={() => this.renderPicker('mobilePushNotifications')} onPress={title => this.pickerSelection(title, 'mobilePushNotifications')}
right={() => this.renderPickerOption('mobilePushNotifications')}
theme={theme} theme={theme}
/> />
<Separator theme={theme} /> <Separator theme={theme} />
@ -309,21 +320,24 @@ class NotificationPreferencesView extends React.Component {
<ListItem <ListItem
title={I18n.t('Audio')} title={I18n.t('Audio')}
testID='notification-preference-view-audio' testID='notification-preference-view-audio'
right={() => this.renderPicker('audioNotifications')} onPress={title => this.pickerSelection(title, 'audioNotifications')}
right={() => this.renderPickerOption('audioNotifications')}
theme={theme} theme={theme}
/> />
<Separator theme={theme} /> <Separator theme={theme} />
<ListItem <ListItem
title={I18n.t('Sound')} title={I18n.t('Sound')}
testID='notification-preference-view-sound' testID='notification-preference-view-sound'
right={() => this.renderPicker('audioNotificationValue')} onPress={title => this.pickerSelection(title, 'audioNotificationValue')}
right={() => this.renderPickerOption('audioNotificationValue')}
theme={theme} theme={theme}
/> />
<Separator theme={theme} /> <Separator theme={theme} />
<ListItem <ListItem
title={I18n.t('Notification_Duration')} title={I18n.t('Notification_Duration')}
testID='notification-preference-view-notification-duration' testID='notification-preference-view-notification-duration'
right={() => this.renderPicker('desktopNotificationDuration')} onPress={title => this.pickerSelection(title, 'desktopNotificationDuration')}
right={() => this.renderPickerOption('desktopNotificationDuration')}
theme={theme} theme={theme}
/> />
<Separator theme={theme} /> <Separator theme={theme} />
@ -335,7 +349,8 @@ class NotificationPreferencesView extends React.Component {
<ListItem <ListItem
title={I18n.t('Alert')} title={I18n.t('Alert')}
testID='notification-preference-view-email-alert' testID='notification-preference-view-email-alert'
right={() => this.renderPicker('emailNotifications')} onPress={title => this.pickerSelection(title, 'emailNotifications')}
right={() => this.renderPickerOption('emailNotifications')}
theme={theme} theme={theme}
/> />
<Separator theme={theme} /> <Separator theme={theme} />

View File

@ -24,9 +24,6 @@ export default StyleSheet.create({
paddingVertical: 10, paddingVertical: 10,
fontSize: 14 fontSize: 14
}, },
viewContainer: {
justifyContent: 'center'
},
pickerText: { pickerText: {
...sharedStyles.textRegular, ...sharedStyles.textRegular,
fontSize: 16 fontSize: 16

96
app/views/PickerView.js Normal file
View File

@ -0,0 +1,96 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FlatList, StyleSheet } from 'react-native';
import I18n from '../i18n';
import { themedHeader } from '../utils/navigation';
import { withTheme } from '../theme';
import { themes } from '../constants/colors';
import sharedStyles from './Styles';
import ListItem from '../containers/ListItem';
import Check from '../containers/Check';
import Separator from '../containers/Separator';
const styles = StyleSheet.create({
check: {
marginHorizontal: 0
}
});
const Item = React.memo(({
item,
selected,
onItemPress,
theme
}) => (
<ListItem
title={item.label}
right={selected && (() => <Check theme={theme} style={styles.check} />)}
onPress={onItemPress}
theme={theme}
/>
));
Item.propTypes = {
item: PropTypes.object,
selected: PropTypes.bool,
onItemPress: PropTypes.func,
theme: PropTypes.string
};
class PickerView extends React.PureComponent {
static navigationOptions = ({ navigation, screenProps }) => ({
title: navigation.getParam('title', I18n.t('Select_an_option')),
...themedHeader(screenProps.theme)
})
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.string
}
constructor(props) {
super(props);
const data = props.navigation.getParam('data', []);
const value = props.navigation.getParam('value');
this.state = { data, value };
}
onChangeValue = (value) => {
const { navigation } = this.props;
const onChange = navigation.getParam('onChangeValue', () => {});
onChange(value);
navigation.goBack();
}
render() {
const { data, value } = this.state;
const { theme } = this.props;
return (
<FlatList
data={data}
keyExtractor={item => item.value}
renderItem={({ item }) => (
<Item
item={item}
theme={theme}
selected={(value || data[0]?.value) === item.value}
onItemPress={() => this.onChangeValue(item.value)}
/>
)}
ItemSeparatorComponent={() => <Separator theme={theme} />}
contentContainerStyle={[
sharedStyles.listContentContainer,
{
backgroundColor: themes[theme].auxiliaryBackground,
borderColor: themes[theme].separatorColor
}
]}
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
/>
);
}
}
export default withTheme(PickerView);

View File

@ -87,6 +87,7 @@ class RoomActionsView extends React.Component {
async componentDidMount() { async componentDidMount() {
this.mounted = true; this.mounted = true;
const { room, member } = this.state; const { room, member } = this.state;
if (room.rid) {
if (!room.id) { if (!room.id) {
try { try {
const result = await RocketChat.getChannelInfo(room.rid); const result = await RocketChat.getChannelInfo(room.rid);
@ -117,6 +118,7 @@ class RoomActionsView extends React.Component {
this.canAddUser(); this.canAddUser();
this.canInviteUser(); this.canInviteUser();
} }
}
componentWillUnmount() { componentWillUnmount() {
if (this.subscription && this.subscription.unsubscribe) { if (this.subscription && this.subscription.unsubscribe) {
@ -384,11 +386,10 @@ class RoomActionsView extends React.Component {
updateRoomMember = async() => { updateRoomMember = async() => {
const { room } = this.state; const { room } = this.state;
const { user } = this.props;
try { try {
if (!RocketChat.isGroupChat(room)) { if (!RocketChat.isGroupChat(room)) {
const roomUserId = RocketChat.getUidDirectMessage(room, user.id); const roomUserId = RocketChat.getUidDirectMessage(room);
const result = await RocketChat.getUserInfo(roomUserId); const result = await RocketChat.getUserInfo(roomUserId);
if (result.success) { if (result.success) {
this.setState({ member: result.user }); this.setState({ member: result.user });
@ -443,7 +444,7 @@ class RoomActionsView extends React.Component {
Alert.alert( Alert.alert(
I18n.t('Are_you_sure_question_mark'), I18n.t('Are_you_sure_question_mark'),
I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: room.t === 'd' ? room.fname : room.name }), I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }),
[ [
{ {
text: I18n.t('Cancel'), text: I18n.t('Cancel'),
@ -485,7 +486,7 @@ class RoomActionsView extends React.Component {
: ( : (
<View style={styles.roomTitleRow}> <View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.prid ? 'discussion' : room.t} theme={theme} /> <RoomTypeIcon type={room.prid ? 'discussion' : room.t} theme={theme} />
<Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.prid ? room.fname : room.name}</Text> <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{RocketChat.getRoomTitle(room)}</Text>
</View> </View>
) )
} }

View File

@ -125,13 +125,13 @@ class RoomInfoEditView extends React.Component {
init = (room) => { init = (room) => {
const { const {
name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired, sysMes description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired, sysMes
} = room; } = room;
// fake password just to user knows about it // fake password just to user knows about it
this.randomValue = random(15); this.randomValue = random(15);
this.setState({ this.setState({
room, room,
name, name: RocketChat.getRoomTitle(room),
description, description,
topic, topic,
announcement, announcement,

View File

@ -36,7 +36,7 @@ const getRoomTitle = (room, type, name, username, statusText, theme) => (type ==
: ( : (
<View style={styles.roomTitleRow}> <View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' theme={theme} /> <RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' theme={theme} />
<Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]} key='room-info-name'>{room.prid ? room.fname : room.name}</Text> <Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]} key='room-info-name'>{RocketChat.getRoomTitle(room)}</Text>
</View> </View>
) )
); );

View File

@ -37,7 +37,6 @@ const RoomHeaderLeft = ({
style={styles.avatar} style={styles.avatar}
userId={userId} userId={userId}
token={token} token={token}
theme={theme}
onPress={goRoomActionsView} onPress={goRoomActionsView}
/> />
); );

View File

@ -101,8 +101,10 @@ const mapStateToProps = (state, ownProps) => {
if (type === 'd') { if (type === 'd') {
const user = getUserSelector(state); const user = getUserSelector(state);
if (user.id) { if (user.id) {
if (state.activeUsers[roomUserId]) { if (state.activeUsers[roomUserId] && state.meteor.connected) {
({ status, statusText } = state.activeUsers[roomUserId]); ({ status, statusText } = state.activeUsers[roomUserId]);
} else {
status = 'offline';
} }
} }
} }

View File

@ -96,7 +96,7 @@ class List extends React.Component {
this.messagesSubscription = this.messagesObservable this.messagesSubscription = this.messagesObservable
.subscribe((data) => { .subscribe((data) => {
this.interaction = InteractionManager.runAfterInteractions(() => { this.interaction = InteractionManager.runAfterInteractions(() => {
if (tmid) { if (tmid && this.thread) {
data = [this.thread, ...data]; data = [this.thread, ...data];
} }
const messages = orderBy(data, ['ts'], ['desc']); const messages = orderBy(data, ['ts'], ['desc']);

View File

@ -34,7 +34,8 @@ import { themes } from '../../constants/colors';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import ReactionsModal from '../../containers/ReactionsModal'; import ReactionsModal from '../../containers/ReactionsModal';
import { LISTENER } from '../../containers/Toast'; import { LISTENER } from '../../containers/Toast';
import { isReadOnly, isBlocked } from '../../utils/room'; import { isBlocked } from '../../utils/room';
import { isReadOnly } from '../../utils/isReadOnly';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../../utils/deviceInfo';
import { showErrorAlert } from '../../utils/info'; import { showErrorAlert } from '../../utils/info';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
@ -65,9 +66,10 @@ const stateAttrsUpdate = [
'editing', 'editing',
'replying', 'replying',
'reacting', 'reacting',
'readOnly',
'member' 'member'
]; ];
const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'muted', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname']; const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'muted', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles'];
class RoomView extends React.Component { class RoomView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ navigation, screenProps }) => {
@ -164,6 +166,7 @@ class RoomView extends React.Component {
const selectedMessage = props.navigation.getParam('message'); const selectedMessage = props.navigation.getParam('message');
const name = props.navigation.getParam('name'); const name = props.navigation.getParam('name');
const fname = props.navigation.getParam('fname'); const fname = props.navigation.getParam('fname');
const search = props.navigation.getParam('search');
const prid = props.navigation.getParam('prid'); const prid = props.navigation.getParam('prid');
this.state = { this.state = {
joined: true, joined: true,
@ -183,7 +186,7 @@ class RoomView extends React.Component {
replying: !!selectedMessage, replying: !!selectedMessage,
replyWithMention: false, replyWithMention: false,
reacting: false, reacting: false,
announcement: null readOnly: false
}; };
if (room && room.observe) { if (room && room.observe) {
@ -192,6 +195,12 @@ class RoomView extends React.Component {
this.findAndObserveRoom(this.rid); this.findAndObserveRoom(this.rid);
} }
this.setReadOnly();
if (search) {
this.updateRoom();
}
this.messagebox = React.createRef(); this.messagebox = React.createRef();
this.list = React.createRef(); this.list = React.createRef();
this.mounted = false; this.mounted = false;
@ -209,7 +218,7 @@ class RoomView extends React.Component {
} = this.props; } = this.props;
if ((room.id || room.rid) && !this.tmid) { if ((room.id || room.rid) && !this.tmid) {
navigation.setParams({ navigation.setParams({
name: this.getRoomTitle(room), name: RocketChat.getRoomTitle(room),
subtitle: room.topic, subtitle: room.topic,
avatar: room.name, avatar: room.name,
t: room.t, t: room.t,
@ -222,11 +231,14 @@ class RoomView extends React.Component {
if (this.tmid) { if (this.tmid) {
navigation.setParams({ toggleFollowThread: this.toggleFollowThread, goRoomActionsView: this.goRoomActionsView }); navigation.setParams({ toggleFollowThread: this.toggleFollowThread, goRoomActionsView: this.goRoomActionsView });
} }
if (isAuthenticated && this.rid) { if (this.rid) {
this.sub.subscribe();
if (isAuthenticated) {
this.init(); this.init();
} else if (this.rid) { } else {
EventEmitter.addEventListener('connected', this.handleConnected); EventEmitter.addEventListener('connected', this.handleConnected);
} }
}
if (isIOS && this.rid) { if (isIOS && this.rid) {
this.updateUnreadCount(); this.updateUnreadCount();
} }
@ -275,9 +287,12 @@ class RoomView extends React.Component {
if (roomUpdate.topic !== prevState.roomUpdate.topic) { if (roomUpdate.topic !== prevState.roomUpdate.topic) {
navigation.setParams({ subtitle: roomUpdate.topic }); navigation.setParams({ subtitle: roomUpdate.topic });
} }
if (!isEqual(prevState.roomUpdate.roles, roomUpdate.roles)) {
this.setReadOnly();
}
} }
if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) { if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) {
navigation.setParams({ name: this.getRoomTitle(room) }); navigation.setParams({ name: RocketChat.getRoomTitle(room) });
} }
} }
@ -343,6 +358,32 @@ class RoomView extends React.Component {
}); });
} }
setReadOnly = async() => {
const { room } = this.state;
const { user } = this.props;
const readOnly = await isReadOnly(room, user);
this.setState({ readOnly });
}
updateRoom = async() => {
const db = database.active;
try {
const subCollection = db.collections.get('subscriptions');
const sub = await subCollection.find(this.rid);
const { room } = await RocketChat.getRoomInfo(this.rid);
await db.action(async() => {
await sub.update((s) => {
Object.assign(s, room);
});
});
} catch {
// do nothing
}
}
init = async() => { init = async() => {
try { try {
this.setState({ loading: true }); this.setState({ loading: true });
@ -361,7 +402,6 @@ class RoomView extends React.Component {
this.setLastOpen(null); this.setLastOpen(null);
} }
RocketChat.readMessages(room.rid, newLastOpen, true).catch(e => console.log(e)); RocketChat.readMessages(room.rid, newLastOpen, true).catch(e => console.log(e));
this.sub.subscribe();
} }
} }
@ -388,10 +428,10 @@ class RoomView extends React.Component {
const { t } = room; const { t } = room;
if (t === 'd' && !RocketChat.isGroupChat(room)) { if (t === 'd' && !RocketChat.isGroupChat(room)) {
const { user, navigation } = this.props; const { navigation } = this.props;
try { try {
const roomUserId = RocketChat.getUidDirectMessage(room, user.id); const roomUserId = RocketChat.getUidDirectMessage(room);
navigation.setParams({ roomUserId }); navigation.setParams({ roomUserId });
@ -416,7 +456,7 @@ class RoomView extends React.Component {
this.setState({ room }); this.setState({ room });
if (!this.tmid) { if (!this.tmid) {
navigation.setParams({ navigation.setParams({
name: this.getRoomTitle(room), name: RocketChat.getRoomTitle(room),
subtitle: room.topic, subtitle: room.topic,
avatar: room.name, avatar: room.name,
t: room.t t: room.t
@ -605,7 +645,7 @@ class RoomView extends React.Component {
const { room } = this.state; const { room } = this.state;
if (rid === this.rid) { if (rid === this.rid) {
Navigation.navigate('RoomsListView'); Navigation.navigate('RoomsListView');
showErrorAlert(I18n.t('You_were_removed_from_channel', { channel: this.getRoomTitle(room) }), I18n.t('Oops')); showErrorAlert(I18n.t('You_were_removed_from_channel', { channel: RocketChat.getRoomTitle(room) }), I18n.t('Oops'));
} }
} }
@ -627,11 +667,6 @@ class RoomView extends React.Component {
}); });
}; };
getRoomTitle = (room) => {
const { useRealName } = this.props;
return ((room.prid || useRealName) && room.fname) || room.name;
}
getMessages = () => { getMessages = () => {
const { room } = this.state; const { room } = this.state;
if (room.lastOpen) { if (room.lastOpen) {
@ -668,7 +703,6 @@ class RoomView extends React.Component {
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
fetchThreadName = async(tmid, messageId) => { fetchThreadName = async(tmid, messageId) => {
try { try {
const { room } = this.state;
const db = database.active; const db = database.active;
const threadCollection = db.collections.get('threads'); const threadCollection = db.collections.get('threads');
const messageCollection = db.collections.get('messages'); const messageCollection = db.collections.get('messages');
@ -691,7 +725,7 @@ class RoomView extends React.Component {
await db.batch( await db.batch(
threadCollection.prepareCreate((t) => { threadCollection.prepareCreate((t) => {
t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema); t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema);
t.subscription.set(room); t.subscription.id = this.rid;
Object.assign(t, thread); Object.assign(t, thread);
}), }),
messageRecord.prepareUpdate((m) => { messageRecord.prepareUpdate((m) => {
@ -701,7 +735,7 @@ class RoomView extends React.Component {
}); });
} }
} catch (e) { } catch (e) {
log(e); // log(e);
} }
} }
@ -761,12 +795,6 @@ class RoomView extends React.Component {
} }
} }
get isReadOnly() {
const { room } = this.state;
const { user } = this.props;
return isReadOnly(room, user);
}
blockAction = ({ blockAction = ({
actionId, appId, value, blockId, rid, mid actionId, appId, value, blockId, rid, mid
}) => RocketChat.triggerBlockAction({ }) => RocketChat.triggerBlockAction({
@ -854,7 +882,7 @@ class RoomView extends React.Component {
renderFooter = () => { renderFooter = () => {
const { const {
joined, room, selectedMessage, editing, replying, replyWithMention joined, room, selectedMessage, editing, replying, replyWithMention, readOnly
} = this.state; } = this.state;
const { navigation, theme } = this.props; const { navigation, theme } = this.props;
@ -875,7 +903,7 @@ class RoomView extends React.Component {
</View> </View>
); );
} }
if (this.isReadOnly || room.archived) { if (readOnly) {
return ( return (
<View style={styles.readOnly}> <View style={styles.readOnly}>
<Text style={[styles.previewMode, { color: themes[theme].titleText }]} accessibilityLabel={I18n.t('This_room_is_read_only')}>{I18n.t('This_room_is_read_only')}</Text> <Text style={[styles.previewMode, { color: themes[theme].titleText }]} accessibilityLabel={I18n.t('This_room_is_read_only')}>{I18n.t('This_room_is_read_only')}</Text>
@ -913,7 +941,7 @@ class RoomView extends React.Component {
renderActions = () => { renderActions = () => {
const { const {
room, selectedMessage, showActions, showErrorActions, joined room, selectedMessage, showActions, showErrorActions, joined, readOnly
} = this.state; } = this.state;
const { const {
user, navigation user, navigation
@ -934,7 +962,7 @@ class RoomView extends React.Component {
editInit={this.onEditInit} editInit={this.onEditInit}
replyInit={this.onReplyInit} replyInit={this.onReplyInit}
reactionInit={this.onReactionInit} reactionInit={this.onReactionInit}
isReadOnly={this.isReadOnly} isReadOnly={readOnly}
/> />
) )
: null : null

View File

@ -174,6 +174,7 @@ class RoomsListView extends React.Component {
roomsRequest: PropTypes.func, roomsRequest: PropTypes.func,
closeServerDropdown: PropTypes.func, closeServerDropdown: PropTypes.func,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
connected: PropTypes.bool,
split: PropTypes.bool split: PropTypes.bool
}; };
@ -211,13 +212,17 @@ class RoomsListView extends React.Component {
this.willFocusListener = navigation.addListener('willFocus', () => { this.willFocusListener = navigation.addListener('willFocus', () => {
// Check if there were changes while not focused (it's set on sCU) // Check if there were changes while not focused (it's set on sCU)
if (this.shouldUpdate) { if (this.shouldUpdate) {
// animateNextTransition();
this.forceUpdate(); this.forceUpdate();
this.shouldUpdate = false; this.shouldUpdate = false;
} }
}); });
this.didFocusListener = navigation.addListener('didFocus', () => { this.didFocusListener = navigation.addListener('didFocus', () => {
this.animated = true; this.animated = true;
// Check if there were changes while not focused (it's set on sCU)
if (this.shouldUpdate) {
this.forceUpdate();
this.shouldUpdate = false;
}
this.backHandler = BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); this.backHandler = BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
}); });
this.willBlurListener = navigation.addListener('willBlur', () => { this.willBlurListener = navigation.addListener('willBlur', () => {
@ -302,6 +307,7 @@ class RoomsListView extends React.Component {
showFavorites, showFavorites,
showUnread, showUnread,
appState, appState,
connected,
roomsRequest roomsRequest
} = this.props; } = this.props;
@ -317,6 +323,7 @@ class RoomsListView extends React.Component {
} else if ( } else if (
appState === 'foreground' appState === 'foreground'
&& appState !== prevProps.appState && appState !== prevProps.appState
&& connected
) { ) {
roomsRequest(); roomsRequest();
} }
@ -528,10 +535,7 @@ class RoomsListView extends React.Component {
getUserPresence = uid => RocketChat.getUserPresence(uid) getUserPresence = uid => RocketChat.getUserPresence(uid)
getUidDirectMessage = (room) => { getUidDirectMessage = room => RocketChat.getUidDirectMessage(room);
const { user: { id } } = this.props;
return RocketChat.getUidDirectMessage(room, id);
}
goRoom = (item) => { goRoom = (item) => {
const { navigation } = this.props; const { navigation } = this.props;
@ -542,12 +546,17 @@ class RoomsListView extends React.Component {
name: this.getRoomTitle(item), name: this.getRoomTitle(item),
t: item.t, t: item.t,
prid: item.prid, prid: item.prid,
roomUserId: this.getUidDirectMessage(item), room: item,
room: item search: item.search,
roomUserId: this.getUidDirectMessage(item)
}); });
} }
_onPressItem = async(item = {}) => { _onPressItem = async(item = {}) => {
const { navigation } = this.props;
if (!navigation.isFocused()) {
return;
}
if (!item.search) { if (!item.search) {
return this.goRoom(item); return this.goRoom(item);
} }
@ -894,6 +903,7 @@ class RoomsListView extends React.Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
user: getUserSelector(state), user: getUserSelector(state),
server: state.server.server, server: state.server.server,
connected: state.server.connected,
searchText: state.rooms.searchText, searchText: state.rooms.searchText,
loadingServer: state.server.loading, loadingServer: state.server.loading,
showServerDropdown: state.rooms.showServerDropdown, showServerDropdown: state.rooms.showServerDropdown,

View File

@ -13,7 +13,8 @@ import styles from './styles';
import TextInput from '../../containers/TextInput'; import TextInput from '../../containers/TextInput';
import ActivityIndicator from '../../containers/ActivityIndicator'; import ActivityIndicator from '../../containers/ActivityIndicator';
import { CustomHeaderButtons, Item } from '../../containers/HeaderButton'; import { CustomHeaderButtons, Item } from '../../containers/HeaderButton';
import { isReadOnly, isBlocked } from '../../utils/room'; import { isBlocked } from '../../utils/room';
import { isReadOnly } from '../../utils/isReadOnly';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation'; import { themedHeader } from '../../utils/navigation';
@ -69,18 +70,29 @@ class ShareView extends React.Component {
fileInfo, fileInfo,
room, room,
loading: false, loading: false,
readOnly: false,
file: { file: {
name: fileInfo ? fileInfo.name : '', name: fileInfo ? fileInfo.name : '',
description: '' description: ''
} }
}; };
this.setReadOnly();
} }
componentDidMount() { componentDidMount() {
const { navigation } = this.props;
navigation.setParams({ sendMessage: this._sendMessage });
}
setReadOnly = async() => {
const { room } = this.state; const { room } = this.state;
const { navigation, user } = this.props; const { navigation, user } = this.props;
const { username } = user; const { username } = user;
navigation.setParams({ sendMessage: this._sendMessage, canSend: !(isReadOnly(room, { username }) || isBlocked(room)) }); const readOnly = await isReadOnly(room, { username });
this.setState({ readOnly });
navigation.setParams({ canSend: !(readOnly || isBlocked(room)) });
} }
bytesToSize = bytes => `${ (bytes / 1048576).toFixed(2) }MB`; bytesToSize = bytes => `${ (bytes / 1048576).toFixed(2) }MB`;
@ -237,8 +249,9 @@ class ShareView extends React.Component {
renderError = () => { renderError = () => {
const { room } = this.state; const { room } = this.state;
const { theme } = this.props;
return ( return (
<View style={[styles.container, styles.centered]}> <View style={[styles.container, styles.centered, { backgroundColor: themes[theme].backgroundColor }]}>
<Text style={styles.title}> <Text style={styles.title}>
{ {
isBlocked(room) ? I18n.t('This_room_is_blocked') : I18n.t('This_room_is_read_only') isBlocked(room) ? I18n.t('This_room_is_blocked') : I18n.t('This_room_is_read_only')
@ -249,13 +262,12 @@ class ShareView extends React.Component {
} }
render() { render() {
const { user, theme } = this.props; const { theme } = this.props;
const { username } = user;
const { const {
name, loading, isMedia, room name, loading, isMedia, room, readOnly
} = this.state; } = this.state;
if (isReadOnly(room, { username }) || isBlocked(room)) { if (readOnly || isBlocked(room)) {
return this.renderError(); return this.renderError();
} }

View File

@ -21,6 +21,8 @@ import { withSplit } from '../split';
import { themedHeader } from '../utils/navigation'; import { themedHeader } from '../utils/navigation';
import { getUserSelector } from '../selectors/login'; import { getUserSelector } from '../selectors/login';
import { CustomHeaderButtons, Item, CancelModalButton } from '../containers/HeaderButton'; import { CustomHeaderButtons, Item, CancelModalButton } from '../containers/HeaderButton';
import store from '../lib/createStore';
import { setUser } from '../actions/login';
const STATUS = [{ const STATUS = [{
id: 'online', id: 'online',
@ -75,6 +77,7 @@ class StatusView extends React.Component {
static propTypes = { static propTypes = {
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string,
status: PropTypes.string, status: PropTypes.string,
statusText: PropTypes.string statusText: PropTypes.string
}), }),
@ -112,11 +115,12 @@ class StatusView extends React.Component {
setCustomStatus = async() => { setCustomStatus = async() => {
const { statusText } = this.state; const { statusText } = this.state;
const { user } = this.props;
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
const result = await RocketChat.setUserStatus(statusText); const result = await RocketChat.setUserStatus(user.status, statusText);
if (result.success) { if (result.success) {
EventEmitter.emit(LISTENER, { message: I18n.t('Status_saved_successfully') }); EventEmitter.emit(LISTENER, { message: I18n.t('Status_saved_successfully') });
} else { } else {
@ -163,6 +167,7 @@ class StatusView extends React.Component {
} }
renderItem = ({ item }) => { renderItem = ({ item }) => {
const { statusText } = this.state;
const { theme, user } = this.props; const { theme, user } = this.props;
const { id, name } = item; const { id, name } = item;
return ( return (
@ -171,7 +176,10 @@ class StatusView extends React.Component {
onPress={async() => { onPress={async() => {
if (user.status !== item.id) { if (user.status !== item.id) {
try { try {
await RocketChat.setUserPresenceDefaultStatus(item.id); const result = await RocketChat.setUserStatus(item.id, statusText);
if (result.success) {
store.dispatch(setUser({ status: item.id }));
}
} catch (e) { } catch (e) {
log(e); log(e);
} }

View File

@ -71,12 +71,10 @@ class ThreadMessagesView extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
console.countReset(`${ this.constructor.name }.render calls`);
if (this.mountInteraction && this.mountInteraction.cancel) { if (this.mountInteraction && this.mountInteraction.cancel) {
this.mountInteraction.cancel(); this.mountInteraction.cancel();
} }
if (this.loadInteraction && this.loadInteraction.cancel) {
this.loadInteraction.cancel();
}
if (this.syncInteraction && this.syncInteraction.cancel) { if (this.syncInteraction && this.syncInteraction.cancel) {
this.syncInteraction.cancel(); this.syncInteraction.cancel();
} }
@ -89,13 +87,14 @@ class ThreadMessagesView extends React.Component {
} }
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
subscribeData = () => { subscribeData = async() => {
try { try {
const db = database.active; const db = database.active;
this.subObservable = db.collections const subscription = await db.collections
.get('subscriptions') .get('subscriptions')
.findAndObserve(this.rid); .find(this.rid);
this.subSubscription = this.subObservable const observable = subscription.observe();
this.subSubscription = observable
.subscribe((data) => { .subscribe((data) => {
this.subscription = data; this.subscription = data;
}); });
@ -116,14 +115,14 @@ class ThreadMessagesView extends React.Component {
} }
}); });
} catch (e) { } catch (e) {
log(e); // Do nothing
} }
} }
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
init = () => { init = () => {
if (!this.subscription) { if (!this.subscription) {
return; this.load();
} }
try { try {
const lastThreadSync = new Date(); const lastThreadSync = new Date();
@ -138,6 +137,13 @@ class ThreadMessagesView extends React.Component {
} }
updateThreads = async({ update, remove, lastThreadSync }) => { updateThreads = async({ update, remove, lastThreadSync }) => {
// if there's no subscription, manage data on this.state.messages
// note: sync will never be called without subscription
if (!this.subscription) {
this.setState(({ messages }) => ({ messages: [...messages, ...update] }));
return;
}
try { try {
const db = database.active; const db = database.active;
const threadsCollection = db.collections.get('threads'); const threadsCollection = db.collections.get('threads');
@ -198,14 +204,11 @@ class ThreadMessagesView extends React.Component {
rid: this.rid, count: API_FETCH_COUNT, offset: messages.length rid: this.rid, count: API_FETCH_COUNT, offset: messages.length
}); });
if (result.success) { if (result.success) {
this.loadInteraction = InteractionManager.runAfterInteractions(() => {
this.updateThreads({ update: result.threads, lastThreadSync }); this.updateThreads({ update: result.threads, lastThreadSync });
this.setState({ this.setState({
loading: false, loading: false,
end: result.count < API_FETCH_COUNT end: result.count < API_FETCH_COUNT
}); });
});
} }
} catch (e) { } catch (e) {
log(e); log(e);
@ -319,6 +322,7 @@ class ThreadMessagesView extends React.Component {
} }
render() { render() {
console.count(`${ this.constructor.name }.render calls`);
const { loading, messages } = this.state; const { loading, messages } = this.state;
const { theme } = this.props; const { theme } = this.props;
@ -335,7 +339,7 @@ class ThreadMessagesView extends React.Component {
renderItem={this.renderItem} renderItem={this.renderItem}
style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
keyExtractor={item => item.id} keyExtractor={item => item._id}
onEndReached={this.load} onEndReached={this.load}
onEndReachedThreshold={0.5} onEndReachedThreshold={0.5}
maxToRenderPerBatch={5} maxToRenderPerBatch={5}

View File

@ -309,11 +309,6 @@ PODS:
- React - React
- react-native-slider (2.0.5): - react-native-slider (2.0.5):
- React - React
- react-native-video (5.0.2):
- React
- react-native-video/Video (= 5.0.2)
- react-native-video/Video (5.0.2):
- React
- react-native-webview (7.5.1): - react-native-webview (7.5.1):
- React - React
- React-RCTActionSheet (0.61.5): - React-RCTActionSheet (0.61.5):
@ -408,6 +403,7 @@ PODS:
- SDWebImageWebPCoder (0.2.5): - SDWebImageWebPCoder (0.2.5):
- libwebp (~> 1.0) - libwebp (~> 1.0)
- SDWebImage/Core (~> 5.0) - SDWebImage/Core (~> 5.0)
- SocketRocket (0.5.1)
- UMBarCodeScannerInterface (3.0.0) - UMBarCodeScannerInterface (3.0.0)
- UMCameraInterface (3.0.0) - UMCameraInterface (3.0.0)
- UMConstantsInterface (3.0.0) - UMConstantsInterface (3.0.0)
@ -462,7 +458,6 @@ DEPENDENCIES:
- react-native-notifications (from `../node_modules/react-native-notifications`) - react-native-notifications (from `../node_modules/react-native-notifications`)
- react-native-orientation-locker (from `../node_modules/react-native-orientation-locker`) - react-native-orientation-locker (from `../node_modules/react-native-orientation-locker`)
- "react-native-slider (from `../node_modules/@react-native-community/slider`)" - "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-video (from `../node_modules/react-native-video`)
- react-native-webview (from `../node_modules/react-native-webview`) - react-native-webview (from `../node_modules/react-native-webview`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
@ -492,6 +487,7 @@ DEPENDENCIES:
- RNScreens (from `../node_modules/react-native-screens`) - RNScreens (from `../node_modules/react-native-screens`)
- RNUserDefaults (from `../node_modules/rn-user-defaults`) - RNUserDefaults (from `../node_modules/rn-user-defaults`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`) - RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- SocketRocket (from `../node_modules/detox/ios_src/SocketRocket`)
- UMBarCodeScannerInterface (from `../node_modules/unimodules-barcode-scanner-interface/ios`) - UMBarCodeScannerInterface (from `../node_modules/unimodules-barcode-scanner-interface/ios`)
- UMCameraInterface (from `../node_modules/unimodules-camera-interface/ios`) - UMCameraInterface (from `../node_modules/unimodules-camera-interface/ios`)
- UMConstantsInterface (from `../node_modules/unimodules-constants-interface/ios`) - UMConstantsInterface (from `../node_modules/unimodules-constants-interface/ios`)
@ -605,8 +601,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-orientation-locker" :path: "../node_modules/react-native-orientation-locker"
react-native-slider: react-native-slider:
:path: "../node_modules/@react-native-community/slider" :path: "../node_modules/@react-native-community/slider"
react-native-video:
:path: "../node_modules/react-native-video"
react-native-webview: react-native-webview:
:path: "../node_modules/react-native-webview" :path: "../node_modules/react-native-webview"
React-RCTActionSheet: React-RCTActionSheet:
@ -663,6 +657,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/rn-user-defaults" :path: "../node_modules/rn-user-defaults"
RNVectorIcons: RNVectorIcons:
:path: "../node_modules/react-native-vector-icons" :path: "../node_modules/react-native-vector-icons"
SocketRocket:
:path: "../node_modules/detox/ios_src/SocketRocket"
UMBarCodeScannerInterface: UMBarCodeScannerInterface:
:path: !ruby/object:Pathname :path: !ruby/object:Pathname
path: "../node_modules/unimodules-barcode-scanner-interface/ios" path: "../node_modules/unimodules-barcode-scanner-interface/ios"
@ -759,7 +755,6 @@ SPEC CHECKSUMS:
react-native-notifications: 163ddedac6fcc8d850ea15b06abdadcacdff00f1 react-native-notifications: 163ddedac6fcc8d850ea15b06abdadcacdff00f1
react-native-orientation-locker: 23918c400376a7043e752c639c122fcf6bce8f1c react-native-orientation-locker: 23918c400376a7043e752c639c122fcf6bce8f1c
react-native-slider: 39208600e44f885e2d2c0510b5c6435a0f62d087 react-native-slider: 39208600e44f885e2d2c0510b5c6435a0f62d087
react-native-video: d01ed7ff1e38fa7dcc6c15c94cf505e661b7bfd0
react-native-webview: 2aadbfef6b9eaa9e89b306ae3e31e6e870a6306d react-native-webview: 2aadbfef6b9eaa9e89b306ae3e31e6e870a6306d
React-RCTActionSheet: 600b4d10e3aea0913b5a92256d2719c0cdd26d76 React-RCTActionSheet: 600b4d10e3aea0913b5a92256d2719c0cdd26d76
React-RCTAnimation: 791a87558389c80908ed06cc5dfc5e7920dfa360 React-RCTAnimation: 791a87558389c80908ed06cc5dfc5e7920dfa360
@ -791,6 +786,7 @@ SPEC CHECKSUMS:
RSKImageCropper: a446db0e8444a036b34f3c43db01b2373baa4b2a RSKImageCropper: a446db0e8444a036b34f3c43db01b2373baa4b2a
SDWebImage: 4d5c027c935438f341ed33dbac53ff9f479922ca SDWebImage: 4d5c027c935438f341ed33dbac53ff9f479922ca
SDWebImageWebPCoder: 947093edd1349d820c40afbd9f42acb6cdecd987 SDWebImageWebPCoder: 947093edd1349d820c40afbd9f42acb6cdecd987
SocketRocket: dbb1554b8fc288ef8ef370d6285aeca7361be31e
UMBarCodeScannerInterface: 84ea2d6b58ff0dc27ef9b68bab71286be18ee020 UMBarCodeScannerInterface: 84ea2d6b58ff0dc27ef9b68bab71286be18ee020
UMCameraInterface: 26b26005d1756a0d5f4f04f1e168e39ea9154535 UMCameraInterface: 26b26005d1756a0d5f4f04f1e168e39ea9154535
UMConstantsInterface: 038bacb19de12b6fd328c589122c8dc977cccf61 UMConstantsInterface: 038bacb19de12b6fd328c589122c8dc977cccf61

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/NSRunLoop+SRWebSocket.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/NSRunLoop+SRWebSocketPrivate.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/NSURLRequest+SRWebSocket.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/NSURLRequest+SRWebSocketPrivate.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/SRConstants.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/Delegate/SRDelegateController.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/Utilities/SRError.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/Utilities/SRHTTPConnectMessage.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/Utilities/SRHash.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/IOConsumer/SRIOConsumer.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/IOConsumer/SRIOConsumerPool.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/Utilities/SRLog.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/Utilities/SRMutex.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/Security/SRPinningSecurityPolicy.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/Proxy/SRProxyConnect.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/Utilities/SRRandom.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/RunLoop/SRRunLoopThread.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/Utilities/SRSIMDHelpers.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/SRSecurityPolicy.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/Internal/Utilities/SRURLUtilities.h

View File

@ -0,0 +1 @@
../../../../../node_modules/detox/ios_src/SocketRocket/SocketRocket/SRWebSocket.h

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