This commit is contained in:
Anant Bhasin 2021-05-28 21:28:00 +05:30
commit 40615a81ea
84 changed files with 20467 additions and 11899 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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} />
<Text <View style={styles.titleContainer}>
numberOfLines={1} <Text
style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]} numberOfLines={1}
> style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]}
{item.title} >
</Text> {item.title}
</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

View File

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

View File

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

View File

@ -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 = {

View File

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

View File

@ -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' : '' }`;

View File

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

View File

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

View File

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

View File

@ -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 = {

View File

@ -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,24 +8,29 @@ 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() => {
const threadName = await fetchThreadName(tmid, id);
setMsg(threadName);
};
useEffect(() => {
if (!msg) {
fetch();
}
}, []);
if (!msg) {
return null; return null;
} }
let msg = tmsg;
if (isEncrypted) {
msg = I18n.t('Encrypted_message');
}
return ( return (
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}> <View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
<CustomIcon name='threads' size={20} style={styles.repliedThreadIcon} color={themes[theme].tintColor} /> <CustomIcon name='threads' size={20} style={styles.repliedThreadIcon} color={themes[theme].tintColor} />
@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

@ -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 = {

View File

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

View File

@ -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,54 +212,70 @@ 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}>
<PanGestureHandler
minDeltaX={20}
onGestureEvent={this._onGestureEvent}
onHandlerStateChange={this._onHandlerStateChange}
enabled={swipeEnabled}
>
<Animated.View> <Animated.View>
<LeftActions <PanGestureHandler
transX={this.transXReverse} minDeltaX={20}
isRead={isRead} onGestureEvent={this._onGestureEvent}
width={width} onHandlerStateChange={this._onHandlerStateChange}
onToggleReadPress={this.onToggleReadPress} enabled={swipeEnabled}
theme={theme}
/>
<RightActions
transX={this.transXReverse}
favorite={favorite}
width={width}
toggleFav={this.toggleFav}
onHidePress={this.onHidePress}
theme={theme}
/>
<Animated.View
style={{
transform: [{ translateX: this.transX }]
}}
> >
<Touch <Animated.View>
onPress={this.onPress} <LeftActions
theme={theme} transX={this.transXReverse}
testID={testID} isRead={isRead}
style={{ width={width}
backgroundColor: isFocused ? themes[theme].chatComponentBackground : themes[theme].backgroundColor onToggleReadPress={this.onToggleReadPress}
}} theme={theme}
> />
{children} <RightActions
</Touch> transX={this.transXReverse}
</Animated.View> favorite={favorite}
</Animated.View> width={width}
toggleFav={this.toggleFav}
onHidePress={this.onHidePress}
theme={theme}
/>
<Animated.View
style={{
transform: [{ translateX: this.transX }]
}}
>
<Touch
onPress={this.onPress}
theme={theme}
testID={testID}
style={{
backgroundColor: isFocused ? themes[theme].chatComponentBackground : themes[theme].backgroundColor
}}
>
{children}
</Touch>
</Animated.View>
</Animated.View>
</PanGestureHandler> </PanGestureHandler>
</Animated.View>
</LongPressGestureHandler>
); );
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
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,21 +396,72 @@ 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') }),
[ onPress: () => leaveRoom(room.rid, room.t)
{ });
text: I18n.t('Cancel'), }
style: 'cancel'
}, handleLeaveTeam = async(selected) => {
{ try {
text: I18n.t('Yes_action_it', { action: I18n.t('leave') }), const { room } = this.state;
style: 'destructive', const { navigation, isMasterDetail } = this.props;
onPress: () => leaveRoom(room.rid, room.t) 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 = () => {
@ -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'],

View File

@ -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 => ({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) {
name = I18n.t('Encrypted_message');
}
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) if (!message.tmid && message.rid === this.rid) {
return false;
}
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 });
}
}
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,48 +948,55 @@ class RoomView extends React.Component {
} }
} }
const message = ( let content = null;
<Message if (MESSAGE_TYPE_ANY_LOAD.includes(item.t)) {
item={item} content = <LoadMore load={() => this.onLoadMoreMessages(item)} type={item.t} runOnRender={item.t === MESSAGE_TYPE_LOAD_MORE && !previousItem} />;
user={user} } else {
rid={room.rid} content = (
archived={room.archived} <Message
broadcast={room.broadcast} item={item}
status={item.status} user={user}
isThreadRoom={!!this.tmid} rid={room.rid}
isIgnored={this.isIgnored(item)} archived={room.archived}
previousItem={previousItem} broadcast={room.broadcast}
fetchThreadName={this.fetchThreadName} status={item.status}
onReactionPress={this.onReactionPress} isThreadRoom={!!this.tmid}
onReactionLongPress={this.onReactionLongPress} isIgnored={this.isIgnored(item)}
onLongPress={this.onMessageLongPress} previousItem={previousItem}
onEncryptedPress={this.onEncryptedPress} fetchThreadName={this.getThreadName}
onDiscussionPress={this.onDiscussionPress} onReactionPress={this.onReactionPress}
onThreadPress={this.onThreadPress} onReactionLongPress={this.onReactionLongPress}
showAttachment={this.showAttachment} onLongPress={this.onMessageLongPress}
reactionInit={this.onReactionInit} onEncryptedPress={this.onEncryptedPress}
replyBroadcast={this.replyBroadcast} onDiscussionPress={this.onDiscussionPress}
errorActionsShow={this.errorActionsShow} onThreadPress={this.onThreadPress}
baseUrl={baseUrl} showAttachment={this.showAttachment}
Message_GroupingPeriod={Message_GroupingPeriod} reactionInit={this.onReactionInit}
timeFormat={Message_TimeFormat} replyBroadcast={this.replyBroadcast}
useRealName={useRealName} errorActionsShow={this.errorActionsShow}
isReadReceiptEnabled={Message_Read_Receipt_Enabled} baseUrl={baseUrl}
autoTranslateRoom={canAutoTranslate && room.autoTranslate} Message_GroupingPeriod={Message_GroupingPeriod}
autoTranslateLanguage={room.autoTranslateLanguage} timeFormat={Message_TimeFormat}
navToRoomInfo={this.navToRoomInfo} useRealName={useRealName}
getCustomEmoji={this.getCustomEmoji} isReadReceiptEnabled={Message_Read_Receipt_Enabled}
callJitsi={this.callJitsi} autoTranslateRoom={canAutoTranslate && room.autoTranslate}
blockAction={this.blockAction} autoTranslateLanguage={room.autoTranslateLanguage}
threadBadgeColor={this.getBadgeColor(item?.id)} navToRoomInfo={this.navToRoomInfo}
toggleFollowThread={this.toggleFollowThread} getCustomEmoji={this.getCustomEmoji}
/> callJitsi={this.callJitsi}
); blockAction={this.blockAction}
threadBadgeColor={this.getBadgeColor(item?.id)}
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>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import RocketChat from '../../../lib/rocketchat';
const readMessages = (rid, newLastOpen) => RocketChat.readMessages(rid, newLastOpen, true);
export default readMessages;

View File

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

View File

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

141
app/views/SelectListView.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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', () => (
<> <>

View File

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

View File

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