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

feat: add invite modal #4009

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

feat: add invite modal #4009

wants to merge 6 commits into from

Conversation

CzarekDryl
Copy link
Contributor

@CzarekDryl CzarekDryl commented Dec 27, 2024

Description

  • Added invite button to header
  • Added invite modal
image image

Testing

  1. Header Button:
  • Visible only to connected and joined users within a colony.
  • Triggers the "Invite Members" modal.
  1. Invite Members Modal:
  • Includes description, number of invites remaining, and a shareable unique invite URL.
  • Provides an easy way to copy the URL.
  1. Member Limits (Early Access):
  • Default limit: 100 members.
  • UI shows invites available, used, and remaining.
  • Notifies users if the limit is reached.
  • Includes a button to request more invites, linking to: https://colony.io/request-member-invites?colony={colony_name}

Resolves #3927

@CzarekDryl CzarekDryl self-assigned this Dec 27, 2024
@CzarekDryl CzarekDryl marked this pull request as ready for review December 30, 2024 14:22
@CzarekDryl CzarekDryl requested a review from a team as a code owner December 30, 2024 14:22
Copy link
Member

@arrenv arrenv left a comment

Choose a reason for hiding this comment

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

@CzarekDryl Nice work, thank you for the PR, working as expected. Just a couple of points of feedback.

  1. Can we have the hover state consistent with the other header buttons with the blue border:
    image

