Show call recorders as calendar event participants (#23380)
Closes twentyhq/core-team-issues#2729 Call recordings attached to a calendar event are now displayed next to the human participants, in the timeline event card (`EventCardCalendarEvent`). They are rendered as the source app's `AppChip`, rounded so it sits in the participant avatar group, with the recording status in tooltip https://github.com/user-attachments/assets/e0c393c8-fd65-4468-8c4f-503dd22c13d5 ## Before No call recorder chip displayed TODO: add this in the calendar views (`CalendarEventRow`)
This commit is contained in:
+74
@@ -0,0 +1,74 @@
|
||||
import { AppChip } from '@/applications/components/AppChip';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useId } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/surfaces';
|
||||
import { CallRecordingStatus } from '~/generated/graphql';
|
||||
|
||||
const IN_PROGRESS_CALL_RECORDING_STATUSES: CallRecordingStatus[] = [
|
||||
CallRecordingStatus.JOINING,
|
||||
CallRecordingStatus.RECORDING,
|
||||
CallRecordingStatus.PROCESSING,
|
||||
];
|
||||
|
||||
const getCallRecordingStatusLabel = (status: CallRecordingStatus) => {
|
||||
switch (status) {
|
||||
case CallRecordingStatus.SCHEDULED:
|
||||
return t`Scheduled`;
|
||||
case CallRecordingStatus.JOINING:
|
||||
return t`Joining`;
|
||||
case CallRecordingStatus.RECORDING:
|
||||
return t`Recording`;
|
||||
case CallRecordingStatus.PROCESSING:
|
||||
return t`Processing`;
|
||||
case CallRecordingStatus.COMPLETED:
|
||||
return t`Completed`;
|
||||
case CallRecordingStatus.FAILED:
|
||||
return t`Failed`;
|
||||
}
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div<{ isDisabled: boolean }>`
|
||||
display: flex;
|
||||
filter: ${({ isDisabled }) => (isDisabled ? 'grayscale(1)' : 'none')};
|
||||
`;
|
||||
|
||||
type CalendarEventCallRecorderAvatarProps = {
|
||||
applicationId?: string | null;
|
||||
status: CallRecordingStatus;
|
||||
};
|
||||
|
||||
export const CalendarEventCallRecorderAvatar = ({
|
||||
applicationId,
|
||||
status,
|
||||
}: CalendarEventCallRecorderAvatarProps) => {
|
||||
const instanceId = useId();
|
||||
const hasFailed = status === CallRecordingStatus.FAILED;
|
||||
const tooltipAnchorId = `call-recorder-${instanceId.replace(/[^a-zA-Z0-9-_]/g, '-')}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContainer id={tooltipAnchorId} isDisabled={hasFailed}>
|
||||
<AppChip
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={{ name: t`Call recorder` }}
|
||||
pulsing={IN_PROGRESS_CALL_RECORDING_STATUSES.includes(status)}
|
||||
size="md"
|
||||
rounded
|
||||
chipOnly
|
||||
/>
|
||||
</StyledContainer>
|
||||
{createPortal(
|
||||
<AppTooltip
|
||||
anchorSelect={`#${tooltipAnchorId}`}
|
||||
content={getCallRecordingStatusLabel(status)}
|
||||
delay={TooltipDelay.shortDelay}
|
||||
place={TooltipPosition.Top}
|
||||
positionStrategy="fixed"
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+30
-17
@@ -1,3 +1,5 @@
|
||||
import { CalendarEventCallRecorderAvatar } from '@/activities/calendar/components/CalendarEventCallRecorderAvatar';
|
||||
import { type CalendarEventCallRecording } from '@/activities/calendar/types/CalendarEventCallRecording';
|
||||
import { type CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
|
||||
import { isTimelineCalendarEventParticipant } from '@/activities/calendar/types/guards/IsTimelineCalendarEventParticipant';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -7,10 +9,12 @@ import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
|
||||
type CalendarEventParticipantsAvatarGroupProps = {
|
||||
participants: CalendarEventParticipant[] | TimelineCalendarEventParticipant[];
|
||||
callRecordings?: CalendarEventCallRecording[];
|
||||
};
|
||||
|
||||
export const CalendarEventParticipantsAvatarGroup = ({
|
||||
participants,
|
||||
callRecordings = [],
|
||||
}: CalendarEventParticipantsAvatarGroupProps) => {
|
||||
const timelineParticipants: TimelineCalendarEventParticipant[] =
|
||||
participants.map((participant) => {
|
||||
@@ -47,23 +51,32 @@ export const CalendarEventParticipantsAvatarGroup = ({
|
||||
|
||||
return (
|
||||
<AvatarGroup
|
||||
avatars={timelineParticipants.map((participant) => (
|
||||
<Avatar
|
||||
key={[participant.workspaceMemberId, participant.displayName]
|
||||
.filter(isDefined)
|
||||
.join('-')}
|
||||
avatarUrl={getAbsoluteImageUrl(participant.avatarUrl)}
|
||||
placeholder={
|
||||
participant.firstName && participant.lastName
|
||||
? `${participant.firstName} ${participant.lastName}`
|
||||
: participant.displayName
|
||||
}
|
||||
placeholderColorSeed={
|
||||
participant.workspaceMemberId || participant.personId || ''
|
||||
}
|
||||
type="rounded"
|
||||
/>
|
||||
))}
|
||||
avatars={[
|
||||
...callRecordings.map((callRecording) => (
|
||||
<CalendarEventCallRecorderAvatar
|
||||
key={callRecording.id}
|
||||
applicationId={callRecording.applicationId}
|
||||
status={callRecording.status}
|
||||
/>
|
||||
)),
|
||||
...timelineParticipants.map((participant) => (
|
||||
<Avatar
|
||||
key={[participant.workspaceMemberId, participant.displayName]
|
||||
.filter(isDefined)
|
||||
.join('-')}
|
||||
avatarUrl={getAbsoluteImageUrl(participant.avatarUrl)}
|
||||
placeholder={
|
||||
participant.firstName && participant.lastName
|
||||
? `${participant.firstName} ${participant.lastName}`
|
||||
: participant.displayName
|
||||
}
|
||||
placeholderColorSeed={
|
||||
participant.workspaceMemberId || participant.personId || ''
|
||||
}
|
||||
type="rounded"
|
||||
/>
|
||||
)),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-2
@@ -125,9 +125,11 @@ export const CalendarEventRow = ({
|
||||
<CalendarEventNotSharedContent />
|
||||
)}
|
||||
</StyledLabels>
|
||||
{!!calendarEvent.participants?.length && (
|
||||
{(!!calendarEvent.participants?.length ||
|
||||
!!calendarEvent.callRecordings?.length) && (
|
||||
<CalendarEventParticipantsAvatarGroup
|
||||
participants={calendarEvent.participants}
|
||||
participants={calendarEvent.participants ?? []}
|
||||
callRecordings={calendarEvent.callRecordings}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const timelineCalendarEventCallRecordingFragment = gql`
|
||||
fragment TimelineCalendarEventCallRecordingFragment on TimelineCalendarEventCallRecording {
|
||||
id
|
||||
status
|
||||
applicationId
|
||||
}
|
||||
`;
|
||||
+5
@@ -1,3 +1,4 @@
|
||||
import { timelineCalendarEventCallRecordingFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventCallRecordingFragment';
|
||||
import { timelineCalendarEventParticipantFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventParticipantFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
@@ -14,6 +15,10 @@ export const timelineCalendarEventFragment = gql`
|
||||
participants {
|
||||
...TimelineCalendarEventParticipantFragment
|
||||
}
|
||||
callRecordings {
|
||||
...TimelineCalendarEventCallRecordingFragment
|
||||
}
|
||||
}
|
||||
${timelineCalendarEventParticipantFragment}
|
||||
${timelineCalendarEventCallRecordingFragment}
|
||||
`;
|
||||
|
||||
+4
@@ -23,6 +23,7 @@ const calendarEvents: TimelineCalendarEvent[] = [
|
||||
isCanceled: false,
|
||||
location: 'Location',
|
||||
participants: [],
|
||||
callRecordings: [],
|
||||
title: 'Title',
|
||||
__typename: 'TimelineCalendarEvent',
|
||||
},
|
||||
@@ -42,6 +43,7 @@ const calendarEvents: TimelineCalendarEvent[] = [
|
||||
isCanceled: false,
|
||||
location: 'Location',
|
||||
participants: [],
|
||||
callRecordings: [],
|
||||
title: 'Title',
|
||||
__typename: 'TimelineCalendarEvent',
|
||||
},
|
||||
@@ -61,6 +63,7 @@ const calendarEvents: TimelineCalendarEvent[] = [
|
||||
isCanceled: false,
|
||||
location: 'Location',
|
||||
participants: [],
|
||||
callRecordings: [],
|
||||
title: 'Title',
|
||||
__typename: 'TimelineCalendarEvent',
|
||||
},
|
||||
@@ -80,6 +83,7 @@ const calendarEvents: TimelineCalendarEvent[] = [
|
||||
isCanceled: false,
|
||||
location: 'Location',
|
||||
participants: [],
|
||||
callRecordings: [],
|
||||
title: 'Title',
|
||||
__typename: 'TimelineCalendarEvent',
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { type CalendarEventCallRecording } from '@/activities/calendar/types/CalendarEventCallRecording';
|
||||
import { type CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
|
||||
import { type CalendarChannelVisibility } from '~/generated/graphql';
|
||||
|
||||
@@ -18,5 +19,6 @@ export type CalendarEvent = {
|
||||
title?: string;
|
||||
visibility: CalendarChannelVisibility;
|
||||
calendarEventParticipants?: CalendarEventParticipant[];
|
||||
callRecordings?: CalendarEventCallRecording[];
|
||||
__typename: 'CalendarEvent';
|
||||
};
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type CallRecordingStatus } from '~/generated/graphql';
|
||||
|
||||
export type CalendarEventCallRecording = {
|
||||
id: string;
|
||||
status: CallRecordingStatus;
|
||||
applicationId?: string | null;
|
||||
};
|
||||
+9
-2
@@ -116,6 +116,11 @@ export const EventCardCalendarEvent = ({
|
||||
handle: true,
|
||||
displayName: true,
|
||||
},
|
||||
callRecordings: {
|
||||
id: true,
|
||||
status: true,
|
||||
applicationId: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -195,9 +200,11 @@ export const EventCardCalendarEvent = ({
|
||||
{calendarEvent.title}
|
||||
</StyledCalendarEventTitle>
|
||||
)}
|
||||
{!!calendarEvent.calendarEventParticipants?.length && (
|
||||
{(!!calendarEvent.calendarEventParticipants?.length ||
|
||||
!!calendarEvent.callRecordings?.length) && (
|
||||
<CalendarEventParticipantsAvatarGroup
|
||||
participants={calendarEvent.calendarEventParticipants}
|
||||
participants={calendarEvent.calendarEventParticipants ?? []}
|
||||
callRecordings={calendarEvent.callRecordings}
|
||||
/>
|
||||
)}
|
||||
</StyledCalendarEventTop>
|
||||
|
||||
@@ -17,6 +17,8 @@ type AppChipProps = {
|
||||
};
|
||||
className?: string;
|
||||
chipOnly?: boolean;
|
||||
rounded?: boolean;
|
||||
pulsing?: boolean;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -38,6 +40,8 @@ export const AppChip = ({
|
||||
fallbackApplicationData,
|
||||
className,
|
||||
chipOnly = false,
|
||||
rounded = false,
|
||||
pulsing = false,
|
||||
}: AppChipProps) => {
|
||||
const { applicationChipData } = useApplicationChipData({
|
||||
applicationId,
|
||||
@@ -47,7 +51,7 @@ export const AppChip = ({
|
||||
return (
|
||||
<StyledContainer className={className}>
|
||||
<Avatar
|
||||
type="app"
|
||||
type={rounded ? 'rounded' : 'app'}
|
||||
size={size}
|
||||
avatarUrl={getAbsoluteImageUrl(logoUrl ?? applicationChipData.logo)}
|
||||
placeholder={applicationChipData.name}
|
||||
@@ -55,6 +59,7 @@ export const AppChip = ({
|
||||
color={applicationChipData.colors?.color}
|
||||
backgroundColor={applicationChipData.colors?.backgroundColor}
|
||||
borderColor={applicationChipData.colors?.borderColor}
|
||||
pulsing={pulsing}
|
||||
/>
|
||||
{!chipOnly && (
|
||||
<OverflowingTextWithTooltip text={applicationChipData.name} />
|
||||
|
||||
+1
@@ -49,6 +49,7 @@ export const SettingsAccountsCalendarChannelsGeneral = () => {
|
||||
workspaceMemberId: currentWorkspaceMember?.id || '',
|
||||
},
|
||||
],
|
||||
callRecordings: [],
|
||||
endsAt: exampleEndDate.toISOString(),
|
||||
isFullDay: false,
|
||||
startsAt: exampleStartDate.toISOString(),
|
||||
|
||||
Reference in New Issue
Block a user