Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lang/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,9 +344,11 @@
"contactSupport": "Contact support",
"copied": "Copied",
"copy": "Copy",
"currentLinks": "Current Links",
"demoteParticipantDialog": "Are you sure you want to move this participant to viewer?",
"demoteParticipantTitle": "Move to viewer",
"dismiss": "Dismiss",
"dismissAll": "Dismiss All",
"displayNameRequired": "Hi! What's your name?",
"done": "Done",
"e2eeDescription": "End-to-End Encryption is currently EXPERIMENTAL. Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.",
Expand Down Expand Up @@ -374,6 +376,7 @@
"kickSystemTitle": "Ouch! You were kicked out of the meeting",
"kickTitle": "Ouch! {{participantDisplayName}} kicked you out of the meeting",
"learnMore": "Learn more",
"link": "Link",
"linkMeeting": "Link meeting",
"linkMeetingTitle": "Link meeting to Salesforce",
"liveStreaming": "Live Streaming",
Expand Down Expand Up @@ -439,6 +442,8 @@
"passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) not supported",
"passwordRequired": "$t(lockRoomPasswordUppercase) required",
"pendingDealsDescription": "These opportunities may be related to this meeting",
"pendingDescription": "We found these potential matches based on participant emails",
"permissionCameraRequiredError": "Camera permission is required to participate in conferences with video. Please grant it in Settings",
"permissionErrorTitle": "Permission required",
"permissionMicRequiredError": "Microphone permission is required to participate in conferences with audio. Please grant it in Settings",
Expand All @@ -465,6 +470,7 @@
"reservationError": "Reservation system error",
"reservationErrorMsg": "Error code: {{code}}, message: {{msg}}",
"retry": "Retry",
"salesforceDataError": "Failed to load Salesforce data",
"screenSharingAudio": "Share audio",
"screenSharingFailed": "Oops! Something went wrong, we weren't able to start screen sharing!",
"screenSharingFailedTitle": "Screen sharing failed!",
Expand Down Expand Up @@ -516,6 +522,8 @@
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
"stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
"streamKey": "Live stream key",
"suggestedAccounts": "Suggested Accounts",
"suggestedDeals": "Suggested Deals",
"thankYou": "Thank you for using {{appName}}!",
"token": "token",
"tokenAuthFailed": "Sorry, you're not allowed to join this call.",
Expand All @@ -540,6 +548,7 @@
"tokenAuthUnsupported": "Token URL is not supported.",
"transcribing": "Transcribing",
"unauthenticatedAccessDisabled": "This call requires authentication. Please login in order to proceed.",
"unlink": "Unlink",
"unlockRoom": "Remove meeting $t(lockRoomPassword)",
"user": "User",
"userIdentifier": "User identifier",
Expand Down Expand Up @@ -820,6 +829,10 @@
"audioUnmuteBlockedDescription": "Mic unmute operation has been temporarily blocked because of system limits.",
"audioUnmuteBlockedTitle": "Mic unmute blocked!",
"chatMessages": "Chat messages",
"confirmAccountError": "Failed to confirm account",
"confirmAccountSuccess": "Account confirmed",
"confirmDealError": "Failed to confirm deal",
"confirmDealSuccess": "Deal confirmed",
"connectedOneMember": "{{name}} joined the meeting",
"connectedThreePlusMembers": "{{name}} and many others joined the meeting",
"connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
Expand All @@ -833,6 +846,8 @@
"disabledIframeSecondaryNative": "Embedding {{domain}} is only meant for demo purposes, so this call will disconnect in {{timeout}} minutes.",
"disabledIframeSecondaryWeb": "Embedding {{domain}} is only meant for demo purposes, so this call will disconnect in {{timeout}} minutes. Please use <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> for production embedding!",
"disconnected": "disconnected",
"dismissError": "Failed to dismiss suggestions",
"dismissSuccess": "Suggestions dismissed",
"displayNotifications": "Display notifications for",
"dontRemindMe": "Do not remind me",
"focus": "Conference focus",
Expand Down Expand Up @@ -909,6 +924,8 @@
"suggestRecordingAction": "Start",
"suggestRecordingDescription": "Would you like to start a recording?",
"suggestRecordingTitle": "Record this meeting",
"unlinkFromSalesforceError": "Failed to unlink from Salesforce",
"unlinkFromSalesforceSuccess": "Unlinked from Salesforce",
"unmute": "Unmute Audio",
"unmuteScreen": "Start screen sharing",
"unmuteVideo": "Unmute Video",
Expand Down
111 changes: 111 additions & 0 deletions react/features/salesforce/components/native/CurrentLinksSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Text, View, ViewStyle } from 'react-native';

