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

Merged
merged 33 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
92c3eb0
Merge branch 'main' into shared-message-thread
lucasbordeau Jul 29, 2024
56d8efe
Fix typing
lucasbordeau Jul 29, 2024
07d8091
Fix
lucasbordeau Jul 29, 2024
c077fa2
Push
lucasbordeau Jul 29, 2024
92d4206
Changes
lucasbordeau Jul 30, 2024
a748f8e
WIP
lucasbordeau Jul 30, 2024
976d081
Merge branch 'main' into shared-message-thread
lucasbordeau Jul 31, 2024
2200ebd
Fix
lucasbordeau Jul 31, 2024
12755bf
Fix
lucasbordeau Jul 31, 2024
dc4552c
Fixed feature flag
lucasbordeau Jul 31, 2024
39ec7e2
Fixed seeds
lucasbordeau Jul 31, 2024
3bf1d98
Fixed lint
lucasbordeau Jul 31, 2024
b01f17e
Removed
lucasbordeau Jul 31, 2024
6f5016f
Fix
lucasbordeau Jul 31, 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,16 @@
import { MessageThreadSubscribersDropdownButton } from '@/activities/emails/components/MessageThreadSubscribersDropdownButton';
import { MessageThread } from '@/activities/emails/types/MessageThread';

