Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat.ReactNative into detoxRunner
This commit is contained in:
commit
40615a81ea
File diff suppressed because it is too large
Load Diff
|
@ -14,9 +14,10 @@ export function createChannelSuccess(data) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createChannelFailure(err) {
|
export function createChannelFailure(err, isTeam) {
|
||||||
return {
|
return {
|
||||||
type: types.CREATE_CHANNEL.FAILURE,
|
type: types.CREATE_CHANNEL.FAILURE,
|
||||||
err
|
err,
|
||||||
|
isTeam
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export const MESSAGE_TYPE_LOAD_MORE = 'load_more';
|
||||||
|
export const MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK = 'load_previous_chunk';
|
||||||
|
export const MESSAGE_TYPE_LOAD_NEXT_CHUNK = 'load_next_chunk';
|
||||||
|
|
||||||
|
export const MESSAGE_TYPE_ANY_LOAD = [MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK, MESSAGE_TYPE_LOAD_NEXT_CHUNK];
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Text } from 'react-native';
|
import { Text, View } from 'react-native';
|
||||||
|
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
@ -20,12 +20,19 @@ export const Item = React.memo(({ item, hide, theme }) => {
|
||||||
theme={theme}
|
theme={theme}
|
||||||
>
|
>
|
||||||
<CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} />
|
<CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} />
|
||||||
|
<View style={styles.titleContainer}>
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]}
|
style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
|
{ item.right ? (
|
||||||
|
<View style={styles.rightContainer}>
|
||||||
|
{item.right ? item.right() : null}
|
||||||
|
</View>
|
||||||
|
) : null }
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -34,7 +41,8 @@ Item.propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
danger: PropTypes.bool,
|
danger: PropTypes.bool,
|
||||||
onPress: PropTypes.func
|
onPress: PropTypes.func,
|
||||||
|
right: PropTypes.func
|
||||||
}),
|
}),
|
||||||
hide: PropTypes.func,
|
hide: PropTypes.func,
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string
|
||||||
|
|
|
@ -22,6 +22,9 @@ export default StyleSheet.create({
|
||||||
content: {
|
content: {
|
||||||
paddingTop: 16
|
paddingTop: 16
|
||||||
},
|
},
|
||||||
|
titleContainer: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
marginLeft: 16,
|
marginLeft: 16,
|
||||||
|
@ -58,5 +61,8 @@ export default StyleSheet.create({
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
...sharedStyles.textMedium,
|
...sharedStyles.textMedium,
|
||||||
...sharedStyles.textAlignCenter
|
...sharedStyles.textAlignCenter
|
||||||
|
},
|
||||||
|
rightContainer: {
|
||||||
|
paddingLeft: 12
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
|
import { ICON_SIZE } from './constants';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
icon: {
|
icon: {
|
||||||
|
@ -23,7 +24,7 @@ const ListIcon = React.memo(({
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
name={name}
|
name={name}
|
||||||
color={color ?? themes[theme].auxiliaryText}
|
color={color ?? themes[theme].auxiliaryText}
|
||||||
size={20}
|
size={ICON_SIZE}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
));
|
));
|
||||||
|
|
|
@ -10,8 +10,9 @@ import sharedStyles from '../../views/Styles';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import { Icon } from '.';
|
import { Icon } from '.';
|
||||||
import { BASE_HEIGHT, PADDING_HORIZONTAL } from './constants';
|
import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants';
|
||||||
import { withDimensions } from '../../dimensions';
|
import { withDimensions } from '../../dimensions';
|
||||||
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
@ -34,7 +35,15 @@ const styles = StyleSheet.create({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
},
|
},
|
||||||
|
textAlertContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
alertIcon: {
|
||||||
|
paddingLeft: 4
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
|
flexShrink: 1,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
...sharedStyles.textRegular
|
...sharedStyles.textRegular
|
||||||
},
|
},
|
||||||
|
@ -50,7 +59,7 @@ const styles = StyleSheet.create({
|
||||||
});
|
});
|
||||||
|
|
||||||
const Content = React.memo(({
|
const Content = React.memo(({
|
||||||
title, subtitle, disabled, testID, left, right, color, theme, translateTitle, translateSubtitle, showActionIndicator, fontScale
|
title, subtitle, disabled, testID, left, right, color, theme, translateTitle, translateSubtitle, showActionIndicator, fontScale, alert
|
||||||
}) => (
|
}) => (
|
||||||
<View style={[styles.container, disabled && styles.disabled, { height: BASE_HEIGHT * fontScale }]} testID={testID}>
|
<View style={[styles.container, disabled && styles.disabled, { height: BASE_HEIGHT * fontScale }]} testID={testID}>
|
||||||
{left
|
{left
|
||||||
|
@ -61,7 +70,12 @@ const Content = React.memo(({
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
<View style={styles.textContainer}>
|
<View style={styles.textContainer}>
|
||||||
<Text style={[styles.title, { color: color || themes[theme].titleText }]} accessibilityLabel={title} numberOfLines={1}>{translateTitle ? I18n.t(title) : title}</Text>
|
<View style={styles.textAlertContainer}>
|
||||||
|
<Text style={[styles.title, { color: color || themes[theme].titleText }]} numberOfLines={1}>{translateTitle ? I18n.t(title) : title}</Text>
|
||||||
|
{alert ? (
|
||||||
|
<CustomIcon style={[styles.alertIcon, { color: themes[theme].dangerColor }]} size={ICON_SIZE} name='info' />
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
{subtitle
|
{subtitle
|
||||||
? <Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{translateSubtitle ? I18n.t(subtitle) : subtitle}</Text>
|
? <Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{translateSubtitle ? I18n.t(subtitle) : subtitle}</Text>
|
||||||
: null
|
: null
|
||||||
|
@ -123,7 +137,8 @@ Content.propTypes = {
|
||||||
translateTitle: PropTypes.bool,
|
translateTitle: PropTypes.bool,
|
||||||
translateSubtitle: PropTypes.bool,
|
translateSubtitle: PropTypes.bool,
|
||||||
showActionIndicator: PropTypes.bool,
|
showActionIndicator: PropTypes.bool,
|
||||||
fontScale: PropTypes.number
|
fontScale: PropTypes.number,
|
||||||
|
alert: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
Content.defaultProps = {
|
Content.defaultProps = {
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export const PADDING_HORIZONTAL = 12;
|
export const PADDING_HORIZONTAL = 12;
|
||||||
export const BASE_HEIGHT = 46;
|
export const BASE_HEIGHT = 46;
|
||||||
|
export const ICON_SIZE = 20;
|
||||||
|
|
|
@ -30,6 +30,7 @@ const RoomTypeIcon = React.memo(({
|
||||||
return <Status style={[iconStyle, { color: STATUS_COLORS[status] ?? STATUS_COLORS.offline }]} size={size} status={status} />;
|
return <Status style={[iconStyle, { color: STATUS_COLORS[status] ?? STATUS_COLORS.offline }]} size={size} status={status} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: move this to a separate function
|
||||||
let icon = 'channel-private';
|
let icon = 'channel-private';
|
||||||
if (teamMain) {
|
if (teamMain) {
|
||||||
icon = `teams${ type === 'p' ? '-private' : '' }`;
|
icon = `teams${ type === 'p' ? '-private' : '' }`;
|
||||||
|
|
|
@ -4,19 +4,18 @@ import { Text, Clipboard } from 'react-native';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import openLink from '../../utils/openLink';
|
|
||||||
import { LISTENER } from '../Toast';
|
import { LISTENER } from '../Toast';
|
||||||
import EventEmitter from '../../utils/events';
|
import EventEmitter from '../../utils/events';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
|
|
||||||
const Link = React.memo(({
|
const Link = React.memo(({
|
||||||
children, link, theme
|
children, link, theme, onLinkPress
|
||||||
}) => {
|
}) => {
|
||||||
const handlePress = () => {
|
const handlePress = () => {
|
||||||
if (!link) {
|
if (!link) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openLink(link, theme);
|
onLinkPress(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
const childLength = React.Children.toArray(children).filter(o => o).length;
|
const childLength = React.Children.toArray(children).filter(o => o).length;
|
||||||
|
@ -40,7 +39,8 @@ const Link = React.memo(({
|
||||||
Link.propTypes = {
|
Link.propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
link: PropTypes.string,
|
link: PropTypes.string,
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string,
|
||||||
|
onLinkPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Link;
|
export default Link;
|
||||||
|
|
|
@ -82,7 +82,8 @@ class Markdown extends PureComponent {
|
||||||
preview: PropTypes.bool,
|
preview: PropTypes.bool,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
testID: PropTypes.string,
|
testID: PropTypes.string,
|
||||||
style: PropTypes.array
|
style: PropTypes.array,
|
||||||
|
onLinkPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -218,11 +219,12 @@ class Markdown extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
renderLink = ({ children, href }) => {
|
renderLink = ({ children, href }) => {
|
||||||
const { theme } = this.props;
|
const { theme, onLinkPress } = this.props;
|
||||||
return (
|
return (
|
||||||
<MarkdownLink
|
<MarkdownLink
|
||||||
link={href}
|
link={href}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
onLinkPress={onLinkPress}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</MarkdownLink>
|
</MarkdownLink>
|
||||||
|
|
|
@ -45,7 +45,7 @@ const Content = React.memo((props) => {
|
||||||
} else if (props.isEncrypted) {
|
} else if (props.isEncrypted) {
|
||||||
content = <Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}>{I18n.t('Encrypted_message')}</Text>;
|
content = <Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}>{I18n.t('Encrypted_message')}</Text>;
|
||||||
} else {
|
} else {
|
||||||
const { baseUrl, user } = useContext(MessageContext);
|
const { baseUrl, user, onLinkPress } = useContext(MessageContext);
|
||||||
content = (
|
content = (
|
||||||
<Markdown
|
<Markdown
|
||||||
msg={props.msg}
|
msg={props.msg}
|
||||||
|
@ -61,6 +61,7 @@ const Content = React.memo((props) => {
|
||||||
tmid={props.tmid}
|
tmid={props.tmid}
|
||||||
useRealName={props.useRealName}
|
useRealName={props.useRealName}
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
|
onLinkPress={onLinkPress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import Discussion from './Discussion';
|
||||||
import Content from './Content';
|
import Content from './Content';
|
||||||
import ReadReceipt from './ReadReceipt';
|
import ReadReceipt from './ReadReceipt';
|
||||||
import CallButton from './CallButton';
|
import CallButton from './CallButton';
|
||||||
|
import { themes } from '../../constants/colors';
|
||||||
|
|
||||||
const MessageInner = React.memo((props) => {
|
const MessageInner = React.memo((props) => {
|
||||||
if (props.type === 'discussion-created') {
|
if (props.type === 'discussion-created') {
|
||||||
|
@ -120,6 +121,7 @@ const MessageTouchable = React.memo((props) => {
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
disabled={(props.isInfo && !props.isThreadReply) || props.archived || props.isTemp}
|
disabled={(props.isInfo && !props.isThreadReply) || props.archived || props.isTemp}
|
||||||
|
style={{ backgroundColor: props.highlighted ? themes[props.theme].headerBackground : null }}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Message {...props} />
|
<Message {...props} />
|
||||||
|
@ -134,7 +136,9 @@ MessageTouchable.propTypes = {
|
||||||
isInfo: PropTypes.bool,
|
isInfo: PropTypes.bool,
|
||||||
isThreadReply: PropTypes.bool,
|
isThreadReply: PropTypes.bool,
|
||||||
isTemp: PropTypes.bool,
|
isTemp: PropTypes.bool,
|
||||||
archived: PropTypes.bool
|
archived: PropTypes.bool,
|
||||||
|
highlighted: PropTypes.bool,
|
||||||
|
theme: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
Message.propTypes = {
|
Message.propTypes = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { memo, useEffect, useState } from 'react';
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
@ -8,22 +8,27 @@ import { themes } from '../../constants/colors';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import Markdown from '../markdown';
|
import Markdown from '../markdown';
|
||||||
|
|
||||||
const RepliedThread = React.memo(({
|
const RepliedThread = memo(({
|
||||||
tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme
|
tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme
|
||||||
}) => {
|
}) => {
|
||||||
if (!tmid || !isHeader) {
|
if (!tmid || !isHeader) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tmsg) {
|
const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg);
|
||||||
fetchThreadName(tmid, id);
|
const fetch = async() => {
|
||||||
return null;
|
const threadName = await fetchThreadName(tmid, id);
|
||||||
|
setMsg(threadName);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!msg) {
|
||||||
|
fetch();
|
||||||
}
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
let msg = tmsg;
|
if (!msg) {
|
||||||
|
return null;
|
||||||
if (isEncrypted) {
|
|
||||||
msg = I18n.t('Encrypted_message');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -45,23 +50,6 @@ const RepliedThread = React.memo(({
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => {
|
|
||||||
if (prevProps.tmid !== nextProps.tmid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (prevProps.tmsg !== nextProps.tmsg) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (prevProps.isEncrypted !== nextProps.isEncrypted) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (prevProps.isHeader !== nextProps.isHeader) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (prevProps.theme !== nextProps.theme) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
RepliedThread.propTypes = {
|
RepliedThread.propTypes = {
|
||||||
|
|
|
@ -142,10 +142,13 @@ const Reply = React.memo(({
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { baseUrl, user } = useContext(MessageContext);
|
const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
|
||||||
|
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
let url = attachment.title_link || attachment.author_link;
|
let url = attachment.title_link || attachment.author_link;
|
||||||
|
if (attachment.message_link) {
|
||||||
|
return jumpToMessage(attachment.message_link);
|
||||||
|
}
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
|
||||||
});
|
});
|
||||||
|
|
||||||
const Url = React.memo(({ url, index, theme }) => {
|
const Url = React.memo(({ url, index, theme }) => {
|
||||||
if (!url) {
|
if (!url || url?.ignoreParse) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
|
||||||
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
|
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
|
||||||
import messagesStatus from '../../constants/messagesStatus';
|
import messagesStatus from '../../constants/messagesStatus';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
|
import openLink from '../../utils/openLink';
|
||||||
|
|
||||||
class MessageContainer extends React.Component {
|
class MessageContainer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -33,6 +34,7 @@ class MessageContainer extends React.Component {
|
||||||
autoTranslateLanguage: PropTypes.string,
|
autoTranslateLanguage: PropTypes.string,
|
||||||
status: PropTypes.number,
|
status: PropTypes.number,
|
||||||
isIgnored: PropTypes.bool,
|
isIgnored: PropTypes.bool,
|
||||||
|
highlighted: PropTypes.bool,
|
||||||
getCustomEmoji: PropTypes.func,
|
getCustomEmoji: PropTypes.func,
|
||||||
onLongPress: PropTypes.func,
|
onLongPress: PropTypes.func,
|
||||||
onReactionPress: PropTypes.func,
|
onReactionPress: PropTypes.func,
|
||||||
|
@ -50,7 +52,9 @@ class MessageContainer extends React.Component {
|
||||||
blockAction: PropTypes.func,
|
blockAction: PropTypes.func,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
threadBadgeColor: PropTypes.string,
|
threadBadgeColor: PropTypes.string,
|
||||||
toggleFollowThread: PropTypes.func
|
toggleFollowThread: PropTypes.func,
|
||||||
|
jumpToMessage: PropTypes.func,
|
||||||
|
onPress: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -89,10 +93,15 @@ class MessageContainer extends React.Component {
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const { isManualUnignored } = this.state;
|
const { isManualUnignored } = this.state;
|
||||||
const { theme, threadBadgeColor, isIgnored } = this.props;
|
const {
|
||||||
|
theme, threadBadgeColor, isIgnored, highlighted
|
||||||
|
} = this.props;
|
||||||
if (nextProps.theme !== theme) {
|
if (nextProps.theme !== theme) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (nextProps.highlighted !== highlighted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (nextProps.threadBadgeColor !== threadBadgeColor) {
|
if (nextProps.threadBadgeColor !== threadBadgeColor) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -112,10 +121,15 @@ class MessageContainer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onPress = debounce(() => {
|
onPress = debounce(() => {
|
||||||
|
const { onPress } = this.props;
|
||||||
if (this.isIgnored) {
|
if (this.isIgnored) {
|
||||||
return this.onIgnoredMessagePress();
|
return this.onIgnoredMessagePress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onPress) {
|
||||||
|
return onPress();
|
||||||
|
}
|
||||||
|
|
||||||
const { item, isThreadRoom } = this.props;
|
const { item, isThreadRoom } = this.props;
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
|
|
||||||
|
@ -265,12 +279,69 @@ class MessageContainer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLinkPress = (link) => {
|
||||||
|
const { item, theme, jumpToMessage } = this.props;
|
||||||
|
const isMessageLink = item?.attachments?.findIndex(att => att?.message_link === link) !== -1;
|
||||||
|
if (isMessageLink) {
|
||||||
|
return jumpToMessage(link);
|
||||||
|
}
|
||||||
|
openLink(link, theme);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme, threadBadgeColor, toggleFollowThread
|
item,
|
||||||
|
user,
|
||||||
|
style,
|
||||||
|
archived,
|
||||||
|
baseUrl,
|
||||||
|
useRealName,
|
||||||
|
broadcast,
|
||||||
|
fetchThreadName,
|
||||||
|
showAttachment,
|
||||||
|
timeFormat,
|
||||||
|
isReadReceiptEnabled,
|
||||||
|
autoTranslateRoom,
|
||||||
|
autoTranslateLanguage,
|
||||||
|
navToRoomInfo,
|
||||||
|
getCustomEmoji,
|
||||||
|
isThreadRoom,
|
||||||
|
callJitsi,
|
||||||
|
blockAction,
|
||||||
|
rid,
|
||||||
|
theme,
|
||||||
|
threadBadgeColor,
|
||||||
|
toggleFollowThread,
|
||||||
|
jumpToMessage,
|
||||||
|
highlighted
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
id, msg, ts, attachments, urls, reactions, t, avatar, emoji, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage, replies
|
id,
|
||||||
|
msg,
|
||||||
|
ts,
|
||||||
|
attachments,
|
||||||
|
urls,
|
||||||
|
reactions,
|
||||||
|
t,
|
||||||
|
avatar,
|
||||||
|
emoji,
|
||||||
|
u,
|
||||||
|
alias,
|
||||||
|
editedBy,
|
||||||
|
role,
|
||||||
|
drid,
|
||||||
|
dcount,
|
||||||
|
dlm,
|
||||||
|
tmid,
|
||||||
|
tcount,
|
||||||
|
tlm,
|
||||||
|
tmsg,
|
||||||
|
mentions,
|
||||||
|
channels,
|
||||||
|
unread,
|
||||||
|
blocks,
|
||||||
|
autoTranslate: autoTranslateMessage,
|
||||||
|
replies
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
let message = msg;
|
let message = msg;
|
||||||
|
@ -294,6 +365,8 @@ class MessageContainer extends React.Component {
|
||||||
onEncryptedPress: this.onEncryptedPress,
|
onEncryptedPress: this.onEncryptedPress,
|
||||||
onDiscussionPress: this.onDiscussionPress,
|
onDiscussionPress: this.onDiscussionPress,
|
||||||
onReactionLongPress: this.onReactionLongPress,
|
onReactionLongPress: this.onReactionLongPress,
|
||||||
|
onLinkPress: this.onLinkPress,
|
||||||
|
jumpToMessage,
|
||||||
threadBadgeColor,
|
threadBadgeColor,
|
||||||
toggleFollowThread,
|
toggleFollowThread,
|
||||||
replies
|
replies
|
||||||
|
@ -347,6 +420,7 @@ class MessageContainer extends React.Component {
|
||||||
callJitsi={callJitsi}
|
callJitsi={callJitsi}
|
||||||
blockAction={blockAction}
|
blockAction={blockAction}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
highlighted={highlighted}
|
||||||
/>
|
/>
|
||||||
</MessageContext.Provider>
|
</MessageContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
"error-message-editing-blocked": "Message editing is blocked",
|
"error-message-editing-blocked": "Message editing is blocked",
|
||||||
"error-message-size-exceeded": "Message size exceeds Message_MaxAllowedSize",
|
"error-message-size-exceeded": "Message size exceeds Message_MaxAllowedSize",
|
||||||
"error-missing-unsubscribe-link": "You must provide the [unsubscribe] link.",
|
"error-missing-unsubscribe-link": "You must provide the [unsubscribe] link.",
|
||||||
|
"error-no-owner-channel":"You don't own the channel",
|
||||||
"error-no-tokens-for-this-user": "There are no tokens for this user",
|
"error-no-tokens-for-this-user": "There are no tokens for this user",
|
||||||
"error-not-allowed": "Not allowed",
|
"error-not-allowed": "Not allowed",
|
||||||
"error-not-authorized": "Not authorized",
|
"error-not-authorized": "Not authorized",
|
||||||
|
@ -90,6 +91,7 @@
|
||||||
"alert": "alert",
|
"alert": "alert",
|
||||||
"alerts": "alerts",
|
"alerts": "alerts",
|
||||||
"All_users_in_the_channel_can_write_new_messages": "All users in the channel can write new messages",
|
"All_users_in_the_channel_can_write_new_messages": "All users in the channel can write new messages",
|
||||||
|
"All_users_in_the_team_can_write_new_messages": "All users in the team can write new messages",
|
||||||
"A_meaningful_name_for_the_discussion_room": "A meaningful name for the discussion room",
|
"A_meaningful_name_for_the_discussion_room": "A meaningful name for the discussion room",
|
||||||
"All": "All",
|
"All": "All",
|
||||||
"All_Messages": "All Messages",
|
"All_Messages": "All Messages",
|
||||||
|
@ -225,6 +227,7 @@
|
||||||
"Encryption_error_title": "Your encryption password seems wrong",
|
"Encryption_error_title": "Your encryption password seems wrong",
|
||||||
"Encryption_error_desc": "It wasn't possible to decode your encryption key to be imported.",
|
"Encryption_error_desc": "It wasn't possible to decode your encryption key to be imported.",
|
||||||
"Everyone_can_access_this_channel": "Everyone can access this channel",
|
"Everyone_can_access_this_channel": "Everyone can access this channel",
|
||||||
|
"Everyone_can_access_this_team": "Everyone can access this team",
|
||||||
"Error_uploading": "Error uploading",
|
"Error_uploading": "Error uploading",
|
||||||
"Expiration_Days": "Expiration (Days)",
|
"Expiration_Days": "Expiration (Days)",
|
||||||
"Favorite": "Favorite",
|
"Favorite": "Favorite",
|
||||||
|
@ -286,10 +289,12 @@
|
||||||
"Join_our_open_workspace": "Join our open workspace",
|
"Join_our_open_workspace": "Join our open workspace",
|
||||||
"Join_your_workspace": "Join your workspace",
|
"Join_your_workspace": "Join your workspace",
|
||||||
"Just_invited_people_can_access_this_channel": "Just invited people can access this channel",
|
"Just_invited_people_can_access_this_channel": "Just invited people can access this channel",
|
||||||
|
"Just_invited_people_can_access_this_team": "Just invited people can access this team",
|
||||||
"Language": "Language",
|
"Language": "Language",
|
||||||
"last_message": "last message",
|
"last_message": "last message",
|
||||||
"Leave_channel": "Leave channel",
|
"Leave_channel": "Leave channel",
|
||||||
"leaving_room": "leaving room",
|
"leaving_room": "leaving room",
|
||||||
|
"Leave": "Leave",
|
||||||
"leave": "leave",
|
"leave": "leave",
|
||||||
"Legal": "Legal",
|
"Legal": "Legal",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
|
@ -435,6 +440,7 @@
|
||||||
"Review_app_unable_store": "Unable to open {{store}}",
|
"Review_app_unable_store": "Unable to open {{store}}",
|
||||||
"Review_this_app": "Review this app",
|
"Review_this_app": "Review this app",
|
||||||
"Remove": "Remove",
|
"Remove": "Remove",
|
||||||
|
"remove": "remove",
|
||||||
"Roles": "Roles",
|
"Roles": "Roles",
|
||||||
"Room_actions": "Room actions",
|
"Room_actions": "Room actions",
|
||||||
"Room_changed_announcement": "Room announcement changed to: {{announcement}} by {{userBy}}",
|
"Room_changed_announcement": "Room announcement changed to: {{announcement}} by {{userBy}}",
|
||||||
|
@ -681,12 +687,9 @@
|
||||||
"No_threads_following": "You are not following any threads",
|
"No_threads_following": "You are not following any threads",
|
||||||
"No_threads_unread": "There are no unread threads",
|
"No_threads_unread": "There are no unread threads",
|
||||||
"Messagebox_Send_to_channel": "Send to channel",
|
"Messagebox_Send_to_channel": "Send to channel",
|
||||||
"Set_as_leader": "Set as leader",
|
"Leader": "Leader",
|
||||||
"Set_as_moderator": "Set as moderator",
|
"Moderator": "Moderator",
|
||||||
"Set_as_owner": "Set as owner",
|
"Owner": "Owner",
|
||||||
"Remove_as_leader": "Remove as leader",
|
|
||||||
"Remove_as_moderator": "Remove as moderator",
|
|
||||||
"Remove_as_owner": "Remove as owner",
|
|
||||||
"Remove_from_room": "Remove from room",
|
"Remove_from_room": "Remove from room",
|
||||||
"Ignore": "Ignore",
|
"Ignore": "Ignore",
|
||||||
"Unignore": "Unignore",
|
"Unignore": "Unignore",
|
||||||
|
@ -716,5 +719,36 @@
|
||||||
"Read_Only_Team": "Read Only Team",
|
"Read_Only_Team": "Read Only Team",
|
||||||
"Broadcast_Team": "Broadcast Team",
|
"Broadcast_Team": "Broadcast Team",
|
||||||
"creating_team": "creating team",
|
"creating_team": "creating team",
|
||||||
"team-name-already-exists": "A team with that name already exists"
|
"team-name-already-exists": "A team with that name already exists",
|
||||||
|
"Add_Channel_to_Team": "Add Channel to Team",
|
||||||
|
"Create_New": "Create New",
|
||||||
|
"Add_Existing": "Add Existing",
|
||||||
|
"Add_Existing_Channel": "Add Existing Channel",
|
||||||
|
"Remove_from_Team": "Remove from Team",
|
||||||
|
"Auto-join": "Auto-join",
|
||||||
|
"Remove_Team_Room_Warning": "Woud you like to remove this channel from the team? The channel will be moved back to the workspace",
|
||||||
|
"Confirmation": "Confirmation",
|
||||||
|
"invalid-room": "Invalid room",
|
||||||
|
"You_are_leaving_the_team": "You are leaving the team '{{team}}'",
|
||||||
|
"Leave_Team": "Leave Team",
|
||||||
|
"Select_Team_Channels": "Select the Team's channels you would like to leave.",
|
||||||
|
"Cannot_leave": "Cannot leave",
|
||||||
|
"Cannot_remove": "Cannot remove",
|
||||||
|
"Cannot_delete": "Cannot delete",
|
||||||
|
"Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.",
|
||||||
|
"last-owner-can-not-be-removed": "Last owner cannot be removed",
|
||||||
|
"Remove_User_Teams": "Select channels you want the user to be removed from.",
|
||||||
|
"Delete_Team": "Delete Team",
|
||||||
|
"Select_channels_to_delete": "This can't be undone. Once you delete a team, all chat content and configuration will be deleted. \n\nSelect the channels you would like to delete. The ones you decide to keep will be available on your workspace. Notice that public channels will still be public and visible to everyone.",
|
||||||
|
"You_are_deleting_the_team": "You are deleting this team.",
|
||||||
|
"Removing_user_from_this_team": "You are removing {{user}} from this team",
|
||||||
|
"Remove_User_Team_Channels": "Select the channels you want the user to be removed from.",
|
||||||
|
"Remove_Member": "Remove Member",
|
||||||
|
"leaving_team": "leaving team",
|
||||||
|
"removing_team": "removing from team",
|
||||||
|
"deleting_team": "deleting team",
|
||||||
|
"member-does-not-exist": "Member does not exist",
|
||||||
|
"Load_More": "Load More",
|
||||||
|
"Load_Newer": "Load Newer",
|
||||||
|
"Load_Older": "Load Older"
|
||||||
}
|
}
|
|
@ -667,5 +667,6 @@
|
||||||
"Teams": "Times",
|
"Teams": "Times",
|
||||||
"No_team_channels_found": "Nenhum canal encontrado",
|
"No_team_channels_found": "Nenhum canal encontrado",
|
||||||
"Team_not_found": "Time não encontrado",
|
"Team_not_found": "Time não encontrado",
|
||||||
"Private_Team": "Equipe Privada"
|
"Private_Team": "Equipe Privada",
|
||||||
|
"Add_Existing_Channel": "Adicionar Canal Existente"
|
||||||
}
|
}
|
|
@ -5,8 +5,10 @@ import {
|
||||||
|
|
||||||
import { sanitizer } from '../utils';
|
import { sanitizer } from '../utils';
|
||||||
|
|
||||||
|
export const TABLE_NAME = 'messages';
|
||||||
|
|
||||||
export default class Message extends Model {
|
export default class Message extends Model {
|
||||||
static table = 'messages';
|
static table = TABLE_NAME;
|
||||||
|
|
||||||
static associations = {
|
static associations = {
|
||||||
subscriptions: { type: 'belongs_to', key: 'rid' }
|
subscriptions: { type: 'belongs_to', key: 'rid' }
|
||||||
|
|
|
@ -4,8 +4,10 @@ import {
|
||||||
} from '@nozbe/watermelondb/decorators';
|
} from '@nozbe/watermelondb/decorators';
|
||||||
import { sanitizer } from '../utils';
|
import { sanitizer } from '../utils';
|
||||||
|
|
||||||
|
export const TABLE_NAME = 'subscriptions';
|
||||||
|
|
||||||
export default class Subscription extends Model {
|
export default class Subscription extends Model {
|
||||||
static table = 'subscriptions';
|
static table = TABLE_NAME;
|
||||||
|
|
||||||
static associations = {
|
static associations = {
|
||||||
messages: { type: 'has_many', foreignKey: 'rid' },
|
messages: { type: 'has_many', foreignKey: 'rid' },
|
||||||
|
|
|
@ -5,8 +5,10 @@ import {
|
||||||
|
|
||||||
import { sanitizer } from '../utils';
|
import { sanitizer } from '../utils';
|
||||||
|
|
||||||
|
export const TABLE_NAME = 'threads';
|
||||||
|
|
||||||
export default class Thread extends Model {
|
export default class Thread extends Model {
|
||||||
static table = 'threads';
|
static table = TABLE_NAME;
|
||||||
|
|
||||||
static associations = {
|
static associations = {
|
||||||
subscriptions: { type: 'belongs_to', key: 'rid' }
|
subscriptions: { type: 'belongs_to', key: 'rid' }
|
||||||
|
|
|
@ -5,8 +5,10 @@ import {
|
||||||
|
|
||||||
import { sanitizer } from '../utils';
|
import { sanitizer } from '../utils';
|
||||||
|
|
||||||
|
export const TABLE_NAME = 'thread_messages';
|
||||||
|
|
||||||
export default class ThreadMessage extends Model {
|
export default class ThreadMessage extends Model {
|
||||||
static table = 'thread_messages';
|
static table = TABLE_NAME;
|
||||||
|
|
||||||
static associations = {
|
static associations = {
|
||||||
subscriptions: { type: 'belongs_to', key: 'subscription_id' }
|
subscriptions: { type: 'belongs_to', key: 'subscription_id' }
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import database from '..';
|
||||||
|
import { TABLE_NAME } from '../model/Message';
|
||||||
|
|
||||||
|
const getCollection = db => db.get(TABLE_NAME);
|
||||||
|
|
||||||
|
export const getMessageById = async(messageId) => {
|
||||||
|
const db = database.active;
|
||||||
|
const messageCollection = getCollection(db);
|
||||||
|
try {
|
||||||
|
const result = await messageCollection.find(messageId);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import database from '..';
|
||||||
|
import { TABLE_NAME } from '../model/Subscription';
|
||||||
|
|
||||||
|
const getCollection = db => db.get(TABLE_NAME);
|
||||||
|
|
||||||
|
export const getSubscriptionByRoomId = async(rid) => {
|
||||||
|
const db = database.active;
|
||||||
|
const subCollection = getCollection(db);
|
||||||
|
try {
|
||||||
|
const result = await subCollection.find(rid);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import database from '..';
|
||||||
|
import { TABLE_NAME } from '../model/Thread';
|
||||||
|
|
||||||
|
const getCollection = db => db.get(TABLE_NAME);
|
||||||
|
|
||||||
|
export const getThreadById = async(tmid) => {
|
||||||
|
const db = database.active;
|
||||||
|
const threadCollection = getCollection(db);
|
||||||
|
try {
|
||||||
|
const result = await threadCollection.find(tmid);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import database from '..';
|
||||||
|
import { TABLE_NAME } from '../model/ThreadMessage';
|
||||||
|
|
||||||
|
const getCollection = db => db.get(TABLE_NAME);
|
||||||
|
|
||||||
|
export const getThreadMessageById = async(messageId) => {
|
||||||
|
const db = database.active;
|
||||||
|
const threadMessageCollection = getCollection(db);
|
||||||
|
try {
|
||||||
|
const result = await threadMessageCollection.find(messageId);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -13,19 +13,24 @@ const PERMISSIONS = [
|
||||||
'add-user-to-any-c-room',
|
'add-user-to-any-c-room',
|
||||||
'add-user-to-any-p-room',
|
'add-user-to-any-p-room',
|
||||||
'add-user-to-joined-room',
|
'add-user-to-joined-room',
|
||||||
|
'add-team-channel',
|
||||||
'archive-room',
|
'archive-room',
|
||||||
'auto-translate',
|
'auto-translate',
|
||||||
'create-invite-links',
|
'create-invite-links',
|
||||||
'delete-c',
|
'delete-c',
|
||||||
'delete-message',
|
'delete-message',
|
||||||
'delete-p',
|
'delete-p',
|
||||||
|
'delete-team',
|
||||||
'edit-message',
|
'edit-message',
|
||||||
'edit-room',
|
'edit-room',
|
||||||
|
'edit-team-member',
|
||||||
|
'edit-team-channel',
|
||||||
'force-delete-message',
|
'force-delete-message',
|
||||||
'mute-user',
|
'mute-user',
|
||||||
'pin-message',
|
'pin-message',
|
||||||
'post-readonly',
|
'post-readonly',
|
||||||
'remove-user',
|
'remove-user',
|
||||||
|
'remove-team-channel',
|
||||||
'set-leader',
|
'set-leader',
|
||||||
'set-moderator',
|
'set-moderator',
|
||||||
'set-owner',
|
'set-owner',
|
||||||
|
@ -38,7 +43,9 @@ const PERMISSIONS = [
|
||||||
'view-privileged-setting',
|
'view-privileged-setting',
|
||||||
'view-room-administration',
|
'view-room-administration',
|
||||||
'view-statistics',
|
'view-statistics',
|
||||||
'view-user-administration'
|
'view-user-administration',
|
||||||
|
'view-all-teams',
|
||||||
|
'view-all-team-channels'
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function setPermissions() {
|
export async function setPermissions() {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { getSubscriptionByRoomId } from '../database/services/Subscription';
|
||||||
|
import RocketChat from '../rocketchat';
|
||||||
|
|
||||||
|
const getRoomInfo = async(rid) => {
|
||||||
|
let result;
|
||||||
|
result = await getSubscriptionByRoomId(rid);
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
rid,
|
||||||
|
name: result.name,
|
||||||
|
fname: result.fname,
|
||||||
|
t: result.t
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await RocketChat.getRoomInfo(rid);
|
||||||
|
if (result?.success) {
|
||||||
|
return {
|
||||||
|
rid,
|
||||||
|
name: result.room.name,
|
||||||
|
fname: result.room.fname,
|
||||||
|
t: result.room.t
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getRoomInfo;
|
|
@ -0,0 +1,15 @@
|
||||||
|
import RocketChat from '../rocketchat';
|
||||||
|
|
||||||
|
const getSingleMessage = messageId => new Promise(async(resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const result = await RocketChat.getSingleMessage(messageId);
|
||||||
|
if (result.success) {
|
||||||
|
return resolve(result.message);
|
||||||
|
}
|
||||||
|
return reject();
|
||||||
|
} catch (e) {
|
||||||
|
return reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getSingleMessage;
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
||||||
|
|
||||||
|
import database from '../database';
|
||||||
|
import { getMessageById } from '../database/services/Message';
|
||||||
|
import { getThreadById } from '../database/services/Thread';
|
||||||
|
import log from '../../utils/log';
|
||||||
|
import getSingleMessage from './getSingleMessage';
|
||||||
|
import { Encryption } from '../encryption';
|
||||||
|
|
||||||
|
const buildThreadName = thread => thread.msg || thread?.attachments?.[0]?.title;
|
||||||
|
|
||||||
|
const getThreadName = async(rid, tmid, messageId) => {
|
||||||
|
let tmsg;
|
||||||
|
try {
|
||||||
|
const db = database.active;
|
||||||
|
const threadCollection = db.get('threads');
|
||||||
|
const messageRecord = await getMessageById(messageId);
|
||||||
|
const threadRecord = await getThreadById(tmid);
|
||||||
|
if (threadRecord) {
|
||||||
|
tmsg = buildThreadName(threadRecord);
|
||||||
|
await db.action(async() => {
|
||||||
|
await messageRecord?.update((m) => {
|
||||||
|
m.tmsg = tmsg;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let thread = await getSingleMessage(tmid);
|
||||||
|
thread = await Encryption.decryptMessage(thread);
|
||||||
|
tmsg = buildThreadName(thread);
|
||||||
|
await db.action(async() => {
|
||||||
|
await db.batch(
|
||||||
|
threadCollection?.prepareCreate((t) => {
|
||||||
|
t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema);
|
||||||
|
t.subscription.id = rid;
|
||||||
|
Object.assign(t, thread);
|
||||||
|
}),
|
||||||
|
messageRecord?.prepareUpdate((m) => {
|
||||||
|
m.tmsg = tmsg;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
return tmsg;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getThreadName;
|
|
@ -1,8 +1,15 @@
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { MESSAGE_TYPE_LOAD_MORE } from '../../constants/messageTypeLoad';
|
||||||
import log from '../../utils/log';
|
import log from '../../utils/log';
|
||||||
|
import { getMessageById } from '../database/services/Message';
|
||||||
import updateMessages from './updateMessages';
|
import updateMessages from './updateMessages';
|
||||||
|
import { generateLoadMoreId } from '../utils';
|
||||||
|
|
||||||
|
const COUNT = 50;
|
||||||
|
|
||||||
async function load({ rid: roomId, latest, t }) {
|
async function load({ rid: roomId, latest, t }) {
|
||||||
let params = { roomId, count: 50 };
|
let params = { roomId, count: COUNT };
|
||||||
if (latest) {
|
if (latest) {
|
||||||
params = { ...params, latest: new Date(latest).toISOString() };
|
params = { ...params, latest: new Date(latest).toISOString() };
|
||||||
}
|
}
|
||||||
|
@ -24,9 +31,20 @@ export default function loadMessagesForRoom(args) {
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const data = await load.call(this, args);
|
const data = await load.call(this, args);
|
||||||
|
if (data?.length) {
|
||||||
if (data && data.length) {
|
const lastMessage = data[data.length - 1];
|
||||||
await updateMessages({ rid: args.rid, update: data });
|
const lastMessageRecord = await getMessageById(lastMessage._id);
|
||||||
|
if (!lastMessageRecord && data.length === COUNT) {
|
||||||
|
const loadMoreItem = {
|
||||||
|
_id: generateLoadMoreId(lastMessage._id),
|
||||||
|
rid: lastMessage.rid,
|
||||||
|
ts: moment(lastMessage.ts).subtract(1, 'millisecond'),
|
||||||
|
t: MESSAGE_TYPE_LOAD_MORE,
|
||||||
|
msg: lastMessage.msg
|
||||||
|
};
|
||||||
|
data.push(loadMoreItem);
|
||||||
|
}
|
||||||
|
await updateMessages({ rid: args.rid, update: data, loaderItem: args.loaderItem });
|
||||||
return resolve(data);
|
return resolve(data);
|
||||||
} else {
|
} else {
|
||||||
return resolve([]);
|
return resolve([]);
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import EJSON from 'ejson';
|
||||||
|
import moment from 'moment';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
|
||||||
|
import log from '../../utils/log';
|
||||||
|
import updateMessages from './updateMessages';
|
||||||
|
import { getMessageById } from '../database/services/Message';
|
||||||
|
import { MESSAGE_TYPE_LOAD_NEXT_CHUNK } from '../../constants/messageTypeLoad';
|
||||||
|
import { generateLoadMoreId } from '../utils';
|
||||||
|
|
||||||
|
const COUNT = 50;
|
||||||
|
|
||||||
|
export default function loadNextMessages(args) {
|
||||||
|
return new Promise(async(resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const data = await this.methodCallWrapper('loadNextMessages', args.rid, args.ts, COUNT);
|
||||||
|
let messages = EJSON.fromJSONValue(data?.messages);
|
||||||
|
messages = orderBy(messages, 'ts');
|
||||||
|
if (messages?.length) {
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
const lastMessageRecord = await getMessageById(lastMessage._id);
|
||||||
|
if (!lastMessageRecord && messages.length === COUNT) {
|
||||||
|
const loadMoreItem = {
|
||||||
|
_id: generateLoadMoreId(lastMessage._id),
|
||||||
|
rid: lastMessage.rid,
|
||||||
|
tmid: args.tmid,
|
||||||
|
ts: moment(lastMessage.ts).add(1, 'millisecond'),
|
||||||
|
t: MESSAGE_TYPE_LOAD_NEXT_CHUNK
|
||||||
|
};
|
||||||
|
messages.push(loadMoreItem);
|
||||||
|
}
|
||||||
|
await updateMessages({ rid: args.rid, update: messages, loaderItem: args.loaderItem });
|
||||||
|
return resolve(messages);
|
||||||
|
} else {
|
||||||
|
return resolve([]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import EJSON from 'ejson';
|
||||||
|
import moment from 'moment';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
|
||||||
|
import log from '../../utils/log';
|
||||||
|
import updateMessages from './updateMessages';
|
||||||
|
import { getMessageById } from '../database/services/Message';
|
||||||
|
import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../constants/messageTypeLoad';
|
||||||
|
import { generateLoadMoreId } from '../utils';
|
||||||
|
|
||||||
|
const COUNT = 50;
|
||||||
|
|
||||||
|
export default function loadSurroundingMessages({ messageId, rid }) {
|
||||||
|
return new Promise(async(resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const data = await this.methodCallWrapper('loadSurroundingMessages', { _id: messageId, rid }, COUNT);
|
||||||
|
let messages = EJSON.fromJSONValue(data?.messages);
|
||||||
|
messages = orderBy(messages, 'ts');
|
||||||
|
|
||||||
|
const message = messages.find(m => m._id === messageId);
|
||||||
|
const { tmid } = message;
|
||||||
|
|
||||||
|
if (messages?.length) {
|
||||||
|
if (data?.moreBefore) {
|
||||||
|
const firstMessage = messages[0];
|
||||||
|
const firstMessageRecord = await getMessageById(firstMessage._id);
|
||||||
|
if (!firstMessageRecord) {
|
||||||
|
const loadMoreItem = {
|
||||||
|
_id: generateLoadMoreId(firstMessage._id),
|
||||||
|
rid: firstMessage.rid,
|
||||||
|
tmid,
|
||||||
|
ts: moment(firstMessage.ts).subtract(1, 'millisecond'),
|
||||||
|
t: MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK,
|
||||||
|
msg: firstMessage.msg
|
||||||
|
};
|
||||||
|
messages.unshift(loadMoreItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.moreAfter) {
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
const lastMessageRecord = await getMessageById(lastMessage._id);
|
||||||
|
if (!lastMessageRecord) {
|
||||||
|
const loadMoreItem = {
|
||||||
|
_id: generateLoadMoreId(lastMessage._id),
|
||||||
|
rid: lastMessage.rid,
|
||||||
|
tmid,
|
||||||
|
ts: moment(lastMessage.ts).add(1, 'millisecond'),
|
||||||
|
t: MESSAGE_TYPE_LOAD_NEXT_CHUNK,
|
||||||
|
msg: lastMessage.msg
|
||||||
|
};
|
||||||
|
messages.push(loadMoreItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await updateMessages({ rid, update: messages });
|
||||||
|
return resolve(messages);
|
||||||
|
} else {
|
||||||
|
return resolve([]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
||||||
|
import EJSON from 'ejson';
|
||||||
|
|
||||||
import buildMessage from './helpers/buildMessage';
|
import buildMessage from './helpers/buildMessage';
|
||||||
import database from '../database';
|
import database from '../database';
|
||||||
|
@ -7,30 +8,27 @@ import log from '../../utils/log';
|
||||||
import protectedFunction from './helpers/protectedFunction';
|
import protectedFunction from './helpers/protectedFunction';
|
||||||
import { Encryption } from '../encryption';
|
import { Encryption } from '../encryption';
|
||||||
|
|
||||||
async function load({ tmid, offset }) {
|
async function load({ tmid }) {
|
||||||
try {
|
try {
|
||||||
// RC 1.0
|
// RC 1.0
|
||||||
const result = await this.sdk.get('chat.getThreadMessages', {
|
const result = await this.methodCallWrapper('getThreadMessages', { tmid });
|
||||||
tmid, count: 50, offset, sort: { ts: -1 }, query: { _hidden: { $ne: true } }
|
if (!result) {
|
||||||
});
|
|
||||||
if (!result || !result.success) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return result.messages;
|
return EJSON.fromJSONValue(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function loadThreadMessages({ tmid, rid, offset = 0 }) {
|
export default function loadThreadMessages({ tmid, rid }) {
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
let data = await load.call(this, { tmid, offset });
|
let data = await load.call(this, { tmid });
|
||||||
|
|
||||||
if (data && data.length) {
|
if (data && data.length) {
|
||||||
try {
|
try {
|
||||||
data = data.map(m => buildMessage(m));
|
data = data.filter(m => m.tmid).map(m => buildMessage(m));
|
||||||
data = await Encryption.decryptMessages(data);
|
data = await Encryption.decryptMessages(data);
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const threadMessagesCollection = db.get('thread_messages');
|
const threadMessagesCollection = db.get('thread_messages');
|
||||||
|
|
|
@ -159,7 +159,7 @@ export default class RoomSubscription {
|
||||||
updateMessage = message => (
|
updateMessage = message => (
|
||||||
new Promise(async(resolve) => {
|
new Promise(async(resolve) => {
|
||||||
if (this.rid !== message.rid) {
|
if (this.rid !== message.rid) {
|
||||||
return;
|
return resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
|
|
|
@ -6,8 +6,12 @@ import log from '../../utils/log';
|
||||||
import database from '../database';
|
import database from '../database';
|
||||||
import protectedFunction from './helpers/protectedFunction';
|
import protectedFunction from './helpers/protectedFunction';
|
||||||
import { Encryption } from '../encryption';
|
import { Encryption } from '../encryption';
|
||||||
|
import { MESSAGE_TYPE_ANY_LOAD } from '../../constants/messageTypeLoad';
|
||||||
|
import { generateLoadMoreId } from '../utils';
|
||||||
|
|
||||||
export default function updateMessages({ rid, update = [], remove = [] }) {
|
export default function updateMessages({
|
||||||
|
rid, update = [], remove = [], loaderItem
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
if (!((update && update.length) || (remove && remove.length))) {
|
if (!((update && update.length) || (remove && remove.length))) {
|
||||||
return;
|
return;
|
||||||
|
@ -30,7 +34,13 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
|
||||||
const threadCollection = db.get('threads');
|
const threadCollection = db.get('threads');
|
||||||
const threadMessagesCollection = db.get('thread_messages');
|
const threadMessagesCollection = db.get('thread_messages');
|
||||||
const allMessagesRecords = await msgCollection
|
const allMessagesRecords = await msgCollection
|
||||||
.query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds)))
|
.query(
|
||||||
|
Q.where('rid', rid),
|
||||||
|
Q.or(
|
||||||
|
Q.where('id', Q.oneOf(messagesIds)),
|
||||||
|
Q.where('t', Q.oneOf(MESSAGE_TYPE_ANY_LOAD))
|
||||||
|
)
|
||||||
|
)
|
||||||
.fetch();
|
.fetch();
|
||||||
const allThreadsRecords = await threadCollection
|
const allThreadsRecords = await threadCollection
|
||||||
.query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds)))
|
.query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds)))
|
||||||
|
@ -55,6 +65,9 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
|
||||||
let threadMessagesToCreate = allThreadMessages.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id));
|
let threadMessagesToCreate = allThreadMessages.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id));
|
||||||
let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => allThreadMessages.find(i2 => i1.id === i2._id));
|
let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => allThreadMessages.find(i2 => i1.id === i2._id));
|
||||||
|
|
||||||
|
// filter loaders to delete
|
||||||
|
let loadersToDelete = allMessagesRecords.filter(i1 => update.find(i2 => i1.id === generateLoadMoreId(i2._id)));
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
msgsToCreate = msgsToCreate.map(message => msgCollection.prepareCreate(protectedFunction((m) => {
|
msgsToCreate = msgsToCreate.map(message => msgCollection.prepareCreate(protectedFunction((m) => {
|
||||||
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
|
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
|
||||||
|
@ -121,6 +134,12 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
|
||||||
threadMessagesToDelete = threadMessagesToDelete.map(tm => tm.prepareDestroyPermanently());
|
threadMessagesToDelete = threadMessagesToDelete.map(tm => tm.prepareDestroyPermanently());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete loaders
|
||||||
|
loadersToDelete = loadersToDelete.map(m => m.prepareDestroyPermanently());
|
||||||
|
if (loaderItem) {
|
||||||
|
loadersToDelete.push(loaderItem.prepareDestroyPermanently());
|
||||||
|
}
|
||||||
|
|
||||||
const allRecords = [
|
const allRecords = [
|
||||||
...msgsToCreate,
|
...msgsToCreate,
|
||||||
...msgsToUpdate,
|
...msgsToUpdate,
|
||||||
|
@ -130,7 +149,8 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
|
||||||
...threadsToDelete,
|
...threadsToDelete,
|
||||||
...threadMessagesToCreate,
|
...threadMessagesToCreate,
|
||||||
...threadMessagesToUpdate,
|
...threadMessagesToUpdate,
|
||||||
...threadMessagesToDelete
|
...threadMessagesToDelete,
|
||||||
|
...loadersToDelete
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { InteractionManager } from 'react-native';
|
import { InteractionManager } from 'react-native';
|
||||||
|
import EJSON from 'ejson';
|
||||||
import {
|
import {
|
||||||
Rocketchat as RocketchatClient,
|
Rocketchat as RocketchatClient,
|
||||||
settings as RocketChatSettings
|
settings as RocketChatSettings
|
||||||
|
@ -41,6 +42,8 @@ import canOpenRoom from './methods/canOpenRoom';
|
||||||
import triggerBlockAction, { triggerSubmitView, triggerCancel } from './methods/actions';
|
import triggerBlockAction, { triggerSubmitView, triggerCancel } from './methods/actions';
|
||||||
|
|
||||||
import loadMessagesForRoom from './methods/loadMessagesForRoom';
|
import loadMessagesForRoom from './methods/loadMessagesForRoom';
|
||||||
|
import loadSurroundingMessages from './methods/loadSurroundingMessages';
|
||||||
|
import loadNextMessages from './methods/loadNextMessages';
|
||||||
import loadMissedMessages from './methods/loadMissedMessages';
|
import loadMissedMessages from './methods/loadMissedMessages';
|
||||||
import loadThreadMessages from './methods/loadThreadMessages';
|
import loadThreadMessages from './methods/loadThreadMessages';
|
||||||
|
|
||||||
|
@ -95,10 +98,19 @@ const RocketChat = {
|
||||||
},
|
},
|
||||||
canOpenRoom,
|
canOpenRoom,
|
||||||
createChannel({
|
createChannel({
|
||||||
name, users, type, readOnly, broadcast, encrypted
|
name, users, type, readOnly, broadcast, encrypted, teamId
|
||||||
}) {
|
}) {
|
||||||
// RC 0.51.0
|
const params = {
|
||||||
return this.methodCallWrapper(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast, encrypted });
|
name,
|
||||||
|
members: users,
|
||||||
|
readOnly,
|
||||||
|
extraData: {
|
||||||
|
broadcast,
|
||||||
|
encrypted,
|
||||||
|
...(teamId && { teamId })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return this.post(type ? 'groups.create' : 'channels.create', params);
|
||||||
},
|
},
|
||||||
async getWebsocketInfo({ server }) {
|
async getWebsocketInfo({ server }) {
|
||||||
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
|
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
|
||||||
|
@ -615,6 +627,8 @@ const RocketChat = {
|
||||||
},
|
},
|
||||||
loadMissedMessages,
|
loadMissedMessages,
|
||||||
loadMessagesForRoom,
|
loadMessagesForRoom,
|
||||||
|
loadSurroundingMessages,
|
||||||
|
loadNextMessages,
|
||||||
loadThreadMessages,
|
loadThreadMessages,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
getRooms,
|
getRooms,
|
||||||
|
@ -648,7 +662,8 @@ const RocketChat = {
|
||||||
avatarETag: sub.avatarETag,
|
avatarETag: sub.avatarETag,
|
||||||
t: sub.t,
|
t: sub.t,
|
||||||
encrypted: sub.encrypted,
|
encrypted: sub.encrypted,
|
||||||
lastMessage: sub.lastMessage
|
lastMessage: sub.lastMessage,
|
||||||
|
...(sub.teamId && { teamId: sub.teamId })
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
@ -751,6 +766,38 @@ const RocketChat = {
|
||||||
// RC 3.13.0
|
// RC 3.13.0
|
||||||
return this.post('teams.create', params);
|
return this.post('teams.create', params);
|
||||||
},
|
},
|
||||||
|
addRoomsToTeam({ teamId, rooms }) {
|
||||||
|
// RC 3.13.0
|
||||||
|
return this.post('teams.addRooms', { teamId, rooms });
|
||||||
|
},
|
||||||
|
removeTeamRoom({ roomId, teamId }) {
|
||||||
|
// RC 3.13.0
|
||||||
|
return this.post('teams.removeRoom', { roomId, teamId });
|
||||||
|
},
|
||||||
|
leaveTeam({ teamName, rooms }) {
|
||||||
|
// RC 3.13.0
|
||||||
|
return this.post('teams.leave', { teamName, rooms });
|
||||||
|
},
|
||||||
|
removeTeamMember({
|
||||||
|
teamId, teamName, userId, rooms
|
||||||
|
}) {
|
||||||
|
// RC 3.13.0
|
||||||
|
return this.post('teams.removeMember', {
|
||||||
|
teamId, teamName, userId, rooms
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateTeamRoom({ roomId, isDefault }) {
|
||||||
|
// RC 3.13.0
|
||||||
|
return this.post('teams.updateRoom', { roomId, isDefault });
|
||||||
|
},
|
||||||
|
deleteTeam({ teamId, roomsToRemove }) {
|
||||||
|
// RC 3.13.0
|
||||||
|
return this.post('teams.delete', { teamId, roomsToRemove });
|
||||||
|
},
|
||||||
|
teamListRoomsOfUser({ teamId, userId }) {
|
||||||
|
// RC 3.13.0
|
||||||
|
return this.sdk.get('teams.listRoomsOfUser', { teamId, userId });
|
||||||
|
},
|
||||||
joinRoom(roomId, joinCode, type) {
|
joinRoom(roomId, joinCode, type) {
|
||||||
// TODO: join code
|
// TODO: join code
|
||||||
// RC 0.48.0
|
// RC 0.48.0
|
||||||
|
@ -912,9 +959,15 @@ const RocketChat = {
|
||||||
methodCallWrapper(method, ...params) {
|
methodCallWrapper(method, ...params) {
|
||||||
const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings;
|
const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings;
|
||||||
if (API_Use_REST_For_DDP_Calls) {
|
if (API_Use_REST_For_DDP_Calls) {
|
||||||
return this.post(`method.call/${ method }`, { message: JSON.stringify({ method, params }) });
|
return this.post(`method.call/${ method }`, { message: EJSON.stringify({ method, params }) });
|
||||||
}
|
}
|
||||||
return this.methodCall(method, ...params);
|
const parsedParams = params.map((param) => {
|
||||||
|
if (param instanceof Date) {
|
||||||
|
return { $date: new Date(param).getTime() };
|
||||||
|
}
|
||||||
|
return param;
|
||||||
|
});
|
||||||
|
return this.methodCall(method, ...parsedParams);
|
||||||
},
|
},
|
||||||
|
|
||||||
getUserRoles() {
|
getUserRoles() {
|
||||||
|
|
|
@ -20,3 +20,5 @@ export const methods = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const compareServerVersion = (currentServerVersion, versionToCompare, func) => currentServerVersion && func(coerce(currentServerVersion), versionToCompare);
|
export const compareServerVersion = (currentServerVersion, versionToCompare, func) => currentServerVersion && func(coerce(currentServerVersion), versionToCompare);
|
||||||
|
|
||||||
|
export const generateLoadMoreId = id => `load-more-${ id }`;
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const onNotification = (notification) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
rid, name, sender, type, host, messageType
|
rid, name, sender, type, host, messageType, messageId
|
||||||
} = EJSON.parse(data.ejson);
|
} = EJSON.parse(data.ejson);
|
||||||
|
|
||||||
const types = {
|
const types = {
|
||||||
|
@ -24,6 +24,7 @@ export const onNotification = (notification) => {
|
||||||
const params = {
|
const params = {
|
||||||
host,
|
host,
|
||||||
rid,
|
rid,
|
||||||
|
messageId,
|
||||||
path: `${ types[type] }/${ roomName }`,
|
path: `${ types[type] }/${ roomName }`,
|
||||||
isCall: messageType === 'jitsi_call_started'
|
isCall: messageType === 'jitsi_call_started'
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,8 @@ import LastMessage from './LastMessage';
|
||||||
import Title from './Title';
|
import Title from './Title';
|
||||||
import UpdatedAt from './UpdatedAt';
|
import UpdatedAt from './UpdatedAt';
|
||||||
import Touchable from './Touchable';
|
import Touchable from './Touchable';
|
||||||
|
import Tag from './Tag';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
|
||||||
const RoomItem = ({
|
const RoomItem = ({
|
||||||
rid,
|
rid,
|
||||||
|
@ -42,13 +44,16 @@ const RoomItem = ({
|
||||||
testID,
|
testID,
|
||||||
swipeEnabled,
|
swipeEnabled,
|
||||||
onPress,
|
onPress,
|
||||||
|
onLongPress,
|
||||||
toggleFav,
|
toggleFav,
|
||||||
toggleRead,
|
toggleRead,
|
||||||
hideChannel,
|
hideChannel,
|
||||||
teamMain
|
teamMain,
|
||||||
|
autoJoin
|
||||||
}) => (
|
}) => (
|
||||||
<Touchable
|
<Touchable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
width={width}
|
width={width}
|
||||||
favorite={favorite}
|
favorite={favorite}
|
||||||
toggleFav={toggleFav}
|
toggleFav={toggleFav}
|
||||||
|
@ -88,6 +93,9 @@ const RoomItem = ({
|
||||||
hideUnreadStatus={hideUnreadStatus}
|
hideUnreadStatus={hideUnreadStatus}
|
||||||
alert={alert}
|
alert={alert}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
autoJoin ? <Tag name={I18n.t('Auto-join')} /> : null
|
||||||
|
}
|
||||||
<UpdatedAt
|
<UpdatedAt
|
||||||
date={date}
|
date={date}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
@ -132,6 +140,9 @@ const RoomItem = ({
|
||||||
hideUnreadStatus={hideUnreadStatus}
|
hideUnreadStatus={hideUnreadStatus}
|
||||||
alert={alert}
|
alert={alert}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
autoJoin ? <Tag name={I18n.t('Auto-join')} /> : null
|
||||||
|
}
|
||||||
<UnreadBadge
|
<UnreadBadge
|
||||||
unread={unread}
|
unread={unread}
|
||||||
userMentions={userMentions}
|
userMentions={userMentions}
|
||||||
|
@ -181,7 +192,9 @@ RoomItem.propTypes = {
|
||||||
toggleFav: PropTypes.func,
|
toggleFav: PropTypes.func,
|
||||||
toggleRead: PropTypes.func,
|
toggleRead: PropTypes.func,
|
||||||
onPress: PropTypes.func,
|
onPress: PropTypes.func,
|
||||||
hideChannel: PropTypes.func
|
onLongPress: PropTypes.func,
|
||||||
|
hideChannel: PropTypes.func,
|
||||||
|
autoJoin: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomItem.defaultProps = {
|
RoomItem.defaultProps = {
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { themes } from '../../constants/colors';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
|
const Tag = React.memo(({ name }) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.tagContainer, { backgroundColor: themes[theme].borderColor }]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.tagText, { color: themes[theme].infoText }
|
||||||
|
]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Tag.propTypes = {
|
||||||
|
name: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tag;
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Animated } from 'react-native';
|
import { Animated } from 'react-native';
|
||||||
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
import {
|
||||||
|
LongPressGestureHandler, PanGestureHandler, State
|
||||||
|
} from 'react-native-gesture-handler';
|
||||||
|
|
||||||
import Touch from '../../utils/touch';
|
import Touch from '../../utils/touch';
|
||||||
import {
|
import {
|
||||||
|
@ -17,6 +19,7 @@ class Touchable extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
onPress: PropTypes.func,
|
onPress: PropTypes.func,
|
||||||
|
onLongPress: PropTypes.func,
|
||||||
testID: PropTypes.string,
|
testID: PropTypes.string,
|
||||||
width: PropTypes.number,
|
width: PropTypes.number,
|
||||||
favorite: PropTypes.bool,
|
favorite: PropTypes.bool,
|
||||||
|
@ -59,6 +62,12 @@ class Touchable extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLongPressHandlerStateChange = ({ nativeEvent }) => {
|
||||||
|
if (nativeEvent.state === State.ACTIVE) {
|
||||||
|
this.onLongPress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_handleRelease = (nativeEvent) => {
|
_handleRelease = (nativeEvent) => {
|
||||||
const { translationX } = nativeEvent;
|
const { translationX } = nativeEvent;
|
||||||
|
@ -203,13 +212,27 @@ class Touchable extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onLongPress = () => {
|
||||||
|
const { rowState } = this.state;
|
||||||
|
const { onLongPress } = this.props;
|
||||||
|
if (rowState !== 0) {
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onLongPress) {
|
||||||
|
onLongPress();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
testID, isRead, width, favorite, children, theme, isFocused, swipeEnabled
|
testID, isRead, width, favorite, children, theme, isFocused, swipeEnabled
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<LongPressGestureHandler onHandlerStateChange={this.onLongPressHandlerStateChange}>
|
||||||
|
<Animated.View>
|
||||||
<PanGestureHandler
|
<PanGestureHandler
|
||||||
minDeltaX={20}
|
minDeltaX={20}
|
||||||
onGestureEvent={this._onGestureEvent}
|
onGestureEvent={this._onGestureEvent}
|
||||||
|
@ -251,6 +274,8 @@ class Touchable extends React.Component {
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
</PanGestureHandler>
|
</PanGestureHandler>
|
||||||
|
</Animated.View>
|
||||||
|
</LongPressGestureHandler>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,8 @@ const attrs = [
|
||||||
'theme',
|
'theme',
|
||||||
'isFocused',
|
'isFocused',
|
||||||
'forceUpdate',
|
'forceUpdate',
|
||||||
'showLastMessage'
|
'showLastMessage',
|
||||||
|
'autoJoin'
|
||||||
];
|
];
|
||||||
|
|
||||||
class RoomItemContainer extends React.Component {
|
class RoomItemContainer extends React.Component {
|
||||||
|
@ -25,6 +26,7 @@ class RoomItemContainer extends React.Component {
|
||||||
showLastMessage: PropTypes.bool,
|
showLastMessage: PropTypes.bool,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
onPress: PropTypes.func,
|
onPress: PropTypes.func,
|
||||||
|
onLongPress: PropTypes.func,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
avatarSize: PropTypes.number,
|
avatarSize: PropTypes.number,
|
||||||
width: PropTypes.number,
|
width: PropTypes.number,
|
||||||
|
@ -41,7 +43,8 @@ class RoomItemContainer extends React.Component {
|
||||||
getRoomAvatar: PropTypes.func,
|
getRoomAvatar: PropTypes.func,
|
||||||
getIsGroupChat: PropTypes.func,
|
getIsGroupChat: PropTypes.func,
|
||||||
getIsRead: PropTypes.func,
|
getIsRead: PropTypes.func,
|
||||||
swipeEnabled: PropTypes.bool
|
swipeEnabled: PropTypes.bool,
|
||||||
|
autoJoin: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -112,6 +115,11 @@ class RoomItemContainer extends React.Component {
|
||||||
return onPress(item);
|
return onPress(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLongPress = () => {
|
||||||
|
const { item, onLongPress } = this.props;
|
||||||
|
return onLongPress(item);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
item,
|
item,
|
||||||
|
@ -129,7 +137,8 @@ class RoomItemContainer extends React.Component {
|
||||||
showLastMessage,
|
showLastMessage,
|
||||||
username,
|
username,
|
||||||
useRealName,
|
useRealName,
|
||||||
swipeEnabled
|
swipeEnabled,
|
||||||
|
autoJoin
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const name = getRoomTitle(item);
|
const name = getRoomTitle(item);
|
||||||
const testID = `rooms-list-view-item-${ name }`;
|
const testID = `rooms-list-view-item-${ name }`;
|
||||||
|
@ -160,6 +169,7 @@ class RoomItemContainer extends React.Component {
|
||||||
isGroupChat={this.isGroupChat}
|
isGroupChat={this.isGroupChat}
|
||||||
isRead={isRead}
|
isRead={isRead}
|
||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
|
onLongPress={this.onLongPress}
|
||||||
date={date}
|
date={date}
|
||||||
accessibilityLabel={accessibilityLabel}
|
accessibilityLabel={accessibilityLabel}
|
||||||
width={width}
|
width={width}
|
||||||
|
@ -189,6 +199,7 @@ class RoomItemContainer extends React.Component {
|
||||||
tunreadGroup={item.tunreadGroup}
|
tunreadGroup={item.tunreadGroup}
|
||||||
swipeEnabled={swipeEnabled}
|
swipeEnabled={swipeEnabled}
|
||||||
teamMain={item.teamMain}
|
teamMain={item.teamMain}
|
||||||
|
autoJoin={autoJoin}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,5 +96,16 @@ export default StyleSheet.create({
|
||||||
height: '100%',
|
height: '100%',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
tagContainer: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 4,
|
||||||
|
marginHorizontal: 4
|
||||||
|
},
|
||||||
|
tagText: {
|
||||||
|
fontSize: 13,
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
...sharedStyles.textSemibold
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,18 +40,26 @@ const handleRequest = function* handleRequest({ data }) {
|
||||||
broadcast,
|
broadcast,
|
||||||
encrypted
|
encrypted
|
||||||
} = data;
|
} = data;
|
||||||
logEvent(events.CR_CREATE, {
|
logEvent(events.CT_CREATE, {
|
||||||
type,
|
type,
|
||||||
readOnly,
|
readOnly,
|
||||||
broadcast,
|
broadcast,
|
||||||
encrypted
|
encrypted
|
||||||
});
|
});
|
||||||
sub = yield call(createTeam, data);
|
const result = yield call(createTeam, data);
|
||||||
|
sub = {
|
||||||
|
rid: result?.team?.roomId,
|
||||||
|
...result.team,
|
||||||
|
t: result.team.type ? 'p' : 'c'
|
||||||
|
};
|
||||||
} else if (data.group) {
|
} else if (data.group) {
|
||||||
logEvent(events.SELECTED_USERS_CREATE_GROUP);
|
logEvent(events.SELECTED_USERS_CREATE_GROUP);
|
||||||
const result = yield call(createGroupChat);
|
const result = yield call(createGroupChat);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
({ room: sub } = result);
|
sub = {
|
||||||
|
rid: result.room?._id,
|
||||||
|
...result.room
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const {
|
const {
|
||||||
|
@ -66,36 +74,29 @@ const handleRequest = function* handleRequest({ data }) {
|
||||||
broadcast,
|
broadcast,
|
||||||
encrypted
|
encrypted
|
||||||
});
|
});
|
||||||
sub = yield call(createChannel, data);
|
const result = yield call(createChannel, data);
|
||||||
|
sub = {
|
||||||
|
rid: result?.channel?._id || result?.group?._id,
|
||||||
|
...result?.channel,
|
||||||
|
...result?.group
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const subCollection = db.get('subscriptions');
|
const subCollection = db.get('subscriptions');
|
||||||
yield db.action(async() => {
|
yield db.action(async() => {
|
||||||
await subCollection.create((s) => {
|
await subCollection.create((s) => {
|
||||||
s._raw = sanitizedRaw({ id: sub.team ? sub.team.roomId : sub.rid }, subCollection.schema);
|
s._raw = sanitizedRaw({ id: sub.rid }, subCollection.schema);
|
||||||
Object.assign(s, sub);
|
Object.assign(s, sub);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
yield put(createChannelSuccess(sub));
|
||||||
let successParams = {};
|
|
||||||
if (data.isTeam) {
|
|
||||||
successParams = {
|
|
||||||
...sub.team,
|
|
||||||
rid: sub.team.roomId,
|
|
||||||
t: sub.team.type ? 'p' : 'c'
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
successParams = data;
|
|
||||||
}
|
|
||||||
yield put(createChannelSuccess(successParams));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logEvent(events[data.group ? 'SELECTED_USERS_CREATE_GROUP_F' : 'CR_CREATE_F']);
|
logEvent(events[data.group ? 'SELECTED_USERS_CREATE_GROUP_F' : 'CR_CREATE_F']);
|
||||||
yield put(createChannelFailure(err));
|
yield put(createChannelFailure(err, data.isTeam));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -107,10 +108,10 @@ const handleSuccess = function* handleSuccess({ data }) {
|
||||||
goRoom({ item: data, isMasterDetail });
|
goRoom({ item: data, isMasterDetail });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFailure = function handleFailure({ err }) {
|
const handleFailure = function handleFailure({ err, isTeam }) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const msg = err.data ? I18n.t(err.data.error) : err.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') });
|
const msg = err.data.errorType ? I18n.t(err.data.errorType, { room_name: err.data.details.channel_name }) : err.reason || I18n.t('There_was_an_error_while_action', { action: isTeam ? I18n.t('creating_team') : I18n.t('creating_channel') });
|
||||||
showErrorAlert(msg);
|
showErrorAlert(msg, isTeam ? I18n.t('Create_Team') : I18n.t('Create_Channel'));
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -60,18 +60,19 @@ const navigate = function* navigate({ params }) {
|
||||||
|
|
||||||
const isMasterDetail = yield select(state => state.app.isMasterDetail);
|
const isMasterDetail = yield select(state => state.app.isMasterDetail);
|
||||||
const focusedRooms = yield select(state => state.room.rooms);
|
const focusedRooms = yield select(state => state.room.rooms);
|
||||||
|
const jumpToMessageId = params.messageId;
|
||||||
|
|
||||||
if (focusedRooms.includes(room.rid)) {
|
if (focusedRooms.includes(room.rid)) {
|
||||||
// if there's one room on the list or last room is the one
|
// if there's one room on the list or last room is the one
|
||||||
if (focusedRooms.length === 1 || focusedRooms[0] === room.rid) {
|
if (focusedRooms.length === 1 || focusedRooms[0] === room.rid) {
|
||||||
yield goRoom({ item, isMasterDetail });
|
yield goRoom({ item, isMasterDetail, jumpToMessageId });
|
||||||
} else {
|
} else {
|
||||||
popToRoot({ isMasterDetail });
|
popToRoot({ isMasterDetail });
|
||||||
yield goRoom({ item, isMasterDetail });
|
yield goRoom({ item, isMasterDetail, jumpToMessageId });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
popToRoot({ isMasterDetail });
|
popToRoot({ isMasterDetail });
|
||||||
yield goRoom({ item, isMasterDetail });
|
yield goRoom({ item, isMasterDetail, jumpToMessageId });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.isCall) {
|
if (params.isCall) {
|
||||||
|
|
|
@ -71,6 +71,9 @@ import ShareView from '../views/ShareView';
|
||||||
import CreateDiscussionView from '../views/CreateDiscussionView';
|
import CreateDiscussionView from '../views/CreateDiscussionView';
|
||||||
|
|
||||||
import QueueListView from '../ee/omnichannel/views/QueueListView';
|
import QueueListView from '../ee/omnichannel/views/QueueListView';
|
||||||
|
import AddChannelTeamView from '../views/AddChannelTeamView';
|
||||||
|
import AddExistingChannelView from '../views/AddExistingChannelView';
|
||||||
|
import SelectListView from '../views/SelectListView';
|
||||||
|
|
||||||
// ChatsStackNavigator
|
// ChatsStackNavigator
|
||||||
const ChatsStack = createStackNavigator();
|
const ChatsStack = createStackNavigator();
|
||||||
|
@ -91,6 +94,11 @@ const ChatsStackNavigator = () => {
|
||||||
component={RoomActionsView}
|
component={RoomActionsView}
|
||||||
options={RoomActionsView.navigationOptions}
|
options={RoomActionsView.navigationOptions}
|
||||||
/>
|
/>
|
||||||
|
<ChatsStack.Screen
|
||||||
|
name='SelectListView'
|
||||||
|
component={SelectListView}
|
||||||
|
options={SelectListView.navigationOptions}
|
||||||
|
/>
|
||||||
<ChatsStack.Screen
|
<ChatsStack.Screen
|
||||||
name='RoomInfoView'
|
name='RoomInfoView'
|
||||||
component={RoomInfoView}
|
component={RoomInfoView}
|
||||||
|
@ -174,6 +182,21 @@ const ChatsStackNavigator = () => {
|
||||||
component={TeamChannelsView}
|
component={TeamChannelsView}
|
||||||
options={TeamChannelsView.navigationOptions}
|
options={TeamChannelsView.navigationOptions}
|
||||||
/>
|
/>
|
||||||
|
<ChatsStack.Screen
|
||||||
|
name='CreateChannelView'
|
||||||
|
component={CreateChannelView}
|
||||||
|
options={CreateChannelView.navigationOptions}
|
||||||
|
/>
|
||||||
|
<ChatsStack.Screen
|
||||||
|
name='AddChannelTeamView'
|
||||||
|
component={AddChannelTeamView}
|
||||||
|
options={AddChannelTeamView.navigationOptions}
|
||||||
|
/>
|
||||||
|
<ChatsStack.Screen
|
||||||
|
name='AddExistingChannelView'
|
||||||
|
component={AddExistingChannelView}
|
||||||
|
options={AddExistingChannelView.navigationOptions}
|
||||||
|
/>
|
||||||
<ChatsStack.Screen
|
<ChatsStack.Screen
|
||||||
name='MarkdownTableView'
|
name='MarkdownTableView'
|
||||||
component={MarkdownTableView}
|
component={MarkdownTableView}
|
||||||
|
|
|
@ -61,6 +61,9 @@ import { setKeyCommands, deleteKeyCommands } from '../../commands';
|
||||||
import ShareView from '../../views/ShareView';
|
import ShareView from '../../views/ShareView';
|
||||||
|
|
||||||
import QueueListView from '../../ee/omnichannel/views/QueueListView';
|
import QueueListView from '../../ee/omnichannel/views/QueueListView';
|
||||||
|
import AddChannelTeamView from '../../views/AddChannelTeamView';
|
||||||
|
import AddExistingChannelView from '../../views/AddExistingChannelView';
|
||||||
|
import SelectListView from '../../views/SelectListView';
|
||||||
|
|
||||||
// ChatsStackNavigator
|
// ChatsStackNavigator
|
||||||
const ChatsStack = createStackNavigator();
|
const ChatsStack = createStackNavigator();
|
||||||
|
@ -117,6 +120,11 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
|
||||||
component={RoomInfoView}
|
component={RoomInfoView}
|
||||||
options={RoomInfoView.navigationOptions}
|
options={RoomInfoView.navigationOptions}
|
||||||
/>
|
/>
|
||||||
|
<ModalStack.Screen
|
||||||
|
name='SelectListView'
|
||||||
|
component={SelectListView}
|
||||||
|
options={SelectListView.navigationOptions}
|
||||||
|
/>
|
||||||
<ModalStack.Screen
|
<ModalStack.Screen
|
||||||
name='RoomInfoEditView'
|
name='RoomInfoEditView'
|
||||||
component={RoomInfoEditView}
|
component={RoomInfoEditView}
|
||||||
|
@ -141,6 +149,16 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
|
||||||
component={InviteUsersView}
|
component={InviteUsersView}
|
||||||
options={InviteUsersView.navigationOptions}
|
options={InviteUsersView.navigationOptions}
|
||||||
/>
|
/>
|
||||||
|
<ModalStack.Screen
|
||||||
|
name='AddChannelTeamView'
|
||||||
|
component={AddChannelTeamView}
|
||||||
|
options={AddChannelTeamView.navigationOptions}
|
||||||
|
/>
|
||||||
|
<ModalStack.Screen
|
||||||
|
name='AddExistingChannelView'
|
||||||
|
component={AddExistingChannelView}
|
||||||
|
options={AddExistingChannelView.navigationOptions}
|
||||||
|
/>
|
||||||
<ModalStack.Screen
|
<ModalStack.Screen
|
||||||
name='InviteUsersEditView'
|
name='InviteUsersEditView'
|
||||||
component={InviteUsersEditView}
|
component={InviteUsersEditView}
|
||||||
|
|
|
@ -14,7 +14,6 @@ const navigate = ({ item, isMasterDetail, ...props }) => {
|
||||||
t: item.t,
|
t: item.t,
|
||||||
prid: item.prid,
|
prid: item.prid,
|
||||||
room: item,
|
room: item,
|
||||||
search: item.search,
|
|
||||||
visitor: item.visitor,
|
visitor: item.visitor,
|
||||||
roomUserId: RocketChat.getUidDirectMessage(item),
|
roomUserId: RocketChat.getUidDirectMessage(item),
|
||||||
...props
|
...props
|
||||||
|
|
|
@ -99,14 +99,22 @@ export default {
|
||||||
SELECTED_USERS_CREATE_GROUP: 'selected_users_create_group',
|
SELECTED_USERS_CREATE_GROUP: 'selected_users_create_group',
|
||||||
SELECTED_USERS_CREATE_GROUP_F: 'selected_users_create_group_f',
|
SELECTED_USERS_CREATE_GROUP_F: 'selected_users_create_group_f',
|
||||||
|
|
||||||
|
// ADD EXISTING CHANNEL VIEW
|
||||||
|
EXISTING_CHANNEL_ADD_CHANNEL: 'existing_channel_add_channel',
|
||||||
|
EXISTING_CHANNEL_REMOVE_CHANNEL: 'existing_channel_remove_channel',
|
||||||
|
|
||||||
// CREATE CHANNEL VIEW
|
// CREATE CHANNEL VIEW
|
||||||
CR_CREATE: 'cr_create',
|
CR_CREATE: 'cr_create',
|
||||||
|
CT_CREATE: 'ct_create',
|
||||||
CR_CREATE_F: 'cr_create_f',
|
CR_CREATE_F: 'cr_create_f',
|
||||||
|
CT_CREATE_F: 'ct_create_f',
|
||||||
CR_TOGGLE_TYPE: 'cr_toggle_type',
|
CR_TOGGLE_TYPE: 'cr_toggle_type',
|
||||||
CR_TOGGLE_READ_ONLY: 'cr_toggle_read_only',
|
CR_TOGGLE_READ_ONLY: 'cr_toggle_read_only',
|
||||||
CR_TOGGLE_BROADCAST: 'cr_toggle_broadcast',
|
CR_TOGGLE_BROADCAST: 'cr_toggle_broadcast',
|
||||||
CR_TOGGLE_ENCRYPTED: 'cr_toggle_encrypted',
|
CR_TOGGLE_ENCRYPTED: 'cr_toggle_encrypted',
|
||||||
CR_REMOVE_USER: 'cr_remove_user',
|
CR_REMOVE_USER: 'cr_remove_user',
|
||||||
|
CT_ADD_ROOM_TO_TEAM: 'ct_add_room_to_team',
|
||||||
|
CT_ADD_ROOM_TO_TEAM_F: 'ct_add_room_to_team_f',
|
||||||
|
|
||||||
// CREATE DISCUSSION VIEW
|
// CREATE DISCUSSION VIEW
|
||||||
CD_CREATE: 'cd_create',
|
CD_CREATE: 'cd_create',
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import * as List from '../containers/List';
|
||||||
|
import StatusBar from '../containers/StatusBar';
|
||||||
|
import { useTheme } from '../theme';
|
||||||
|
import * as HeaderButton from '../containers/HeaderButton';
|
||||||
|
import SafeAreaView from '../containers/SafeAreaView';
|
||||||
|
import I18n from '../i18n';
|
||||||
|
|
||||||
|
const setHeader = (navigation, isMasterDetail) => {
|
||||||
|
const options = {
|
||||||
|
headerTitle: I18n.t('Add_Channel_to_Team')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMasterDetail) {
|
||||||
|
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigation.setOptions(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddChannelTeamView = ({
|
||||||
|
navigation, route, isMasterDetail
|
||||||
|
}) => {
|
||||||
|
const { teamId, teamChannels } = route.params;
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeader(navigation, isMasterDetail);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView testID='add-channel-team-view'>
|
||||||
|
<StatusBar />
|
||||||
|
<List.Container>
|
||||||
|
<List.Separator />
|
||||||
|
<List.Item
|
||||||
|
title='Create_New'
|
||||||
|
onPress={() => (isMasterDetail
|
||||||
|
? navigation.navigate('SelectedUsersViewCreateChannel', { nextAction: () => navigation.navigate('CreateChannelView', { teamId }) })
|
||||||
|
: navigation.navigate('SelectedUsersView', { nextAction: () => navigation.navigate('ChatsStackNavigator', { screen: 'CreateChannelView', params: { teamId } }) }))
|
||||||
|
}
|
||||||
|
testID='add-channel-team-view-create-channel'
|
||||||
|
left={() => <List.Icon name='team' />}
|
||||||
|
right={() => <List.Icon name='chevron-right' />}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<List.Separator />
|
||||||
|
<List.Item
|
||||||
|
title='Add_Existing'
|
||||||
|
onPress={() => navigation.navigate('AddExistingChannelView', { teamId, teamChannels })}
|
||||||
|
testID='add-channel-team-view-create-channel'
|
||||||
|
left={() => <List.Icon name='channel-public' />}
|
||||||
|
right={() => <List.Icon name='chevron-right' />}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<List.Separator />
|
||||||
|
</List.Container>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddChannelTeamView.propTypes = {
|
||||||
|
route: PropTypes.object,
|
||||||
|
navigation: PropTypes.object,
|
||||||
|
isMasterDetail: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
isMasterDetail: state.app.isMasterDetail
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(AddChannelTeamView);
|
|
@ -0,0 +1,215 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
View, FlatList
|
||||||
|
} from 'react-native';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
|
import * as List from '../containers/List';
|
||||||
|
import database from '../lib/database';
|
||||||
|
import RocketChat from '../lib/rocketchat';
|
||||||
|
import I18n from '../i18n';
|
||||||
|
import log, { events, logEvent } from '../utils/log';
|
||||||
|
import SearchBox from '../containers/SearchBox';
|
||||||
|
import * as HeaderButton from '../containers/HeaderButton';
|
||||||
|
import StatusBar from '../containers/StatusBar';
|
||||||
|
import { themes } from '../constants/colors';
|
||||||
|
import { withTheme } from '../theme';
|
||||||
|
import SafeAreaView from '../containers/SafeAreaView';
|
||||||
|
import Loading from '../containers/Loading';
|
||||||
|
import { animateNextTransition } from '../utils/layoutAnimation';
|
||||||
|
import { goRoom } from '../utils/goRoom';
|
||||||
|
import { showErrorAlert } from '../utils/info';
|
||||||
|
import debounce from '../utils/debounce';
|
||||||
|
|
||||||
|
const QUERY_SIZE = 50;
|
||||||
|
|
||||||
|
class AddExistingChannelView extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
navigation: PropTypes.object,
|
||||||
|
route: PropTypes.object,
|
||||||
|
theme: PropTypes.string,
|
||||||
|
isMasterDetail: PropTypes.bool,
|
||||||
|
addTeamChannelPermission: PropTypes.array
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.query();
|
||||||
|
this.teamId = props.route?.params?.teamId;
|
||||||
|
this.state = {
|
||||||
|
search: [],
|
||||||
|
channels: [],
|
||||||
|
selected: [],
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
this.setHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeader = () => {
|
||||||
|
const { navigation, isMasterDetail } = this.props;
|
||||||
|
const { selected } = this.state;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headerTitle: I18n.t('Add_Existing_Channel')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMasterDetail) {
|
||||||
|
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.headerRight = () => selected.length > 0 && (
|
||||||
|
<HeaderButton.Container>
|
||||||
|
<HeaderButton.Item title={I18n.t('Create')} onPress={this.submit} testID='add-existing-channel-view-submit' />
|
||||||
|
</HeaderButton.Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
navigation.setOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
query = async(stringToSearch = '') => {
|
||||||
|
try {
|
||||||
|
const { addTeamChannelPermission } = this.props;
|
||||||
|
const db = database.active;
|
||||||
|
const channels = await db.collections
|
||||||
|
.get('subscriptions')
|
||||||
|
.query(
|
||||||
|
Q.where('team_id', ''),
|
||||||
|
Q.where('t', Q.oneOf(['c', 'p'])),
|
||||||
|
Q.where('name', Q.like(`%${ stringToSearch }%`)),
|
||||||
|
Q.experimentalTake(QUERY_SIZE),
|
||||||
|
Q.experimentalSortBy('room_updated_at', Q.desc)
|
||||||
|
)
|
||||||
|
.fetch();
|
||||||
|
|
||||||
|
const asyncFilter = async(channelsArray) => {
|
||||||
|
const results = await Promise.all(channelsArray.map(async(channel) => {
|
||||||
|
if (channel.prid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const permissions = await RocketChat.hasPermission([addTeamChannelPermission], channel.rid);
|
||||||
|
if (!permissions[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return channelsArray.filter((_v, index) => results[index]);
|
||||||
|
};
|
||||||
|
const channelFiltered = await asyncFilter(channels);
|
||||||
|
this.setState({ channels: channelFiltered });
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchChangeText = debounce((text) => {
|
||||||
|
this.query(text);
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
dismiss = () => {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
return navigation.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = async() => {
|
||||||
|
const { selected } = this.state;
|
||||||
|
const { isMasterDetail } = this.props;
|
||||||
|
|
||||||
|
this.setState({ loading: true });
|
||||||
|
try {
|
||||||
|
logEvent(events.CT_ADD_ROOM_TO_TEAM);
|
||||||
|
const result = await RocketChat.addRoomsToTeam({ rooms: selected, teamId: this.teamId });
|
||||||
|
if (result.success) {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
goRoom({ item: result, isMasterDetail });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(I18n.t(e.data.error), I18n.t('Add_Existing_Channel'), () => {});
|
||||||
|
logEvent(events.CT_ADD_ROOM_TO_TEAM_F);
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader = () => {
|
||||||
|
const { theme } = this.props;
|
||||||
|
return (
|
||||||
|
<View style={{ backgroundColor: themes[theme].auxiliaryBackground }}>
|
||||||
|
<SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='add-existing-channel-view-search' />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isChecked = (rid) => {
|
||||||
|
const { selected } = this.state;
|
||||||
|
return selected.includes(rid);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleChannel = (rid) => {
|
||||||
|
const { selected } = this.state;
|
||||||
|
|
||||||
|
animateNextTransition();
|
||||||
|
if (!this.isChecked(rid)) {
|
||||||
|
logEvent(events.EXISTING_CHANNEL_ADD_CHANNEL);
|
||||||
|
this.setState({ selected: [...selected, rid] }, () => this.setHeader());
|
||||||
|
} else {
|
||||||
|
logEvent(events.EXISTING_CHANNEL_REMOVE_CHANNEL);
|
||||||
|
const filterSelected = selected.filter(el => el !== rid);
|
||||||
|
this.setState({ selected: filterSelected }, () => this.setHeader());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItem = ({ item }) => {
|
||||||
|
const isChecked = this.isChecked(item.rid);
|
||||||
|
// TODO: reuse logic inside RoomTypeIcon
|
||||||
|
const icon = item.t === 'p' && !item.teamId ? 'channel-private' : 'channel-public';
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
title={RocketChat.getRoomTitle(item)}
|
||||||
|
translateTitle={false}
|
||||||
|
onPress={() => this.toggleChannel(item.rid)}
|
||||||
|
testID='add-existing-channel-view-item'
|
||||||
|
left={() => <List.Icon name={icon} />}
|
||||||
|
right={() => (isChecked ? <List.Icon name='check' /> : null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderList = () => {
|
||||||
|
const { search, channels } = this.state;
|
||||||
|
const { theme } = this.props;
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={search.length > 0 ? search : channels}
|
||||||
|
extraData={this.state}
|
||||||
|
keyExtractor={item => item._id}
|
||||||
|
ListHeaderComponent={this.renderHeader}
|
||||||
|
renderItem={this.renderItem}
|
||||||
|
ItemSeparatorComponent={List.Separator}
|
||||||
|
contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }}
|
||||||
|
keyboardShouldPersistTaps='always'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView testID='add-existing-channel-view'>
|
||||||
|
<StatusBar />
|
||||||
|
{this.renderList()}
|
||||||
|
<Loading visible={loading} />
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
isMasterDetail: state.app.isMasterDetail,
|
||||||
|
addTeamChannelPermission: state.permissions['add-team-channel']
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(withTheme(AddExistingChannelView));
|
|
@ -83,13 +83,15 @@ class CreateChannelView extends React.Component {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
token: PropTypes.string
|
token: PropTypes.string
|
||||||
}),
|
}),
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string,
|
||||||
|
teamId: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { route } = this.props;
|
const { route } = this.props;
|
||||||
const isTeam = route?.params?.isTeam || false;
|
const isTeam = route?.params?.isTeam || false;
|
||||||
|
this.teamId = route?.params?.teamId;
|
||||||
this.state = {
|
this.state = {
|
||||||
channelName: '',
|
channelName: '',
|
||||||
type: true,
|
type: true,
|
||||||
|
@ -180,7 +182,7 @@ class CreateChannelView extends React.Component {
|
||||||
|
|
||||||
// create channel or team
|
// create channel or team
|
||||||
create({
|
create({
|
||||||
name: channelName, users, type, readOnly, broadcast, encrypted, isTeam
|
name: channelName, users, type, readOnly, broadcast, encrypted, isTeam, teamId: this.teamId
|
||||||
});
|
});
|
||||||
|
|
||||||
Review.pushPositiveEvent();
|
Review.pushPositiveEvent();
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { withTheme } from '../../theme';
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import { withActionSheet } from '../../containers/ActionSheet';
|
import { withActionSheet } from '../../containers/ActionSheet';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
|
import getThreadName from '../../lib/methods/getThreadName';
|
||||||
|
|
||||||
class MessagesView extends React.Component {
|
class MessagesView extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -26,7 +27,8 @@ class MessagesView extends React.Component {
|
||||||
customEmojis: PropTypes.object,
|
customEmojis: PropTypes.object,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
showActionSheet: PropTypes.func,
|
showActionSheet: PropTypes.func,
|
||||||
useRealName: PropTypes.bool
|
useRealName: PropTypes.bool,
|
||||||
|
isMasterDetail: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -81,6 +83,32 @@ class MessagesView extends React.Component {
|
||||||
navigation.navigate('RoomInfoView', navParam);
|
navigation.navigate('RoomInfoView', navParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jumpToMessage = async({ item }) => {
|
||||||
|
const { navigation, isMasterDetail } = this.props;
|
||||||
|
let params = {
|
||||||
|
rid: this.rid,
|
||||||
|
jumpToMessageId: item._id,
|
||||||
|
t: this.t,
|
||||||
|
room: this.room
|
||||||
|
};
|
||||||
|
if (item.tmid) {
|
||||||
|
if (isMasterDetail) {
|
||||||
|
navigation.navigate('DrawerNavigator');
|
||||||
|
} else {
|
||||||
|
navigation.pop(2);
|
||||||
|
}
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
tmid: item.tmid,
|
||||||
|
name: await getThreadName(this.rid, item.tmid, item._id),
|
||||||
|
t: 'thread'
|
||||||
|
};
|
||||||
|
navigation.push('RoomView', params);
|
||||||
|
} else {
|
||||||
|
navigation.navigate('RoomView', params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defineMessagesViewContent = (name) => {
|
defineMessagesViewContent = (name) => {
|
||||||
const {
|
const {
|
||||||
user, baseUrl, theme, useRealName
|
user, baseUrl, theme, useRealName
|
||||||
|
@ -93,11 +121,13 @@ class MessagesView extends React.Component {
|
||||||
timeFormat: 'MMM Do YYYY, h:mm:ss a',
|
timeFormat: 'MMM Do YYYY, h:mm:ss a',
|
||||||
isEdited: !!item.editedAt,
|
isEdited: !!item.editedAt,
|
||||||
isHeader: true,
|
isHeader: true,
|
||||||
|
isThreadRoom: true,
|
||||||
attachments: item.attachments || [],
|
attachments: item.attachments || [],
|
||||||
useRealName,
|
useRealName,
|
||||||
showAttachment: this.showAttachment,
|
showAttachment: this.showAttachment,
|
||||||
getCustomEmoji: this.getCustomEmoji,
|
getCustomEmoji: this.getCustomEmoji,
|
||||||
navToRoomInfo: this.navToRoomInfo
|
navToRoomInfo: this.navToRoomInfo,
|
||||||
|
onPress: () => this.jumpToMessage({ item })
|
||||||
});
|
});
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
|
@ -315,7 +345,8 @@ const mapStateToProps = state => ({
|
||||||
baseUrl: state.server.server,
|
baseUrl: state.server.server,
|
||||||
user: getUserSelector(state),
|
user: getUserSelector(state),
|
||||||
customEmojis: state.customEmojis,
|
customEmojis: state.customEmojis,
|
||||||
useRealName: state.settings.UI_Use_Real_Name
|
useRealName: state.settings.UI_Use_Real_Name,
|
||||||
|
isMasterDetail: state.app.isMasterDetail
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withTheme(withActionSheet(MessagesView)));
|
export default connect(mapStateToProps)(withTheme(withActionSheet(MessagesView)));
|
||||||
|
|
|
@ -60,7 +60,7 @@ class NewMessageView extends React.Component {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
token: PropTypes.string
|
token: PropTypes.string
|
||||||
}),
|
}),
|
||||||
createChannel: PropTypes.func,
|
create: PropTypes.func,
|
||||||
maxUsers: PropTypes.number,
|
maxUsers: PropTypes.number,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
isMasterDetail: PropTypes.bool
|
isMasterDetail: PropTypes.bool
|
||||||
|
@ -124,9 +124,9 @@ class NewMessageView extends React.Component {
|
||||||
|
|
||||||
createGroupChat = () => {
|
createGroupChat = () => {
|
||||||
logEvent(events.NEW_MSG_CREATE_GROUP_CHAT);
|
logEvent(events.NEW_MSG_CREATE_GROUP_CHAT);
|
||||||
const { createChannel, maxUsers, navigation } = this.props;
|
const { create, maxUsers, navigation } = this.props;
|
||||||
navigation.navigate('SelectedUsersViewCreateChannel', {
|
navigation.navigate('SelectedUsersViewCreateChannel', {
|
||||||
nextAction: () => createChannel({ group: true }),
|
nextAction: () => create({ group: true }),
|
||||||
buttonText: I18n.t('Create'),
|
buttonText: I18n.t('Create'),
|
||||||
maxUsers
|
maxUsers
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,7 +30,7 @@ class ReadReceiptView extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
route: PropTypes.object,
|
route: PropTypes.object,
|
||||||
Message_TimeFormat: PropTypes.string,
|
Message_TimeAndDateFormat: PropTypes.string,
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,8 +94,8 @@ class ReadReceiptView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItem = ({ item }) => {
|
renderItem = ({ item }) => {
|
||||||
const { Message_TimeFormat, theme } = this.props;
|
const { theme, Message_TimeAndDateFormat } = this.props;
|
||||||
const time = moment(item.ts).format(Message_TimeFormat);
|
const time = moment(item.ts).format(Message_TimeAndDateFormat);
|
||||||
if (!item?.user?.username) {
|
if (!item?.user?.username) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -156,7 +156,7 @@ class ReadReceiptView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
Message_TimeFormat: state.settings.Message_TimeFormat
|
Message_TimeAndDateFormat: state.settings.Message_TimeAndDateFormat
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withTheme(ReadReceiptView));
|
export default connect(mapStateToProps)(withTheme(ReadReceiptView));
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
View, Text, Alert, Share, Switch
|
View, Text, Share, Switch
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
@ -53,6 +53,7 @@ class RoomActionsView extends React.Component {
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
fontScale: PropTypes.number,
|
fontScale: PropTypes.number,
|
||||||
serverVersion: PropTypes.string,
|
serverVersion: PropTypes.string,
|
||||||
|
isMasterDetail: PropTypes.bool,
|
||||||
addUserToJoinedRoomPermission: PropTypes.array,
|
addUserToJoinedRoomPermission: PropTypes.array,
|
||||||
addUserToAnyCRoomPermission: PropTypes.array,
|
addUserToAnyCRoomPermission: PropTypes.array,
|
||||||
addUserToAnyPRoomPermission: PropTypes.array,
|
addUserToAnyPRoomPermission: PropTypes.array,
|
||||||
|
@ -395,22 +396,73 @@ class RoomActionsView extends React.Component {
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
const { leaveRoom } = this.props;
|
const { leaveRoom } = this.props;
|
||||||
|
|
||||||
Alert.alert(
|
showConfirmationAlert({
|
||||||
I18n.t('Are_you_sure_question_mark'),
|
message: I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }),
|
||||||
I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }),
|
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
|
||||||
[
|
|
||||||
{
|
|
||||||
text: I18n.t('Cancel'),
|
|
||||||
style: 'cancel'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
|
|
||||||
style: 'destructive',
|
|
||||||
onPress: () => leaveRoom(room.rid, room.t)
|
onPress: () => leaveRoom(room.rid, room.t)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
]
|
|
||||||
|
handleLeaveTeam = async(selected) => {
|
||||||
|
try {
|
||||||
|
const { room } = this.state;
|
||||||
|
const { navigation, isMasterDetail } = this.props;
|
||||||
|
const result = await RocketChat.leaveTeam({ teamName: room.name, ...(selected && { rooms: selected }) });
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
if (isMasterDetail) {
|
||||||
|
navigation.navigate('DrawerNavigator');
|
||||||
|
} else {
|
||||||
|
navigation.navigate('RoomsListView');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
showErrorAlert(
|
||||||
|
e.data.error
|
||||||
|
? I18n.t(e.data.error)
|
||||||
|
: I18n.t('There_was_an_error_while_action', { action: I18n.t('leaving_team') }),
|
||||||
|
I18n.t('Cannot_leave')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveTeam = async() => {
|
||||||
|
const { room } = this.state;
|
||||||
|
const { navigation } = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId: room.u._id });
|
||||||
|
|
||||||
|
if (result.rooms?.length) {
|
||||||
|
const teamChannels = result.rooms.map(r => ({
|
||||||
|
rid: r._id,
|
||||||
|
name: r.name,
|
||||||
|
teamId: r.teamId,
|
||||||
|
alert: r.isLastOwner
|
||||||
|
}));
|
||||||
|
navigation.navigate('SelectListView', {
|
||||||
|
title: 'Leave_Team',
|
||||||
|
data: teamChannels,
|
||||||
|
infoText: 'Select_Team_Channels',
|
||||||
|
nextAction: data => this.handleLeaveTeam(data),
|
||||||
|
showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_leave'))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showConfirmationAlert({
|
||||||
|
message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }),
|
||||||
|
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
|
||||||
|
onPress: () => this.handleLeaveTeam()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showConfirmationAlert({
|
||||||
|
message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }),
|
||||||
|
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
|
||||||
|
onPress: () => this.handleLeaveTeam()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderRoomInfo = () => {
|
renderRoomInfo = () => {
|
||||||
const { room, member } = this.state;
|
const { room, member } = this.state;
|
||||||
|
@ -568,9 +620,9 @@ class RoomActionsView extends React.Component {
|
||||||
<List.Section>
|
<List.Section>
|
||||||
<List.Separator />
|
<List.Separator />
|
||||||
<List.Item
|
<List.Item
|
||||||
title='Leave_channel'
|
title='Leave'
|
||||||
onPress={() => this.onPressTouchable({
|
onPress={() => this.onPressTouchable({
|
||||||
event: this.leaveChannel
|
event: room.teamMain ? this.leaveTeam : this.leaveChannel
|
||||||
})}
|
})}
|
||||||
testID='room-actions-leave-channel'
|
testID='room-actions-leave-channel'
|
||||||
left={() => <List.Icon name='logout' color={themes[theme].dangerColor} />}
|
left={() => <List.Icon name='logout' color={themes[theme].dangerColor} />}
|
||||||
|
@ -588,7 +640,7 @@ class RoomActionsView extends React.Component {
|
||||||
room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue
|
room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
rid, t, encrypted
|
rid, t
|
||||||
} = room;
|
} = room;
|
||||||
const isGroupChat = RocketChat.isGroupChat(room);
|
const isGroupChat = RocketChat.isGroupChat(room);
|
||||||
|
|
||||||
|
@ -713,24 +765,6 @@ class RoomActionsView extends React.Component {
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
{['c', 'p', 'd'].includes(t)
|
|
||||||
? (
|
|
||||||
<>
|
|
||||||
<List.Item
|
|
||||||
title='Search'
|
|
||||||
onPress={() => this.onPressTouchable({
|
|
||||||
route: 'SearchMessagesView',
|
|
||||||
params: { rid, encrypted }
|
|
||||||
})}
|
|
||||||
testID='room-actions-search'
|
|
||||||
left={() => <List.Icon name='search' />}
|
|
||||||
showActionIndicator
|
|
||||||
/>
|
|
||||||
<List.Separator />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
|
|
||||||
{['c', 'p', 'd'].includes(t)
|
{['c', 'p', 'd'].includes(t)
|
||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
|
@ -880,6 +914,7 @@ const mapStateToProps = state => ({
|
||||||
jitsiEnabled: state.settings.Jitsi_Enabled || false,
|
jitsiEnabled: state.settings.Jitsi_Enabled || false,
|
||||||
encryptionEnabled: state.encryption.enabled,
|
encryptionEnabled: state.encryption.enabled,
|
||||||
serverVersion: state.server.version,
|
serverVersion: state.server.version,
|
||||||
|
isMasterDetail: state.app.isMasterDetail,
|
||||||
addUserToJoinedRoomPermission: state.permissions['add-user-to-joined-room'],
|
addUserToJoinedRoomPermission: state.permissions['add-user-to-joined-room'],
|
||||||
addUserToAnyCRoomPermission: state.permissions['add-user-to-any-c-room'],
|
addUserToAnyCRoomPermission: state.permissions['add-user-to-any-c-room'],
|
||||||
addUserToAnyPRoomPermission: state.permissions['add-user-to-any-p-room'],
|
addUserToAnyPRoomPermission: state.permissions['add-user-to-any-p-room'],
|
||||||
|
|
|
@ -8,15 +8,16 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
|
||||||
import ImagePicker from 'react-native-image-crop-picker';
|
import ImagePicker from 'react-native-image-crop-picker';
|
||||||
import { dequal } from 'dequal';
|
import { dequal } from 'dequal';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import { compareServerVersion, methods } from '../../lib/utils';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
|
import { compareServerVersion, methods } from '../../lib/utils';
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
import { deleteRoom as deleteRoomAction } from '../../actions/room';
|
import { deleteRoom as deleteRoomAction } from '../../actions/room';
|
||||||
import KeyboardView from '../../presentation/KeyboardView';
|
import KeyboardView from '../../presentation/KeyboardView';
|
||||||
import sharedStyles from '../Styles';
|
import sharedStyles from '../Styles';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||||
import { showErrorAlert } from '../../utils/info';
|
import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
|
||||||
import { LISTENER } from '../../containers/Toast';
|
import { LISTENER } from '../../containers/Toast';
|
||||||
import EventEmitter from '../../utils/events';
|
import EventEmitter from '../../utils/events';
|
||||||
import RocketChat from '../../lib/rocketchat';
|
import RocketChat from '../../lib/rocketchat';
|
||||||
|
@ -41,6 +42,7 @@ const PERMISSION_ARCHIVE = 'archive-room';
|
||||||
const PERMISSION_UNARCHIVE = 'unarchive-room';
|
const PERMISSION_UNARCHIVE = 'unarchive-room';
|
||||||
const PERMISSION_DELETE_C = 'delete-c';
|
const PERMISSION_DELETE_C = 'delete-c';
|
||||||
const PERMISSION_DELETE_P = 'delete-p';
|
const PERMISSION_DELETE_P = 'delete-p';
|
||||||
|
const PERMISSION_DELETE_TEAM = 'delete-team';
|
||||||
|
|
||||||
class RoomInfoEditView extends React.Component {
|
class RoomInfoEditView extends React.Component {
|
||||||
static navigationOptions = () => ({
|
static navigationOptions = () => ({
|
||||||
|
@ -48,6 +50,7 @@ class RoomInfoEditView extends React.Component {
|
||||||
})
|
})
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
navigation: PropTypes.object,
|
||||||
route: PropTypes.object,
|
route: PropTypes.object,
|
||||||
deleteRoom: PropTypes.func,
|
deleteRoom: PropTypes.func,
|
||||||
serverVersion: PropTypes.string,
|
serverVersion: PropTypes.string,
|
||||||
|
@ -58,7 +61,9 @@ class RoomInfoEditView extends React.Component {
|
||||||
archiveRoomPermission: PropTypes.array,
|
archiveRoomPermission: PropTypes.array,
|
||||||
unarchiveRoomPermission: PropTypes.array,
|
unarchiveRoomPermission: PropTypes.array,
|
||||||
deleteCPermission: PropTypes.array,
|
deleteCPermission: PropTypes.array,
|
||||||
deletePPermission: PropTypes.array
|
deletePPermission: PropTypes.array,
|
||||||
|
deleteTeamPermission: PropTypes.array,
|
||||||
|
isMasterDetail: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -100,7 +105,8 @@ class RoomInfoEditView extends React.Component {
|
||||||
archiveRoomPermission,
|
archiveRoomPermission,
|
||||||
unarchiveRoomPermission,
|
unarchiveRoomPermission,
|
||||||
deleteCPermission,
|
deleteCPermission,
|
||||||
deletePPermission
|
deletePPermission,
|
||||||
|
deleteTeamPermission
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const rid = route.params?.rid;
|
const rid = route.params?.rid;
|
||||||
if (!rid) {
|
if (!rid) {
|
||||||
|
@ -122,7 +128,8 @@ class RoomInfoEditView extends React.Component {
|
||||||
archiveRoomPermission,
|
archiveRoomPermission,
|
||||||
unarchiveRoomPermission,
|
unarchiveRoomPermission,
|
||||||
deleteCPermission,
|
deleteCPermission,
|
||||||
deletePPermission
|
deletePPermission,
|
||||||
|
...(this.room.teamMain ? [deleteTeamPermission] : [])
|
||||||
], rid);
|
], rid);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -132,7 +139,8 @@ class RoomInfoEditView extends React.Component {
|
||||||
[PERMISSION_ARCHIVE]: result[2],
|
[PERMISSION_ARCHIVE]: result[2],
|
||||||
[PERMISSION_UNARCHIVE]: result[3],
|
[PERMISSION_UNARCHIVE]: result[3],
|
||||||
[PERMISSION_DELETE_C]: result[4],
|
[PERMISSION_DELETE_C]: result[4],
|
||||||
[PERMISSION_DELETE_P]: result[5]
|
[PERMISSION_DELETE_P]: result[5],
|
||||||
|
...(this.room.teamMain && { [PERMISSION_DELETE_TEAM]: result[6] })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -284,6 +292,72 @@ class RoomInfoEditView extends React.Component {
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDeleteTeam = async(selected) => {
|
||||||
|
const { navigation, isMasterDetail } = this.props;
|
||||||
|
const { room } = this.state;
|
||||||
|
try {
|
||||||
|
const result = await RocketChat.deleteTeam({ teamId: room.teamId, ...(selected && { roomsToRemove: selected }) });
|
||||||
|
if (result.success) {
|
||||||
|
if (isMasterDetail) {
|
||||||
|
navigation.navigate('DrawerNavigator');
|
||||||
|
} else {
|
||||||
|
navigation.navigate('RoomsListView');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
showErrorAlert(
|
||||||
|
e.data.error
|
||||||
|
? I18n.t(e.data.error)
|
||||||
|
: I18n.t('There_was_an_error_while_action', { action: I18n.t('deleting_team') }),
|
||||||
|
I18n.t('Cannot_delete')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTeam = async() => {
|
||||||
|
const { room } = this.state;
|
||||||
|
const { navigation } = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = database.active;
|
||||||
|
const subCollection = db.get('subscriptions');
|
||||||
|
const teamChannels = await subCollection.query(
|
||||||
|
Q.where('team_id', room.teamId),
|
||||||
|
Q.where('team_main', null)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (teamChannels.length) {
|
||||||
|
navigation.navigate('SelectListView', {
|
||||||
|
title: 'Delete_Team',
|
||||||
|
data: teamChannels,
|
||||||
|
infoText: 'Select_channels_to_delete',
|
||||||
|
nextAction: (selected) => {
|
||||||
|
showConfirmationAlert({
|
||||||
|
message: I18n.t('You_are_deleting_the_team', { team: RocketChat.getRoomTitle(room) }),
|
||||||
|
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
|
||||||
|
onPress: () => this.handleDeleteTeam(selected)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showConfirmationAlert({
|
||||||
|
message: I18n.t('You_are_deleting_the_team', { team: RocketChat.getRoomTitle(room) }),
|
||||||
|
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
|
||||||
|
onPress: () => this.handleDeleteTeam()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
showErrorAlert(
|
||||||
|
e.data.error
|
||||||
|
? I18n.t(e.data.error)
|
||||||
|
: I18n.t('There_was_an_error_while_action', { action: I18n.t('deleting_team') }),
|
||||||
|
I18n.t('Cannot_delete')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delete = () => {
|
delete = () => {
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
const { deleteRoom } = this.props;
|
const { deleteRoom } = this.props;
|
||||||
|
@ -339,9 +413,16 @@ class RoomInfoEditView extends React.Component {
|
||||||
|
|
||||||
hasDeletePermission = () => {
|
hasDeletePermission = () => {
|
||||||
const { room, permissions } = this.state;
|
const { room, permissions } = this.state;
|
||||||
return (
|
|
||||||
room.t === 'p' ? permissions[PERMISSION_DELETE_P] : permissions[PERMISSION_DELETE_C]
|
if (room.teamMain) {
|
||||||
);
|
return permissions[PERMISSION_DELETE_TEAM];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room.t === 'p') {
|
||||||
|
return permissions[PERMISSION_DELETE_P];
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions[PERMISSION_DELETE_C];
|
||||||
}
|
}
|
||||||
|
|
||||||
hasArchivePermission = () => {
|
hasArchivePermission = () => {
|
||||||
|
@ -513,9 +594,9 @@ class RoomInfoEditView extends React.Component {
|
||||||
<SwitchContainer
|
<SwitchContainer
|
||||||
value={t}
|
value={t}
|
||||||
leftLabelPrimary={I18n.t('Public')}
|
leftLabelPrimary={I18n.t('Public')}
|
||||||
leftLabelSecondary={I18n.t('Everyone_can_access_this_channel')}
|
leftLabelSecondary={room.teamMain ? I18n.t('Everyone_can_access_this_team') : I18n.t('Everyone_can_access_this_channel')}
|
||||||
rightLabelPrimary={I18n.t('Private')}
|
rightLabelPrimary={I18n.t('Private')}
|
||||||
rightLabelSecondary={I18n.t('Just_invited_people_can_access_this_channel')}
|
rightLabelSecondary={room.teamMain ? I18n.t('Just_invited_people_can_access_this_team') : I18n.t('Just_invited_people_can_access_this_channel')}
|
||||||
onValueChange={this.toggleRoomType}
|
onValueChange={this.toggleRoomType}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
testID='room-info-edit-view-t'
|
testID='room-info-edit-view-t'
|
||||||
|
@ -523,7 +604,7 @@ class RoomInfoEditView extends React.Component {
|
||||||
<SwitchContainer
|
<SwitchContainer
|
||||||
value={ro}
|
value={ro}
|
||||||
leftLabelPrimary={I18n.t('Collaborative')}
|
leftLabelPrimary={I18n.t('Collaborative')}
|
||||||
leftLabelSecondary={I18n.t('All_users_in_the_channel_can_write_new_messages')}
|
leftLabelSecondary={room.teamMain ? I18n.t('All_users_in_the_team_can_write_new_messages') : I18n.t('All_users_in_the_channel_can_write_new_messages')}
|
||||||
rightLabelPrimary={I18n.t('Read_Only')}
|
rightLabelPrimary={I18n.t('Read_Only')}
|
||||||
rightLabelSecondary={I18n.t('Only_authorized_users_can_write_new_messages')}
|
rightLabelSecondary={I18n.t('Only_authorized_users_can_write_new_messages')}
|
||||||
onValueChange={this.toggleReadOnly}
|
onValueChange={this.toggleReadOnly}
|
||||||
|
@ -647,7 +728,7 @@ class RoomInfoEditView extends React.Component {
|
||||||
{ borderColor: dangerColor },
|
{ borderColor: dangerColor },
|
||||||
!this.hasDeletePermission() && sharedStyles.opacity5
|
!this.hasDeletePermission() && sharedStyles.opacity5
|
||||||
]}
|
]}
|
||||||
onPress={this.delete}
|
onPress={room.teamMain ? this.deleteTeam : this.delete}
|
||||||
disabled={!this.hasDeletePermission()}
|
disabled={!this.hasDeletePermission()}
|
||||||
testID='room-info-edit-view-delete'
|
testID='room-info-edit-view-delete'
|
||||||
>
|
>
|
||||||
|
@ -678,7 +759,9 @@ const mapStateToProps = state => ({
|
||||||
archiveRoomPermission: state.permissions[PERMISSION_ARCHIVE],
|
archiveRoomPermission: state.permissions[PERMISSION_ARCHIVE],
|
||||||
unarchiveRoomPermission: state.permissions[PERMISSION_UNARCHIVE],
|
unarchiveRoomPermission: state.permissions[PERMISSION_UNARCHIVE],
|
||||||
deleteCPermission: state.permissions[PERMISSION_DELETE_C],
|
deleteCPermission: state.permissions[PERMISSION_DELETE_C],
|
||||||
deletePPermission: state.permissions[PERMISSION_DELETE_P]
|
deletePPermission: state.permissions[PERMISSION_DELETE_P],
|
||||||
|
deleteTeamPermission: state.permissions[PERMISSION_DELETE_TEAM],
|
||||||
|
isMasterDetail: state.app.isMasterDetail
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -214,7 +214,7 @@ class RoomInfoView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissions = await RocketChat.hasPermission([editRoomPermission], room.rid);
|
const permissions = await RocketChat.hasPermission([editRoomPermission], room.rid);
|
||||||
if (permissions[0] && !room.prid) {
|
if (permissions[0]) {
|
||||||
this.setState({ showEdit: true }, () => this.setHeader());
|
this.setState({ showEdit: true }, () => this.setHeader());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,10 @@ import { withTheme } from '../../theme';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import { withActionSheet } from '../../containers/ActionSheet';
|
import { withActionSheet } from '../../containers/ActionSheet';
|
||||||
import { showConfirmationAlert } from '../../utils/info';
|
import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
import { goRoom } from '../../utils/goRoom';
|
import { goRoom } from '../../utils/goRoom';
|
||||||
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
|
|
||||||
|
@ -34,6 +35,9 @@ const PERMISSION_SET_LEADER = 'set-leader';
|
||||||
const PERMISSION_SET_OWNER = 'set-owner';
|
const PERMISSION_SET_OWNER = 'set-owner';
|
||||||
const PERMISSION_SET_MODERATOR = 'set-moderator';
|
const PERMISSION_SET_MODERATOR = 'set-moderator';
|
||||||
const PERMISSION_REMOVE_USER = 'remove-user';
|
const PERMISSION_REMOVE_USER = 'remove-user';
|
||||||
|
const PERMISSION_EDIT_TEAM_MEMBER = 'edit-team-member';
|
||||||
|
const PERMISION_VIEW_ALL_TEAMS = 'view-all-teams';
|
||||||
|
const PERMISSION_VIEW_ALL_TEAM_CHANNELS = 'view-all-team-channels';
|
||||||
|
|
||||||
class RoomMembersView extends React.Component {
|
class RoomMembersView extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -55,7 +59,10 @@ class RoomMembersView extends React.Component {
|
||||||
setLeaderPermission: PropTypes.array,
|
setLeaderPermission: PropTypes.array,
|
||||||
setOwnerPermission: PropTypes.array,
|
setOwnerPermission: PropTypes.array,
|
||||||
setModeratorPermission: PropTypes.array,
|
setModeratorPermission: PropTypes.array,
|
||||||
removeUserPermission: PropTypes.array
|
removeUserPermission: PropTypes.array,
|
||||||
|
editTeamMemberPermission: PropTypes.array,
|
||||||
|
viewAllTeamChannelsPermission: PropTypes.array,
|
||||||
|
viewAllTeamsPermission: PropTypes.array
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -94,10 +101,11 @@ class RoomMembersView extends React.Component {
|
||||||
|
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
const {
|
const {
|
||||||
muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission
|
muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission, editTeamMemberPermission, viewAllTeamChannelsPermission, viewAllTeamsPermission
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const result = await RocketChat.hasPermission([
|
const result = await RocketChat.hasPermission([
|
||||||
muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission
|
muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission, ...(room.teamMain ? [editTeamMemberPermission, viewAllTeamChannelsPermission, viewAllTeamsPermission] : [])
|
||||||
], room.rid);
|
], room.rid);
|
||||||
|
|
||||||
this.permissions = {
|
this.permissions = {
|
||||||
|
@ -105,7 +113,12 @@ class RoomMembersView extends React.Component {
|
||||||
[PERMISSION_SET_LEADER]: result[1],
|
[PERMISSION_SET_LEADER]: result[1],
|
||||||
[PERMISSION_SET_OWNER]: result[2],
|
[PERMISSION_SET_OWNER]: result[2],
|
||||||
[PERMISSION_SET_MODERATOR]: result[3],
|
[PERMISSION_SET_MODERATOR]: result[3],
|
||||||
[PERMISSION_REMOVE_USER]: result[4]
|
[PERMISSION_REMOVE_USER]: result[4],
|
||||||
|
...(room.teamMain ? {
|
||||||
|
[PERMISSION_EDIT_TEAM_MEMBER]: result[5],
|
||||||
|
[PERMISSION_VIEW_ALL_TEAM_CHANNELS]: result[6],
|
||||||
|
[PERMISION_VIEW_ALL_TEAMS]: result[7]
|
||||||
|
} : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasSinglePermission = Object.values(this.permissions).some(p => !!p);
|
const hasSinglePermission = Object.values(this.permissions).some(p => !!p);
|
||||||
|
@ -137,6 +150,7 @@ class RoomMembersView extends React.Component {
|
||||||
onSearchChangeText = protectedFunction((text) => {
|
onSearchChangeText = protectedFunction((text) => {
|
||||||
const { members } = this.state;
|
const { members } = this.state;
|
||||||
let membersFiltered = [];
|
let membersFiltered = [];
|
||||||
|
text = text.trim();
|
||||||
|
|
||||||
if (members && members.length > 0 && text) {
|
if (members && members.length > 0 && text) {
|
||||||
membersFiltered = members.filter(m => m.username.toLowerCase().match(text.toLowerCase()) || m.name.toLowerCase().match(text.toLowerCase()));
|
membersFiltered = members.filter(m => m.username.toLowerCase().match(text.toLowerCase()) || m.name.toLowerCase().match(text.toLowerCase()));
|
||||||
|
@ -163,9 +177,80 @@ class RoomMembersView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRemoveFromTeam = async(selectedUser) => {
|
||||||
|
try {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
const { room } = this.state;
|
||||||
|
|
||||||
|
const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId: selectedUser._id });
|
||||||
|
|
||||||
|
if (result.rooms?.length) {
|
||||||
|
const teamChannels = result.rooms.map(r => ({
|
||||||
|
rid: r._id,
|
||||||
|
name: r.name,
|
||||||
|
teamId: r.teamId,
|
||||||
|
alert: r.isLastOwner
|
||||||
|
}));
|
||||||
|
navigation.navigate('SelectListView', {
|
||||||
|
title: 'Remove_Member',
|
||||||
|
infoText: 'Remove_User_Team_Channels',
|
||||||
|
data: teamChannels,
|
||||||
|
nextAction: selected => this.removeFromTeam(selectedUser, selected),
|
||||||
|
showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_remove'))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showConfirmationAlert({
|
||||||
|
message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }),
|
||||||
|
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
|
||||||
|
onPress: () => this.removeFromTeam(selectedUser)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showConfirmationAlert({
|
||||||
|
message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }),
|
||||||
|
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
|
||||||
|
onPress: () => this.removeFromTeam(selectedUser)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromTeam = async(selectedUser, selected) => {
|
||||||
|
try {
|
||||||
|
const { members, membersFiltered, room } = this.state;
|
||||||
|
const { navigation } = this.props;
|
||||||
|
|
||||||
|
const userId = selectedUser._id;
|
||||||
|
const result = await RocketChat.removeTeamMember({
|
||||||
|
teamId: room.teamId,
|
||||||
|
teamName: room.name,
|
||||||
|
userId,
|
||||||
|
...(selected && { rooms: selected })
|
||||||
|
});
|
||||||
|
if (result.success) {
|
||||||
|
const message = I18n.t('User_has_been_removed_from_s', { s: RocketChat.getRoomTitle(room) });
|
||||||
|
EventEmitter.emit(LISTENER, { message });
|
||||||
|
const newMembers = members.filter(member => member._id !== userId);
|
||||||
|
const newMembersFiltered = membersFiltered.filter(member => member._id !== userId);
|
||||||
|
this.setState({
|
||||||
|
members: newMembers,
|
||||||
|
membersFiltered: newMembersFiltered
|
||||||
|
});
|
||||||
|
navigation.navigate('RoomMembersView');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
showErrorAlert(
|
||||||
|
e.data.error
|
||||||
|
? I18n.t(e.data.error)
|
||||||
|
: I18n.t('There_was_an_error_while_action', { action: I18n.t('removing_team') }),
|
||||||
|
I18n.t('Cannot_remove')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onPressUser = (selectedUser) => {
|
onPressUser = (selectedUser) => {
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
const { showActionSheet, user } = this.props;
|
const { showActionSheet, user, theme } = this.props;
|
||||||
|
|
||||||
const options = [{
|
const options = [{
|
||||||
icon: 'message',
|
icon: 'message',
|
||||||
|
@ -173,39 +258,6 @@ class RoomMembersView extends React.Component {
|
||||||
onPress: () => this.navToDirectMessage(selectedUser)
|
onPress: () => this.navToDirectMessage(selectedUser)
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Owner
|
|
||||||
if (this.permissions['set-owner']) {
|
|
||||||
const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
|
|
||||||
const isOwner = userRoleResult?.roles.includes('owner');
|
|
||||||
options.push({
|
|
||||||
icon: 'shield-check',
|
|
||||||
title: I18n.t(isOwner ? 'Remove_as_owner' : 'Set_as_owner'),
|
|
||||||
onPress: () => this.handleOwner(selectedUser, !isOwner)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leader
|
|
||||||
if (this.permissions['set-leader']) {
|
|
||||||
const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
|
|
||||||
const isLeader = userRoleResult?.roles.includes('leader');
|
|
||||||
options.push({
|
|
||||||
icon: 'shield-alt',
|
|
||||||
title: I18n.t(isLeader ? 'Remove_as_leader' : 'Set_as_leader'),
|
|
||||||
onPress: () => this.handleLeader(selectedUser, !isLeader)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moderator
|
|
||||||
if (this.permissions['set-moderator']) {
|
|
||||||
const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
|
|
||||||
const isModerator = userRoleResult?.roles.includes('moderator');
|
|
||||||
options.push({
|
|
||||||
icon: 'shield',
|
|
||||||
title: I18n.t(isModerator ? 'Remove_as_moderator' : 'Set_as_moderator'),
|
|
||||||
onPress: () => this.handleModerator(selectedUser, !isModerator)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore
|
// Ignore
|
||||||
if (selectedUser._id !== user.id) {
|
if (selectedUser._id !== user.id) {
|
||||||
const { ignored } = room;
|
const { ignored } = room;
|
||||||
|
@ -236,8 +288,54 @@ class RoomMembersView extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Owner
|
||||||
|
if (this.permissions['set-owner']) {
|
||||||
|
const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
|
||||||
|
const isOwner = userRoleResult?.roles.includes('owner');
|
||||||
|
options.push({
|
||||||
|
icon: 'shield-check',
|
||||||
|
title: I18n.t('Owner'),
|
||||||
|
onPress: () => this.handleOwner(selectedUser, !isOwner),
|
||||||
|
right: () => <CustomIcon name={isOwner ? 'checkbox-checked' : 'checkbox-unchecked'} size={20} color={isOwner ? themes[theme].tintActive : themes[theme].auxiliaryTintColor} />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leader
|
||||||
|
if (this.permissions['set-leader']) {
|
||||||
|
const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
|
||||||
|
const isLeader = userRoleResult?.roles.includes('leader');
|
||||||
|
options.push({
|
||||||
|
icon: 'shield-alt',
|
||||||
|
title: I18n.t('Leader'),
|
||||||
|
onPress: () => this.handleLeader(selectedUser, !isLeader),
|
||||||
|
right: () => <CustomIcon name={isLeader ? 'checkbox-checked' : 'checkbox-unchecked'} size={20} color={isLeader ? themes[theme].tintActive : themes[theme].auxiliaryTintColor} />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moderator
|
||||||
|
if (this.permissions['set-moderator']) {
|
||||||
|
const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
|
||||||
|
const isModerator = userRoleResult?.roles.includes('moderator');
|
||||||
|
options.push({
|
||||||
|
icon: 'shield',
|
||||||
|
title: I18n.t('Moderator'),
|
||||||
|
onPress: () => this.handleModerator(selectedUser, !isModerator),
|
||||||
|
right: () => <CustomIcon name={isModerator ? 'checkbox-checked' : 'checkbox-unchecked'} size={20} color={isModerator ? themes[theme].tintActive : themes[theme].auxiliaryTintColor} />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from team
|
||||||
|
if (this.permissions['edit-team-member']) {
|
||||||
|
options.push({
|
||||||
|
icon: 'logout',
|
||||||
|
danger: true,
|
||||||
|
title: I18n.t('Remove_from_Team'),
|
||||||
|
onPress: () => this.handleRemoveFromTeam(selectedUser)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Remove from room
|
// Remove from room
|
||||||
if (this.permissions['remove-user']) {
|
if (this.permissions['remove-user'] && !room.teamMain) {
|
||||||
options.push({
|
options.push({
|
||||||
icon: 'logout',
|
icon: 'logout',
|
||||||
title: I18n.t('Remove_from_room'),
|
title: I18n.t('Remove_from_room'),
|
||||||
|
@ -477,7 +575,10 @@ const mapStateToProps = state => ({
|
||||||
setLeaderPermission: state.permissions[PERMISSION_SET_LEADER],
|
setLeaderPermission: state.permissions[PERMISSION_SET_LEADER],
|
||||||
setOwnerPermission: state.permissions[PERMISSION_SET_OWNER],
|
setOwnerPermission: state.permissions[PERMISSION_SET_OWNER],
|
||||||
setModeratorPermission: state.permissions[PERMISSION_SET_MODERATOR],
|
setModeratorPermission: state.permissions[PERMISSION_SET_MODERATOR],
|
||||||
removeUserPermission: state.permissions[PERMISSION_REMOVE_USER]
|
removeUserPermission: state.permissions[PERMISSION_REMOVE_USER],
|
||||||
|
editTeamMemberPermission: state.permissions[PERMISSION_EDIT_TEAM_MEMBER],
|
||||||
|
viewAllTeamChannelsPermission: state.permissions[PERMISSION_VIEW_ALL_TEAM_CHANNELS],
|
||||||
|
viewAllTeamsPermission: state.permissions[PERMISION_VIEW_ALL_TEAMS]
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withActionSheet(withTheme(RoomMembersView)));
|
export default connect(mapStateToProps)(withActionSheet(withTheme(RoomMembersView)));
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FlatList, StyleSheet } from 'react-native';
|
||||||
|
import Animated from 'react-native-reanimated';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { isIOS } from '../../../utils/deviceInfo';
|
||||||
|
import scrollPersistTaps from '../../../utils/scrollPersistTaps';
|
||||||
|
|
||||||
|
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
list: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
paddingTop: 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const List = ({ listRef, ...props }) => (
|
||||||
|
<AnimatedFlatList
|
||||||
|
testID='room-view-messages'
|
||||||
|
ref={listRef}
|
||||||
|
keyExtractor={item => item.id}
|
||||||
|
contentContainerStyle={styles.contentContainer}
|
||||||
|
style={styles.list}
|
||||||
|
inverted
|
||||||
|
removeClippedSubviews={isIOS}
|
||||||
|
initialNumToRender={7}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
maxToRenderPerBatch={5}
|
||||||
|
windowSize={10}
|
||||||
|
{...props}
|
||||||
|
{...scrollPersistTaps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
List.propTypes = {
|
||||||
|
listRef: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default List;
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { View, StyleSheet } from 'react-native';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Animated, {
|
||||||
|
call, cond, greaterOrEq, useCode
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
|
||||||
|
import { themes } from '../../../constants/colors';
|
||||||
|
import { CustomIcon } from '../../../lib/Icons';
|
||||||
|
import { useTheme } from '../../../theme';
|
||||||
|
import Touch from '../../../utils/touch';
|
||||||
|
import { hasNotch } from '../../../utils/deviceInfo';
|
||||||
|
|
||||||
|
const SCROLL_LIMIT = 200;
|
||||||
|
const SEND_TO_CHANNEL_HEIGHT = 40;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 15
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
borderRadius: 25
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
borderRadius: 25,
|
||||||
|
borderWidth: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const NavBottomFAB = ({ y, onPress, isThread }) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const handleOnPress = useCallback(() => onPress());
|
||||||
|
const toggle = v => setShow(v);
|
||||||
|
|
||||||
|
useCode(() => cond(greaterOrEq(y, SCROLL_LIMIT),
|
||||||
|
call([y], () => toggle(true)),
|
||||||
|
call([y], () => toggle(false))),
|
||||||
|
[y]);
|
||||||
|
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bottom = hasNotch ? 100 : 60;
|
||||||
|
if (isThread) {
|
||||||
|
bottom += SEND_TO_CHANNEL_HEIGHT;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Animated.View style={[styles.container, { bottom }]}>
|
||||||
|
<Touch
|
||||||
|
onPress={handleOnPress}
|
||||||
|
theme={theme}
|
||||||
|
style={[styles.button, { backgroundColor: themes[theme].backgroundColor }]}
|
||||||
|
>
|
||||||
|
<View style={[styles.content, { borderColor: themes[theme].borderColor }]}>
|
||||||
|
<CustomIcon name='chevron-down' color={themes[theme].auxiliaryTintColor} size={36} />
|
||||||
|
</View>
|
||||||
|
</Touch>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NavBottomFAB.propTypes = {
|
||||||
|
y: Animated.Value,
|
||||||
|
onPress: PropTypes.func,
|
||||||
|
isThread: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavBottomFAB;
|
|
@ -1,30 +1,39 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FlatList, RefreshControl } from 'react-native';
|
import { RefreshControl } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { dequal } from 'dequal';
|
import { dequal } from 'dequal';
|
||||||
|
import { Value, event } from 'react-native-reanimated';
|
||||||
|
|
||||||
import styles from './styles';
|
import database from '../../../lib/database';
|
||||||
import database from '../../lib/database';
|
import RocketChat from '../../../lib/rocketchat';
|
||||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
import log from '../../../utils/log';
|
||||||
import RocketChat from '../../lib/rocketchat';
|
import EmptyRoom from '../EmptyRoom';
|
||||||
import log from '../../utils/log';
|
import { animateNextTransition } from '../../../utils/layoutAnimation';
|
||||||
import EmptyRoom from './EmptyRoom';
|
import ActivityIndicator from '../../../containers/ActivityIndicator';
|
||||||
import { isIOS } from '../../utils/deviceInfo';
|
import { themes } from '../../../constants/colors';
|
||||||
import { animateNextTransition } from '../../utils/layoutAnimation';
|
import List from './List';
|
||||||
import ActivityIndicator from '../../containers/ActivityIndicator';
|
import NavBottomFAB from './NavBottomFAB';
|
||||||
import { themes } from '../../constants/colors';
|
import debounce from '../../../utils/debounce';
|
||||||
|
|
||||||
const QUERY_SIZE = 50;
|
const QUERY_SIZE = 50;
|
||||||
|
|
||||||
class List extends React.Component {
|
const onScroll = ({ y }) => event(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
nativeEvent: {
|
||||||
|
contentOffset: { y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{ useNativeDriver: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
class ListContainer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onEndReached: PropTypes.func,
|
|
||||||
renderFooter: PropTypes.func,
|
|
||||||
renderRow: PropTypes.func,
|
renderRow: PropTypes.func,
|
||||||
rid: PropTypes.string,
|
rid: PropTypes.string,
|
||||||
t: PropTypes.string,
|
|
||||||
tmid: PropTypes.string,
|
tmid: PropTypes.string,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
|
@ -36,34 +45,28 @@ class List extends React.Component {
|
||||||
showMessageInMainThread: PropTypes.bool
|
showMessageInMainThread: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
// this.state.loading works for this.onEndReached and RoomView.init
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
|
||||||
if (props.loading !== state.loading) {
|
|
||||||
return {
|
|
||||||
loading: props.loading
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
console.time(`${ this.constructor.name } init`);
|
console.time(`${ this.constructor.name } init`);
|
||||||
console.time(`${ this.constructor.name } mount`);
|
console.time(`${ this.constructor.name } mount`);
|
||||||
this.count = 0;
|
this.count = 0;
|
||||||
this.needsFetch = false;
|
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
this.animated = false;
|
this.animated = false;
|
||||||
|
this.jumping = false;
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: true,
|
|
||||||
end: false,
|
|
||||||
messages: [],
|
messages: [],
|
||||||
refreshing: false
|
refreshing: false,
|
||||||
|
highlightedMessage: null
|
||||||
};
|
};
|
||||||
|
this.y = new Value(0);
|
||||||
|
this.onScroll = onScroll({ y: this.y });
|
||||||
this.query();
|
this.query();
|
||||||
this.unsubscribeFocus = props.navigation.addListener('focus', () => {
|
this.unsubscribeFocus = props.navigation.addListener('focus', () => {
|
||||||
this.animated = true;
|
this.animated = true;
|
||||||
});
|
});
|
||||||
|
this.viewabilityConfig = {
|
||||||
|
itemVisiblePercentThreshold: 10
|
||||||
|
};
|
||||||
console.timeEnd(`${ this.constructor.name } init`);
|
console.timeEnd(`${ this.constructor.name } init`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,17 +76,17 @@ class List extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const { loading, end, refreshing } = this.state;
|
const { refreshing, highlightedMessage } = this.state;
|
||||||
const {
|
const {
|
||||||
hideSystemMessages, theme, tunread, ignored
|
hideSystemMessages, theme, tunread, ignored, loading
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (theme !== nextProps.theme) {
|
if (theme !== nextProps.theme) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (loading !== nextState.loading) {
|
if (loading !== nextProps.loading) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (end !== nextState.end) {
|
if (highlightedMessage !== nextState.highlightedMessage) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (refreshing !== nextState.refreshing) {
|
if (refreshing !== nextState.refreshing) {
|
||||||
|
@ -116,32 +119,14 @@ class List extends React.Component {
|
||||||
if (this.unsubscribeFocus) {
|
if (this.unsubscribeFocus) {
|
||||||
this.unsubscribeFocus();
|
this.unsubscribeFocus();
|
||||||
}
|
}
|
||||||
|
this.clearHighlightedMessageTimeout();
|
||||||
console.countReset(`${ this.constructor.name }.render calls`);
|
console.countReset(`${ this.constructor.name }.render calls`);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchData = async() => {
|
clearHighlightedMessageTimeout = () => {
|
||||||
const {
|
if (this.highlightedMessageTimeout) {
|
||||||
loading, end, messages, latest = messages[messages.length - 1]?.ts
|
clearTimeout(this.highlightedMessageTimeout);
|
||||||
} = this.state;
|
this.highlightedMessageTimeout = false;
|
||||||
if (loading || end) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ loading: true });
|
|
||||||
const { rid, t, tmid } = this.props;
|
|
||||||
try {
|
|
||||||
let result;
|
|
||||||
if (tmid) {
|
|
||||||
// `offset` is `messages.length - 1` because we append thread start to `messages` obj
|
|
||||||
result = await RocketChat.loadThreadMessages({ tmid, rid, offset: messages.length - 1 });
|
|
||||||
} else {
|
|
||||||
result = await RocketChat.loadMessagesForRoom({ rid, t, latest });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ end: result.length < QUERY_SIZE, loading: false, latest: result[result.length - 1]?.ts }, () => this.loadMoreMessages(result));
|
|
||||||
} catch (e) {
|
|
||||||
this.setState({ loading: false });
|
|
||||||
log(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,9 +183,6 @@ class List extends React.Component {
|
||||||
this.unsubscribeMessages();
|
this.unsubscribeMessages();
|
||||||
this.messagesSubscription = this.messagesObservable
|
this.messagesSubscription = this.messagesObservable
|
||||||
.subscribe((messages) => {
|
.subscribe((messages) => {
|
||||||
if (messages.length <= this.count) {
|
|
||||||
this.needsFetch = true;
|
|
||||||
}
|
|
||||||
if (tmid && this.thread) {
|
if (tmid && this.thread) {
|
||||||
messages = [...messages, this.thread];
|
messages = [...messages, this.thread];
|
||||||
}
|
}
|
||||||
|
@ -211,6 +193,7 @@ class List extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
this.state.messages = messages;
|
this.state.messages = messages;
|
||||||
}
|
}
|
||||||
|
// TODO: move it away from here
|
||||||
this.readThreads();
|
this.readThreads();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -221,7 +204,7 @@ class List extends React.Component {
|
||||||
this.query();
|
this.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
readThreads = async() => {
|
readThreads = debounce(async() => {
|
||||||
const { tmid } = this.props;
|
const { tmid } = this.props;
|
||||||
|
|
||||||
if (tmid) {
|
if (tmid) {
|
||||||
|
@ -231,39 +214,9 @@ class List extends React.Component {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, 300)
|
||||||
|
|
||||||
onEndReached = async() => {
|
onEndReached = () => this.query()
|
||||||
if (this.needsFetch) {
|
|
||||||
this.needsFetch = false;
|
|
||||||
await this.fetchData();
|
|
||||||
}
|
|
||||||
this.query();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMoreMessages = (result) => {
|
|
||||||
const { end } = this.state;
|
|
||||||
|
|
||||||
if (end) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle servers with version < 3.0.0
|
|
||||||
let { hideSystemMessages = [] } = this.props;
|
|
||||||
if (!Array.isArray(hideSystemMessages)) {
|
|
||||||
hideSystemMessages = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hideSystemMessages.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasReadableMessages = result.filter(message => !message.t || (message.t && !hideSystemMessages.includes(message.t))).length > 0;
|
|
||||||
// if this batch doesn't contain any messages that will be displayed, we'll request a new batch
|
|
||||||
if (!hasReadableMessages) {
|
|
||||||
this.onEndReached();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onRefresh = () => this.setState({ refreshing: true }, async() => {
|
onRefresh = () => this.setState({ refreshing: true }, async() => {
|
||||||
const { messages } = this.state;
|
const { messages } = this.state;
|
||||||
|
@ -272,7 +225,7 @@ class List extends React.Component {
|
||||||
if (messages.length) {
|
if (messages.length) {
|
||||||
try {
|
try {
|
||||||
if (tmid) {
|
if (tmid) {
|
||||||
await RocketChat.loadThreadMessages({ tmid, rid, offset: messages.length - 1 });
|
await RocketChat.loadThreadMessages({ tmid, rid });
|
||||||
} else {
|
} else {
|
||||||
await RocketChat.loadMissedMessages({ rid, lastOpen: moment().subtract(7, 'days').toDate() });
|
await RocketChat.loadMissedMessages({ rid, lastOpen: moment().subtract(7, 'days').toDate() });
|
||||||
}
|
}
|
||||||
|
@ -284,7 +237,6 @@ class List extends React.Component {
|
||||||
this.setState({ refreshing: false });
|
this.setState({ refreshing: false });
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
|
||||||
update = () => {
|
update = () => {
|
||||||
if (this.animated) {
|
if (this.animated) {
|
||||||
animateNextTransition();
|
animateNextTransition();
|
||||||
|
@ -306,9 +258,53 @@ class List extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleScrollToIndexFailed = (params) => {
|
||||||
|
const { listRef } = this.props;
|
||||||
|
listRef.current.getNode().scrollToIndex({ index: params.highestMeasuredFrameIndex, animated: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpToMessage = messageId => new Promise(async(resolve) => {
|
||||||
|
this.jumping = true;
|
||||||
|
const { messages } = this.state;
|
||||||
|
const { listRef } = this.props;
|
||||||
|
const index = messages.findIndex(item => item.id === messageId);
|
||||||
|
if (index > -1) {
|
||||||
|
listRef.current.getNode().scrollToIndex({ index, viewPosition: 0.5 });
|
||||||
|
await new Promise(res => setTimeout(res, 300));
|
||||||
|
if (!this.viewableItems.map(vi => vi.key).includes(messageId)) {
|
||||||
|
if (!this.jumping) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ highlightedMessage: messageId });
|
||||||
|
this.clearHighlightedMessageTimeout();
|
||||||
|
this.highlightedMessageTimeout = setTimeout(() => {
|
||||||
|
this.setState({ highlightedMessage: null });
|
||||||
|
}, 10000);
|
||||||
|
await setTimeout(() => resolve(), 300);
|
||||||
|
} else {
|
||||||
|
listRef.current.getNode().scrollToIndex({ index: messages.length - 1, animated: false });
|
||||||
|
if (!this.jumping) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// this.jumping is checked in between operations to make sure we're not stuck
|
||||||
|
cancelJumpToMessage = () => {
|
||||||
|
this.jumping = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpToBottom = () => {
|
||||||
|
const { listRef } = this.props;
|
||||||
|
listRef.current.getNode().scrollToOffset({ offset: -100 });
|
||||||
|
}
|
||||||
|
|
||||||
renderFooter = () => {
|
renderFooter = () => {
|
||||||
const { loading } = this.state;
|
const { rid, theme, loading } = this.props;
|
||||||
const { rid, theme } = this.props;
|
|
||||||
if (loading && rid) {
|
if (loading && rid) {
|
||||||
return <ActivityIndicator theme={theme} />;
|
return <ActivityIndicator theme={theme} />;
|
||||||
}
|
}
|
||||||
|
@ -316,36 +312,34 @@ class List extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItem = ({ item, index }) => {
|
renderItem = ({ item, index }) => {
|
||||||
const { messages } = this.state;
|
const { messages, highlightedMessage } = this.state;
|
||||||
const { renderRow } = this.props;
|
const { renderRow } = this.props;
|
||||||
return renderRow(item, messages[index + 1]);
|
return renderRow(item, messages[index + 1], highlightedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewableItemsChanged = ({ viewableItems }) => {
|
||||||
|
this.viewableItems = viewableItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.count(`${ this.constructor.name }.render calls`);
|
console.count(`${ this.constructor.name }.render calls`);
|
||||||
const { rid, listRef } = this.props;
|
const { rid, tmid, listRef } = this.props;
|
||||||
const { messages, refreshing } = this.state;
|
const { messages, refreshing } = this.state;
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EmptyRoom rid={rid} length={messages.length} mounted={this.mounted} theme={theme} />
|
<EmptyRoom rid={rid} length={messages.length} mounted={this.mounted} theme={theme} />
|
||||||
<FlatList
|
<List
|
||||||
testID='room-view-messages'
|
onScroll={this.onScroll}
|
||||||
ref={listRef}
|
scrollEventThrottle={16}
|
||||||
keyExtractor={item => item.id}
|
listRef={listRef}
|
||||||
data={messages}
|
data={messages}
|
||||||
extraData={this.state}
|
|
||||||
renderItem={this.renderItem}
|
renderItem={this.renderItem}
|
||||||
contentContainerStyle={styles.contentContainer}
|
|
||||||
style={styles.list}
|
|
||||||
inverted
|
|
||||||
removeClippedSubviews={isIOS}
|
|
||||||
initialNumToRender={7}
|
|
||||||
onEndReached={this.onEndReached}
|
onEndReached={this.onEndReached}
|
||||||
onEndReachedThreshold={0.5}
|
|
||||||
maxToRenderPerBatch={5}
|
|
||||||
windowSize={10}
|
|
||||||
ListFooterComponent={this.renderFooter}
|
ListFooterComponent={this.renderFooter}
|
||||||
|
onScrollToIndexFailed={this.handleScrollToIndexFailed}
|
||||||
|
onViewableItemsChanged={this.onViewableItemsChanged}
|
||||||
|
viewabilityConfig={this.viewabilityConfig}
|
||||||
refreshControl={(
|
refreshControl={(
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
|
@ -353,11 +347,11 @@ class List extends React.Component {
|
||||||
tintColor={themes[theme].auxiliaryText}
|
tintColor={themes[theme].auxiliaryText}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{...scrollPersistTaps}
|
|
||||||
/>
|
/>
|
||||||
|
<NavBottomFAB y={this.y} onPress={this.jumpToBottom} isThread={!!tmid} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default List;
|
export default ListContainer;
|
|
@ -0,0 +1,62 @@
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, react/prop-types, react/destructuring-assignment */
|
||||||
|
import React from 'react';
|
||||||
|
import { ScrollView } from 'react-native';
|
||||||
|
import { storiesOf } from '@storybook/react-native';
|
||||||
|
|
||||||
|
import LoadMore from './index';
|
||||||
|
import { longText } from '../../../../storybook/utils';
|
||||||
|
import { ThemeContext } from '../../../theme';
|
||||||
|
import {
|
||||||
|
Message, StoryProvider, MessageDecorator
|
||||||
|
} from '../../../../storybook/stories/Message';
|
||||||
|
import { themes } from '../../../constants/colors';
|
||||||
|
import { MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad';
|
||||||
|
|
||||||
|
const stories = storiesOf('LoadMore', module);
|
||||||
|
|
||||||
|
// FIXME: for some reason, this promise never resolves on Storybook (it works on the app, so maybe the issue isn't on the component)
|
||||||
|
const load = () => new Promise(res => setTimeout(res, 1000));
|
||||||
|
|
||||||
|
stories.add('basic', () => (
|
||||||
|
<>
|
||||||
|
<LoadMore load={load} />
|
||||||
|
<LoadMore load={load} runOnRender />
|
||||||
|
<LoadMore load={load} type={MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK} />
|
||||||
|
<LoadMore load={load} type={MESSAGE_TYPE_LOAD_NEXT_CHUNK} />
|
||||||
|
</>
|
||||||
|
));
|
||||||
|
|
||||||
|
const ThemeStory = ({ theme }) => (
|
||||||
|
<ThemeContext.Provider
|
||||||
|
value={{ theme }}
|
||||||
|
>
|
||||||
|
<ScrollView style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||||
|
<LoadMore load={load} type={MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK} />
|
||||||
|
<Message msg='Hey!' theme={theme} />
|
||||||
|
<Message msg={longText} theme={theme} isHeader={false} />
|
||||||
|
<Message msg='Older message' theme={theme} isHeader={false} />
|
||||||
|
<LoadMore load={load} type={MESSAGE_TYPE_LOAD_NEXT_CHUNK} />
|
||||||
|
<LoadMore load={load} type={MESSAGE_TYPE_LOAD_MORE} />
|
||||||
|
<Message msg={longText} theme={theme} />
|
||||||
|
<Message msg='This is the third message' isHeader={false} theme={theme} />
|
||||||
|
<Message msg='This is the second message' isHeader={false} theme={theme} />
|
||||||
|
<Message msg='This is the first message' theme={theme} />
|
||||||
|
</ScrollView>
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
stories
|
||||||
|
.addDecorator(StoryProvider)
|
||||||
|
.addDecorator(MessageDecorator)
|
||||||
|
.add('light theme', () => <ThemeStory theme='light' />);
|
||||||
|
|
||||||
|
stories
|
||||||
|
.addDecorator(StoryProvider)
|
||||||
|
.addDecorator(MessageDecorator)
|
||||||
|
.add('dark theme', () => <ThemeStory theme='dark' />);
|
||||||
|
|
||||||
|
stories
|
||||||
|
.addDecorator(StoryProvider)
|
||||||
|
.addDecorator(MessageDecorator)
|
||||||
|
.add('black theme', () => <ThemeStory theme='black' />);
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { useEffect, useCallback, useState } from 'react';
|
||||||
|
import { Text, StyleSheet, ActivityIndicator } from 'react-native';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { themes } from '../../../constants/colors';
|
||||||
|
import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad';
|
||||||
|
import { useTheme } from '../../../theme';
|
||||||
|
import Touch from '../../../utils/touch';
|
||||||
|
import sharedStyles from '../../Styles';
|
||||||
|
import I18n from '../../../i18n';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
button: {
|
||||||
|
paddingVertical: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 16,
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const LoadMore = ({ load, type, runOnRender }) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleLoad = useCallback(async() => {
|
||||||
|
try {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
await load();
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [loading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (runOnRender) {
|
||||||
|
handleLoad();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let text = 'Load_More';
|
||||||
|
if (type === MESSAGE_TYPE_LOAD_NEXT_CHUNK) {
|
||||||
|
text = 'Load_Newer';
|
||||||
|
}
|
||||||
|
if (type === MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK) {
|
||||||
|
text = 'Load_Older';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Touch
|
||||||
|
onPress={handleLoad}
|
||||||
|
style={styles.button}
|
||||||
|
theme={theme}
|
||||||
|
enabled={!loading}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
loading
|
||||||
|
? <ActivityIndicator color={themes[theme].auxiliaryText} />
|
||||||
|
: <Text style={[styles.text, { color: themes[theme].titleText }]}>{I18n.t(text)}</Text>
|
||||||
|
}
|
||||||
|
</Touch>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LoadMore.propTypes = {
|
||||||
|
load: PropTypes.func,
|
||||||
|
type: PropTypes.string,
|
||||||
|
runOnRender: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadMore;
|
|
@ -142,12 +142,12 @@ class RightButtonsContainer extends Component {
|
||||||
goSearchView = () => {
|
goSearchView = () => {
|
||||||
logEvent(events.ROOM_GO_SEARCH);
|
logEvent(events.ROOM_GO_SEARCH);
|
||||||
const {
|
const {
|
||||||
rid, navigation, isMasterDetail
|
rid, t, navigation, isMasterDetail
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (isMasterDetail) {
|
if (isMasterDetail) {
|
||||||
navigation.navigate('ModalStackNavigator', { screen: 'SearchMessagesView', params: { rid, showCloseModal: true } });
|
navigation.navigate('ModalStackNavigator', { screen: 'SearchMessagesView', params: { rid, showCloseModal: true } });
|
||||||
} else {
|
} else {
|
||||||
navigation.navigate('SearchMessagesView', { rid });
|
navigation.navigate('SearchMessagesView', { rid, t });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Text, View, InteractionManager } from 'react-native';
|
import { Text, View, InteractionManager } from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import parse from 'url-parse';
|
||||||
|
|
||||||
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
@ -17,7 +17,6 @@ import {
|
||||||
import List from './List';
|
import List from './List';
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
import RocketChat from '../../lib/rocketchat';
|
import RocketChat from '../../lib/rocketchat';
|
||||||
import { Encryption } from '../../lib/encryption';
|
|
||||||
import Message from '../../containers/message';
|
import Message from '../../containers/message';
|
||||||
import MessageActions from '../../containers/MessageActions';
|
import MessageActions from '../../containers/MessageActions';
|
||||||
import MessageErrorActions from '../../containers/MessageErrorActions';
|
import MessageErrorActions from '../../containers/MessageErrorActions';
|
||||||
|
@ -35,6 +34,7 @@ import RightButtons from './RightButtons';
|
||||||
import StatusBar from '../../containers/StatusBar';
|
import StatusBar from '../../containers/StatusBar';
|
||||||
import Separator from './Separator';
|
import Separator from './Separator';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
import { MESSAGE_TYPE_ANY_LOAD, MESSAGE_TYPE_LOAD_MORE } from '../../constants/messageTypeLoad';
|
||||||
import debounce from '../../utils/debounce';
|
import debounce from '../../utils/debounce';
|
||||||
import ReactionsModal from '../../containers/ReactionsModal';
|
import ReactionsModal from '../../containers/ReactionsModal';
|
||||||
import { LISTENER } from '../../containers/Toast';
|
import { LISTENER } from '../../containers/Toast';
|
||||||
|
@ -64,6 +64,12 @@ import { getHeaderTitlePosition } from '../../containers/Header';
|
||||||
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
|
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
|
||||||
|
|
||||||
import { takeInquiry } from '../../ee/omnichannel/lib';
|
import { takeInquiry } from '../../ee/omnichannel/lib';
|
||||||
|
import Loading from '../../containers/Loading';
|
||||||
|
import LoadMore from './LoadMore';
|
||||||
|
import RoomServices from './services';
|
||||||
|
import { goRoom } from '../../utils/goRoom';
|
||||||
|
import getThreadName from '../../lib/methods/getThreadName';
|
||||||
|
import getRoomInfo from '../../lib/methods/getRoomInfo';
|
||||||
|
|
||||||
const stateAttrsUpdate = [
|
const stateAttrsUpdate = [
|
||||||
'joined',
|
'joined',
|
||||||
|
@ -76,7 +82,8 @@ const stateAttrsUpdate = [
|
||||||
'replying',
|
'replying',
|
||||||
'reacting',
|
'reacting',
|
||||||
'readOnly',
|
'readOnly',
|
||||||
'member'
|
'member',
|
||||||
|
'showingBlockingLoader'
|
||||||
];
|
];
|
||||||
const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor', 'joinCodeRequired'];
|
const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor', 'joinCodeRequired'];
|
||||||
|
|
||||||
|
@ -117,11 +124,11 @@ class RoomView extends React.Component {
|
||||||
const selectedMessage = props.route.params?.message;
|
const selectedMessage = props.route.params?.message;
|
||||||
const name = props.route.params?.name;
|
const name = props.route.params?.name;
|
||||||
const fname = props.route.params?.fname;
|
const fname = props.route.params?.fname;
|
||||||
const search = props.route.params?.search;
|
|
||||||
const prid = props.route.params?.prid;
|
const prid = props.route.params?.prid;
|
||||||
const room = props.route.params?.room ?? {
|
const room = props.route.params?.room ?? {
|
||||||
rid: this.rid, t: this.t, name, fname, prid
|
rid: this.rid, t: this.t, name, fname, prid
|
||||||
};
|
};
|
||||||
|
this.jumpToMessageId = props.route.params?.jumpToMessageId;
|
||||||
const roomUserId = props.route.params?.roomUserId ?? RocketChat.getUidDirectMessage(room);
|
const roomUserId = props.route.params?.roomUserId ?? RocketChat.getUidDirectMessage(room);
|
||||||
this.state = {
|
this.state = {
|
||||||
joined: true,
|
joined: true,
|
||||||
|
@ -133,6 +140,7 @@ class RoomView extends React.Component {
|
||||||
selectedMessage: selectedMessage || {},
|
selectedMessage: selectedMessage || {},
|
||||||
canAutoTranslate: false,
|
canAutoTranslate: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
showingBlockingLoader: false,
|
||||||
editing: false,
|
editing: false,
|
||||||
replying: !!selectedMessage,
|
replying: !!selectedMessage,
|
||||||
replyWithMention: false,
|
replyWithMention: false,
|
||||||
|
@ -151,13 +159,10 @@ class RoomView extends React.Component {
|
||||||
|
|
||||||
this.setReadOnly();
|
this.setReadOnly();
|
||||||
|
|
||||||
if (search) {
|
|
||||||
this.updateRoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.messagebox = React.createRef();
|
this.messagebox = React.createRef();
|
||||||
this.list = React.createRef();
|
this.list = React.createRef();
|
||||||
this.joinCode = React.createRef();
|
this.joinCode = React.createRef();
|
||||||
|
this.flatList = React.createRef();
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
|
|
||||||
// we don't need to subscribe to threads
|
// we don't need to subscribe to threads
|
||||||
|
@ -181,6 +186,9 @@ class RoomView extends React.Component {
|
||||||
EventEmitter.addEventListener('connected', this.handleConnected);
|
EventEmitter.addEventListener('connected', this.handleConnected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.jumpToMessageId) {
|
||||||
|
this.jumpToMessage(this.jumpToMessageId);
|
||||||
|
}
|
||||||
if (isIOS && this.rid) {
|
if (isIOS && this.rid) {
|
||||||
this.updateUnreadCount();
|
this.updateUnreadCount();
|
||||||
}
|
}
|
||||||
|
@ -195,7 +203,9 @@ class RoomView extends React.Component {
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const { state } = this;
|
const { state } = this;
|
||||||
const { roomUpdate, member } = state;
|
const { roomUpdate, member } = state;
|
||||||
const { appState, theme, insets } = this.props;
|
const {
|
||||||
|
appState, theme, insets, route
|
||||||
|
} = this.props;
|
||||||
if (theme !== nextProps.theme) {
|
if (theme !== nextProps.theme) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -212,12 +222,19 @@ class RoomView extends React.Component {
|
||||||
if (!dequal(nextProps.insets, insets)) {
|
if (!dequal(nextProps.insets, insets)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!dequal(nextProps.route?.params, route?.params)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return roomAttrsUpdate.some(key => !dequal(nextState.roomUpdate[key], roomUpdate[key]));
|
return roomAttrsUpdate.some(key => !dequal(nextState.roomUpdate[key], roomUpdate[key]));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const { roomUpdate } = this.state;
|
const { roomUpdate } = this.state;
|
||||||
const { appState, insets } = this.props;
|
const { appState, insets, route } = this.props;
|
||||||
|
|
||||||
|
if (route?.params?.jumpToMessageId !== prevProps.route?.params?.jumpToMessageId) {
|
||||||
|
this.jumpToMessage(route?.params?.jumpToMessageId);
|
||||||
|
}
|
||||||
|
|
||||||
if (appState === 'foreground' && appState !== prevProps.appState && this.rid) {
|
if (appState === 'foreground' && appState !== prevProps.appState && this.rid) {
|
||||||
// Fire List.query() just to keep observables working
|
// Fire List.query() just to keep observables working
|
||||||
|
@ -417,34 +434,15 @@ class RoomView extends React.Component {
|
||||||
this.setState({ readOnly });
|
this.setState({ readOnly });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRoom = async() => {
|
|
||||||
const db = database.active;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const subCollection = db.get('subscriptions');
|
|
||||||
const sub = await subCollection.find(this.rid);
|
|
||||||
|
|
||||||
const { room } = await RocketChat.getRoomInfo(this.rid);
|
|
||||||
|
|
||||||
await db.action(async() => {
|
|
||||||
await sub.update((s) => {
|
|
||||||
Object.assign(s, room);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init = async() => {
|
init = async() => {
|
||||||
try {
|
try {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
const { room, joined } = this.state;
|
const { room, joined } = this.state;
|
||||||
if (this.tmid) {
|
if (this.tmid) {
|
||||||
await this.getThreadMessages();
|
await RoomServices.getThreadMessages(this.tmid, this.rid);
|
||||||
} else {
|
} else {
|
||||||
const newLastOpen = new Date();
|
const newLastOpen = new Date();
|
||||||
await this.getMessages(room);
|
await RoomServices.getMessages(room);
|
||||||
|
|
||||||
// if room is joined
|
// if room is joined
|
||||||
if (joined) {
|
if (joined) {
|
||||||
|
@ -453,7 +451,7 @@ class RoomView extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
this.setLastOpen(null);
|
this.setLastOpen(null);
|
||||||
}
|
}
|
||||||
RocketChat.readMessages(room.rid, newLastOpen, true).catch(e => console.log(e));
|
RoomServices.readMessages(room.rid, newLastOpen, true).catch(e => console.log(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -660,26 +658,69 @@ class RoomView extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onThreadPress = debounce(async(item) => {
|
onThreadPress = debounce(item => this.navToThread(item), 1000, true)
|
||||||
const { roomUserId } = this.state;
|
|
||||||
const { navigation } = this.props;
|
shouldNavigateToRoom = (message) => {
|
||||||
if (item.tmid) {
|
if (message.tmid && message.tmid === this.tmid) {
|
||||||
if (!item.tmsg) {
|
return false;
|
||||||
await this.fetchThreadName(item.tmid, item.id);
|
|
||||||
}
|
}
|
||||||
let name = item.tmsg;
|
if (!message.tmid && message.rid === this.rid) {
|
||||||
if (item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) {
|
return false;
|
||||||
name = I18n.t('Encrypted_message');
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpToMessageByUrl = async(messageUrl) => {
|
||||||
|
if (!messageUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.setState({ showingBlockingLoader: true });
|
||||||
|
const parsedUrl = parse(messageUrl, true);
|
||||||
|
const messageId = parsedUrl.query.msg;
|
||||||
|
await this.jumpToMessage(messageId);
|
||||||
|
this.setState({ showingBlockingLoader: false });
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ showingBlockingLoader: false });
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpToMessage = async(messageId) => {
|
||||||
|
try {
|
||||||
|
this.setState({ showingBlockingLoader: true });
|
||||||
|
const message = await RoomServices.getMessageInfo(messageId);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.shouldNavigateToRoom(message)) {
|
||||||
|
if (message.rid !== this.rid) {
|
||||||
|
this.navToRoom(message);
|
||||||
|
} else {
|
||||||
|
this.navToThread(message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* if it's from server, we don't have it saved locally and so we fetch surroundings
|
||||||
|
* we test if it's not from threads because we're fetching from threads currently with `getThreadMessages`
|
||||||
|
*/
|
||||||
|
if (message.fromServer && !message.tmid) {
|
||||||
|
await RocketChat.loadSurroundingMessages({ messageId, rid: this.rid });
|
||||||
|
}
|
||||||
|
await Promise.race([
|
||||||
|
this.list.current.jumpToMessage(message.id),
|
||||||
|
new Promise(res => setTimeout(res, 5000))
|
||||||
|
]);
|
||||||
|
this.list.current.cancelJumpToMessage();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
} finally {
|
||||||
|
this.setState({ showingBlockingLoader: false });
|
||||||
}
|
}
|
||||||
navigation.push('RoomView', {
|
|
||||||
rid: item.subscription.id, tmid: item.tmid, name, t: 'thread', roomUserId
|
|
||||||
});
|
|
||||||
} else if (item.tlm) {
|
|
||||||
navigation.push('RoomView', {
|
|
||||||
rid: item.subscription.id, tmid: item.id, name: makeThreadName(item), t: 'thread', roomUserId
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, 1000, true)
|
|
||||||
|
|
||||||
replyBroadcast = (message) => {
|
replyBroadcast = (message) => {
|
||||||
const { replyBroadcast } = this.props;
|
const { replyBroadcast } = this.props;
|
||||||
|
@ -718,17 +759,6 @@ class RoomView extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getMessages = () => {
|
|
||||||
const { room } = this.state;
|
|
||||||
if (room.lastOpen) {
|
|
||||||
return RocketChat.loadMissedMessages(room);
|
|
||||||
} else {
|
|
||||||
return RocketChat.loadMessagesForRoom(room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getThreadMessages = () => RocketChat.loadThreadMessages({ tmid: this.tmid, rid: this.rid })
|
|
||||||
|
|
||||||
getCustomEmoji = (name) => {
|
getCustomEmoji = (name) => {
|
||||||
const { customEmojis } = this.props;
|
const { customEmojis } = this.props;
|
||||||
const emoji = customEmojis[name];
|
const emoji = customEmojis[name];
|
||||||
|
@ -767,45 +797,7 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
getThreadName = (tmid, messageId) => getThreadName(this.rid, tmid, messageId)
|
||||||
fetchThreadName = async(tmid, messageId) => {
|
|
||||||
try {
|
|
||||||
const db = database.active;
|
|
||||||
const threadCollection = db.get('threads');
|
|
||||||
const messageCollection = db.get('messages');
|
|
||||||
const messageRecord = await messageCollection.find(messageId);
|
|
||||||
let threadRecord;
|
|
||||||
try {
|
|
||||||
threadRecord = await threadCollection.find(tmid);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Thread not found. We have to search for it.');
|
|
||||||
}
|
|
||||||
if (threadRecord) {
|
|
||||||
await db.action(async() => {
|
|
||||||
await messageRecord.update((m) => {
|
|
||||||
m.tmsg = threadRecord.msg || (threadRecord.attachments && threadRecord.attachments.length && threadRecord.attachments[0].title);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
let { message: thread } = await RocketChat.getSingleMessage(tmid);
|
|
||||||
thread = await Encryption.decryptMessage(thread);
|
|
||||||
await db.action(async() => {
|
|
||||||
await db.batch(
|
|
||||||
threadCollection.prepareCreate((t) => {
|
|
||||||
t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema);
|
|
||||||
t.subscription.id = this.rid;
|
|
||||||
Object.assign(t, thread);
|
|
||||||
}),
|
|
||||||
messageRecord.prepareUpdate((m) => {
|
|
||||||
m.tmsg = thread.msg || (thread.attachments && thread.attachments.length && thread.attachments[0].title);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleFollowThread = async(isFollowingThread, tmid) => {
|
toggleFollowThread = async(isFollowingThread, tmid) => {
|
||||||
try {
|
try {
|
||||||
|
@ -836,6 +828,38 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navToThread = async(item) => {
|
||||||
|
const { roomUserId } = this.state;
|
||||||
|
const { navigation } = this.props;
|
||||||
|
|
||||||
|
if (item.tmid) {
|
||||||
|
let name = item.tmsg;
|
||||||
|
if (!name) {
|
||||||
|
name = await this.getThreadName(item.tmid, item.id);
|
||||||
|
}
|
||||||
|
if (item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) {
|
||||||
|
name = I18n.t('Encrypted_message');
|
||||||
|
}
|
||||||
|
return navigation.push('RoomView', {
|
||||||
|
rid: this.rid, tmid: item.tmid, name, t: 'thread', roomUserId, jumpToMessageId: item.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.tlm) {
|
||||||
|
return navigation.push('RoomView', {
|
||||||
|
rid: this.rid, tmid: item.id, name: makeThreadName(item), t: 'thread', roomUserId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navToRoom = async(message) => {
|
||||||
|
const { navigation, isMasterDetail } = this.props;
|
||||||
|
const roomInfo = await getRoomInfo(message.rid);
|
||||||
|
return goRoom({
|
||||||
|
item: roomInfo, isMasterDetail, navigationMethod: navigation.push, jumpToMessageId: message.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
callJitsi = () => {
|
callJitsi = () => {
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
const { jitsiTimeout } = room;
|
const { jitsiTimeout } = room;
|
||||||
|
@ -900,7 +924,11 @@ class RoomView extends React.Component {
|
||||||
return room?.ignored?.includes?.(message?.u?._id) ?? false;
|
return room?.ignored?.includes?.(message?.u?._id) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItem = (item, previousItem) => {
|
onLoadMoreMessages = loaderItem => RoomServices.getMoreMessages({
|
||||||
|
rid: this.rid, tmid: this.tmid, t: this.t, loaderItem
|
||||||
|
})
|
||||||
|
|
||||||
|
renderItem = (item, previousItem, highlightedMessage) => {
|
||||||
const { room, lastOpen, canAutoTranslate } = this.state;
|
const { room, lastOpen, canAutoTranslate } = this.state;
|
||||||
const {
|
const {
|
||||||
user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme
|
user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme
|
||||||
|
@ -920,7 +948,11 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = (
|
let content = null;
|
||||||
|
if (MESSAGE_TYPE_ANY_LOAD.includes(item.t)) {
|
||||||
|
content = <LoadMore load={() => this.onLoadMoreMessages(item)} type={item.t} runOnRender={item.t === MESSAGE_TYPE_LOAD_MORE && !previousItem} />;
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
<Message
|
<Message
|
||||||
item={item}
|
item={item}
|
||||||
user={user}
|
user={user}
|
||||||
|
@ -931,7 +963,7 @@ class RoomView extends React.Component {
|
||||||
isThreadRoom={!!this.tmid}
|
isThreadRoom={!!this.tmid}
|
||||||
isIgnored={this.isIgnored(item)}
|
isIgnored={this.isIgnored(item)}
|
||||||
previousItem={previousItem}
|
previousItem={previousItem}
|
||||||
fetchThreadName={this.fetchThreadName}
|
fetchThreadName={this.getThreadName}
|
||||||
onReactionPress={this.onReactionPress}
|
onReactionPress={this.onReactionPress}
|
||||||
onReactionLongPress={this.onReactionLongPress}
|
onReactionLongPress={this.onReactionLongPress}
|
||||||
onLongPress={this.onMessageLongPress}
|
onLongPress={this.onMessageLongPress}
|
||||||
|
@ -955,13 +987,16 @@ class RoomView extends React.Component {
|
||||||
blockAction={this.blockAction}
|
blockAction={this.blockAction}
|
||||||
threadBadgeColor={this.getBadgeColor(item?.id)}
|
threadBadgeColor={this.getBadgeColor(item?.id)}
|
||||||
toggleFollowThread={this.toggleFollowThread}
|
toggleFollowThread={this.toggleFollowThread}
|
||||||
|
jumpToMessage={this.jumpToMessageByUrl}
|
||||||
|
highlighted={highlightedMessage === item.id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (showUnreadSeparator || dateSeparator) {
|
if (showUnreadSeparator || dateSeparator) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{message}
|
{content}
|
||||||
<Separator
|
<Separator
|
||||||
ts={dateSeparator}
|
ts={dateSeparator}
|
||||||
unread={showUnreadSeparator}
|
unread={showUnreadSeparator}
|
||||||
|
@ -971,7 +1006,7 @@ class RoomView extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return message;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFooter = () => {
|
renderFooter = () => {
|
||||||
|
@ -1057,12 +1092,10 @@ class RoomView extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setListRef = ref => this.flatList = ref;
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.count(`${ this.constructor.name }.render calls`);
|
console.count(`${ this.constructor.name }.render calls`);
|
||||||
const {
|
const {
|
||||||
room, reactionsModalVisible, selectedMessage, loading, reacting
|
room, reactionsModalVisible, selectedMessage, loading, reacting, showingBlockingLoader
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
user, baseUrl, theme, navigation, Hide_System_Messages, width, height
|
user, baseUrl, theme, navigation, Hide_System_Messages, width, height
|
||||||
|
@ -1087,7 +1120,7 @@ class RoomView extends React.Component {
|
||||||
/>
|
/>
|
||||||
<List
|
<List
|
||||||
ref={this.list}
|
ref={this.list}
|
||||||
listRef={this.setListRef}
|
listRef={this.flatList}
|
||||||
rid={rid}
|
rid={rid}
|
||||||
t={t}
|
t={t}
|
||||||
tmid={this.tmid}
|
tmid={this.tmid}
|
||||||
|
@ -1127,6 +1160,7 @@ class RoomView extends React.Component {
|
||||||
t={t}
|
t={t}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
|
<Loading visible={showingBlockingLoader} />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { getMessageById } from '../../../lib/database/services/Message';
|
||||||
|
import { getThreadMessageById } from '../../../lib/database/services/ThreadMessage';
|
||||||
|
import getSingleMessage from '../../../lib/methods/getSingleMessage';
|
||||||
|
|
||||||
|
const getMessageInfo = async(messageId) => {
|
||||||
|
let result;
|
||||||
|
result = await getMessageById(messageId);
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
id: result.id,
|
||||||
|
rid: result.subscription.id,
|
||||||
|
tmid: result.tmid,
|
||||||
|
msg: result.msg
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await getThreadMessageById(messageId);
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
id: result.id,
|
||||||
|
rid: result.subscription.id,
|
||||||
|
tmid: result.rid,
|
||||||
|
msg: result.msg
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await getSingleMessage(messageId);
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
id: result._id,
|
||||||
|
rid: result.rid,
|
||||||
|
tmid: result.tmid,
|
||||||
|
msg: result.msg,
|
||||||
|
fromServer: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getMessageInfo;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import RocketChat from '../../../lib/rocketchat';
|
||||||
|
|
||||||
|
const getMessages = (room) => {
|
||||||
|
if (room.lastOpen) {
|
||||||
|
return RocketChat.loadMissedMessages(room);
|
||||||
|
} else {
|
||||||
|
return RocketChat.loadMessagesForRoom(room);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default getMessages;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad';
|
||||||
|
import RocketChat from '../../../lib/rocketchat';
|
||||||
|
|
||||||
|
const getMoreMessages = ({
|
||||||
|
rid, t, tmid, loaderItem
|
||||||
|
}) => {
|
||||||
|
if ([MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK].includes(loaderItem.t)) {
|
||||||
|
return RocketChat.loadMessagesForRoom({
|
||||||
|
rid, t, latest: loaderItem.ts, loaderItem
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loaderItem.t === MESSAGE_TYPE_LOAD_NEXT_CHUNK) {
|
||||||
|
return RocketChat.loadNextMessages({
|
||||||
|
rid, tmid, ts: loaderItem.ts, loaderItem
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default getMoreMessages;
|
|
@ -0,0 +1,6 @@
|
||||||
|
import RocketChat from '../../../lib/rocketchat';
|
||||||
|
|
||||||
|
// unlike getMessages, sync isn't required for threads, because loadMissedMessages does it already
|
||||||
|
const getThreadMessages = (tmid, rid) => RocketChat.loadThreadMessages({ tmid, rid });
|
||||||
|
|
||||||
|
export default getThreadMessages;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import getMessages from './getMessages';
|
||||||
|
import getMoreMessages from './getMoreMessages';
|
||||||
|
import getThreadMessages from './getThreadMessages';
|
||||||
|
import readMessages from './readMessages';
|
||||||
|
import getMessageInfo from './getMessageInfo';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getMessages,
|
||||||
|
getMoreMessages,
|
||||||
|
getThreadMessages,
|
||||||
|
readMessages,
|
||||||
|
getMessageInfo
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
import RocketChat from '../../../lib/rocketchat';
|
||||||
|
|
||||||
|
const readMessages = (rid, newLastOpen) => RocketChat.readMessages(rid, newLastOpen, true);
|
||||||
|
|
||||||
|
export default readMessages;
|
|
@ -9,12 +9,6 @@ export default StyleSheet.create({
|
||||||
safeAreaView: {
|
safeAreaView: {
|
||||||
flex: 1
|
flex: 1
|
||||||
},
|
},
|
||||||
list: {
|
|
||||||
flex: 1
|
|
||||||
},
|
|
||||||
contentContainer: {
|
|
||||||
paddingTop: 10
|
|
||||||
},
|
|
||||||
readOnly: {
|
readOnly: {
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
|
@ -23,6 +23,8 @@ import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
import * as HeaderButton from '../../containers/HeaderButton';
|
import * as HeaderButton from '../../containers/HeaderButton';
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
import { sanitizeLikeString } from '../../lib/database/utils';
|
import { sanitizeLikeString } from '../../lib/database/utils';
|
||||||
|
import getThreadName from '../../lib/methods/getThreadName';
|
||||||
|
import getRoomInfo from '../../lib/methods/getRoomInfo';
|
||||||
|
|
||||||
class SearchMessagesView extends React.Component {
|
class SearchMessagesView extends React.Component {
|
||||||
static navigationOptions = ({ navigation, route }) => {
|
static navigationOptions = ({ navigation, route }) => {
|
||||||
|
@ -54,9 +56,14 @@ class SearchMessagesView extends React.Component {
|
||||||
searchText: ''
|
searchText: ''
|
||||||
};
|
};
|
||||||
this.rid = props.route.params?.rid;
|
this.rid = props.route.params?.rid;
|
||||||
|
this.t = props.route.params?.t;
|
||||||
this.encrypted = props.route.params?.encrypted;
|
this.encrypted = props.route.params?.encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
this.room = await getRoomInfo(this.rid);
|
||||||
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const { loading, searchText, messages } = this.state;
|
const { loading, searchText, messages } = this.state;
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
|
@ -126,6 +133,11 @@ class SearchMessagesView extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showAttachment = (attachment) => {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
navigation.navigate('AttachmentView', { attachment });
|
||||||
|
}
|
||||||
|
|
||||||
navToRoomInfo = (navParam) => {
|
navToRoomInfo = (navParam) => {
|
||||||
const { navigation, user } = this.props;
|
const { navigation, user } = this.props;
|
||||||
if (navParam.rid === user.id) {
|
if (navParam.rid === user.id) {
|
||||||
|
@ -134,6 +146,28 @@ class SearchMessagesView extends React.Component {
|
||||||
navigation.navigate('RoomInfoView', navParam);
|
navigation.navigate('RoomInfoView', navParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jumpToMessage = async({ item }) => {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
let params = {
|
||||||
|
rid: this.rid,
|
||||||
|
jumpToMessageId: item._id,
|
||||||
|
t: this.t,
|
||||||
|
room: this.room
|
||||||
|
};
|
||||||
|
if (item.tmid) {
|
||||||
|
navigation.pop();
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
tmid: item.tmid,
|
||||||
|
name: await getThreadName(this.rid, item.tmid, item._id),
|
||||||
|
t: 'thread'
|
||||||
|
};
|
||||||
|
navigation.push('RoomView', params);
|
||||||
|
} else {
|
||||||
|
navigation.navigate('RoomView', params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderEmpty = () => {
|
renderEmpty = () => {
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
return (
|
return (
|
||||||
|
@ -152,13 +186,16 @@ class SearchMessagesView extends React.Component {
|
||||||
item={item}
|
item={item}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
user={user}
|
user={user}
|
||||||
timeFormat='LLL'
|
timeFormat='MMM Do YYYY, h:mm:ss a'
|
||||||
isHeader
|
isHeader
|
||||||
showAttachment={() => {}}
|
isThreadRoom
|
||||||
|
showAttachment={this.showAttachment}
|
||||||
getCustomEmoji={this.getCustomEmoji}
|
getCustomEmoji={this.getCustomEmoji}
|
||||||
navToRoomInfo={this.navToRoomInfo}
|
navToRoomInfo={this.navToRoomInfo}
|
||||||
useRealName={useRealName}
|
useRealName={useRealName}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
onPress={() => this.jumpToMessage({ item })}
|
||||||
|
jumpToMessage={() => this.jumpToMessage({ item })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
View, StyleSheet, FlatList, Text
|
||||||
|
} from 'react-native';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import * as List from '../containers/List';
|
||||||
|
import sharedStyles from './Styles';
|
||||||
|
import I18n from '../i18n';
|
||||||
|
import * as HeaderButton from '../containers/HeaderButton';
|
||||||
|
import StatusBar from '../containers/StatusBar';
|
||||||
|
import { themes } from '../constants/colors';
|
||||||
|
import { withTheme } from '../theme';
|
||||||
|
import SafeAreaView from '../containers/SafeAreaView';
|
||||||
|
import { animateNextTransition } from '../utils/layoutAnimation';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
buttonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
margin: 16,
|
||||||
|
...sharedStyles.textRegular
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class SelectListView extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
navigation: PropTypes.object,
|
||||||
|
route: PropTypes.object,
|
||||||
|
theme: PropTypes.string,
|
||||||
|
isMasterDetail: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const data = props.route?.params?.data;
|
||||||
|
this.title = props.route?.params?.title;
|
||||||
|
this.infoText = props.route?.params?.infoText;
|
||||||
|
this.nextAction = props.route?.params?.nextAction;
|
||||||
|
this.showAlert = props.route?.params?.showAlert;
|
||||||
|
this.state = {
|
||||||
|
data,
|
||||||
|
selected: []
|
||||||
|
};
|
||||||
|
this.setHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeader = () => {
|
||||||
|
const { navigation, isMasterDetail } = this.props;
|
||||||
|
const { selected } = this.state;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headerTitle: I18n.t(this.title)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMasterDetail) {
|
||||||
|
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.headerRight = () => (
|
||||||
|
<HeaderButton.Container>
|
||||||
|
<HeaderButton.Item title={I18n.t('Next')} onPress={() => this.nextAction(selected)} testID='select-list-view-submit' />
|
||||||
|
</HeaderButton.Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
navigation.setOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInfoText = () => {
|
||||||
|
const { theme } = this.props;
|
||||||
|
return (
|
||||||
|
<View style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||||
|
<Text style={[styles.buttonText, { color: themes[theme].bodyText }]}>{I18n.t(this.infoText)}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isChecked = (rid) => {
|
||||||
|
const { selected } = this.state;
|
||||||
|
return selected.includes(rid);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItem = (rid) => {
|
||||||
|
const { selected } = this.state;
|
||||||
|
|
||||||
|
animateNextTransition();
|
||||||
|
if (!this.isChecked(rid)) {
|
||||||
|
this.setState({ selected: [...selected, rid] }, () => this.setHeader());
|
||||||
|
} else {
|
||||||
|
const filterSelected = selected.filter(el => el !== rid);
|
||||||
|
this.setState({ selected: filterSelected }, () => this.setHeader());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItem = ({ item }) => {
|
||||||
|
const { theme } = this.props;
|
||||||
|
const icon = item.t === 'p' ? 'channel-private' : 'channel-public';
|
||||||
|
const checked = this.isChecked(item.rid) ? 'check' : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<List.Separator />
|
||||||
|
<List.Item
|
||||||
|
title={item.name}
|
||||||
|
translateTitle={false}
|
||||||
|
testID={`select-list-view-item-${ item.name }`}
|
||||||
|
onPress={() => (item.alert ? this.showAlert() : this.toggleItem(item.rid))}
|
||||||
|
alert={item.alert}
|
||||||
|
left={() => <List.Icon name={icon} color={themes[theme].controlText} />}
|
||||||
|
right={() => (checked ? <List.Icon name={checked} color={themes[theme].actionTintColor} /> : null)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { data } = this.state;
|
||||||
|
const { theme } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView testID='select-list-view'>
|
||||||
|
<StatusBar />
|
||||||
|
<FlatList
|
||||||
|
data={data}
|
||||||
|
extraData={this.state}
|
||||||
|
keyExtractor={item => item.rid}
|
||||||
|
renderItem={this.renderItem}
|
||||||
|
ListHeaderComponent={this.renderInfoText}
|
||||||
|
contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }}
|
||||||
|
keyboardShouldPersistTaps='always'
|
||||||
|
/>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
isMasterDetail: state.app.isMasterDetail
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(withTheme(SelectListView));
|
|
@ -1,11 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Keyboard } from 'react-native';
|
import { Keyboard, Alert } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
import { withSafeAreaInsets } from 'react-native-safe-area-context';
|
import { withSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { FlatList } from 'react-native-gesture-handler';
|
import { FlatList } from 'react-native-gesture-handler';
|
||||||
import { HeaderBackButton } from '@react-navigation/stack';
|
|
||||||
|
|
||||||
import StatusBar from '../containers/StatusBar';
|
import StatusBar from '../containers/StatusBar';
|
||||||
import RoomHeader from '../containers/RoomHeader';
|
import RoomHeader from '../containers/RoomHeader';
|
||||||
|
@ -23,13 +22,22 @@ import RoomItem, { ROW_HEIGHT } from '../presentation/RoomItem';
|
||||||
import RocketChat from '../lib/rocketchat';
|
import RocketChat from '../lib/rocketchat';
|
||||||
import { withDimensions } from '../dimensions';
|
import { withDimensions } from '../dimensions';
|
||||||
import { isIOS } from '../utils/deviceInfo';
|
import { isIOS } from '../utils/deviceInfo';
|
||||||
import { themes } from '../constants/colors';
|
|
||||||
import debounce from '../utils/debounce';
|
import debounce from '../utils/debounce';
|
||||||
import { showErrorAlert } from '../utils/info';
|
import { showErrorAlert } from '../utils/info';
|
||||||
import { goRoom } from '../utils/goRoom';
|
import { goRoom } from '../utils/goRoom';
|
||||||
import I18n from '../i18n';
|
import I18n from '../i18n';
|
||||||
|
import { withActionSheet } from '../containers/ActionSheet';
|
||||||
|
import { deleteRoom as deleteRoomAction } from '../actions/room';
|
||||||
|
import { CustomIcon } from '../lib/Icons';
|
||||||
|
import { themes } from '../constants/colors';
|
||||||
|
|
||||||
const API_FETCH_COUNT = 25;
|
const API_FETCH_COUNT = 25;
|
||||||
|
const PERMISSION_DELETE_C = 'delete-c';
|
||||||
|
const PERMISSION_DELETE_P = 'delete-p';
|
||||||
|
const PERMISSION_EDIT_TEAM_CHANNEL = 'edit-team-channel';
|
||||||
|
const PERMISSION_REMOVE_TEAM_CHANNEL = 'remove-team-channel';
|
||||||
|
const PERMISSION_ADD_TEAM_CHANNEL = 'add-team-channel';
|
||||||
|
|
||||||
|
|
||||||
const getItemLayout = (data, index) => ({
|
const getItemLayout = (data, index) => ({
|
||||||
length: data.length,
|
length: data.length,
|
||||||
|
@ -47,7 +55,14 @@ class TeamChannelsView extends React.Component {
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
useRealName: PropTypes.bool,
|
useRealName: PropTypes.bool,
|
||||||
width: PropTypes.number,
|
width: PropTypes.number,
|
||||||
StoreLastMessage: PropTypes.bool
|
StoreLastMessage: PropTypes.bool,
|
||||||
|
addTeamChannelPermission: PropTypes.array,
|
||||||
|
editTeamChannelPermission: PropTypes.array,
|
||||||
|
removeTeamChannelPermission: PropTypes.array,
|
||||||
|
deleteCPermission: PropTypes.array,
|
||||||
|
deletePPermission: PropTypes.array,
|
||||||
|
showActionSheet: PropTypes.func,
|
||||||
|
deleteRoom: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -60,9 +75,11 @@ class TeamChannelsView extends React.Component {
|
||||||
isSearching: false,
|
isSearching: false,
|
||||||
searchText: '',
|
searchText: '',
|
||||||
search: [],
|
search: [],
|
||||||
end: false
|
end: false,
|
||||||
|
showCreate: false
|
||||||
};
|
};
|
||||||
this.loadTeam();
|
this.loadTeam();
|
||||||
|
this.setHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -70,6 +87,9 @@ class TeamChannelsView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTeam = async() => {
|
loadTeam = async() => {
|
||||||
|
const { addTeamChannelPermission } = this.props;
|
||||||
|
const { loading, data } = this.state;
|
||||||
|
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
try {
|
try {
|
||||||
const subCollection = db.get('subscriptions');
|
const subCollection = db.get('subscriptions');
|
||||||
|
@ -82,6 +102,15 @@ class TeamChannelsView extends React.Component {
|
||||||
if (!this.team) {
|
if (!this.team) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const permissions = await RocketChat.hasPermission([addTeamChannelPermission], this.team.rid);
|
||||||
|
if (permissions[0]) {
|
||||||
|
this.setState({ showCreate: true }, () => this.setHeader());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && data.length) {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
navigation.pop();
|
navigation.pop();
|
||||||
|
@ -115,14 +144,11 @@ class TeamChannelsView extends React.Component {
|
||||||
loadingMore: false,
|
loadingMore: false,
|
||||||
end: result.rooms.length < API_FETCH_COUNT
|
end: result.rooms.length < API_FETCH_COUNT
|
||||||
};
|
};
|
||||||
const rooms = result.rooms.map((room) => {
|
|
||||||
const record = this.teamChannels?.find(c => c.rid === room._id);
|
|
||||||
return record ?? room;
|
|
||||||
});
|
|
||||||
if (isSearching) {
|
if (isSearching) {
|
||||||
newState.search = [...search, ...rooms];
|
newState.search = [...search, ...result.rooms];
|
||||||
} else {
|
} else {
|
||||||
newState.data = [...data, ...rooms];
|
newState.data = [...data, ...result.rooms];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
|
@ -135,18 +161,16 @@ class TeamChannelsView extends React.Component {
|
||||||
}
|
}
|
||||||
}, 300)
|
}, 300)
|
||||||
|
|
||||||
getHeader = () => {
|
setHeader = () => {
|
||||||
const { isSearching } = this.state;
|
const { isSearching, showCreate, data } = this.state;
|
||||||
const {
|
const { navigation, isMasterDetail, insets } = this.props;
|
||||||
navigation, isMasterDetail, insets, theme
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { team } = this;
|
const { team } = this;
|
||||||
if (!team) {
|
if (!team) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 1 });
|
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 2 });
|
||||||
|
|
||||||
if (isSearching) {
|
if (isSearching) {
|
||||||
return {
|
return {
|
||||||
|
@ -188,27 +212,16 @@ class TeamChannelsView extends React.Component {
|
||||||
|
|
||||||
if (isMasterDetail) {
|
if (isMasterDetail) {
|
||||||
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
|
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
|
||||||
} else {
|
|
||||||
options.headerLeft = () => (
|
|
||||||
<HeaderBackButton
|
|
||||||
labelVisible={false}
|
|
||||||
onPress={() => navigation.pop()}
|
|
||||||
tintColor={themes[theme].headerTintColor}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
options.headerRight = () => (
|
options.headerRight = () => (
|
||||||
<HeaderButton.Container>
|
<HeaderButton.Container>
|
||||||
|
{ showCreate
|
||||||
|
? <HeaderButton.Item iconName='create' onPress={() => navigation.navigate('AddChannelTeamView', { teamId: this.teamId, teamChannels: data })} />
|
||||||
|
: null}
|
||||||
<HeaderButton.Item iconName='search' onPress={this.onSearchPress} />
|
<HeaderButton.Item iconName='search' onPress={this.onSearchPress} />
|
||||||
</HeaderButton.Container>
|
</HeaderButton.Container>
|
||||||
);
|
);
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
setHeader = () => {
|
|
||||||
const { navigation } = this.props;
|
|
||||||
const options = this.getHeader();
|
|
||||||
navigation.setOptions(options);
|
navigation.setOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,6 +300,124 @@ class TeamChannelsView extends React.Component {
|
||||||
}
|
}
|
||||||
}, 1000, true);
|
}, 1000, true);
|
||||||
|
|
||||||
|
toggleAutoJoin = async(item) => {
|
||||||
|
try {
|
||||||
|
const { data } = this.state;
|
||||||
|
const result = await RocketChat.updateTeamRoom({ roomId: item._id, isDefault: !item.teamDefault });
|
||||||
|
if (result.success) {
|
||||||
|
const newData = data.map((i) => {
|
||||||
|
if (i._id === item._id) {
|
||||||
|
i.teamDefault = !i.teamDefault;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
});
|
||||||
|
this.setState({ data: newData });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove = (item) => {
|
||||||
|
Alert.alert(
|
||||||
|
I18n.t('Confirmation'),
|
||||||
|
I18n.t('Remove_Team_Room_Warning'),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: I18n.t('Cancel'),
|
||||||
|
style: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: () => this.removeRoom(item)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{ cancelable: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRoom = async(item) => {
|
||||||
|
try {
|
||||||
|
const { data } = this.state;
|
||||||
|
const result = await RocketChat.removeTeamRoom({ roomId: item._id, teamId: this.team.teamId });
|
||||||
|
if (result.success) {
|
||||||
|
const newData = data.filter(room => result.room._id !== room._id);
|
||||||
|
this.setState({ data: newData });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = (item) => {
|
||||||
|
const { deleteRoom } = this.props;
|
||||||
|
|
||||||
|
Alert.alert(
|
||||||
|
I18n.t('Are_you_sure_question_mark'),
|
||||||
|
I18n.t('Delete_Room_Warning'),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: I18n.t('Cancel'),
|
||||||
|
style: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: () => deleteRoom(item._id, item.t)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{ cancelable: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showChannelActions = async(item) => {
|
||||||
|
logEvent(events.ROOM_SHOW_BOX_ACTIONS);
|
||||||
|
const {
|
||||||
|
showActionSheet, editTeamChannelPermission, deleteCPermission, deletePPermission, theme, removeTeamChannelPermission
|
||||||
|
} = this.props;
|
||||||
|
const isAutoJoinChecked = item.teamDefault;
|
||||||
|
const autoJoinIcon = isAutoJoinChecked ? 'checkbox-checked' : 'checkbox-unchecked';
|
||||||
|
const autoJoinIconColor = isAutoJoinChecked ? themes[theme].tintActive : themes[theme].auxiliaryTintColor;
|
||||||
|
|
||||||
|
const options = [];
|
||||||
|
|
||||||
|
const permissionsTeam = await RocketChat.hasPermission([editTeamChannelPermission], this.team.rid);
|
||||||
|
if (permissionsTeam[0]) {
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Auto-join'),
|
||||||
|
icon: item.t === 'p' ? 'channel-private' : 'channel-public',
|
||||||
|
onPress: () => this.toggleAutoJoin(item),
|
||||||
|
right: () => <CustomIcon name={autoJoinIcon} size={20} color={autoJoinIconColor} />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissionsRemoveTeam = await RocketChat.hasPermission([removeTeamChannelPermission], this.team.rid);
|
||||||
|
if (permissionsRemoveTeam[0]) {
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Remove_from_Team'),
|
||||||
|
icon: 'close',
|
||||||
|
danger: true,
|
||||||
|
onPress: () => this.remove(item)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissionsChannel = await RocketChat.hasPermission([item.t === 'c' ? deleteCPermission : deletePPermission], item._id);
|
||||||
|
if (permissionsChannel[0]) {
|
||||||
|
options.push({
|
||||||
|
title: I18n.t('Delete'),
|
||||||
|
icon: 'delete',
|
||||||
|
danger: true,
|
||||||
|
onPress: () => this.delete(item)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showActionSheet({ options });
|
||||||
|
}
|
||||||
|
|
||||||
renderItem = ({ item }) => {
|
renderItem = ({ item }) => {
|
||||||
const {
|
const {
|
||||||
StoreLastMessage,
|
StoreLastMessage,
|
||||||
|
@ -302,10 +433,12 @@ class TeamChannelsView extends React.Component {
|
||||||
showLastMessage={StoreLastMessage}
|
showLastMessage={StoreLastMessage}
|
||||||
onPress={this.onPressItem}
|
onPress={this.onPressItem}
|
||||||
width={width}
|
width={width}
|
||||||
|
onLongPress={this.showChannelActions}
|
||||||
useRealName={useRealName}
|
useRealName={useRealName}
|
||||||
getRoomTitle={this.getRoomTitle}
|
getRoomTitle={this.getRoomTitle}
|
||||||
getRoomAvatar={this.getRoomAvatar}
|
getRoomAvatar={this.getRoomAvatar}
|
||||||
swipeEnabled={false}
|
swipeEnabled={false}
|
||||||
|
autoJoin={item.teamDefault}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -365,7 +498,16 @@ const mapStateToProps = state => ({
|
||||||
user: getUserSelector(state),
|
user: getUserSelector(state),
|
||||||
useRealName: state.settings.UI_Use_Real_Name,
|
useRealName: state.settings.UI_Use_Real_Name,
|
||||||
isMasterDetail: state.app.isMasterDetail,
|
isMasterDetail: state.app.isMasterDetail,
|
||||||
StoreLastMessage: state.settings.Store_Last_Message
|
StoreLastMessage: state.settings.Store_Last_Message,
|
||||||
|
addTeamChannelPermission: state.permissions[PERMISSION_ADD_TEAM_CHANNEL],
|
||||||
|
editTeamChannelPermission: state.permissions[PERMISSION_EDIT_TEAM_CHANNEL],
|
||||||
|
removeTeamChannelPermission: state.permissions[PERMISSION_REMOVE_TEAM_CHANNEL],
|
||||||
|
deleteCPermission: state.permissions[PERMISSION_DELETE_C],
|
||||||
|
deletePPermission: state.permissions[PERMISSION_DELETE_P]
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withDimensions(withSafeAreaInsets(withTheme(TeamChannelsView))));
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
deleteRoom: (rid, t) => dispatch(deleteRoomAction(rid, t))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withSafeAreaInsets(withTheme(withActionSheet(TeamChannelsView)))));
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
const axios = require('axios').default;
|
const axios = require('axios').default;
|
||||||
const data = require('../data');
|
const data = require('../data');
|
||||||
const { TEAM_TYPE } = require('../../app/definition/ITeam');
|
|
||||||
|
const TEAM_TYPE = {
|
||||||
|
PUBLIC: 0,
|
||||||
|
PRIVATE: 1
|
||||||
|
};
|
||||||
|
|
||||||
let server = data.server
|
let server = data.server
|
||||||
|
|
||||||
|
|
|
@ -131,8 +131,8 @@ describe('Discussion', () => {
|
||||||
await expect(element(by.id('room-info-view'))).toExist();
|
await expect(element(by.id('room-info-view'))).toExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not have edit button', async() => {
|
it('should have edit button', async() => {
|
||||||
await expect(element(by.id('room-info-view-edit-button'))).toBeNotVisible();
|
await expect(element(by.id('room-info-view-edit-button'))).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,6 +23,20 @@ stories.add('title and subtitle', () => (
|
||||||
</List.Container>
|
</List.Container>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
stories.add('alert', () => (
|
||||||
|
<List.Container>
|
||||||
|
<List.Separator />
|
||||||
|
<List.Item title='Chats' alert />
|
||||||
|
<List.Separator />
|
||||||
|
<List.Item title={longText} translateTitle={false} translateSubtitle={false} alert />
|
||||||
|
<List.Separator />
|
||||||
|
<List.Item title='Chats' right={() => <List.Icon name='emoji' />} alert />
|
||||||
|
<List.Separator />
|
||||||
|
<List.Item title={longText} translateTitle={false} translateSubtitle={false} right={() => <List.Icon name='emoji' />} alert />
|
||||||
|
<List.Separator />
|
||||||
|
</List.Container>
|
||||||
|
));
|
||||||
|
|
||||||
stories.add('pressable', () => (
|
stories.add('pressable', () => (
|
||||||
<List.Container>
|
<List.Container>
|
||||||
<List.Separator />
|
<List.Separator />
|
||||||
|
|
|
@ -40,7 +40,7 @@ const getCustomEmoji = (content) => {
|
||||||
return customEmoji;
|
return customEmoji;
|
||||||
};
|
};
|
||||||
|
|
||||||
const messageDecorator = story => (
|
export const MessageDecorator = story => (
|
||||||
<MessageContext.Provider
|
<MessageContext.Provider
|
||||||
value={{
|
value={{
|
||||||
user,
|
user,
|
||||||
|
@ -60,7 +60,7 @@ const messageDecorator = story => (
|
||||||
</MessageContext.Provider>
|
</MessageContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Message = props => (
|
export const Message = props => (
|
||||||
<MessageComponent
|
<MessageComponent
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
user={user}
|
user={user}
|
||||||
|
@ -74,12 +74,14 @@ const Message = props => (
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const StoryProvider = story => <Provider store={store}>{story()}</Provider>;
|
||||||
|
|
||||||
|
const MessageScrollView = story => <ScrollView style={{ backgroundColor: themes[_theme].backgroundColor }}>{story()}</ScrollView>;
|
||||||
|
|
||||||
const stories = storiesOf('Message', module)
|
const stories = storiesOf('Message', module)
|
||||||
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
|
.addDecorator(StoryProvider)
|
||||||
.addDecorator(story => <ScrollView style={{ backgroundColor: themes[_theme].backgroundColor }}>{story()}</ScrollView>)
|
.addDecorator(MessageScrollView)
|
||||||
.addDecorator(messageDecorator);
|
.addDecorator(MessageDecorator);
|
||||||
|
|
||||||
stories.add('Basic', () => (
|
stories.add('Basic', () => (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -3,7 +3,6 @@ import React from 'react';
|
||||||
import { ScrollView, Dimensions } from 'react-native';
|
import { ScrollView, Dimensions } from 'react-native';
|
||||||
import { storiesOf } from '@storybook/react-native';
|
import { storiesOf } from '@storybook/react-native';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
// import moment from 'moment';
|
|
||||||
|
|
||||||
import { themes } from '../../app/constants/colors';
|
import { themes } from '../../app/constants/colors';
|
||||||
import RoomItemComponent from '../../app/presentation/RoomItem/RoomItem';
|
import RoomItemComponent from '../../app/presentation/RoomItem/RoomItem';
|
||||||
|
@ -94,6 +93,15 @@ stories.add('Alerts', () => (
|
||||||
</>
|
</>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
stories.add('Tag', () => (
|
||||||
|
<>
|
||||||
|
<RoomItem autoJoin />
|
||||||
|
<RoomItem showLastMessage autoJoin />
|
||||||
|
<RoomItem name={longText} autoJoin />
|
||||||
|
<RoomItem name={longText} autoJoin showLastMessage />
|
||||||
|
</>
|
||||||
|
));
|
||||||
|
|
||||||
stories.add('Last Message', () => (
|
stories.add('Last Message', () => (
|
||||||
<>
|
<>
|
||||||
<RoomItem
|
<RoomItem
|
||||||
|
|
|
@ -14,6 +14,7 @@ import '../../app/views/ThreadMessagesView/Item.stories.js';
|
||||||
import './Avatar';
|
import './Avatar';
|
||||||
import '../../app/containers/BackgroundContainer/index.stories.js';
|
import '../../app/containers/BackgroundContainer/index.stories.js';
|
||||||
import '../../app/containers/RoomHeader/RoomHeader.stories.js';
|
import '../../app/containers/RoomHeader/RoomHeader.stories.js';
|
||||||
|
import '../../app/views/RoomView/LoadMore/LoadMore.stories';
|
||||||
|
|
||||||
// Change here to see themed storybook
|
// Change here to see themed storybook
|
||||||
export const theme = 'light';
|
export const theme = 'light';
|
||||||
|
|
Loading…
Reference in New Issue