import {
IconRecordAccount,
IconRecordContact,
IconRecordLead,
IconRecordOpportunity
} from '../../../base/icons/svg';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { ISalesforceData, SalesforceObjectType } from '../../types';

import { RecordListItem } from './RecordListItem';
import styles from './styles';

interface IProps {
onUnlink: (type: SalesforceObjectType, id: string) => void;
salesforceData: ISalesforceData | null;
unlinkingId: string | null;
}

/**
* Component for displaying currently linked Salesforce objects with unlink buttons.
*
* @param {IProps} props - Component props.
* @returns {React.ReactElement}
*/
export const CurrentLinksSection = ({ salesforceData, unlinkingId, onUnlink }: IProps) => {
const { t } = useTranslation();

const hasAccount = Boolean(salesforceData?.account);
const hasLeads = (salesforceData?.leads?.length ?? 0) > 0;
const hasContacts = (salesforceData?.contacts?.length ?? 0) > 0;
const hasDeal = Boolean(salesforceData?.deal);
const hasAnyLinks = hasAccount || hasLeads || hasContacts || hasDeal;

if (!hasAnyLinks) {
return null;
}

return (
<View style = { styles.currentLinksSection as ViewStyle }>
<Text style = { styles.sectionTitle }>{t('dialog.currentLinks')}</Text>

{salesforceData?.account && (
<RecordListItem
actionLabelKey = 'dialog.unlink'
actionType = { BUTTON_TYPES.TERTIARY }
compact = { true }
disabled = { unlinkingId !== null }
icon = { IconRecordAccount }
isLoading = { unlinkingId === salesforceData.account.accountId }
metadata = { t('record.type.account') }
name = { salesforceData.account.accountName }
/* eslint-disable-next-line react/jsx-no-bind */
onAction = { () => onUnlink('Account', salesforceData.account!.accountId) } />
)}

{salesforceData?.leads?.map(lead => (
<RecordListItem
actionLabelKey = 'dialog.unlink'
actionType = { BUTTON_TYPES.TERTIARY }
compact = { true }
disabled = { unlinkingId !== null }
icon = { IconRecordLead }
isLoading = { unlinkingId === lead.leadId }
key = { lead.leadId }
metadata = { `${t('record.type.lead')}${lead.leadCompany ? ` \u2022 ${lead.leadCompany}` : ''}` }
name = { lead.leadName }
/* eslint-disable-next-line react/jsx-no-bind */
onAction = { () => onUnlink('Lead', lead.leadId) } />
))}

{salesforceData?.contacts?.map(contact => (
<RecordListItem
actionLabelKey = 'dialog.unlink'
actionType = { BUTTON_TYPES.TERTIARY }
compact = { true }
disabled = { unlinkingId !== null }
icon = { IconRecordContact }
isLoading = { unlinkingId === contact.contactId }
key = { contact.contactId }
metadata = { t('record.type.contact') }
name = { contact.contactName }
/* eslint-disable-next-line react/jsx-no-bind */
onAction = { () => onUnlink('Contact', contact.contactId) } />
))}

{salesforceData?.deal && (
<RecordListItem
actionLabelKey = 'dialog.unlink'
actionType = { BUTTON_TYPES.TERTIARY }
compact = { true }
disabled = { unlinkingId !== null }
icon = { IconRecordOpportunity }
isLoading = { unlinkingId === salesforceData.deal.opportunityId }
metadata = { `${salesforceData.deal.opportunityStage}${
salesforceData.deal.amount !== undefined
? ` \u2022 $${salesforceData.deal.amount.toLocaleString()}`
: ''
}` }
name = { salesforceData.deal.opportunityName }
/* eslint-disable-next-line react/jsx-no-bind */
onAction = { () => onUnlink('Opportunity', salesforceData.deal!.opportunityId) } />
)}
</View>
);
};

