import { CompositeNavigationProp, RouteProp } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; import isEmpty from 'lodash/isEmpty'; import React from 'react'; import { ScrollView, Text, View } from 'react-native'; import { BorderlessButton } from 'react-native-gesture-handler'; import { connect } from 'react-redux'; import { Observable, Subscription } from 'rxjs'; import UAParser from 'ua-parser-js'; import { AvatarWithEdit } from '../../containers/Avatar'; import { CustomIcon, TIconsName } from '../../containers/CustomIcon'; import * as HeaderButton from '../../containers/HeaderButton'; import { MarkdownPreview } from '../../containers/markdown'; import RoomTypeIcon from '../../containers/RoomTypeIcon'; import SafeAreaView from '../../containers/SafeAreaView'; import Status from '../../containers/Status'; import StatusBar from '../../containers/StatusBar'; import { LISTENER } from '../../containers/Toast'; import { IApplicationState, ISubscription, IUser, SubscriptionType, TSubscriptionModel } from '../../definitions'; import { ILivechatVisitor } from '../../definitions/ILivechatVisitor'; import I18n from '../../i18n'; import { themes } from '../../lib/constants'; import { getSubscriptionByRoomId } from '../../lib/database/services/Subscription'; import { getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers'; import EventEmitter from '../../lib/methods/helpers/events'; import { goRoom } from '../../lib/methods/helpers/goRoom'; import { handleIgnore } from '../../lib/methods/helpers/handleIgnore'; import log, { events, logEvent } from '../../lib/methods/helpers/log'; import Navigation from '../../lib/navigation/appNavigation'; import { Services } from '../../lib/services'; import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import { ChatsStackParamList } from '../../stacks/types'; import { TSupportedThemes, withTheme } from '../../theme'; import sharedStyles from '../Styles'; import Channel from './Channel'; import { CallButton } from './components/UserInfoButton'; import Direct from './Direct'; import Livechat from './Livechat'; import styles from './styles'; interface IGetRoomTitle { room: ISubscription; type: SubscriptionType; name?: string; username: string; statusText?: string; theme: TSupportedThemes; } const renderRoomTitle = ({ room, type, name, username, statusText, theme }: IGetRoomTitle) => type === SubscriptionType.DIRECT ? ( <> {name} {username && ( {`@${username}`} )} {!!statusText && ( )} ) : ( {getRoomTitle(room)} ); interface IRoomInfoViewProps { navigation: CompositeNavigationProp< StackNavigationProp, StackNavigationProp >; route: RouteProp; subscribedRoom: string; theme: TSupportedThemes; isMasterDetail: boolean; jitsiEnabled: boolean; editRoomPermission?: string[]; editOmnichannelContact?: string[]; editLivechatRoomCustomfields?: string[]; roles: { [key: string]: string }; } export interface IUserParsed extends IUser { parsedRoles?: string[]; } export interface ILivechatVisitorModified extends ILivechatVisitor { os?: string; browser?: string; } interface IRoomInfoViewState { room: ISubscription; roomUser: IUserParsed | ILivechatVisitorModified; showEdit: boolean; roomFromRid?: TSubscriptionModel; } class RoomInfoView extends React.Component { private rid: string; private t: SubscriptionType; private unsubscribeFocus?: () => void; private subscription?: Subscription; private roomObservable?: Observable; private fromRid?: string; private subscriptionRoomFromRid?: Subscription; constructor(props: IRoomInfoViewProps) { super(props); const room = props.route.params?.room; const roomUser = props.route.params?.member; this.rid = props.route.params?.rid; this.t = props.route.params?.t; this.fromRid = props.route.params?.fromRid; this.state = { room: (room || { rid: this.rid, t: this.t }) as any, roomUser: roomUser || {}, showEdit: false, roomFromRid: undefined }; } componentDidMount() { if (this.isDirect) { this.loadUser(); this.loadRoomFromRid(); } else { this.loadRoom(); } this.setHeader(); const { navigation } = this.props; this.unsubscribeFocus = navigation.addListener('focus', () => { if (this.isLivechat) { this.loadVisitor(); } }); } componentWillUnmount() { if (this.subscription && this.subscription.unsubscribe) { this.subscription.unsubscribe(); } if (this.subscriptionRoomFromRid && this.subscriptionRoomFromRid.unsubscribe) { this.subscriptionRoomFromRid.unsubscribe(); } if (this.unsubscribeFocus) { this.unsubscribeFocus(); } } setHeader = () => { const { roomUser, room, showEdit } = this.state; const { navigation, route } = this.props; const t = route.params?.t; const rid = route.params?.rid; const showCloseModal = route.params?.showCloseModal; navigation.setOptions({ headerLeft: showCloseModal ? () => : undefined, title: t === SubscriptionType.DIRECT ? I18n.t('User_Info') : I18n.t('Room_Info'), headerRight: showEdit ? () => ( { const isLivechat = t === SubscriptionType.OMNICHANNEL; logEvent(events[`RI_GO_${isLivechat ? 'LIVECHAT' : 'RI'}_EDIT`]); navigation.navigate(isLivechat ? 'LivechatEditView' : 'RoomInfoEditView', { rid, room, roomUser }); }} testID='room-info-view-edit-button' /> ) : undefined }); }; get isDirect() { const { room } = this.state; return room.t === SubscriptionType.DIRECT; } get isLivechat() { const { room } = this.state; return room.t === SubscriptionType.OMNICHANNEL; } getRoleDescription = (id: string) => { const { roles } = this.props; return roles[id]; }; loadVisitor = async () => { const { room } = this.state; try { if (room.visitor?._id) { const result = await Services.getVisitorInfo(room.visitor._id); if (result.success) { const { visitor } = result; const params: { os?: string; browser?: string } = {}; if (visitor.userAgent) { const ua = new UAParser(); ua.setUA(visitor.userAgent); params.os = `${ua.getOS().name} ${ua.getOS().version}`; params.browser = `${ua.getBrowser().name} ${ua.getBrowser().version}`; } this.setState({ roomUser: { ...visitor, ...params } as ILivechatVisitorModified }, () => this.setHeader()); } } } catch (error) { // Do nothing } }; parseRoles = (roleArray: string[]) => Promise.all( roleArray.map(async role => { const description = await this.getRoleDescription(role); return description; }) ); loadUser = async () => { const { room, roomUser } = this.state; if (isEmpty(roomUser)) { try { const roomUserId = getUidDirectMessage(room); const result = await Services.getUserInfo(roomUserId); if (result.success) { const { user } = result; const { roles } = user; const parsedRoles: { parsedRoles?: string[] } = {}; if (roles && roles.length) { parsedRoles.parsedRoles = await this.parseRoles(roles); } this.setState({ roomUser: { ...user, ...parsedRoles } as IUserParsed }); } } catch { // do nothing } } else { try { const { roles } = roomUser as IUserParsed; if (roles && roles.length) { const parsedRoles = await this.parseRoles(roles); this.setState({ roomUser: { ...roomUser, parsedRoles } }); } else { this.setState({ roomUser }); } } catch (e) { // do nothing } } }; loadRoomFromRid = async () => { if (this.fromRid) { try { const sub = await getSubscriptionByRoomId(this.fromRid); this.subscriptionRoomFromRid = sub?.observe().subscribe(roomFromRid => { this.setState({ roomFromRid }); }); } catch (e) { // do nothing } } }; loadRoom = async () => { const { room: roomState } = this.state; const { route, editRoomPermission, editOmnichannelContact, editLivechatRoomCustomfields } = this.props; let room = route.params?.room as any; const roomModel = room as TSubscriptionModel; if (roomModel && roomModel.observe) { this.roomObservable = roomModel.observe(); this.subscription = this.roomObservable.subscribe(changes => { this.setState({ room: changes }, () => this.setHeader()); }); } else { try { const result = await Services.getRoomInfo(this.rid); if (result.success) { ({ room } = result); this.setState({ room: { ...roomState, ...room } }); } } catch (e) { log(e); } } const permissionToEdit = this.isLivechat ? [editOmnichannelContact, editLivechatRoomCustomfields] : [editRoomPermission]; const permissions = await hasPermission(permissionToEdit, room.rid); if (permissions.some(Boolean)) { this.setState({ showEdit: true }, () => this.setHeader()); } }; createDirect = () => new Promise(async (resolve, reject) => { const { route } = this.props; // We don't need to create a direct const member = route.params?.member; if (!isEmpty(member)) { return resolve(); } // TODO: Check if some direct with the user already exists on database try { const { roomUser: { username } } = this.state; const result = await Services.createDirectMessage(username); if (result.success) { const { room: { rid } } = result; return this.setState(({ room }) => ({ room: { ...room, rid } }), resolve); } } catch { // do nothing } reject(); }); goRoom = () => { logEvent(events.RI_GO_ROOM_USER); const { room } = this.state; const { navigation, isMasterDetail, subscribedRoom } = this.props; const params = { rid: room.rid, name: getRoomTitle(room), t: room.t, roomUserId: getUidDirectMessage(room) }; if (room.rid) { if (room.rid === subscribedRoom) { if (isMasterDetail) { return Navigation.navigate('DrawerNavigator'); } return navigation.goBack(); } // if it's on master detail layout, we close the modal and replace RoomView goRoom({ item: params, isMasterDetail, popToRoot: true }); } }; handleCreateDirectMessage = async (onPress: () => void) => { try { if (this.isDirect) { await this.createDirect(); } onPress(); } catch { EventEmitter.emit(LISTENER, { message: I18n.t('error-action-not-allowed', { action: I18n.t('Create_Direct_Messages') }) }); } }; handleBlockUser = async (rid: string, blocked: string, block: boolean) => { logEvent(events.RI_TOGGLE_BLOCK_USER); try { await Services.toggleBlockUser(rid, blocked, block); } catch (e) { log(e); } }; handleEditAvatar = () => { const { navigation } = this.props; const { room } = this.state; navigation.navigate('ChangeAvatarView', { titleHeader: I18n.t('Room_Info'), room, t: this.t, context: 'room' }); }; renderAvatar = (room: ISubscription, roomUser: IUserParsed) => { const { theme } = this.props; const { showEdit } = this.state; const showAvatarEdit = showEdit && this.t !== SubscriptionType.OMNICHANNEL; return ( {this.t === SubscriptionType.DIRECT && roomUser._id ? ( ) : null} ); }; renderButton = (onPress: () => void, iconName: TIconsName, text: string, danger?: boolean) => { const { theme } = this.props; const color = danger ? themes[theme].dangerColor : themes[theme].actionTintColor; return ( {text} ); }; renderButtons = () => { const { roomFromRid, roomUser, room } = this.state; const isFromDm = roomFromRid?.rid ? new RegExp(roomUser._id).test(roomFromRid.rid) : false; const isDirectFromSaved = this.isDirect && this.fromRid && roomFromRid; // Following the web behavior, when is a DM with myself, shouldn't appear block or ignore option const isDmWithMyself = roomFromRid?.uids && roomFromRid.uids?.filter(uid => uid !== roomUser._id).length === 0; const ignored = roomFromRid?.ignored; const isIgnored = ignored?.includes?.(roomUser._id); const blocker = roomFromRid?.blocker; return ( {this.renderButton(() => this.handleCreateDirectMessage(this.goRoom), 'message', I18n.t('Message'))} {isDirectFromSaved && !isFromDm && !isDmWithMyself ? this.renderButton( () => handleIgnore(roomUser._id, !isIgnored, roomFromRid.rid), 'ignore', I18n.t(isIgnored ? 'Unignore' : 'Ignore'), true ) : null} {isDirectFromSaved && isFromDm ? this.renderButton( () => this.handleBlockUser(roomFromRid.rid, roomUser._id, !blocker), 'ignore', I18n.t(`${blocker ? 'Unblock' : 'Block'}_user`), true ) : null} ); }; renderContent = () => { const { room, roomUser } = this.state; if (this.isDirect) { return ; } if (this.t === SubscriptionType.OMNICHANNEL) { return ; } return ; }; render() { const { room, roomUser } = this.state; const { theme } = this.props; const roomUserParsed = roomUser as IUserParsed; return ( {this.renderAvatar(room, roomUserParsed)} {renderRoomTitle({ room, type: this.t, name: roomUserParsed?.name, username: roomUserParsed?.username, statusText: roomUserParsed?.statusText, theme })} {this.renderButtons()} {this.renderContent()} ); } } const mapStateToProps = (state: IApplicationState) => ({ subscribedRoom: state.room.subscribedRoom, isMasterDetail: state.app.isMasterDetail, jitsiEnabled: (state.settings.Jitsi_Enabled as boolean) || false, editRoomPermission: state.permissions['edit-room'], editOmnichannelContact: state.permissions['edit-omnichannel-contact'], editLivechatRoomCustomfields: state.permissions['edit-livechat-room-customfields'], roles: state.roles }); export default connect(mapStateToProps)(withTheme(RoomInfoView));