Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share an email thread to workspace members chip and dropdown (#4199) #5640

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4f3345f
feat: add everyone column to messageThread table
pereira0x May 20, 2024
fa523a5
feat: add messageThreadMembers table
pereira0x May 21, 2024
49ef2a7
feat: add messageThreadMember repository functionality
simaosanguinho May 22, 2024
408abf0
feat: add messageThreadMembersBar
simaosanguinho May 22, 2024
a890f5b
feat: add initial messageThreadMembersChip boilerplate
pereira0x May 22, 2024
97dcab2
feat: add member prop to emailThreadMembersChip
simaosanguinho May 22, 2024
b6ff717
feat: return everyone field on MessageThreads fetching
pereira0x May 25, 2024
069ee17
feat: add MessageThread info when fetching Messages
pereira0x May 26, 2024
74e5028
feat: pass MessageThread data to MessageThreadMembersBar
pereira0x May 26, 2024
31c535d
feat: display messageThreadMembersChip depending on number of members
pereira0x May 26, 2024
8209fbb
feat: seed a new messageThreadMember
simaosanguinho May 27, 2024
3a409d1
feat: add messageThreadMembers share button and dropdown
simaosanguinho May 27, 2024
cbabdfa
feat: add participant in messageThreadMembers chip (#4199)
pereira0x May 27, 2024
5e0e316
fix: rename object-metadata to workspace-entity
pereira0x May 28, 2024
4074fed
fix: remove unwanted menu separator
simaosanguinho May 29, 2024
0f08608
fix: apply greptile-apps bot suggestions
pereira0x Jun 4, 2024
6ad1047
fix: resolve rebase conflicts
simaosanguinho Jun 10, 2024
df594e5
fix: resolve rebase conflicts
pereira0x Jun 17, 2024
0e94dc5
fix: resolve rebase conflicts
pereira0x Jun 29, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { MessageThread } from '@/activities/emails/types/MessageThread';
import { SharedDropdownMenu } from '@/ui/layout/dropdown/components/SharedDropdownMenu';

export const EmailThreadMembersChip = ({
messageThread,
}: {
messageThread: MessageThread | null;
}) => {
const renderChip = () => {
if (!messageThread) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming isEveryone to isVisibleToEveryone for better clarity.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't feel the need for this change, as we wanted the variable to be consistent with the database table.

return null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using optional chaining (?.) when the variable is already checked for null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct, we will be fixing this in our next commit.

const isEveryone = messageThread?.everyone;
const numberOfMessageThreadMembers =
messageThread.messageThreadMember.length;
switch (isEveryone) {
case false:
if (numberOfMessageThreadMembers === 1) {
return (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label prop for SharedDropdownMenu is missing when numberOfMessageThreadMembers is not 1. Consider adding a default label.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Label already has a default value (empty string).

<SharedDropdownMenu
label="Private"
messageThreadMembers={messageThread.messageThreadMember}
/>
);
} else {
return (
<SharedDropdownMenu
messageThreadMembers={messageThread.messageThreadMember}
/>
);
}
case true:
return (
<SharedDropdownMenu
label="Everyone"
messageThreadMembers={messageThread.messageThreadMember}
everyone={true}
/>
);
default:
return null;
}
};

return <>{renderChip()} </>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,16 @@ export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperation
text: true,
receivedAt: true,
messageParticipants: true,
messageThread: {
id: true,
everyone: true,
messageThreadMember: {
workspaceMember: {
id: true,
name: true,
avatarUrl: true,
},
},
},
},
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect } from 'react';
import styled from '@emotion/styled';
import { useRecoilCallback } from 'recoil';
import { useRecoilCallback, useSetRecoilState } from 'recoil';

import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
Expand All @@ -10,6 +11,7 @@ import { useRightDrawerEmailThread } from '@/activities/emails/right-drawer/hook
import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState';
import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the declaration of setMessageThread above the useEffect hook for better readability.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will apply this change as well.

import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';

const StyledContainer = styled.div`
Expand All @@ -34,8 +36,17 @@ const getVisibleMessages = (messages: EmailThreadMessageType[]) =>
});

export const RightDrawerEmailThread = () => {
const setMessageThread = useSetRecoilState(messageThreadState);

const { thread, messages, fetchMoreMessages, loading } =
useRightDrawerEmailThread();
const visibleMessages = getVisibleMessages(messages);
useEffect(() => {
if (!visibleMessages[0]?.messageThread) {
return;
}
setMessageThread(visibleMessages[0]?.messageThread);
});

const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener(
RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
Expand All @@ -60,7 +71,6 @@ export const RightDrawerEmailThread = () => {
return null;
}

const visibleMessages = getVisibleMessages(messages);
const visibleMessagesCount = visibleMessages.length;
const is5OrMoreMessages = visibleMessagesCount >= 5;
const firstMessages = visibleMessages.slice(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
import { MessageThread } from '@/activities/emails/types/MessageThread';

export type EmailThreadMessage = {
id: string;
Expand All @@ -7,5 +8,6 @@ export type EmailThreadMessage = {
subject: string;
messageThreadId: string;
messageParticipants: EmailThreadMessageParticipant[];
messageThread: MessageThread;
__typename: 'EmailThreadMessage';
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { MessageThreadMember } from '@/activities/emails/types/MessageThreadMember';

export type MessageThread = {
id: string;
everyone: boolean;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming everyone to something more descriptive like isVisibleToAll for better clarity.

Copy link
Contributor

@simaosanguinho simaosanguinho Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We think everyone is fine for this case.

messageThreadMember: MessageThreadMember[];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';

export type MessageThreadMember = {
workspaceMember: WorkspaceMember;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';

import { EmailThreadMembersChip } from '@/activities/emails/components/EmailThreadMembersChip';
import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState';

const StyledButtonContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
margin-left: ${({ theme }) => theme.spacing(3)};
`;

export const MessageThreadMembersBar = () => {
const messageThread = useRecoilValue(messageThreadState);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle the case where messageThread might be null or undefined to avoid potential runtime errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already handled.

return (
<StyledButtonContainer>
<EmailThreadMembersChip messageThread={messageThread} />
</StyledButtonContainer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import {
SingleEntitySelectMenuItems,
SingleEntitySelectMenuItemsProps,
} from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch';
import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { isDefined } from '~/utils/isDefined';

import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';

export type SingleEntitySelectMenuItemsWithSearchProps = {
excludedRelationRecordIds?: string[];
onCreate?: ((searchInput?: string) => void) | (() => void);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { useTheme } from '@emotion/react';
import { offset } from '@floating-ui/react';
import {
Avatar,
Chip,
ChipVariant,
IconChevronDown,
IconPlus,
IconUserCircle,
MultiChip,
} from 'twenty-ui';

import { MessageThreadMember } from '@/activities/emails/types/MessageThreadMember';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';

export const SharedDropdownMenu = ({
messageThreadMembers,
label = '',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a type check for messageThreadMembers to ensure it is an array before mapping.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already use Typescript that says that MessageThreadMember is MessageThreadMember[].

everyone = false,
}: {
messageThreadMembers: MessageThreadMember[] | null;
label?: string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle the case where workspaceMember or workspaceMember.name might be undefined to avoid potential runtime errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We followed the same approach as with the other components, so we don't believe this is necessary.

everyone?: boolean;
}) => {
const messageThreadMembersAvatarUrls = messageThreadMembers?.map(
(member) => member.workspaceMember.avatarUrl,
);

const messageThreadMembersNames = messageThreadMembers?.map(
(member) => member.workspaceMember?.name.firstName,
);

const theme = useTheme();

const { closeDropdown } = useDropdown('message-thread-share');

const handleAddParticipantClick = () => {
closeDropdown();
};

const handleParticipantClick = () => {
closeDropdown();
};

return (
<Dropdown
dropdownId={'message-thread-share'}
clickableComponent={
everyone ? (
<Chip
label="Everyone"
variant={ChipVariant.Highlighted}
leftComponent={<IconUserCircle size={theme.icon.size.md} />}
rightComponent={<IconChevronDown size={theme.icon.size.sm} />}
/>
) : messageThreadMembers?.length === 1 ? (
<Chip
label={label}
variant={ChipVariant.Highlighted}
leftComponent={
<Avatar
avatarUrl={getImageAbsoluteURIOrBase64(
messageThreadMembersAvatarUrls?.[0],
)}
entityId={messageThreadMembers?.[0].workspaceMember.id}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting the avatar rendering logic into a separate function to improve readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't think that this is necessary for this case.

placeholder={messageThreadMembersNames?.[0]}
size="md"
type={'rounded'}
/>
}
rightComponent={<IconChevronDown size={theme.icon.size.sm} />}
/>
) : (
<MultiChip
names={messageThreadMembersNames ?? []}
RightIcon={IconChevronDown}
avatarUrls={
(messageThreadMembersAvatarUrls?.filter(Boolean) as string[]) ??
[]
}
/>
)
}
dropdownComponents={
<DropdownMenu width="160px" z-index={offset(1)}>
<DropdownMenuItemsContainer>
{messageThreadMembers?.map((member) => (
<MenuItemSelectAvatar
key={member.workspaceMember.id}
selected={false}
testId="menu-item"
onClick={handleParticipantClick}
text={
member.workspaceMember.name.firstName +
' ' +
member.workspaceMember.name.lastName
}
avatar={
<Avatar
avatarUrl={getImageAbsoluteURIOrBase64(
member.workspaceMember.avatarUrl,
)}
entityId={member.workspaceMember.id}
placeholder={member.workspaceMember.name.firstName}
size="md"
type={'rounded'}
/>
}
/>
))}
<DropdownMenuSeparator />
<MenuItem
LeftIcon={IconPlus}
onClick={handleAddParticipantClick}
text="Add participant"
/>
</DropdownMenuItemsContainer>
</DropdownMenu>
}
dropdownHotkeyScope={{
scope: 'message-thread-share',
}}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import { Chip, ChipAccent, ChipSize, useIcons } from 'twenty-ui';

import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar';
import { MessageThreadMembersBar } from '@/activities/right-drawer/components/MessageThreadMembersBar';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { RightDrawerTopBarCloseButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton';
Expand Down Expand Up @@ -103,6 +104,9 @@ export const RightDrawerTopBar = ({ page }: { page: RightDrawerPages }) => {
</StyledMinimizeTopBarTitleContainer>
)}
<StyledTopBarWrapper>
{page === RightDrawerPages.ViewEmailThread && (
<MessageThreadMembersBar />
)}
{!isMobile && !isRightDrawerMinimized && (
<RightDrawerTopBarMinimizeButton />
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createState } from 'twenty-ui';

import { MessageThread } from '@/activities/emails/types/MessageThread';

export const messageThreadState = createState<MessageThread | null>({
key: 'messageThreadState',
defaultValue: null,
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { seedMessageChannel } from 'src/database/typeorm-seeds/workspace/message
import { seedMessageChannelMessageAssociation } from 'src/database/typeorm-seeds/workspace/message-channel-message-associations';
import { seedMessageParticipant } from 'src/database/typeorm-seeds/workspace/message-participants';
import { seedMessageThread } from 'src/database/typeorm-seeds/workspace/message-threads';
import { seedMessageThreadMember } from 'src/database/typeorm-seeds/workspace/message-thread-members';
import { viewPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/view';
import { seedCalendarEvents } from 'src/database/typeorm-seeds/workspace/calendar-events';
import { seedCalendarChannels } from 'src/database/typeorm-seeds/workspace/calendar-channel';
Expand Down Expand Up @@ -131,6 +132,10 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
entityManager,
dataSourceMetadata.schema,
);
await seedMessageThreadMember(
entityManager,
dataSourceMetadata.schema,
);
await seedMessage(entityManager, dataSourceMetadata.schema);
await seedMessageChannel(
entityManager,
Expand Down
Loading
Loading