diff --git a/app/containers/RoomTypeIcon/index.tsx b/app/containers/RoomTypeIcon/index.tsx index 231245fe8..ef6f30b94 100644 --- a/app/containers/RoomTypeIcon/index.tsx +++ b/app/containers/RoomTypeIcon/index.tsx @@ -15,8 +15,8 @@ const styles = StyleSheet.create({ }); interface IRoomTypeIcon { + type?: string; userId?: string | null; - type: string; isGroupChat?: boolean; teamMain?: boolean; status?: TUserStatus; diff --git a/app/definitions/ILivechatVisitor.ts b/app/definitions/ILivechatVisitor.ts index 85f90102e..86143ce49 100644 --- a/app/definitions/ILivechatVisitor.ts +++ b/app/definitions/ILivechatVisitor.ts @@ -36,6 +36,11 @@ export interface ILivechatVisitor extends IRocketChatRecord { utc?: number; } +export interface ILivechatVisitorModified extends ILivechatVisitor { + os?: string; + browser?: string; +} + export interface ILivechatVisitorDTO { id: string; token: string; diff --git a/app/definitions/ISubscription.ts b/app/definitions/ISubscription.ts index f8f7749f9..a5a044e6a 100644 --- a/app/definitions/ISubscription.ts +++ b/app/definitions/ISubscription.ts @@ -111,7 +111,9 @@ export interface ISubscription { uploads: RelationModified; } -export type TSubscriptionModel = ISubscription & Model; +export type TSubscriptionModel = ISubscription & Model & { + asPlain: () => ISubscription; +}; export type TSubscription = TSubscriptionModel | ISubscription; // https://github.com/RocketChat/Rocket.Chat/blob/a88a96fcadd925b678ff27ada37075e029f78b5e/definition/ISubscription.ts#L8 diff --git a/app/lib/database/model/Subscription.js b/app/lib/database/model/Subscription.js index a79d5a59d..2675a6f52 100644 --- a/app/lib/database/model/Subscription.js +++ b/app/lib/database/model/Subscription.js @@ -142,4 +142,70 @@ export default class Subscription extends Model { @field('users_count') usersCount; @json('source', sanitizer) source; + + asPlain() { + return { + _id: this._id, + f: this.f, + t: this.t, + ts: this.ts, + ls: this.ls, + name: this.name, + fname: this.fname, + sanitizedFname: this.sanitizedFname, + rid: this.rid, + open: this.open, + alert: this.alert, + roles: this.roles, + unread: this.unread, + userMentions: this.userMentions, + groupMentions: this.groupMentions, + tunread: this.tunread, + tunreadUser: this.tunreadUser, + tunreadGroup: this.tunreadGroup, + roomUpdatedAt: this.roomUpdatedAt, + ro: this.ro, + lastOpen: this.lastOpen, + description: this.description, + announcement: this.announcement, + bannerClosed: this.bannerClosed, + topic: this.topic, + blocked: this.blocked, + blocker: this.blocker, + reactWhenReadOnly: this.reactWhenReadOnly, + archived: this.archived, + joinCodeRequired: this.joinCodeRequired, + notifications: this.notifications, + muted: this.muted, + ignored: this.ignored, + broadcast: this.broadcast, + prid: this.prid, + draftMessage: this.draftMessage, + lastThreadSync: this.lastThreadSync, + jitsiTimeout: this.jitsiTimeout, + autoTranslate: this.autoTranslate, + autoTranslateLanguage: this.autoTranslateLanguage, + lastMessage: this.lastMessage, + hideUnreadStatus: this.hideUnreadStatus, + hideMentionStatus: this.hideMentionStatus, + sysMes: this.sysMes, + uids: this.uids, + usernames: this.usernames, + visitor: this.visitor, + departmentId: this.departmentId, + servedBy: this.servedBy, + livechatData: this.livechatData, + tags: this.tags, + E2EKey: this.E2EKey, + E2ESuggestedKey: this.E2ESuggestedKey, + encrypted: this.encrypted, + e2eKeyId: this.e2eKeyId, + avatarETag: this.avatarETag, + teamId: this.teamId, + teamMain: this.teamMain, + onHold: this.onHold, + usersCount: this.usersCount, + source: this.source + }; + } } diff --git a/app/lib/methods/helpers/helpers.ts b/app/lib/methods/helpers/helpers.ts index bfacfc015..218f5422c 100644 --- a/app/lib/methods/helpers/helpers.ts +++ b/app/lib/methods/helpers/helpers.ts @@ -4,7 +4,7 @@ import { store as reduxStore } from '../../store/auxStore'; import database from '../../database'; export function isGroupChat(room): boolean { - return ((room.uids && room.uids.length > 2) || (room.usernames && room.usernames.length > 2)) ?? false; + return ((room?.uids && room.uids.length > 2) || (room?.usernames && room.usernames.length > 2)) ?? false; } export function getRoomAvatar(room) { @@ -49,7 +49,7 @@ export function getRoomTitle(room) { if (allowSpecialChars && room.t !== 'd') { return room.fname || room.name; } - return ((room.prid || useRealName) && room.fname) || room.name; + return ((room?.prid || useRealName) && room?.fname) || room?.name; } export function getSenderName(sender) { diff --git a/app/stacks/InsideStack.tsx b/app/stacks/InsideStack.tsx index 4c59c96b3..56374a36e 100644 --- a/app/stacks/InsideStack.tsx +++ b/app/stacks/InsideStack.tsx @@ -98,7 +98,8 @@ const ChatsStackNavigator = () => { - + + {/* @ts-ignore */} diff --git a/app/stacks/MasterDetailStack/index.tsx b/app/stacks/MasterDetailStack/index.tsx index 5618c49f7..163d963cd 100644 --- a/app/stacks/MasterDetailStack/index.tsx +++ b/app/stacks/MasterDetailStack/index.tsx @@ -115,7 +115,9 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => { screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation } as StackNavigationOptions} > - + {/* @ts-ignore */} + + {/* @ts-ignore */} diff --git a/app/views/RoomActionsView/index.tsx b/app/views/RoomActionsView/index.tsx index 53461183d..c405e4cc0 100644 --- a/app/views/RoomActionsView/index.tsx +++ b/app/views/RoomActionsView/index.tsx @@ -773,9 +773,9 @@ class RoomActionsView extends React.Component { - const { description, topic, announcement } = room; +const Channel = ({ room }: { room?: ISubscription }): React.ReactElement => { + const description = room?.description || `__${I18n.t('No_label_provided', { label: 'description' })}__`; + const topic = room?.topic || `__${I18n.t('No_label_provided', { label: 'topic' })}__`; + const announcement = room?.announcement || `__${I18n.t('No_label_provided', { label: 'announcement' })}__`; + const broadcast = room?.broadcast ? I18n.t('Broadcast_hint') : ''; return ( <> - - - - + + + + ); }; diff --git a/app/views/RoomInfoView/CustomFields.tsx b/app/views/RoomInfoView/CustomFields.tsx index ccdf863d3..4716cf1fa 100644 --- a/app/views/RoomInfoView/CustomFields.tsx +++ b/app/views/RoomInfoView/CustomFields.tsx @@ -2,14 +2,12 @@ import React from 'react'; import Item from './Item'; -const CustomFields = ({ customFields }: { customFields?: { [key: string]: string } }) => { +const CustomFields = ({ customFields }: { customFields?: { [key: string]: string } }): React.ReactElement | null => { if (customFields) { return ( <> {Object.keys(customFields).map((title: string) => { - if (!customFields[title]) { - return null; - } + if (!customFields[title]) return null; return ; })} diff --git a/app/views/RoomInfoView/Direct.tsx b/app/views/RoomInfoView/Direct.tsx index 506827ff1..c8d258914 100644 --- a/app/views/RoomInfoView/Direct.tsx +++ b/app/views/RoomInfoView/Direct.tsx @@ -1,29 +1,27 @@ import React from 'react'; import { Text, View } from 'react-native'; -import { IUserParsed } from '.'; import I18n from '../../i18n'; import { useTheme } from '../../theme'; import CustomFields from './CustomFields'; import Timezone from './Timezone'; import styles from './styles'; +import { IUser } from '../../definitions'; const Roles = ({ roles }: { roles?: string[] }) => { const { colors } = useTheme(); if (roles?.length) { return ( - - - {I18n.t('Roles')} - + + {I18n.t('Roles')} {roles.map(role => role ? ( {role} @@ -37,9 +35,9 @@ const Roles = ({ roles }: { roles?: string[] }) => { return null; }; -const Direct = ({ roomUser }: { roomUser: IUserParsed }): React.ReactElement => ( +const Direct = ({ roomUser }: { roomUser: IUser }): React.ReactElement => ( <> - + diff --git a/app/views/RoomInfoView/Item.tsx b/app/views/RoomInfoView/Item.tsx index bc37e6b60..260005f1f 100644 --- a/app/views/RoomInfoView/Item.tsx +++ b/app/views/RoomInfoView/Item.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { Text, View } from 'react-native'; import Markdown from '../../containers/markdown'; -import { themes } from '../../lib/constants'; import { useTheme } from '../../theme'; import styles from './styles'; @@ -12,19 +11,17 @@ interface IItem { testID?: string; } -const Item = ({ label, content, testID }: IItem) => { - const { theme } = useTheme(); +const Item = ({ label, content, testID }: IItem): React.ReactElement | null => { + const { colors } = useTheme(); - if (!content) { - return null; - } + if (!content) return null; return ( - + {label} - + ); }; diff --git a/app/views/RoomInfoView/Livechat.tsx b/app/views/RoomInfoView/Livechat.tsx index 3dec3be1b..409332ec8 100644 --- a/app/views/RoomInfoView/Livechat.tsx +++ b/app/views/RoomInfoView/Livechat.tsx @@ -1,17 +1,16 @@ import React, { useEffect, useState } from 'react'; import { StyleSheet, Text } from 'react-native'; -import { TSupportedThemes, useTheme } from '../../theme'; -import sharedStyles from '../Styles'; -import { themes } from '../../lib/constants'; -import I18n from '../../i18n'; import { ISubscription } from '../../definitions'; -import { ILivechatVisitorModified } from './index'; +import { ILivechatDepartment } from '../../definitions/ILivechatDepartment'; +import { ILivechatVisitorModified } from '../../definitions/ILivechatVisitor'; +import I18n from '../../i18n'; +import { Services } from '../../lib/services'; +import { useTheme } from '../../theme'; +import sharedStyles from '../Styles'; import CustomFields from './CustomFields'; import Item from './Item'; import Timezone from './Timezone'; -import { ILivechatDepartment } from '../../definitions/ILivechatDepartment'; -import { Services } from '../../lib/services'; const styles = StyleSheet.create({ title: { @@ -21,13 +20,13 @@ const styles = StyleSheet.create({ } }); -const Title = ({ title, theme }: { title: string; theme: TSupportedThemes }) => ( - {title} -); +const Title = ({ title }: { title: string }) => { + const { colors } = useTheme(); + return {title}; +}; -const Livechat = ({ room, roomUser }: { room: ISubscription; roomUser: ILivechatVisitorModified }) => { +const Livechat = ({ room, roomUser }: { room: ISubscription; roomUser: ILivechatVisitorModified }): React.ReactElement => { const [department, setDepartment] = useState({} as ILivechatDepartment); - const { theme } = useTheme(); const getDepartment = async (id: string) => { if (id) { @@ -38,19 +37,16 @@ const Livechat = ({ room, roomUser }: { room: ISubscription; roomUser: ILivechat } }; - const getRoom = () => { - if (room.departmentId) { - getDepartment(room.departmentId); - } - }; - useEffect(() => { + const getRoom = () => { + if (room.departmentId) getDepartment(room.departmentId); + }; getRoom(); - }, [room]); + }, [room.departmentId]); return ( <> - + <Title title={I18n.t('User')} /> <Timezone utcOffset={roomUser.utc} /> <Item label={I18n.t('Username')} content={roomUser.username} /> <Item @@ -65,7 +61,7 @@ const Livechat = ({ room, roomUser }: { room: ISubscription; roomUser: ILivechat <Item label={I18n.t('OS')} content={roomUser.os} /> <Item label={I18n.t('Browser')} content={roomUser.browser} /> <CustomFields customFields={roomUser.livechatData} /> - <Title title={I18n.t('Conversation')} theme={theme} /> + <Title title={I18n.t('Conversation')} /> <Item label={I18n.t('Agent')} content={room.servedBy?.username} /> {/* TODO: Will be deprecated */} {/* @ts-ignore */} diff --git a/app/views/RoomInfoView/Timezone.tsx b/app/views/RoomInfoView/Timezone.tsx index cf2e68f9c..a2bc4f1ec 100644 --- a/app/views/RoomInfoView/Timezone.tsx +++ b/app/views/RoomInfoView/Timezone.tsx @@ -1,34 +1,18 @@ -import React from 'react'; -import { connect } from 'react-redux'; import moment from 'moment'; +import React from 'react'; -import { IApplicationState } from '../../definitions'; import I18n from '../../i18n'; +import { useAppSelector } from '../../lib/hooks'; import Item from './Item'; -import { TSettingsValues } from '../../reducers/settings'; -interface ITimezone { - utcOffset?: number; - Message_TimeFormat?: TSettingsValues; -} +const Timezone = ({ utcOffset }: { utcOffset?: number }): React.ReactElement | null => { + const Message_TimeFormat = useAppSelector(state => state.settings.Message_TimeFormat as string); -const Timezone = ({ utcOffset, Message_TimeFormat }: ITimezone) => { - if (!utcOffset) { - return null; - } + if (!utcOffset) return null; return ( - <Item - label={I18n.t('Timezone')} - content={`${moment() - .utcOffset(utcOffset) - .format(Message_TimeFormat as string)} (UTC ${utcOffset})`} - /> + <Item label={I18n.t('Timezone')} content={`${moment().utcOffset(utcOffset).format(Message_TimeFormat)} (UTC ${utcOffset})`} /> ); }; -const mapStateToProps = (state: IApplicationState) => ({ - Message_TimeFormat: state.settings.Message_TimeFormat -}); - -export default connect(mapStateToProps)(Timezone); +export default Timezone; diff --git a/app/views/RoomInfoView/components/RoomInfoButtons.tsx b/app/views/RoomInfoView/components/RoomInfoButtons.tsx new file mode 100644 index 000000000..9a82d8113 --- /dev/null +++ b/app/views/RoomInfoView/components/RoomInfoButtons.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Text, View } from 'react-native'; +import { BorderlessButton } from 'react-native-gesture-handler'; + +import { CustomIcon, TIconsName } from '../../../containers/CustomIcon'; +import { ISubscription, SubscriptionType } from '../../../definitions'; +import i18n from '../../../i18n'; +import { useVideoConf } from '../../../lib/hooks/useVideoConf'; +import { useTheme } from '../../../theme'; +import styles from '../styles'; + +function BaseButton({ + danger, + iconName, + onPress, + label, + showIcon = true, + enabled = true +}: { + danger?: boolean; + iconName: TIconsName; + onPress?: (prop: any) => void; + label: string; + showIcon?: boolean; + enabled?: boolean; +}): React.ReactElement | null { + const { colors } = useTheme(); + const color = danger ? colors.dangerColor : colors.actionTintColor; + + if (showIcon) + return ( + <BorderlessButton enabled={enabled} testID={`room-info-view-${iconName}`} onPress={onPress} style={styles.roomButton}> + <CustomIcon name={iconName} size={30} color={color} /> + <Text style={[styles.roomButtonText, { color }]}>{label}</Text> + </BorderlessButton> + ); + return null; +} + +function CallButton({ rid, roomFromRid }: { rid: string; isDirect: boolean; roomFromRid: boolean }): React.ReactElement | null { + const { callEnabled, disabledTooltip, showInitCallActionSheet } = useVideoConf(rid); + return ( + <BaseButton + onPress={showInitCallActionSheet} + iconName='phone' + label={i18n.t('Call')} + enabled={!disabledTooltip} + showIcon={callEnabled && !roomFromRid} + /> + ); +} + +interface IRoomInfoButtons { + rid: string; + room: ISubscription | undefined; + roomUserId?: string; + isDirect: boolean; + fromRid?: string; + handleCreateDirectMessage: () => void; + handleIgnoreUser: () => void; + handleBlockUser: () => void; + roomFromRid: ISubscription | undefined; +} + +export const RoomInfoButtons = ({ + rid, + room: roomFromProps, + roomUserId, + isDirect, + fromRid, + handleCreateDirectMessage, + handleIgnoreUser, + handleBlockUser, + roomFromRid +}: IRoomInfoButtons): React.ReactElement => { + const room = roomFromRid || roomFromProps; + // Following the web behavior, when is a DM with myself, shouldn't appear block or ignore option + const isDmWithMyself = room?.uids && room.uids?.filter((uid: string) => uid !== roomUserId).length === 0; + + const isFromDm = room?.t === SubscriptionType.DIRECT; + const isDirectFromSaved = isDirect && fromRid && room; + const isIgnored = room?.ignored?.includes?.(roomUserId || ''); + const isBlocked = room?.blocker; + + const renderIgnoreUser = isDirectFromSaved && !isFromDm && !isDmWithMyself; + const renderBlockUser = isDirectFromSaved && isFromDm; + + return ( + <View style={styles.roomButtonsContainer}> + <BaseButton onPress={handleCreateDirectMessage} label={i18n.t('Message')} iconName='message' /> + <CallButton isDirect={isDirect} rid={rid} roomFromRid={!!roomFromRid} /> + <BaseButton + onPress={handleIgnoreUser} + label={i18n.t(isIgnored ? 'Unignore' : 'Ignore')} + iconName='ignore' + showIcon={!!renderIgnoreUser} + danger + /> + <BaseButton + onPress={handleBlockUser} + label={i18n.t(`${isBlocked ? 'Unblock' : 'Block'}_user`)} + iconName='ignore' + showIcon={!!renderBlockUser} + danger + /> + </View> + ); +}; + +export default RoomInfoButtons; diff --git a/app/views/RoomInfoView/components/RoomInfoViewAvatar.tsx b/app/views/RoomInfoView/components/RoomInfoViewAvatar.tsx new file mode 100644 index 000000000..dd6f2ff1d --- /dev/null +++ b/app/views/RoomInfoView/components/RoomInfoViewAvatar.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { View } from 'react-native'; + +import { AvatarWithEdit } from '../../../containers/Avatar'; +import Status from '../../../containers/Status'; +import { SubscriptionType } from '../../../definitions'; +import { useTheme } from '../../../theme'; +import sharedStyles from '../../Styles'; +import styles from '../styles'; + +const RoomInfoViewAvatar = ({ + showEdit, + type, + username, + rid, + handleEditAvatar, + userId +}: { + showEdit: boolean; + type: SubscriptionType; + username: string; + rid?: string; + handleEditAvatar: () => void; + userId: string; +}): React.ReactElement => { + const { colors } = useTheme(); + + const showAvatarEdit = showEdit && type !== SubscriptionType.OMNICHANNEL; + + return ( + <AvatarWithEdit + text={username} + style={styles.avatar} + type={type} + rid={rid} + handleEdit={showAvatarEdit ? handleEditAvatar : undefined} + > + {type === SubscriptionType.DIRECT && userId ? ( + <View style={[sharedStyles.status, { backgroundColor: colors.auxiliaryBackground }]}> + <Status size={20} id={userId} /> + </View> + ) : null} + </AvatarWithEdit> + ); +}; + +export default RoomInfoViewAvatar; diff --git a/app/views/RoomInfoView/components/RoomInfoViewBody.tsx b/app/views/RoomInfoView/components/RoomInfoViewBody.tsx new file mode 100644 index 000000000..9ec46bdce --- /dev/null +++ b/app/views/RoomInfoView/components/RoomInfoViewBody.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { ISubscription, SubscriptionType, IUser } from '../../../definitions'; +import { ILivechatVisitorModified } from '../../../definitions/ILivechatVisitor'; +import Channel from '../Channel'; +import Direct from '../Direct'; +import Livechat from '../Livechat'; + +const RoomInfoViewBody = ({ + isDirect, + roomUser, + room +}: { + isDirect: boolean; + roomUser: IUser | ILivechatVisitorModified; + room?: ISubscription; +}): React.ReactElement => { + if (isDirect) { + return <Direct roomUser={roomUser as IUser} />; + } + + if (room?.t === SubscriptionType.OMNICHANNEL && room) { + return <Livechat room={room} roomUser={roomUser as ILivechatVisitorModified} />; + } + + return <Channel room={room} />; +}; + +export default RoomInfoViewBody; diff --git a/app/views/RoomInfoView/components/RoomInfoViewTitle.tsx b/app/views/RoomInfoView/components/RoomInfoViewTitle.tsx new file mode 100644 index 000000000..d2ef0875f --- /dev/null +++ b/app/views/RoomInfoView/components/RoomInfoViewTitle.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Text, View } from 'react-native'; + +import { ISubscription, SubscriptionType } from '../../../definitions'; +import styles from '../styles'; +import { useTheme } from '../../../theme'; +import { MarkdownPreview } from '../../../containers/markdown'; +import RoomTypeIcon from '../../../containers/RoomTypeIcon'; +import { getRoomTitle } from '../../../lib/methods/helpers'; + +interface IRoomInfoViewTitle { + room?: ISubscription; + name?: string; + username: string; + statusText?: string; + type: SubscriptionType; +} + +const RoomInfoViewTitle = ({ room, name, username, statusText, type }: IRoomInfoViewTitle): React.ReactElement => { + const { colors } = useTheme(); + if (type === SubscriptionType.DIRECT) { + return ( + <> + <Text testID='room-info-view-name' style={[styles.roomTitle, { color: colors.titleText }]}> + {name} + </Text> + {username && ( + <Text + testID='room-info-view-username' + style={[styles.roomUsername, { color: colors.auxiliaryText }]} + >{`@${username}`}</Text> + )} + {!!statusText && ( + <View testID='room-info-view-custom-status'> + <MarkdownPreview msg={statusText} style={[styles.roomUsername, { color: colors.auxiliaryText }]} /> + </View> + )} + </> + ); + } + return ( + <View style={styles.roomTitleContainer}> + <RoomTypeIcon + type={room?.prid ? 'discussion' : type} + teamMain={room?.teamMain} + key='room-info-type' + status={room?.visitor?.status} + sourceType={room?.source} + /> + <Text testID='room-info-view-name' style={[styles.roomTitle, { color: colors.titleText }]} key='room-info-name'> + {getRoomTitle(room)} + </Text> + </View> + ); +}; + +export default RoomInfoViewTitle; diff --git a/app/views/RoomInfoView/components/UserInfoButton.tsx b/app/views/RoomInfoView/components/UserInfoButton.tsx deleted file mode 100644 index 39a8000e6..000000000 --- a/app/views/RoomInfoView/components/UserInfoButton.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { Text } from 'react-native'; -import { BorderlessButton } from 'react-native-gesture-handler'; - -import { CustomIcon, TIconsName } from '../../../containers/CustomIcon'; -import styles from '../styles'; -import { useTheme } from '../../../theme'; -import { useVideoConf } from '../../../lib/hooks/useVideoConf'; -import i18n from '../../../i18n'; -import { useAppSelector } from '../../../lib/hooks'; -import { compareServerVersion } from '../../../lib/methods/helpers'; - -// TODO: change other icons on future -function UserInfoButton({ - danger, - iconName, - onPress, - label, - showIcon, - enabled = true -}: { - danger?: boolean; - iconName: TIconsName; - onPress?: (prop: any) => void; - label: string; - showIcon?: boolean; - enabled?: boolean; -}): React.ReactElement | null { - const { colors } = useTheme(); - let color = danger ? colors.dangerColor : colors.actionTintColor; - if (!enabled) color = colors.auxiliaryText; - if (showIcon) - return ( - <BorderlessButton enabled={enabled} testID={`room-info-view-${iconName}`} onPress={onPress} style={styles.roomButton}> - <CustomIcon name={iconName} size={30} color={color} /> - <Text style={[styles.roomButtonText, { color }]}>{label}</Text> - </BorderlessButton> - ); - return null; -} - -export function CallButton({ rid, isDirect }: { rid: string; isDirect: boolean }): React.ReactElement | null { - const { callEnabled, showInitCallActionSheet, disabledTooltip } = useVideoConf(rid); - const serverVersion = useAppSelector(state => state.server.version); - const greaterThanFive = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0'); - - const showIcon = greaterThanFive ? callEnabled : callEnabled && isDirect; - - return ( - <UserInfoButton - enabled={!disabledTooltip} - onPress={showInitCallActionSheet} - iconName='phone' - label={i18n.t('Call')} - showIcon={showIcon} - /> - ); -} diff --git a/app/views/RoomInfoView/index.tsx b/app/views/RoomInfoView/index.tsx index f4bedee05..94c77a3dd 100644 --- a/app/views/RoomInfoView/index.tsx +++ b/app/views/RoomInfoView/index.tsx @@ -1,226 +1,124 @@ -import { CompositeNavigationProp, RouteProp } from '@react-navigation/native'; +import { CompositeNavigationProp, RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; import { uniq } from 'lodash'; 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 React, { useEffect, useRef, useState } from 'react'; +import { ScrollView, View } from 'react-native'; +import { 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 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 { MarkdownPreview } from '../../containers/markdown'; -import { IApplicationState, ISubscription, IUser, SubscriptionType, TSubscriptionModel } from '../../definitions'; -import { ILivechatVisitor } from '../../definitions/ILivechatVisitor'; +import { ISubscription, IUser, SubscriptionType } from '../../definitions'; import I18n from '../../i18n'; -import { themes } from '../../lib/constants'; import { getSubscriptionByRoomId } from '../../lib/database/services/Subscription'; +import { useAppSelector } from '../../lib/hooks'; 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 { TUsersRoles } from '../../reducers/usersRoles'; 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 Direct from './Direct'; -import Livechat from './Livechat'; -import { CallButton } from './components/UserInfoButton'; +import { useTheme } from '../../theme'; +import RoomInfoButtons from './components/RoomInfoButtons'; +import RoomInfoViewAvatar from './components/RoomInfoViewAvatar'; +import RoomInfoViewBody from './components/RoomInfoViewBody'; +import RoomInfoViewTitle from './components/RoomInfoViewTitle'; import styles from './styles'; -interface IGetRoomTitle { - room: ISubscription; - type: SubscriptionType; - name?: string; - username: string; - statusText?: string; - theme: TSupportedThemes; -} +type TRoomInfoViewNavigationProp = CompositeNavigationProp< + StackNavigationProp<ChatsStackParamList, 'RoomInfoView'>, + StackNavigationProp<MasterDetailInsideStackParamList> +>; -const renderRoomTitle = ({ room, type, name, username, statusText, theme }: IGetRoomTitle) => - type === SubscriptionType.DIRECT ? ( - <> - <Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]}> - {name} - </Text> - {username && ( - <Text - testID='room-info-view-username' - style={[styles.roomUsername, { color: themes[theme].auxiliaryText }]} - >{`@${username}`}</Text> - )} - {!!statusText && ( - <View testID='room-info-view-custom-status'> - <MarkdownPreview msg={statusText} style={[styles.roomUsername, { color: themes[theme].auxiliaryText }]} /> - </View> - )} - </> - ) : ( - <View style={styles.roomTitleRow}> - <RoomTypeIcon - type={room.prid ? 'discussion' : room.t} - teamMain={room.teamMain} - key='room-info-type' - status={room.visitor?.status} - sourceType={room.source} - /> - <Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]} key='room-info-name'> - {getRoomTitle(room)} - </Text> - </View> +type TRoomInfoViewRouteProp = RouteProp<ChatsStackParamList, 'RoomInfoView'>; + +const RoomInfoView = (): React.ReactElement => { + const { + params: { rid, t, fromRid, member, room: roomParam, showCloseModal } + } = useRoute<TRoomInfoViewRouteProp>(); + const { addListener, setOptions, navigate, goBack } = useNavigation<TRoomInfoViewNavigationProp>(); + + const [room, setRoom] = useState(roomParam); + const [roomFromRid, setRoomFromRid] = useState<ISubscription | undefined>(); + const [roomUser, setRoomUser] = useState(member || {}); + const [showEdit, setShowEdit] = useState(false); + + const roomType = room?.t || t; + const isDirect = roomType === SubscriptionType.DIRECT; + const isLivechat = roomType === SubscriptionType.OMNICHANNEL; + + const subscription = useRef<Subscription | undefined>(undefined); + + const { + isMasterDetail, + subscribedRoom, + usersRoles, + roles, + // permissions + editRoomPermission, + editOmnichannelContact, + editLivechatRoomCustomfields + } = useAppSelector(state => ({ + subscribedRoom: state.room.subscribedRoom, + isMasterDetail: state.app.isMasterDetail, + roles: state.roles, + usersRoles: state.usersRoles, + // permissions + editRoomPermission: state.permissions['edit-room'], + editOmnichannelContact: state.permissions['edit-omnichannel-contact'], + editLivechatRoomCustomfields: state.permissions['edit-livechat-room-customfields'] + })); + + const { colors } = useTheme(); + + useEffect(() => { + const listener = addListener('focus', () => (isLivechat ? loadVisitor() : null)); + return () => listener(); + }, []); + + useEffect( + () => () => { + subscription.current?.unsubscribe(); + }, + [] ); -interface IRoomInfoViewProps { - navigation: CompositeNavigationProp< - StackNavigationProp<ChatsStackParamList, 'RoomInfoView'>, - StackNavigationProp<MasterDetailInsideStackParamList> - >; - route: RouteProp<ChatsStackParamList, 'RoomInfoView'>; - subscribedRoom: string; - theme: TSupportedThemes; - isMasterDetail: boolean; - jitsiEnabled: boolean; - editRoomPermission?: string[]; - editOmnichannelContact?: string[]; - editLivechatRoomCustomfields?: string[]; - roles: { [key: string]: string }; - usersRoles: TUsersRoles; -} + useEffect(() => { + loadRoom(); + if (isDirect) loadUser(); + }, []); -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<IRoomInfoViewProps, IRoomInfoViewState> { - private rid: string; - - private t: SubscriptionType; - - private unsubscribeFocus?: () => void; - - private subscription?: Subscription; - - private roomObservable?: Observable<TSubscriptionModel>; - - 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 ? () => <HeaderButton.CloseModal navigation={navigation} /> : undefined, - title: t === SubscriptionType.DIRECT ? I18n.t('User_Info') : I18n.t('Room_Info'), - headerRight: showEdit - ? () => ( - <HeaderButton.Container> - <HeaderButton.Item - iconName='edit' - onPress={() => { - 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' - /> - </HeaderButton.Container> - ) - : undefined + const setHeader = (canEdit?: boolean) => { + const HeaderRight = () => ( + <HeaderButton.Container> + <HeaderButton.Item + iconName='edit' + onPress={() => { + if (!room) return; + logEvent(events[`RI_GO_${isLivechat ? 'LIVECHAT' : 'RI'}_EDIT`]); + const navigationProps = { room, roomUser }; + if (isLivechat) navigate('LivechatEditView', navigationProps); + else navigate('RoomInfoEditView', { rid, ...navigationProps }); + }} + testID='room-info-view-edit-button' + /> + </HeaderButton.Container> + ); + setOptions({ + headerLeft: showCloseModal ? () => <HeaderButton.CloseModal /> : undefined, + title: isDirect ? I18n.t('User_Info') : I18n.t('Room_Info'), + headerRight: canEdit ? () => <HeaderRight /> : 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; + const loadVisitor = async () => { try { - if (room.visitor?._id) { + if (room?.visitor?._id) { const result = await Services.getVisitorInfo(room.visitor._id); if (result.success) { const { visitor } = result; @@ -231,7 +129,8 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat 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()); + setRoomUser({ ...visitor, ...params }); + setHeader(); } } } catch (error) { @@ -239,150 +138,116 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat } }; - parseRoles = (roleArray: string[]) => - Promise.all( - roleArray.map(async role => { - const description = await this.getRoleDescription(role); - return description; - }) - ); + const parseRoles = (roleArray: string[]) => roleArray.map(role => roles[role]); - setUser = async (user: IUser) => { - const roles = (() => { - const { usersRoles } = this.props; + const handleRoles = (user: Pick<IUser, 'username' | 'roles'>) => { + const rrr = (() => { const userRoles = usersRoles.find(u => u?.username === user.username); let r: string[] = []; if (userRoles?.roles?.length) r = userRoles.roles; if (user.roles?.length) r = [...r, ...user.roles]; return uniq(r); })(); - if (roles.length) { - const parsedRoles = await this.parseRoles(roles); - this.setState({ roomUser: { ...user, parsedRoles } }); - } else { - this.setState({ roomUser: user }); + if (rrr.length) { + const parsedRoles = parseRoles(rrr); + return parsedRoles; } }; - loadUser = async () => { - const { room, roomUser } = this.state; - + const loadUser = async () => { if (isEmpty(roomUser)) { try { - const roomUserId = getUidDirectMessage(room); + const roomUserId = getUidDirectMessage(room || { rid, t }); const result = await Services.getUserInfo(roomUserId); if (result.success) { const { user } = result; - this.setUser(user as IUser); + const r = handleRoles(user); + setRoomUser({ ...user, roles: r }); } } catch { // do nothing } - } else { - this.setUser(roomUser as IUser); } + const r = handleRoles(roomUser); + if (r) setRoomUser({ ...roomUser, roles: r }); }; - loadRoomFromRid = async () => { - if (this.fromRid) { - try { - const sub = await getSubscriptionByRoomId(this.fromRid); - this.subscriptionRoomFromRid = sub?.observe().subscribe(roomFromRid => { - this.setState({ roomFromRid }); + const loadRoom = async () => { + const permissionToEdit = isLivechat ? [editOmnichannelContact, editLivechatRoomCustomfields] : [editRoomPermission]; + const permissions = await hasPermission(permissionToEdit, rid); + const canEdit = permissions.some(Boolean); + const subRoom = await getSubscriptionByRoomId(rid); + if (!subRoom && isDirect && fromRid) { + const roomFromRid = await getSubscriptionByRoomId(fromRid); + if (roomFromRid?.observe) { + const sub = roomFromRid.observe(); + subscription.current = sub.subscribe(changes => { + setRoomFromRid(changes.asPlain()); }); - } 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 if (subRoom?.observe) { + const sub = subRoom.observe(); + subscription.current = sub.subscribe(changes => { + setRoom(changes.asPlain()); + setHeader(canEdit); }); } else { try { - const result = await Services.getRoomInfo(this.rid); - if (result.success) { - ({ room } = result); - this.setState({ room: { ...roomState, ...room } }); + if (!isDirect) { + const result = await Services.getRoomInfo(rid); + if (result.success) setRoom({ ...room, ...(result.room as unknown as ISubscription) }); } } 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()); - } + setShowEdit(canEdit); + setHeader(canEdit); }; - createDirect = () => - new Promise<void>(async (resolve, reject) => { - const { route } = this.props; - + const createDirect = () => + new Promise<void | ISubscription>(async (resolve, reject) => { // 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 + if (!isEmpty(member)) return resolve(); 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); - } + const result = await Services.createDirectMessage(roomUser.username); + if (result.success) return resolve({ ...roomUser, rid: result.room.rid }); } catch { - // do nothing + reject(); } - reject(); }); - goRoom = () => { + const handleGoRoom = (r?: ISubscription) => { 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) + rid: r?.rid, + name: getRoomTitle(r), + t: r?.t, + roomUserId: getUidDirectMessage(r) }; - if (room.rid) { - if (room.rid === subscribedRoom) { + if (r?.rid) { + if (r.rid === subscribedRoom) { if (isMasterDetail) { - return Navigation.navigate('DrawerNavigator'); + return navigate('DrawerNavigator'); } - return navigation.goBack(); + goBack(); + goBack(); + return; } // if it's on master detail layout, we close the modal and replace RoomView goRoom({ item: params, isMasterDetail, popToRoot: true }); } }; - handleCreateDirectMessage = async (onPress: () => void) => { + const handleCreateDirectMessage = async () => { try { - if (this.isDirect) { - await this.createDirect(); + let r = room; + if (isDirect) { + const direct = await createDirect(); + if (direct) r = direct; } - onPress(); + handleGoRoom(r); } catch { EventEmitter.emit(LISTENER, { message: I18n.t('error-action-not-allowed', { action: I18n.t('Create_Direct_Messages') }) @@ -390,144 +255,61 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat } }; - handleBlockUser = async (rid: string, blocked: string, block: boolean) => { + const handleBlockUser = async () => { + const r = roomFromRid || room; + const userBlocked = roomUser._id; + const blocker = r?.blocker; + if (!r?.rid) return; logEvent(events.RI_TOGGLE_BLOCK_USER); try { - await Services.toggleBlockUser(rid, blocked, block); + await Services.toggleBlockUser(r.rid, userBlocked, !blocker); } 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' }); + const handleIgnoreUser = () => { + const r = roomFromRid || room; + const isIgnored = r?.ignored?.includes?.(roomUser._id); + if (r?.rid) handleIgnore(roomUser._id, !isIgnored, r?.rid); }; - renderAvatar = (room: ISubscription, roomUser: IUserParsed) => { - const { theme } = this.props; - const { showEdit } = this.state; - const showAvatarEdit = showEdit && this.t !== SubscriptionType.OMNICHANNEL; + return ( + <ScrollView style={[styles.scroll, { backgroundColor: colors.backgroundColor }]}> + <StatusBar /> + <SafeAreaView style={{ backgroundColor: colors.backgroundColor }} testID='room-info-view'> + <View style={[styles.avatarContainer, { backgroundColor: colors.auxiliaryBackground }]}> + <RoomInfoViewAvatar + username={room?.name || roomUser.username} + rid={room?.rid} + userId={roomUser?._id} + handleEditAvatar={() => navigate('ChangeAvatarView', { titleHeader: I18n.t('Room_Info'), room, t, context: 'room' })} + showEdit={showEdit} + type={t} + /> + <RoomInfoViewTitle + type={t} + room={room || roomUser} + name={roomUser?.name} + username={roomUser?.username} + statusText={roomUser?.statusText} + /> + <RoomInfoButtons + rid={room?.rid || rid} + fromRid={fromRid} + handleBlockUser={handleBlockUser} + handleCreateDirectMessage={handleCreateDirectMessage} + handleIgnoreUser={handleIgnoreUser} + isDirect={isDirect} + room={room || roomUser} + roomUserId={roomUser?._id} + roomFromRid={roomFromRid} + /> + </View> + <RoomInfoViewBody isDirect={isDirect} room={room} roomUser={roomUser} /> + </SafeAreaView> + </ScrollView> + ); +}; - return ( - <AvatarWithEdit - text={room.name || roomUser.username} - style={styles.avatar} - type={this.t} - rid={room?.rid} - handleEdit={showAvatarEdit ? this.handleEditAvatar : undefined} - > - {this.t === SubscriptionType.DIRECT && roomUser._id ? ( - <View style={[sharedStyles.status, { backgroundColor: themes[theme].auxiliaryBackground }]}> - <Status size={20} id={roomUser._id} /> - </View> - ) : null} - </AvatarWithEdit> - ); - }; - - renderButton = (onPress: () => void, iconName: TIconsName, text: string, danger?: boolean) => { - const { theme } = this.props; - const color = danger ? themes[theme].dangerColor : themes[theme].actionTintColor; - return ( - <BorderlessButton testID={`room-info-view-${iconName}`} onPress={onPress} style={styles.roomButton}> - <CustomIcon name={iconName} size={30} color={color} /> - <Text style={[styles.roomButtonText, { color }]}>{text}</Text> - </BorderlessButton> - ); - }; - - 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 ( - <View style={styles.roomButtonsContainer}> - {this.renderButton(() => this.handleCreateDirectMessage(this.goRoom), 'message', I18n.t('Message'))} - <CallButton isDirect={this.isDirect} rid={room.rid} /> - {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} - </View> - ); - }; - - renderContent = () => { - const { room, roomUser } = this.state; - - if (this.isDirect) { - return <Direct roomUser={roomUser as IUserParsed} />; - } - - if (this.t === SubscriptionType.OMNICHANNEL) { - return <Livechat room={room} roomUser={roomUser as ILivechatVisitorModified} />; - } - return <Channel room={room} />; - }; - - render() { - const { room, roomUser } = this.state; - const { theme } = this.props; - const roomUserParsed = roomUser as IUserParsed; - - return ( - <ScrollView style={[styles.scroll, { backgroundColor: themes[theme].backgroundColor }]}> - <StatusBar /> - <SafeAreaView style={{ backgroundColor: themes[theme].backgroundColor }} testID='room-info-view'> - <View style={[styles.avatarContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}> - {this.renderAvatar(room, roomUserParsed)} - <View style={styles.roomTitleContainer}> - {renderRoomTitle({ - room, - type: this.t, - name: roomUserParsed?.name, - username: roomUserParsed?.username, - statusText: roomUserParsed?.statusText, - theme - })} - </View> - {this.renderButtons()} - </View> - {this.renderContent()} - </SafeAreaView> - </ScrollView> - ); - } -} - -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, - usersRoles: state.usersRoles -}); - -export default connect(mapStateToProps)(withTheme(RoomInfoView)); +export default RoomInfoView; diff --git a/app/views/RoomInfoView/styles.ts b/app/views/RoomInfoView/styles.ts index 1d405a968..22773af5f 100644 --- a/app/views/RoomInfoView/styles.ts +++ b/app/views/RoomInfoView/styles.ts @@ -30,7 +30,8 @@ export default StyleSheet.create({ roomTitleContainer: { paddingTop: 32, marginHorizontal: 16, - alignItems: 'center' + alignItems: 'center', + flexDirection: 'row' }, roomTitle: { fontSize: 20, diff --git a/e2e/helpers/data_setup.ts b/e2e/helpers/data_setup.ts index c191375ed..de9489ebc 100644 --- a/e2e/helpers/data_setup.ts +++ b/e2e/helpers/data_setup.ts @@ -196,4 +196,4 @@ export const initApi = async (user: string, password: string): Promise<AxiosInst api.defaults.headers.common['X-User-Id'] = userId; api.defaults.headers.common['X-Auth-Token'] = authToken; return api; -}; \ No newline at end of file +};