image

  1. The "Up to 100 people can join" is hard coded, can the number 100 be dynamic based on the actual invites available.

},
modalDescription: {
id: `${displayName}.modalDescription`,
defaultMessage:
'You can invite up to 100 members to your Colony during the private beta using the invite link below.',
'Up to 100 people can join to follow and be apart of this colony during early access. If you exceed the limit, you can request more.',
Copy link
Member

Choose a reason for hiding this comment

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

Can the 100 be dynamic based on actual limit returned from DB.

/>
) : (
<Link
to={`https://colony.io/request-member-invites?colony=${colonyName}`}
Copy link
Member

Choose a reason for hiding this comment

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

Can this be added to the const file with all other URLs so that it can easily be updated.

@CzarekDryl
Copy link
Contributor Author

@arrenv All the things have been fixed and are it's ready for review again

@CzarekDryl CzarekDryl requested a review from arrenv January 7, 2025 07:58
Copy link
Contributor

@mmioana mmioana left a comment

Choose a reason for hiding this comment

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

Hey @CzarekDryl really nice start on this issue 💯

The modal is working as expected

Screen.Recording.2025-01-07.at.12.24.28.mov

Screenshot 2025-01-07 at 12 24 10

However I left some refactoring comments and found a couple of misalignments with Figma if you could please be so kind to tackle them 🙌

Comment on lines 93 to 103
{!isActionSidebarOpen && isWalletConnected && user && isInColony ? (
<Button
text={isMobile ? undefined : MSG.invite}
mode="tertiary"
icon={UserPlus}
size="small"
isFullRounded
className="md:hover:!border-blue-400"
onClick={() => setIsInviteMembersModalOpen(true)}
/>
) : null}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to add iconSize={16} here according to Figma

icon={UserPlus}
size="small"
isFullRounded
className="md:hover:!border-blue-400"
Copy link
Contributor

Choose a reason for hiding this comment

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

And please add gap-1

@@ -54,43 +76,76 @@ const InviteMembersModal = ({ isOpen, onClose }: Props) => {
`/invite/${colonyName}/${colonyMemberInvite?.id}`,
);
const invitesUsed = 100 - (colonyMemberInvite?.invitesRemaining || 0);
const isOutOfInvites = invitesUsed >= invitesAvailable;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we could make use of the const { totalMemberCount } = useMemberContext(); here to compute how many invites are used and colonyMemberInvite?.invitesRemaining for deciding how many invites are available, thus replacing lines 74-79 with

  const inviteLink = useBaseUrl(
    `/invite/${colonyName}/${colonyMemberInvite?.id}`,
  );
  const { totalMemberCount } = useMemberContext();
  const invitesAvailable = colonyMemberInvite?.invitesRemaining || 0;
  const invitesUsed = totalMemberCount;
  const isOutOfInvites = invitesUsed >= invitesAvailable;

Copy link
Contributor

Choose a reason for hiding this comment

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

@arrenv do we want to hardcode the invites limit to 100?
I think it would be best to actually use the stored remaining invites, as well as the number of existing followers and decide based on that if we are or are not out of invites

Copy link
Member

@arrenv arrenv Jan 7, 2025

Choose a reason for hiding this comment

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

@CzarekDryl & @mmioana no, I was not previously aware this was hardcoded, this is a value that can be increased in the database. As a result of this and to minimise impact on the database, we updated the design last week to remove the need to show the upper limit and just show the remaining available.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for your response @arrenv 🙌

In this case and after looking over the code, I saw we actually have a lambda in place that will subtract 1 from the remaining invites once a user accept it. Initially, due to our dev env I thought we need to account also for the existing number of followers 😶‍🌫️

So, in this case @CzarekDryl, I believe isOutOfInvites should just check if invitesAvailable is 0.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually let's please refactor this component a bit, given we have lots of conditional content based on the isOutOfInvites flag

So my proposal would be the following

const InviteMembersModal = ({ isOpen, onClose }: Props) => {
  const {
    colony: { colonyMemberInvite, name: colonyName },
  } = useColonyContext();
  const inviteLink = useBaseUrl(
    `/invite/${colonyName}/${colonyMemberInvite?.id}`,
  );
  const { totalMemberCount } = useMemberContext();
  const invitesAvailable = colonyMemberInvite?.invitesRemaining || 0;
  const invitesUsed = totalMemberCount;
  const isOutOfInvites = invitesUsed >= invitesAvailable;

  const { handleClipboardCopy, isCopied } = useCopyToClipboard();

  const commonClassName = 'rounded-lg p-2 text-sm font-medium';

  const getModalSubtitle = () => {
    if (isOutOfInvites) {
      <FormattedMessage
        {...MSG.modalDescriptionReached}
        values={{ invitesAvailable }}
      />;
    }

    return (
      <FormattedMessage
        {...MSG.modalDescription}
        values={{ invitesAvailable }}
      />
    );
  };

  const getModalContent = () => {
    if (isOutOfInvites) {
      return (
        <CardWithCallout
          title={
            <span
              className={clsx(
                commonClassName,
                'bg-negative-100 text-negative-400',
              )}
            >
              <FormattedMessage
                {...MSG.invitesUsed}
                values={{ invitesAvailable }}
              />
            </span>
          }
          subtitle={<FormattedMessage {...MSG.limitReached} />}
          button={
            <Link
              to={getRequestInvitesLink(colonyName)}
              target="_blank"
              className="flex min-h-8.5 items-center justify-center gap-2 whitespace-nowrap 
              rounded-lg border border-gray-900 bg-base-white px-2.5 py-1.5 text-sm font-medium text-gray-900 
              transition-all duration-normal disabled:border-gray-300 disabled:text-gray-300 md:hover:border-gray-900 
              md:hover:bg-gray-900 md:hover:!text-base-white"
            >
              {formatText(MSG.requestInvites)}
            </Link>
          }
        >
          <FormattedMessage {...MSG.requestMoreInvites} />
        </CardWithCallout>
      );
    }

    return (
      <CardWithCallout
        title={
          <span className={clsx(commonClassName, 'bg-blue-100 text-blue-400')}>
            <FormattedMessage
              {...MSG.invitesUsed}
              values={{ invitesAvailable }}
            />
          </span>
        }
        subtitle={<FormattedMessage {...MSG.inviteLinkHeading} />}
        button={
          <Button
            text={MSG.buttonText}
            mode={isCopied ? 'completed' : 'quinary'}
            icon={isCopied ? undefined : CopySimple}
            onClick={() => handleClipboardCopy(inviteLink)}
            size="small"
            textValues={{ isCopied }}
          />
        }
      >
        {inviteLink}
      </CardWithCallout>
    );
  };

  return (
    <Modal isOpen={isOpen} onClose={() => onClose()}>
      <div className="mb-8 mt-10 flex flex-col items-center">
        <SmileySticker size={42} className="mb-3" />
        <Heading3
          appearance={{ theme: 'dark' }}
          className="font-semibold text-gray-900"
          text={MSG.modalTitle}
        />
        <p className="mt-1 text-center text-sm text-gray-600">
          {getModalSubtitle()}
        </p>
      </div>
      {getModalContent()}
    </Modal>
  );
};

Of course, bonus points if you feel like splitting the code up into partial components 💯

feat: add request button

fix: modal changes, button hover

fix: description copy
@CzarekDryl CzarekDryl force-pushed the feat/16205063-invite-modal branch from 26ccfd5 to 2bd744a Compare January 15, 2025 15:45
@CzarekDryl CzarekDryl requested a review from mmioana January 15, 2025 16:02
Copy link
Contributor

@mmioana mmioana left a comment

Choose a reason for hiding this comment

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

Thanks for implementing my suggestions @CzarekDryl 🥇

The invite modal works really nicely 🌻

Screen.Recording.2025-01-16.at.19.46.38.mov

I took the chance to manually modify the invites remaining value
Screenshot 2025-01-16 at 19 48 33
Screenshot 2025-01-16 at 19 51 10
And it showed up in the modal 👍
Screenshot 2025-01-16 at 19 51 27

However @CzarekDryl, due to my misunderstanding, we shouldn't account for totalMemberCount for the isOutOfInvites flag. I left a comment in the code, if you can please check 🙏

@@ -54,43 +76,76 @@ const InviteMembersModal = ({ isOpen, onClose }: Props) => {
`/invite/${colonyName}/${colonyMemberInvite?.id}`,
);
const invitesUsed = 100 - (colonyMemberInvite?.invitesRemaining || 0);
const isOutOfInvites = invitesUsed >= invitesAvailable;
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for your response @arrenv 🙌

In this case and after looking over the code, I saw we actually have a lambda in place that will subtract 1 from the remaining invites once a user accept it. Initially, due to our dev env I thought we need to account also for the existing number of followers 😶‍🌫️

So, in this case @CzarekDryl, I believe isOutOfInvites should just check if invitesAvailable is 0.

@CzarekDryl
Copy link
Contributor Author

@mmioana Fixed :)

