snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 15:56:32 +05:00
commit 1d2bfcac0c
3159 changed files with 464524 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
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 { formatAmount } from '../../functions';
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;
const hasContacts = salesforceData?.contacts?.length;
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 $${formatAmount(salesforceData.deal.amount)}`
: ''
}` }
name = { salesforceData.deal.opportunityName }
/* eslint-disable-next-line react/jsx-no-bind */
onAction = { () => onUnlink('Opportunity', salesforceData.deal!.opportunityId) } />
)}
</View>
);
};
export default CurrentLinksSection;

View File

@@ -0,0 +1,137 @@
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 { formatAmount } from '../../functions';
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 $${formatAmount(deal.amount)}`}
</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;

View File

@@ -0,0 +1,114 @@
import React from 'react';
import { Text, View, ViewStyle } from 'react-native';
import Icon from '../../../base/icons/components/Icon';
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 styles from './styles';
interface IProps {
/**
* Translation key for the action button.
*/
actionLabelKey: string;
/**
* Button type for the action button.
*/
actionType?: BUTTON_TYPES;
/**
* Whether to use compact styling (no background, less padding).
*/
compact?: boolean;
/**
* Whether the action button should be disabled.
*/
disabled?: boolean;
/**
* The icon component to display.
*/
icon: Function;
/**
* Whether this item's action is in progress.
*/
isLoading?: boolean;
/**
* Secondary text (type label, company, stage, etc.).
*/
metadata?: string;
/**
* The primary display name.
*/
name: string;
/**
* Click handler for the action button.
*/
onAction: () => void;
}
/**
* Reusable list item component for Salesforce records with an action button.
*
* @param {IProps} props - Component props.
* @returns {React.ReactElement}
*/
export const RecordListItem = ({
actionLabelKey,
actionType = BUTTON_TYPES.PRIMARY,
compact = false,
disabled = false,
icon,
isLoading = false,
metadata,
name,
onAction
}: IProps) => {
const itemStyle = compact ? styles.listItemCompact : styles.listItem;
return (
<View style = { itemStyle as ViewStyle }>
<View style = { styles.listItemIcon as ViewStyle }>
<Icon
size = { 24 }
src = { icon } />
</View>
<View style = { styles.listItemInfo as ViewStyle }>
<Text
numberOfLines = { 1 }
style = { styles.listItemName }>
{name}
</Text>
{metadata && (
<Text style = { styles.listItemMeta }>
{metadata}
</Text>
)}
</View>
<View style = { styles.listItemButtonContainer as ViewStyle }>
{isLoading ? (
<View style = { styles.listItemSpinnerContainer as ViewStyle }>
<LoadingIndicator size = 'small' />
</View>
) : (
<Button
disabled = { disabled }
labelKey = { actionLabelKey }
onClick = { onAction }
type = { actionType } />
)}
</View>
</View>
);
};
export default RecordListItem;

View File

@@ -0,0 +1,138 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, Text, View, ViewStyle } from 'react-native';
import { IconSearch } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
import { StyleType } from '../../../base/styles/functions.any';
import Input from '../../../base/ui/components/native/Input';
import { useSalesforceLinkDialog } from '../../useSalesforceLinkDialog';
import { CurrentLinksSection } from './CurrentLinksSection';
import { PendingSection } from './PendingSection';
import { SearchResultsSection } from './SearchResultsSection';
import styles from './styles';
/**
* Component that renders the Salesforce link dialog.
*
* @returns {React$Element<any>}
*/
const SalesforceLinkDialog = () => {
const { t } = useTranslation();
const {
salesforceData,
searchResults,
hasSearchResults,
hasPendingAccounts,
hasPendingDeals,
searchTerm,
isLoading,
isSearching,
error,
confirmingAccountId,
confirmingDealId,
rejectingAccounts,
rejectingDeals,
linkingId,
unlinkingId,
setSearchTerm,
handleConfirmAccount,
handleRejectAllAccounts,
handleConfirmDeal,
handleRejectAllDeals,
handleLinkRecord,
handleUnlinkRecord
} = useSalesforceLinkDialog();
const handleSearchChange = useCallback((value: string) => {
setSearchTerm(value);
}, [ setSearchTerm ]);
const renderSearch = () => (
<View style = { styles.searchContainer as ViewStyle }>
<Input
icon = { IconSearch }
onChange = { handleSearchChange }
placeholder = { t('dialog.searchInSalesforce') }
value = { searchTerm } />
</View>
);
const renderContent = () => {
if (isLoading) {
return (
<View style = { styles.spinnerContainer as ViewStyle }>
<LoadingIndicator />
</View>
);
}
return (
<ScrollView
bounces = { false }
contentContainerStyle = { styles.scrollContent }>
{/* Search spinner */}
{isSearching && (
<View style = { styles.spinnerContainer as ViewStyle }>
<LoadingIndicator />
</View>
)}
{/* Search results */}
{!isSearching && hasSearchResults && searchResults && (
<SearchResultsSection
linkingId = { linkingId }
onLink = { handleLinkRecord }
results = { searchResults } />
)}
{/* Error (only show when not searching and no results) */}
{!isSearching && !hasSearchResults && error && (
<Text style = { styles.errorText }>
{error}
</Text>
)}
{/* Pending accounts (when not searching) */}
{!searchTerm && hasPendingAccounts && salesforceData?.pendingAccounts && (
<PendingSection
confirmingId = { confirmingAccountId }
items = { salesforceData.pendingAccounts }
onConfirm = { handleConfirmAccount }
onRejectAll = { handleRejectAllAccounts }
rejecting = { rejectingAccounts }
type = 'accounts' />
)}
{/* Pending deals (when not searching) */}
{!searchTerm && hasPendingDeals && salesforceData?.pendingDeals && (
<PendingSection
confirmingId = { confirmingDealId }
items = { salesforceData.pendingDeals }
onConfirm = { handleConfirmDeal }
onRejectAll = { handleRejectAllDeals }
rejecting = { rejectingDeals }
type = 'deals' />
)}
{/* Current links */}
<CurrentLinksSection
onUnlink = { handleUnlinkRecord }
salesforceData = { salesforceData }
unlinkingId = { unlinkingId } />
</ScrollView>
);
};
return (
<JitsiScreen style = { styles.salesforceDialogContainer as StyleType }>
{renderSearch()}
{renderContent()}
</JitsiScreen>
);
};
export default SalesforceLinkDialog;

View File

@@ -0,0 +1,137 @@
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 { formatAmount } from '../../functions';
import {
IAccountMatch,
IContactMatch,
ILeadMatch,
IOpportunityMatch,
ISearchResults,
SalesforceObjectType
} from '../../types';
import { RecordListItem } from './RecordListItem';
import styles from './styles';
interface IProps {
linkingId: string | null;
onLink: (type: SalesforceObjectType, data: IAccountMatch | ILeadMatch | IContactMatch | IOpportunityMatch) => void;
results: ISearchResults;
}
/**
* Component for displaying Salesforce search results with link buttons.
*
* @param {IProps} props - Component props.
* @returns {React.ReactElement}
*/
export const SearchResultsSection = ({ results, linkingId, onLink }: IProps) => {
const { t } = useTranslation();
const hasResults = results.accounts.length
|| results.leads.length
|| results.contacts.length
|| results.opportunities.length;
if (!hasResults) {
return (
<View style = { styles.noResultsContainer as ViewStyle }>
<Text style = { styles.noResultsText }>
{t('dialog.searchResultsNotFound')}
</Text>
</View>
);
}
return (
<View>
{!!results.accounts.length && (
<>
<Text style = { styles.groupTitle }>{t('record.type.account')}</Text>
{results.accounts.map(account => (
<RecordListItem
actionLabelKey = 'dialog.link'
actionType = { BUTTON_TYPES.PRIMARY }
disabled = { linkingId !== null }
icon = { IconRecordAccount }
isLoading = { linkingId === account.accountId }
key = { account.accountId }
name = { account.accountName }
/* eslint-disable-next-line react/jsx-no-bind */
onAction = { () => onLink('Account', account) } />
))}
</>
)}
{!!results.leads.length && (
<>
<Text style = { styles.groupTitle }>{t('record.type.lead')}</Text>
{results.leads.map(lead => (
<RecordListItem
actionLabelKey = 'dialog.link'
actionType = { BUTTON_TYPES.PRIMARY }
disabled = { linkingId !== null }
icon = { IconRecordLead }
isLoading = { linkingId === lead.leadId }
key = { lead.leadId }
metadata = { lead.leadCompany }
name = { lead.leadName }
/* eslint-disable-next-line react/jsx-no-bind */
onAction = { () => onLink('Lead', lead) } />
))}
</>
)}
{!!results.contacts.length && (
<>
<Text style = { styles.groupTitle }>{t('record.type.contact')}</Text>
{results.contacts.map(contact => (
<RecordListItem
actionLabelKey = 'dialog.link'
actionType = { BUTTON_TYPES.PRIMARY }
disabled = { linkingId !== null }
icon = { IconRecordContact }
isLoading = { linkingId === contact.contactId }
key = { contact.contactId }
metadata = { contact.contactEmail }
name = { contact.contactName }
/* eslint-disable-next-line react/jsx-no-bind */
onAction = { () => onLink('Contact', contact) } />
))}
</>
)}
{!!results.opportunities.length && (
<>
<Text style = { styles.groupTitle }>{t('record.type.opportunity')}</Text>
{results.opportunities.map(opp => (
<RecordListItem
actionLabelKey = 'dialog.link'
actionType = { BUTTON_TYPES.PRIMARY }
disabled = { linkingId !== null }
icon = { IconRecordOpportunity }
isLoading = { linkingId === opp.opportunityId }
key = { opp.opportunityId }
metadata = { `${opp.opportunityStage}${
opp.amount !== undefined ? ` \u2022 $${formatAmount(opp.amount)}` : ''
}` }
name = { opp.opportunityName }
/* eslint-disable-next-line react/jsx-no-bind */
onAction = { () => onLink('Opportunity', opp) } />
))}
</>
)}
</View>
);
};
export default SearchResultsSection;

View File

@@ -0,0 +1,164 @@
import { TextStyle, ViewStyle } from 'react-native';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
interface IStyles {
currentLinksSection: ViewStyle;
errorText: TextStyle;
groupTitle: TextStyle;
listItem: ViewStyle;
listItemButtonContainer: ViewStyle;
listItemCompact: ViewStyle;
listItemIcon: ViewStyle;
listItemInfo: ViewStyle;
listItemMeta: TextStyle;
listItemName: TextStyle;
listItemSpinnerContainer: ViewStyle;
noResultsContainer: ViewStyle;
noResultsText: TextStyle;
pendingHeader: ViewStyle;
salesforceDialogContainer: ViewStyle;
scrollContent: ViewStyle;
searchContainer: ViewStyle;
section: ViewStyle;
sectionDescription: TextStyle;
sectionTitle: TextStyle;
spinnerContainer: ViewStyle;
}
const styles: IStyles = {
// Main container
salesforceDialogContainer: {
position: 'relative',
flexDirection: 'column',
flex: 1,
backgroundColor: BaseTheme.palette.ui01
},
// Search
searchContainer: {
alignSelf: 'stretch',
backgroundColor: BaseTheme.palette.ui01,
paddingHorizontal: BaseTheme.spacing[3],
paddingTop: BaseTheme.spacing[2],
paddingBottom: BaseTheme.spacing[3]
},
// Loading/spinner
spinnerContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: BaseTheme.spacing[6]
},
// Error
errorText: {
color: BaseTheme.palette.textError,
textAlign: 'center',
padding: BaseTheme.spacing[4]
},
// No results
noResultsContainer: {
alignItems: 'center',
justifyContent: 'center',
padding: BaseTheme.spacing[4]
},
noResultsText: {
color: BaseTheme.palette.text03,
textAlign: 'center'
},
// Section styles
section: {
marginBottom: BaseTheme.spacing[4],
paddingHorizontal: BaseTheme.spacing[3]
},
sectionTitle: {
fontSize: 15,
fontWeight: '600',
color: BaseTheme.palette.text01,
marginBottom: BaseTheme.spacing[2]
},
sectionDescription: {
fontSize: 13,
color: BaseTheme.palette.text03,
marginBottom: BaseTheme.spacing[3]
},
// Pending section header
pendingHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: BaseTheme.spacing[2]
},
// Current links section
currentLinksSection: {
marginTop: BaseTheme.spacing[4],
paddingTop: BaseTheme.spacing[3],
paddingHorizontal: BaseTheme.spacing[3],
borderTopWidth: 1,
borderTopColor: BaseTheme.palette.ui05
},
// Group title (for search results)
groupTitle: {
fontSize: 12,
fontWeight: '600',
color: BaseTheme.palette.text03,
textTransform: 'uppercase',
marginTop: BaseTheme.spacing[3],
marginBottom: BaseTheme.spacing[2],
paddingHorizontal: BaseTheme.spacing[3]
},
// RecordListItem styles
listItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: BaseTheme.palette.ui03,
borderRadius: BaseTheme.shape.borderRadius,
padding: BaseTheme.spacing[3],
marginBottom: BaseTheme.spacing[2],
marginHorizontal: BaseTheme.spacing[3]
},
listItemCompact: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: BaseTheme.spacing[2]
},
listItemIcon: {
width: 24,
height: 24,
marginRight: BaseTheme.spacing[3]
},
listItemInfo: {
flex: 1
},
listItemName: {
fontSize: 15,
color: BaseTheme.palette.text01
},
listItemMeta: {
fontSize: 13,
color: BaseTheme.palette.text03,
marginTop: 2
},
listItemButtonContainer: {
marginLeft: BaseTheme.spacing[3]
},
listItemSpinnerContainer: {
width: 70,
alignItems: 'center',
justifyContent: 'center'
},
// Scroll container
scrollContent: {
flexGrow: 1
}
};
export default styles;