export const EmailThreadMembersChip = ({
messageThread,
}: {
messageThread: MessageThread;
}) => {
const subscribers = messageThread.subscribers ?? [];

return (
<MessageThreadSubscribersDropdownButton
messageThreadSubscribers={subscribers}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { MessageThreadSubscriberDropdownAddSubscriberMenuItem } from '@/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriberMenuItem';
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';

export const MessageThreadSubscriberDropdownAddSubscriber = ({
existingSubscribers,
}: {
existingSubscribers: MessageThreadSubscriber[];
}) => {
const { records: workspaceMembersLeftToAdd } =
useFindManyRecords<WorkspaceMember>({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
filter: {
not: {
id: {
in: existingSubscribers.map(
({ workspaceMember }) => workspaceMember.id,
),
},
},
},
});

return (
<DropdownMenuItemsContainer>
<DropdownMenuSearchInput />
<DropdownMenuSeparator />
{workspaceMembersLeftToAdd.map((workspaceMember) => (
<MessageThreadSubscriberDropdownAddSubscriberMenuItem
workspaceMember={workspaceMember}
/>
))}
</DropdownMenuItemsContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { MenuItemAvatar } from '@/ui/navigation/menu-item/components/MenuItemAvatar';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { IconPlus } from 'twenty-ui';

export const MessageThreadSubscriberDropdownAddSubscriberMenuItem = ({
workspaceMember,
}: {
workspaceMember: WorkspaceMember;
}) => {
const text = `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`;

const { createOneRecord } = useCreateOneRecord<MessageThreadSubscriber>({
objectNameSingular: CoreObjectNameSingular.MessageThreadSubscriber,
});

const handleAddButtonClick = () => {
createOneRecord({
workspaceMember,
});
};

return (
<MenuItemAvatar
avatar={{
placeholder: workspaceMember.name.firstName,
avatarUrl: workspaceMember.avatarUrl,
placeholderColorSeed: workspaceMember.id,
size: 'md',
type: 'rounded',
}}
text={text}
iconButtons={[
{
Icon: IconPlus,
onClick: handleAddButtonClick,
},
]}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext } from 'react';
import {
Avatar,
AvatarGroup,
Chip,
ChipVariant,
IconChevronDown,
ThemeContext,
} from 'twenty-ui';

const MAX_NUMBER_OF_AVATARS = 3;

export const MessageThreadSubscribersChip = ({
messageThreadSubscribers,
}: {
messageThreadSubscribers: MessageThreadSubscriber[];
}) => {
const { theme } = useContext(ThemeContext);

const numberOfMessageThreadSubscribers = messageThreadSubscribers.length;

const isOnlyOneSubscriber = numberOfMessageThreadSubscribers === 1;

const isPrivateThread = isOnlyOneSubscriber;

const privateLabel = 'Private';

const susbcriberAvatarUrls = messageThreadSubscribers
.map((member) => member.workspaceMember.avatarUrl)
.filter(isNonEmptyString);

const firstAvatarUrl = susbcriberAvatarUrls[0];
const firstAvatarColorSeed = messageThreadSubscribers?.[0].workspaceMember.id;
const firstAvatarPlaceholder =
messageThreadSubscribers?.[0].workspaceMember.name.firstName;

const subscriberNames = messageThreadSubscribers.map(
(member) => member.workspaceMember?.name.firstName,
);

const moreAvatarsLabel =
numberOfMessageThreadSubscribers > MAX_NUMBER_OF_AVATARS
? `+${numberOfMessageThreadSubscribers - MAX_NUMBER_OF_AVATARS}`
: null;

const label = isPrivateThread ? privateLabel : moreAvatarsLabel ?? '';

return (
<Chip
label={label}
variant={ChipVariant.Highlighted}
leftComponent={
isOnlyOneSubscriber ? (
<Avatar
avatarUrl={firstAvatarUrl}
placeholderColorSeed={firstAvatarColorSeed}
placeholder={firstAvatarPlaceholder}
size="md"
type={'rounded'}
/>
) : (
<AvatarGroup
avatars={subscriberNames.map((name, index) => (
<Avatar
key={name}
avatarUrl={susbcriberAvatarUrls[index] ?? ''}
placeholder={name}
type="rounded"
/>
))}
/>
)
}
rightComponent={<IconChevronDown size={theme.icon.size.sm} />}
clickable
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { offset } from '@floating-ui/react';
import { IconMinus, IconPlus } from 'twenty-ui';

import { MessageThreadSubscriberDropdownAddSubscriber } from '@/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriber';
import { MessageThreadSubscribersChip } from '@/activities/emails/components/MessageThreadSubscribersChip';
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
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 { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemAvatar } from '@/ui/navigation/menu-item/components/MenuItemAvatar';
import { useState } from 'react';

export const MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID =
'message-thread-subscriber';

export const MessageThreadSubscribersDropdownButton = ({
messageThreadSubscribers,
}: {
messageThreadSubscribers: MessageThreadSubscriber[];
}) => {
const [isAddingSubscriber, setIsAddingSubscriber] = useState(false);

const { closeDropdown } = useDropdown(MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID);

const mockSubscribers = [
...messageThreadSubscribers,
...messageThreadSubscribers,
...messageThreadSubscribers,
...messageThreadSubscribers,
];

// TODO: implement
const handleAddSubscriberClick = () => {
setIsAddingSubscriber(true);
};

// TODO: implement
const handleRemoveSubscriber = (_subscriber: MessageThreadSubscriber) => {
closeDropdown();
};

useListenRightDrawerClose(() => {
closeDropdown();
});

return (
<Dropdown
dropdownId={MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID}
clickableComponent={
<MessageThreadSubscribersChip
messageThreadSubscribers={mockSubscribers}
/>
}
dropdownComponents={
<DropdownMenu width="160px" z-index={offset(1)}>
{isAddingSubscriber ? (
<MessageThreadSubscriberDropdownAddSubscriber
existingSubscribers={messageThreadSubscribers}
/>
) : (
<DropdownMenuItemsContainer>
{messageThreadSubscribers?.map((subscriber) => (
<MenuItemAvatar
key={subscriber.workspaceMember.id}
testId="menu-item"
onClick={() => {
handleRemoveSubscriber(subscriber);
}}
text={
subscriber.workspaceMember.name.firstName +
' ' +
subscriber.workspaceMember.name.lastName
}
avatar={{
placeholder: subscriber.workspaceMember.name.firstName,
avatarUrl: subscriber.workspaceMember.avatarUrl,
placeholderColorSeed: subscriber.workspaceMember.id,
size: 'md',
type: 'rounded',
}}
iconButtons={[
{
Icon: IconMinus,
onClick: () => {
handleRemoveSubscriber(subscriber);
},
},
]}
/>
))}
<DropdownMenuSeparator />
<MenuItem
LeftIcon={IconPlus}
onClick={handleAddSubscriberClick}
text="Add subscriber"
/>
</DropdownMenuItemsContainer>
)}
</DropdownMenu>
}
dropdownHotkeyScope={{
scope: MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID,
}}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';

export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperationSignatureFactory =
({ messageThreadId }: { messageThreadId: string }) => ({
({
messageThreadId,
isSubscribersEnabled,
}: {
messageThreadId: string;
isSubscribersEnabled: boolean;
}) => ({
objectNameSingular: CoreObjectNameSingular.Message,
variables: {
filter: {
Expand All @@ -25,6 +31,18 @@ export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperation
subject: true,
text: true,
receivedAt: true,
messageThread: {
id: true,
subscribers: isSubscribersEnabled
? {
workspaceMember: {
id: true,
name: true,
avatarUrl: true,
},
}
: undefined,
},
messageParticipants: {
id: true,
role: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import styled from '@emotion/styled';
import { useRecoilCallback } from 'recoil';
import { useEffect, useMemo } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';

import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
Expand All @@ -8,8 +9,8 @@ import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMe
import { IntermediaryMessages } from '@/activities/emails/right-drawer/components/IntermediaryMessages';
import { useRightDrawerEmailThread } from '@/activities/emails/right-drawer/hooks/useRightDrawerEmailThread';
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';
lucasbordeau marked this conversation as resolved.
Show resolved Hide resolved
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';

const StyledContainer = styled.div`
Expand All @@ -22,21 +23,31 @@ const StyledContainer = styled.div`
position: relative;
`;

const getVisibleMessages = (messages: EmailThreadMessageType[]) =>
messages.filter(({ messageParticipants }) => {
const from = messageParticipants.find(
(participant) => participant.role === 'from',
);
const receivers = messageParticipants.filter(
(participant) => participant.role !== 'from',
);
return from && receivers.length > 0;
});

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

const { thread, messages, fetchMoreMessages, loading } =
useRightDrawerEmailThread();

const visibleMessages = useMemo(() => {
return messages.filter(({ messageParticipants }) => {
const from = messageParticipants.find(
(participant) => participant.role === 'from',
);
const receivers = messageParticipants.filter(
(participant) => participant.role !== 'from',
);
return from && receivers.length > 0;
});
}, [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
Loading
Loading