@CzarekDryl CzarekDryl requested a review from mmioana January 18, 2025 15:02
arrenv
arrenv previously approved these changes Jan 20, 2025
Copy link
Member

@arrenv arrenv left a comment

Choose a reason for hiding this comment

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

@CzarekDryl Thank you for the updates, looks to be working well!

Modal working well

image

Hover state updated

image

Invite works

image

Count updates

image

No invites remaining

image

Goes to the correct link and populates the form:

image

Copy link
Contributor

@mmioana mmioana left a comment

Choose a reason for hiding this comment

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

Hey @CzarekDryl really nice work, now it behaves properly 🙌

With 100 invites available
Screenshot 2025-01-20 at 11 31 16
With 12 invites available
Screenshot 2025-01-20 at 11 32 45
With no invites available
Screenshot 2025-01-20 at 11 33 06
Got redirected to the proper page when requesting more invites
Screenshot 2025-01-20 at 11 33 13
The copy-link functionality works as expected
Screenshot 2025-01-20 at 11 33 35

However @CzarekDryl I did find some mismatches according to Figma for the mobile version

  1. The copy link action is on a new line
    Screenshot 2025-01-20 at 11 34 27
    While in Figma
    Screenshot 2025-01-20 at 11 34 41
  2. Same for Request invites
    Screenshot 2025-01-20 at 11 35 09
    While in Figma
    Screenshot 2025-01-20 at 11 35 19
  3. The invite button is fully rounded
    Screenshot 2025-01-20 at 11 36 06
    While in Figma there is some horizontal padding
    Screenshot 2025-01-20 at 11 36 12

After these issues are fixed, we're good to go 🎉

@CzarekDryl
Copy link
Contributor Author

@mmioana

Button unified with others so it's 46px width now:
image

Moved button to the side of texts in both cases:
image

@CzarekDryl CzarekDryl requested a review from mmioana January 20, 2025 13:29
mmioana
mmioana previously approved these changes Jan 20, 2025
Copy link
Contributor

@mmioana mmioana left a comment

Choose a reason for hiding this comment

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

Nice work @CzarekDryl 🥇

Everything looks good also on mobile now 💯
Screenshot 2025-01-20 at 22 09 15
Screenshot 2025-01-20 at 22 10 17
Screenshot 2025-01-20 at 22 12 25

Copy link
Contributor

@rumzledz rumzledz left a comment

Choose a reason for hiding this comment

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