export default CurrentLinksSection;
136 changes: 136 additions & 0 deletions react/features/salesforce/components/native/PendingSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Text, View, ViewStyle } from 'react-native';

import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { IPendingAccount, IPendingDeal } from '../../types';

import styles from './styles';

interface IPendingAccountsSectionProps {
confirmingId: string | null;
items: IPendingAccount[];
onConfirm: (accountId: string) => void;
onRejectAll: () => void;
rejecting: boolean;
type: 'accounts';
}

interface IPendingDealsSectionProps {
confirmingId: string | null;
items: IPendingDeal[];
onConfirm: (opportunityId: string) => void;
onRejectAll: () => void;
rejecting: boolean;
type: 'deals';
}

type IProps = IPendingAccountsSectionProps | IPendingDealsSectionProps;

/**
* Component for displaying pending Salesforce suggestions (accounts or deals).
*
* @param {IProps} props - Component props.
* @returns {React.ReactElement}
*/
export const PendingSection = (props: IProps) => {
const { t } = useTranslation();
const { items, confirmingId, rejecting, onConfirm, onRejectAll, type } = props;

if (items.length === 0) {
return null;
}

const isAccounts = type === 'accounts';
const title = isAccounts
? t('dialog.suggestedAccounts')
: t('dialog.suggestedDeals');
const description = isAccounts
? t('dialog.pendingDescription')
: t('dialog.pendingDealsDescription');

return (
<View style = { styles.section as ViewStyle }>
<View style = { styles.pendingHeader as ViewStyle }>
<Text style = { styles.sectionTitle }>{title}</Text>
<Button
disabled = { rejecting }
labelKey = 'dialog.dismissAll'
onClick = { onRejectAll }
type = { BUTTON_TYPES.TERTIARY } />
</View>
<Text style = { styles.sectionDescription }>{description}</Text>

{isAccounts ? (
(items as IPendingAccount[]).map(account => (
<View
key = { account.accountId }
style = { styles.listItem as ViewStyle }>
<View style = { styles.listItemInfo as ViewStyle }>
<Text
numberOfLines = { 1 }
style = { styles.listItemName }>
{account.accountName}
</Text>
{account.matchedEmailDomain && (
<Text style = { styles.listItemMeta }>
{account.matchedEmailDomain}
</Text>
)}
</View>
<View style = { styles.listItemButtonContainer as ViewStyle }>
{confirmingId === account.accountId ? (
<View style = { styles.listItemSpinnerContainer as ViewStyle }>
<LoadingIndicator size = 'small' />
</View>
) : (
<Button
disabled = { confirmingId !== null }
labelKey = 'dialog.confirm'
/* eslint-disable-next-line react/jsx-no-bind */
onClick = { () => onConfirm(account.accountId) }
type = { BUTTON_TYPES.PRIMARY } />
)}
</View>
</View>
))
) : (
(items as IPendingDeal[]).map(deal => (
<View
key = { deal.opportunityId }
style = { styles.listItem as ViewStyle }>
<View style = { styles.listItemInfo as ViewStyle }>
<Text
numberOfLines = { 1 }
style = { styles.listItemName }>
{deal.opportunityName}
</Text>
<Text style = { styles.listItemMeta }>
{deal.opportunityStage}
{deal.amount !== undefined && ` \u2022 $${deal.amount.toLocaleString()}`}
</Text>
</View>
<View style = { styles.listItemButtonContainer as ViewStyle }>
{confirmingId === deal.opportunityId ? (
<View style = { styles.listItemSpinnerContainer as ViewStyle }>
<LoadingIndicator size = 'small' />
</View>
) : (
<Button
disabled = { confirmingId !== null }
labelKey = 'dialog.confirm'
/* eslint-disable-next-line react/jsx-no-bind */
onClick = { () => onConfirm(deal.opportunityId) }
type = { BUTTON_TYPES.PRIMARY } />
)}
</View>
</View>
))
)}
</View>
);
};

export default PendingSection;
Loading
Loading