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