Great start on this PR @CzarekDryl ! I left bits and bobs on your PR about areas of improvement. Give me a shout when you want me to re-test this PR! 👌

},
modalDescription: {
id: `${displayName}.modalDescription`,
defaultMessage:
'You can invite up to 100 members to your Colony during the private beta using the invite link below.',
'You can invite {invitesAvailable} more people to join and follow this colony during early access. If you run out, you will be able to request more.',
Copy link
Contributor

@rumzledz rumzledz Jan 21, 2025

Choose a reason for hiding this comment

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

Checking with @JoinColony/product if you want "people" to be "person" if there's only 1 invite available

},
invitesUsed: {
id: `${displayName}.invitesUsed`,
defaultMessage: '{invitesUsed}/{invitesAvailable} invites used',
defaultMessage:
'{invitesAvailable} {invitesAvailable, plural, one {invite} other {invites}} remaining',
Copy link
Contributor

Choose a reason for hiding this comment

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

modalDescription: {
id: `${displayName}.modalDescription`,
defaultMessage:
'You can invite {invitesAvailable} more people to join and follow this colony during early access. If you run out, you will be able to request more.',
Copy link
Contributor

Choose a reason for hiding this comment

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

Same question about "people" pluralisation here @JoinColony/product

id: `${displayName}.buttonText`,
defaultMessage: `{isCopied, select,
true {Link copied}
other {Copy link}
Copy link
Contributor

Choose a reason for hiding this comment

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

Noice ✨

const inviteLink = useBaseUrl(
`/invite/${colonyName}/${colonyMemberInvite?.id}`,
);
const commonClassName = 'rounded-lg p-2 text-sm font-medium';
Copy link
Contributor

Choose a reason for hiding this comment

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

You can write this outside the component since it's just a constant, so it's not part of the re-render cycle

@@ -79,6 +90,18 @@ const UserNavigation: FC<UserNavigationProps> = ({

return (
<div data-tour={TourTargets.UserMenu} className="flex gap-1 md:relative">
{!isActionSidebarOpen && isWalletConnected && user && isInColony ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

I see that this isInColony boolean is prop drilled at least 3 levels deep. I think it's better if you just use the context within this component:

const colonyContext = useColonyContext({ nullableContext: true });

const isInColony = !!colonyContext;

...

{!isActionSidebarOpen && isWalletConnected && user && isInColony ? (

iconSize={16}
size="small"
isFullRounded
className="gap-1 px-[0.875rem] sm:px-2.5 md:hover:!border-blue-400"
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's cleaner if you just do px-3.5

@CzarekDryl
Copy link
Contributor Author

@rumzledz Thanks for checking, I've made changes with description pluralisation - for better UX. Please check it again :)

@CzarekDryl CzarekDryl requested a review from rumzledz January 22, 2025 16:57
Copy link
Contributor

@mmioana mmioana left a comment

Choose a reason for hiding this comment

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

Hey @CzarekDryl thanks for your latest changes. It all looks good 💯

Screenshot 2025-01-23 at 10 45 44
Screenshot 2025-01-23 at 10 46 04
Screenshot 2025-01-23 at 10 53 28

However, after @rumzledz pointed out the messages pluralisation, realised we have unused translations in both ModalContent and InviteMembersModal. I left a few comments about what needs to be removed. Once this is done, I'll be more than happy to approve your PR 🙌 Thanks for your hard and continuous work on this issue! ✨

Comment on lines 23 to 36
modalTitle: {
id: `${displayName}.modalTitle`,
defaultMessage: 'Invite people to this colony',
},
modalDescription: {
id: `${displayName}.modalDescription`,
defaultMessage:
'You can invite {invitesAvailable} more {invitesAvailable, plural, one {person} other {people}} to join and follow this colony during early access. If you run out, you will be able to request more.',
},
modalDescriptionReached: {
id: `${displayName}.modalDescriptionReached`,
defaultMessage:
'You have reached your invite limit during early access. If you need more, please make a request.',
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove these unused translations

Comment on lines 33 to 52
invitesUsed: {
id: `${displayName}.invitesUsed`,
defaultMessage: '{invitesUsed}/{invitesAvailable} invites used',
defaultMessage:
'{invitesAvailable} {invitesAvailable, plural, one {invite} other {invites}} remaining',
},
inviteLinkHeading: {
id: `${displayName}.inviteLinkHeading`,
defaultMessage: 'Your unique invite link:',
defaultMessage: 'Unique colony invite link:',
},
limitReached: {
id: `${displayName}.limitReached`,
defaultMessage: 'Invite limit reached',
},
requestInvites: {
id: `${displayName}.requestInvites`,
defaultMessage: 'Request invites',
},
requestMoreInvites: {
id: `${displayName}.requestMoreInvites`,
defaultMessage: 'Request more invites for your colony',
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove these unused descriptions

},
requestMoreInvites: {
id: `${displayName}.requestMoreInvites`,
defaultMessage: 'Request more invites for your colony',
},
buttonText: {
Copy link
Contributor

Choose a reason for hiding this comment

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

Also this one

@CzarekDryl
Copy link
Contributor Author

@mmioana Unused messages removed

@CzarekDryl CzarekDryl requested a review from mmioana January 23, 2025 18:48
Copy link
Contributor

@mmioana mmioana left a comment

Choose a reason for hiding this comment

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

Thanks a lot @CzarekDryl for all your work! Good to go from my side! 🎉

Copy link
Contributor

@bassgeta bassgeta left a comment

Choose a reason for hiding this comment

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

Nice job, code looks good and almost everything works 😎

  1. The header button shows up to logged in users ✔️
    image
    image
  2. The modal shows up and looks like expected ✔️
    image
    image
  3. THe only issue is that when we have 0 invites remaining, the if condition doesn't work :/
    image

const invitesUsed = 100 - (colonyMemberInvite?.invitesRemaining || 0);
const getModalSubtitle = () => {
if (isOutOfInvites) {
<FormattedMessage
Copy link
Contributor

Choose a reason for hiding this comment

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

dropped a return here, we have a tiny bug because of it 👀

Copy link
Member

@arrenv arrenv left a comment

Choose a reason for hiding this comment

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

Thank you @CzarekDryl everything still looks to be working well. After fixing the bug @bassgeta raised, let's get it merged.

image

image

image

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Open up colony invite functionality
5 participants