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
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "4.6.0"
versionName "4.7.0"
vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
}

View File

@ -31,7 +31,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_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 MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);

View File

@ -1,5 +1,19 @@
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) {
return {
type: types.ROOM.LEAVE,

View File

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

View File

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

View File

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

View File

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

View File

@ -64,7 +64,7 @@ const MentionItem = ({
content = (
<>
<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,
Channels: 1,
AudioQuality: 'Low',
AudioEncoding: 'aac'
AudioEncoding: 'aac',
OutputFormat: 'aac_adts'
});
AudioRecorder.onProgress = (data) => {

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import Video from './Video';
import Reply from './Reply';
const Attachments = React.memo(({
attachments, timeFormat, user, baseUrl, showAttachment, getCustomEmoji, theme
attachments, timeFormat, showAttachment, getCustomEmoji, theme
}) => {
if (!attachments || attachments.length === 0) {
return null;
@ -16,25 +16,23 @@ const Attachments = React.memo(({
return attachments.map((file, index) => {
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) {
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) {
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
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);
Attachments.propTypes = {
attachments: PropTypes.array,
timeFormat: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string,
showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string

View File

@ -3,18 +3,30 @@ import PropTypes from 'prop-types';
import {
View, StyleSheet, Text, Easing, Dimensions
} from 'react-native';
import Video from 'react-native-video';
import { Audio } from 'expo-av';
import Slider from '@react-native-community/slider';
import moment from 'moment';
import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import Touchable from './Touchable';
import Markdown from '../markdown';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import { isAndroid, isIOS } from '../../utils/deviceInfo';
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({
audioContainer: {
@ -31,6 +43,9 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: 'transparent'
},
audioLoading: {
marginHorizontal: 8
},
slider: {
flex: 1
},
@ -51,29 +66,36 @@ const sliderAnimationConfig = {
delay: 0
};
const Button = React.memo(({ paused, onPress, theme }) => (
const Button = React.memo(({
loading, paused, onPress, theme
}) => (
<Touchable
style={styles.playPauseButton}
onPress={onPress}
hitSlop={BUTTON_HIT_SLOP}
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>
));
Button.propTypes = {
loading: PropTypes.bool,
paused: PropTypes.bool,
theme: PropTypes.string,
onPress: PropTypes.func
};
Button.displayName = 'MessageAudioButton';
class Audio extends React.Component {
class MessageAudio extends React.Component {
static contextType = MessageContext;
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
theme: PropTypes.string,
split: PropTypes.bool,
getCustomEmoji: PropTypes.func
@ -81,18 +103,34 @@ class Audio extends React.Component {
constructor(props) {
super(props);
const { baseUrl, file, user } = props;
this.state = {
loading: false,
currentTime: 0,
duration: 0,
paused: true,
uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
paused: true
};
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) {
const {
currentTime, duration, paused, uri
currentTime, duration, paused, loading
} = this.state;
const { file, split, theme } = this.props;
if (nextProps.theme !== theme) {
@ -107,58 +145,99 @@ class Audio extends React.Component {
if (nextState.paused !== paused) {
return true;
}
if (nextState.uri !== uri) {
return true;
}
if (!equal(nextProps.file, file)) {
return true;
}
if (nextProps.split !== split) {
return true;
}
if (nextState.loading !== loading) {
return true;
}
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) => {
this.setState({ duration: data.duration > 0 ? data.duration : 0 });
const duration = data.durationMillis / 1000;
this.setState({ duration: duration > 0 ? duration : 0 });
}
onProgress = (data) => {
const { duration } = this.state;
if (data.currentTime <= duration) {
this.setState({ currentTime: data.currentTime });
const currentTime = data.positionMillis / 1000;
if (currentTime <= duration) {
this.setState({ currentTime });
}
}
onEnd = () => {
onEnd = async(data) => {
if (data.didJustFinish) {
try {
await this.sound.stopAsync();
this.setState({ paused: true, currentTime: 0 });
requestAnimationFrame(() => {
this.player.seek(0);
});
} catch {
// do nothing
}
}
}
get duration() {
const { duration } = this.state;
return formatTime(duration);
const { currentTime, duration } = this.state;
return formatTime(currentTime || duration);
}
setRef = ref => this.player = ref;
togglePlayPause = () => {
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() {
const {
uri, paused, currentTime, duration
loading, paused, currentTime, duration
} = this.state;
const {
user, baseUrl, file, getCustomEmoji, split, theme
file, getCustomEmoji, split, theme
} = this.props;
const { description } = file;
const { baseUrl, user } = this.context;
if (!baseUrl) {
return null;
@ -173,17 +252,7 @@ class Audio extends React.Component {
split && sharedStyles.tabletContent
]}
>
<Video
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} />
<Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
<Slider
style={styles.slider}
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
}) => {
if (blocks && blocks.length > 0) {
const [, secondBlock] = blocks;
const { appId = '' } = secondBlock;
const appId = blocks[0]?.appId || '';
return React.createElement(
messageBlockWithContext({
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 Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils';
import I18n from '../../i18n';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Broadcast = React.memo(({
author, user, broadcast, replyBroadcast, theme
author, broadcast, theme
}) => {
const { user, replyBroadcast } = useContext(MessageContext);
const isOwn = author._id === user.id;
if (broadcast && !isOwn) {
return (
@ -36,10 +38,8 @@ const Broadcast = React.memo(({
Broadcast.propTypes = {
author: PropTypes.object,
user: PropTypes.object,
broadcast: PropTypes.bool,
theme: PropTypes.string,
replyBroadcast: PropTypes.func
theme: PropTypes.string
};
Broadcast.displayName = 'MessageBroadcast';

View File

@ -1,8 +1,8 @@
import React from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { formatLastMessage, BUTTON_HIT_SLOP } from './utils';
import styles from './styles';
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 PropTypes from 'prop-types';
import equal from 'deep-equal';
@ -8,6 +8,7 @@ import styles from './styles';
import Markdown from '../markdown';
import { getInfoMessage } from './utils';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Content = React.memo((props) => {
if (props.isInfo) {
@ -26,12 +27,13 @@ const Content = React.memo((props) => {
if (props.tmid && !props.msg) {
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
} else {
const { baseUrl, user } = useContext(MessageContext);
content = (
<Markdown
msg={props.msg}
baseUrl={props.baseUrl}
baseUrl={baseUrl}
getCustomEmoji={props.getCustomEmoji}
username={props.user.username}
username={user.username}
isEdited={props.isEdited}
numberOfLines={(props.tmid && !props.isThreadRoom) ? 1 : 0}
preview={props.tmid && !props.isThreadRoom}
@ -77,8 +79,6 @@ Content.propTypes = {
msg: PropTypes.string,
theme: PropTypes.string,
isEdited: PropTypes.bool,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func,
channels: 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 Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
import styles from './styles';
import I18n from '../../i18n';
import { CustomIcon } from '../../lib/Icons';
import { DISCUSSION } from './constants';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Discussion = React.memo(({
msg, dcount, dlm, onDiscussionPress, theme
msg, dcount, dlm, theme
}) => {
const time = formatLastMessage(dlm);
const buttonText = formatMessageCount(dcount, DISCUSSION);
const { onDiscussionPress } = useContext(MessageContext);
return (
<>
<Text style={[styles.startedDiscussion, { color: themes[theme].auxiliaryText }]}>{I18n.t('Started_discussion')}</Text>
@ -55,8 +57,7 @@ Discussion.propTypes = {
msg: PropTypes.string,
dcount: PropTypes.number,
dlm: PropTypes.string,
theme: PropTypes.string,
onDiscussionPress: PropTypes.func
theme: PropTypes.string
};
Discussion.displayName = 'MessageDiscussion';

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,18 @@
import React from 'react';
import Touchable from 'react-native-platform-touchable';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
const MessageError = React.memo(({ hasError, theme }) => {
if (!hasError) {
return null;
}
const { onErrorPress } = useContext(MessageContext);
return (
<Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}>
<CustomIcon name='warning' color={themes[theme].dangerColor} size={18} />
@ -20,7 +22,6 @@ const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
MessageError.propTypes = {
hasError: PropTypes.bool,
onErrorPress: PropTypes.func,
theme: PropTypes.string
};
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 Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import Emoji from './Emoji';
import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors';
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
onPress={reactionInit}
key='message-add-reaction'
@ -23,11 +26,15 @@ const AddReaction = React.memo(({ reactionInit, theme }) => (
<CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} />
</View>
</Touchable>
));
);
});
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;
return (
<Touchable
@ -54,7 +61,7 @@ const Reaction = React.memo(({
});
const Reactions = React.memo(({
reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji, theme
reactions, getCustomEmoji, theme
}) => {
if (!Array.isArray(reactions) || reactions.length === 0) {
return null;
@ -65,25 +72,17 @@ const Reactions = React.memo(({
<Reaction
key={reaction.emoji}
reaction={reaction}
user={user}
baseUrl={baseUrl}
onReactionLongPress={onReactionLongPress}
onReactionPress={onReactionPress}
getCustomEmoji={getCustomEmoji}
theme={theme}
/>
))}
<AddReaction reactionInit={reactionInit} theme={theme} />
<AddReaction theme={theme} />
</View>
);
});
Reaction.propTypes = {
reaction: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};
@ -91,18 +90,12 @@ Reaction.displayName = 'MessageReaction';
Reactions.propTypes = {
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,
theme: PropTypes.string
};
Reactions.displayName = 'MessageReactions';
AddReaction.propTypes = {
reactionInit: PropTypes.func,
theme: PropTypes.string
};
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 PropTypes from 'prop-types';
import moment from 'moment';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal';
import Touchable from './Touchable';
import Markdown from '../markdown';
import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import { withSplit } from '../../split';
import MessageContext from './Context';
const styles = StyleSheet.create({
button: {
@ -79,12 +80,13 @@ const Title = React.memo(({ attachment, timeFormat, theme }) => {
});
const Description = React.memo(({
attachment, baseUrl, user, getCustomEmoji, theme
attachment, getCustomEmoji, theme
}) => {
const text = attachment.text || attachment.title;
if (!text) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
return (
<Markdown
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);
const Reply = React.memo(({
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, split, theme
attachment, timeFormat, index, getCustomEmoji, split, theme
}) => {
if (!attachment) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
const onPress = () => {
let url = attachment.title_link || attachment.author_link;
@ -160,8 +163,6 @@ const Reply = React.memo(({
<Description
attachment={attachment}
timeFormat={timeFormat}
baseUrl={baseUrl}
user={user}
getCustomEmoji={getCustomEmoji}
theme={theme}
/>
@ -174,8 +175,6 @@ const Reply = React.memo(({
Reply.propTypes = {
attachment: PropTypes.object,
timeFormat: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
index: PropTypes.number,
theme: PropTypes.string,
getCustomEmoji: PropTypes.func,
@ -192,8 +191,6 @@ Title.displayName = 'MessageReplyTitle';
Description.propTypes = {
attachment: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func,
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 {
View, Text, StyleSheet, Clipboard
} from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'lodash/isEqual';
import Touchable from './Touchable';
import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
@ -15,6 +15,7 @@ import { withSplit } from '../../split';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import I18n from '../../i18n';
import MessageContext from './Context';
const styles = StyleSheet.create({
button: {
@ -52,10 +53,11 @@ const styles = StyleSheet.create({
}
});
const UrlImage = React.memo(({ image, user, baseUrl }) => {
const UrlImage = React.memo(({ image }) => {
if (!image) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
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} />;
}, (prevProps, nextProps) => prevProps.image === nextProps.image);
@ -79,7 +81,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
});
const Url = React.memo(({
url, index, user, baseUrl, split, theme
url, index, split, theme
}) => {
if (!url) {
return null;
@ -109,7 +111,7 @@ const Url = React.memo(({
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} />
</>
</Touchable>
@ -117,21 +119,19 @@ const Url = React.memo(({
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
const Urls = React.memo(({
urls, user, baseUrl, split, theme
urls, split, theme
}) => {
if (!urls || urls.length === 0) {
return null;
}
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);
UrlImage.propTypes = {
image: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string
image: PropTypes.string
};
UrlImage.displayName = 'MessageUrlImage';
@ -145,8 +145,6 @@ UrlContent.displayName = 'MessageUrlContent';
Url.propTypes = {
url: PropTypes.object.isRequired,
index: PropTypes.number,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string,
split: PropTypes.bool
};
@ -154,8 +152,6 @@ Url.displayName = 'MessageUrl';
Urls.propTypes = {
urls: PropTypes.array,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string,
split: PropTypes.bool
};

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ export default {
'error-email-domain-blacklisted': 'The email domain is blacklisted',
'error-email-send-failed': 'Error trying to send email: {{message}}',
'error-save-image': 'Error while saving image',
'error-save-video': 'Error while saving video',
'error-field-unavailable': '{{field}} is already in use :(',
'error-file-too-large': 'File is too large',
'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_Users: 'Select Users',
Select_a_Channel: 'Select a Channel',
Select_an_option: 'Select an option',
Send: 'Send',
Send_audio_message: 'Send audio message',
Send_crash_report: 'Send crash report',

View File

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

View File

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

View File

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

View File

@ -61,8 +61,15 @@ export default async function canOpenRoom({ rid, path }) {
if (rid) {
try {
await subsCollection.find(rid);
return { rid };
const room = await subsCollection.find(rid);
return {
rid,
t: room.t,
name: room.name,
fname: room.fname,
prid: room.prid,
uids: room.uids
};
} catch (e) {
// 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'];
// 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 serversDB = database.servers;
const serverId = reduxStore.getState().server.server;
@ -54,7 +70,7 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
export async function getLoginSettings({ server }) {
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());
if (result.success && result.settings.length) {
@ -84,7 +100,7 @@ export async function setSettings() {
export default async function() {
try {
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
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 { setActiveUsers } from '../../actions/activeUsers';
import { setUser } from '../../actions/login';
export function subscribeUsersPresence() {
const serverVersion = reduxStore.getState().server.version;
// 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) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
@ -25,35 +26,43 @@ let ids = [];
export default async function getUsersPresence() {
const serverVersion = reduxStore.getState().server.version;
const { user: loggedUser } = reduxStore.getState().login;
// 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 = {};
// 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 (!ids.length) {
return;
}
// Request userPresence on demand
params = { ids: ids.join(',') };
ids = [];
}
try {
// RC 1.1.0
const result = await this.sdk.get('users.presence', params);
if (result.success) {
const activeUsers = result.users.reduce((ret, item) => {
ret[item._id] = {
status: item.status,
statusText: item.statusText
};
const { _id, status, statusText } = item;
if (loggedUser && loggedUser.id === _id) {
reduxStore.dispatch(setUser({ status, statusText }));
}
ret[_id] = { status, statusText };
return ret;
}, {});
InteractionManager.runAfterInteractions(() => {
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 normalizeMessage from './normalizeMessage';
import findSubscriptionsRooms from './findSubscriptionsRooms';
// TODO: delete and update
export const merge = (subscription, room) => {
@ -46,11 +47,14 @@ export const merge = (subscription, room) => {
return subscription;
};
export default (subscriptions = [], rooms = []) => {
export default async(subscriptions = [], rooms = []) => {
if (subscriptions.update) {
subscriptions = subscriptions.update;
rooms = rooms.update;
}
({ subscriptions, rooms } = await findSubscriptionsRooms(subscriptions, rooms));
return {
subscriptions: subscriptions.map((s) => {
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 }) {
if (this.roomsSub) {
this.roomsSub.stop();
this.roomsSub = null;
}
if (this.activeUsersSubTimeout) {

View File

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

View File

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

View File

@ -72,6 +72,7 @@ const createOrUpdateSubscription = async(subscription, room) => {
autoTranslate: s.autoTranslate,
autoTranslateLanguage: s.autoTranslateLanguage,
lastMessage: s.lastMessage,
roles: s.roles,
usernames: s.usernames,
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 messagesCollection = db.collections.get('messages');
let messageRecord;

View File

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

View File

@ -27,6 +27,7 @@ const attrs = [
'isRead',
'favorite',
'status',
'connected',
'theme'
];
@ -40,15 +41,15 @@ const arePropsEqual = (oldProps, newProps) => {
};
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(() => {
if (type === 'd') {
if (connected && type === 'd' && id) {
getUserPresence(id);
}
}, []);
}, [connected]);
const date = formatDate(_updatedAt);
const date = lastMessage && formatDate(lastMessage.ts);
let accessibilityLabel = name;
if (unread === 1) {
@ -197,6 +198,7 @@ RoomItem.propTypes = {
hideUnreadStatus: PropTypes.bool,
useRealName: PropTypes.bool,
getUserPresence: PropTypes.func,
connected: PropTypes.bool,
isGroupChat: PropTypes.bool,
theme: PropTypes.string
};
@ -208,6 +210,7 @@ RoomItem.defaultProps = {
};
const mapStateToProps = (state, ownProps) => ({
connected: state.meteor.connected,
status:
state.meteor.connected && ownProps.type === 'd'
? state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status

View File

@ -2,11 +2,23 @@ import { ROOM } from '../actions/actionsTypes';
const initialState = {
rid: null,
isDeleting: false
isDeleting: false,
rooms: []
};
export default function(state = initialState, action) {
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:
return {
...state,

View File

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

View File

@ -30,15 +30,22 @@ const handleInviteLink = function* handleInviteLink({ params, requireLogin = fal
const navigate = function* navigate({ params }) {
yield put(appStart('inside'));
if (params.path) {
const room = yield RocketChat.canOpenRoom(params);
const [type, name] = params.path.split('/');
if (type !== 'invite') {
const room = yield RocketChat.canOpenRoom(params);
if (room) {
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 {
yield handleInviteLink({ params });
}
}
};
const handleOpen = function* handleOpen({ params }) {

View File

@ -21,6 +21,7 @@ import database from '../lib/database';
import EventEmitter from '../utils/events';
import { inviteLinksRequest } from '../actions/inviteLinks';
import { showErrorAlert } from '../utils/info';
import { setActiveUsers } from '../actions/activeUsers';
const getServer = state => state.server.server;
const loginWithPasswordCall = args => RocketChat.loginWithPassword(args);
@ -73,7 +74,7 @@ const registerPushToken = function* registerPushToken() {
const fetchUsersPresence = function* fetchUserPresence() {
yield RocketChat.getUsersPresence();
yield RocketChat.subscribeUsersPresence();
RocketChat.subscribeUsersPresence();
};
const handleLoginSuccess = function* handleLoginSuccess({ user }) {
@ -81,6 +82,8 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
const adding = yield select(state => state.server.adding);
yield RNUserDefaults.set(RocketChat.TOKEN_KEY, user.token);
RocketChat.getUserPresence(user.id);
const server = yield select(getServer);
yield put(roomsRequest());
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) {
I18n.locale = 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() {

View File

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

View File

@ -12,8 +12,8 @@ import * as actions from '../actions';
import {
serverFailure, selectServerRequest, selectServerSuccess, selectServerFailure
} from '../actions/server';
import { setUser } from '../actions/login';
import { clearSettings } from '../actions/settings';
import { setUser } from '../actions/login';
import RocketChat from '../lib/rocketchat';
import database from '../lib/database';
import log, { logServerVersion } from '../utils/log';
@ -38,7 +38,10 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
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 serversCollection = serversDB.collections.get('servers');
@ -46,12 +49,12 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
try {
const serverRecord = await serversCollection.find(server);
await serverRecord.update((record) => {
record.version = validVersion;
record.version = serverVersion;
});
} catch (e) {
await serversCollection.create((record) => {
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 }`);
setBasicAuth(basicAuth);
yield put(clearSettings());
if (user) {
yield put(clearSettings());
yield RocketChat.connect({ server, user, logoutOnError: true });
yield put(setUser(user));
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';
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) => {
if (room) {
const { t, blocked, blocker } = room;

View File

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

View File

@ -79,11 +79,12 @@ class AuthenticationWebView extends React.PureComponent {
if (this.authType === 'saml' || this.authType === 'cas') {
const { navigation } = this.props;
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;
if (this.authType === 'saml') {
const parsedUrl = parse(url, true);
const token = (parsedUrl.query && parsedUrl.query.saml_idp_credentialToken) || ssoToken;
const token = parsedUrl.query?.saml_idp_credentialToken || ssoToken;
const credentialToken = { credentialToken: token };
payload = { ...credentialToken, saml: true };
} else {

View File

@ -125,10 +125,14 @@ class DirectoryView extends React.Component {
this.setState(({ showOptionsDropdown }) => ({ showOptionsDropdown: !showOptionsDropdown }));
}
goRoom = async({ rid, name, t }) => {
goRoom = async({
rid, name, t, search
}) => {
const { navigation } = this.props;
await navigation.navigate('RoomsListView');
navigation.navigate('RoomView', { rid, name, t });
navigation.navigate('RoomView', {
rid, name, t, search
});
}
onPressItem = async(item) => {
@ -139,7 +143,9 @@ class DirectoryView extends React.Component {
this.goRoom({ rid: result.room._id, name: item.username, t: 'd' });
}
} 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 {
title: textParser([title]),
...themedHeader(theme),
headerLeft: (
headerLeft: close ? (
<CustomHeaderButtons>
<Item
title={textParser([close.text])}
@ -76,8 +76,8 @@ class ModalBlockView extends React.Component {
testID='close-modal-uikit'
/>
</CustomHeaderButtons>
),
headerRight: (
) : null,
headerRight: submit ? (
<CustomHeaderButtons>
<Item
title={textParser([submit.text])}
@ -86,7 +86,7 @@ class ModalBlockView extends React.Component {
testID='submit-modal-uikit'
/>
</CustomHeaderButtons>
)
) : null
};
}
@ -136,7 +136,7 @@ class ModalBlockView extends React.Component {
const { navigation } = this.props;
const oldData = prevProps.navigation.getParam('data', {});
const newData = navigation.getParam('data', {});
if (!isEqual(oldData, newData)) {
if (oldData.viewId !== newData.viewId) {
navigation.push('ModalBlockView', { data: newData });
}
}
@ -148,12 +148,14 @@ class ModalBlockView extends React.Component {
}
handleUpdate = ({ type, ...data }) => {
const { navigation } = this.props;
if ([MODAL_ACTIONS.ERRORS].includes(type)) {
const { errors } = data;
this.setState({ errors });
} else {
this.setState({ data });
}
navigation.setParams({ data });
};
cancel = async({ closeModal }) => {

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Text, Keyboard, StyleSheet, TouchableOpacity, View, Alert
Text, Keyboard, StyleSheet, TouchableOpacity, View, Alert, BackHandler
} from 'react-native';
import { connect } from 'react-redux';
import * as FileSystem from 'expo-file-system';
@ -105,6 +105,7 @@ class NewServerView extends React.Component {
certificate: null
};
EventEmitter.addEventListener('NewServer', this.handleNewServerEvent);
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
}
componentDidMount() {
@ -116,6 +117,16 @@ class NewServerView extends React.Component {
componentWillUnmount() {
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) => {

View File

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

View File

@ -24,9 +24,6 @@ export default StyleSheet.create({
paddingVertical: 10,
fontSize: 14
},
viewContainer: {
justifyContent: 'center'
},
pickerText: {
...sharedStyles.textRegular,
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() {
this.mounted = true;
const { room, member } = this.state;
if (room.rid) {
if (!room.id) {
try {
const result = await RocketChat.getChannelInfo(room.rid);
@ -117,6 +118,7 @@ class RoomActionsView extends React.Component {
this.canAddUser();
this.canInviteUser();
}
}
componentWillUnmount() {
if (this.subscription && this.subscription.unsubscribe) {
@ -384,11 +386,10 @@ class RoomActionsView extends React.Component {
updateRoomMember = async() => {
const { room } = this.state;
const { user } = this.props;
try {
if (!RocketChat.isGroupChat(room)) {
const roomUserId = RocketChat.getUidDirectMessage(room, user.id);
const roomUserId = RocketChat.getUidDirectMessage(room);
const result = await RocketChat.getUserInfo(roomUserId);
if (result.success) {
this.setState({ member: result.user });
@ -443,7 +444,7 @@ class RoomActionsView extends React.Component {
Alert.alert(
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'),
@ -485,7 +486,7 @@ class RoomActionsView extends React.Component {
: (
<View style={styles.roomTitleRow}>
<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 File

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

View File

@ -36,7 +36,7 @@ const getRoomTitle = (room, type, name, username, statusText, theme) => (type ==
: (
<View style={styles.roomTitleRow}>
<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 File

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

View File

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

View File

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

View File

@ -34,7 +34,8 @@ import { themes } from '../../constants/colors';
import debounce from '../../utils/debounce';
import ReactionsModal from '../../containers/ReactionsModal';
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 { showErrorAlert } from '../../utils/info';
import { withTheme } from '../../theme';
@ -65,9 +66,10 @@ const stateAttrsUpdate = [
'editing',
'replying',
'reacting',
'readOnly',
'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 {
static navigationOptions = ({ navigation, screenProps }) => {
@ -164,6 +166,7 @@ class RoomView extends React.Component {
const selectedMessage = props.navigation.getParam('message');
const name = props.navigation.getParam('name');
const fname = props.navigation.getParam('fname');
const search = props.navigation.getParam('search');
const prid = props.navigation.getParam('prid');
this.state = {
joined: true,
@ -183,7 +186,7 @@ class RoomView extends React.Component {
replying: !!selectedMessage,
replyWithMention: false,
reacting: false,
announcement: null
readOnly: false
};
if (room && room.observe) {
@ -192,6 +195,12 @@ class RoomView extends React.Component {
this.findAndObserveRoom(this.rid);
}
this.setReadOnly();
if (search) {
this.updateRoom();
}
this.messagebox = React.createRef();
this.list = React.createRef();
this.mounted = false;
@ -209,7 +218,7 @@ class RoomView extends React.Component {
} = this.props;
if ((room.id || room.rid) && !this.tmid) {
navigation.setParams({
name: this.getRoomTitle(room),
name: RocketChat.getRoomTitle(room),
subtitle: room.topic,
avatar: room.name,
t: room.t,
@ -222,11 +231,14 @@ class RoomView extends React.Component {
if (this.tmid) {
navigation.setParams({ toggleFollowThread: this.toggleFollowThread, goRoomActionsView: this.goRoomActionsView });
}
if (isAuthenticated && this.rid) {
if (this.rid) {
this.sub.subscribe();
if (isAuthenticated) {
this.init();
} else if (this.rid) {
} else {
EventEmitter.addEventListener('connected', this.handleConnected);
}
}
if (isIOS && this.rid) {
this.updateUnreadCount();
}
@ -275,9 +287,12 @@ class RoomView extends React.Component {
if (roomUpdate.topic !== prevState.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) {
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() => {
try {
this.setState({ loading: true });
@ -361,7 +402,6 @@ class RoomView extends React.Component {
this.setLastOpen(null);
}
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;
if (t === 'd' && !RocketChat.isGroupChat(room)) {
const { user, navigation } = this.props;
const { navigation } = this.props;
try {
const roomUserId = RocketChat.getUidDirectMessage(room, user.id);
const roomUserId = RocketChat.getUidDirectMessage(room);
navigation.setParams({ roomUserId });
@ -416,7 +456,7 @@ class RoomView extends React.Component {
this.setState({ room });
if (!this.tmid) {
navigation.setParams({
name: this.getRoomTitle(room),
name: RocketChat.getRoomTitle(room),
subtitle: room.topic,
avatar: room.name,
t: room.t
@ -605,7 +645,7 @@ class RoomView extends React.Component {
const { room } = this.state;
if (rid === this.rid) {
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 = () => {
const { room } = this.state;
if (room.lastOpen) {
@ -668,7 +703,6 @@ class RoomView extends React.Component {
// eslint-disable-next-line react/sort-comp
fetchThreadName = async(tmid, messageId) => {
try {
const { room } = this.state;
const db = database.active;
const threadCollection = db.collections.get('threads');
const messageCollection = db.collections.get('messages');
@ -691,7 +725,7 @@ class RoomView extends React.Component {
await db.batch(
threadCollection.prepareCreate((t) => {
t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema);
t.subscription.set(room);
t.subscription.id = this.rid;
Object.assign(t, thread);
}),
messageRecord.prepareUpdate((m) => {
@ -701,7 +735,7 @@ class RoomView extends React.Component {
});
}
} 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 = ({
actionId, appId, value, blockId, rid, mid
}) => RocketChat.triggerBlockAction({
@ -854,7 +882,7 @@ class RoomView extends React.Component {
renderFooter = () => {
const {
joined, room, selectedMessage, editing, replying, replyWithMention
joined, room, selectedMessage, editing, replying, replyWithMention, readOnly
} = this.state;
const { navigation, theme } = this.props;
@ -875,7 +903,7 @@ class RoomView extends React.Component {
</View>
);
}
if (this.isReadOnly || room.archived) {
if (readOnly) {
return (
<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>
@ -913,7 +941,7 @@ class RoomView extends React.Component {
renderActions = () => {
const {
room, selectedMessage, showActions, showErrorActions, joined
room, selectedMessage, showActions, showErrorActions, joined, readOnly
} = this.state;
const {
user, navigation
@ -934,7 +962,7 @@ class RoomView extends React.Component {
editInit={this.onEditInit}
replyInit={this.onReplyInit}
reactionInit={this.onReactionInit}
isReadOnly={this.isReadOnly}
isReadOnly={readOnly}
/>
)
: null

View File

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

View File

@ -13,7 +13,8 @@ import styles from './styles';
import TextInput from '../../containers/TextInput';
import ActivityIndicator from '../../containers/ActivityIndicator';
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 { themedHeader } from '../../utils/navigation';
@ -69,18 +70,29 @@ class ShareView extends React.Component {
fileInfo,
room,
loading: false,
readOnly: false,
file: {
name: fileInfo ? fileInfo.name : '',
description: ''
}
};
this.setReadOnly();
}
componentDidMount() {
const { navigation } = this.props;
navigation.setParams({ sendMessage: this._sendMessage });
}
setReadOnly = async() => {
const { room } = this.state;
const { navigation, user } = this.props;
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`;
@ -237,8 +249,9 @@ class ShareView extends React.Component {
renderError = () => {
const { room } = this.state;
const { theme } = this.props;
return (
<View style={[styles.container, styles.centered]}>
<View style={[styles.container, styles.centered, { backgroundColor: themes[theme].backgroundColor }]}>
<Text style={styles.title}>
{
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() {
const { user, theme } = this.props;
const { username } = user;
const { theme } = this.props;
const {
name, loading, isMedia, room
name, loading, isMedia, room, readOnly
} = this.state;
if (isReadOnly(room, { username }) || isBlocked(room)) {
if (readOnly || isBlocked(room)) {
return this.renderError();
}

View File

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

View File

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

View File

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