Repository: Couchers-org/couchers (language: python)

Issue:
Encourage Complete Profiles - Profile requirements to use certain features
To combat spam and encourage behavior that supports the core values of the platform, certain features should only be accessible to users with a "complete" profile. Users should be encouraged to complete their profile, instructed how to do it, and - if their profile is incomplete - warned when they try to use features that require a complete profile.

- [x] update definition of "complete profile" FROM "has picture" + "about me > 20 characters" TO "has picture" + "about me >150 characters"
- [x] Add reminder to dashboard and all other affected pages (messaging, search, communities, and events) that links to their profile page. Actual minimum character count should not be mentioned.
  - Too short AND missing photo: `Almost there! Before you can [send a request/send a message/create an event], you must write more about yourself in your profile and upload a profile photo. This helps build a trusted community and reduce spam. For more information click [here](link to help page). Thank you for your help!`
  - Too short: `Almost there! Before you can [send a request/send a message/create an event], you must write more about yourself in your profile. This helps build a trusted community and reduce spam. For more information click [here](link to help page). Thank you for your help!`
  - Missing photo: `Almost there! Before you can [send a request/send a message/create an event], you must upload a profile photo. This helps build a trusted community and reduce spam. For more information click [here](link to help page). Thank you for your help!`
- [x] Create help page which explains the following:
  - How it works and why its required
  - What to do if you're afraid of uploading a picture of yourself
  - How to write a good profile / what to write in your profile
- [x] require "complete" profile to use certain features, including:
  - creating requests (eg. clicking "request" button should display modal with warning about why they can't send a request)
  - creating messages (group or 1-on-1) via messages page or profile page
  - creating events
- [x] UI/UX tests with design team prior to going live



Discussion / hints:
@Krishnag09 working on backend
@aapeliv working on frontend
@jesseallhands working on help page, hopefully :pray: 

Fix patch:
diff --git a/app/web/components/ProfileIncompleteDialog/ProfileIncompleteDialog.tsx b/app/web/components/ProfileIncompleteDialog/ProfileIncompleteDialog.tsx
new file mode 100644
index 0000000000..b71b86452d
--- /dev/null
+++ b/app/web/components/ProfileIncompleteDialog/ProfileIncompleteDialog.tsx
@@ -0,0 +1,73 @@
+import Button from "components/Button";
+import {
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogContentText,
+  DialogTitle,
+} from "components/Dialog";
+import StyledLink from "components/StyledLink";
+import { Trans, useTranslation } from "i18n";
+import { DASHBOARD } from "i18n/namespaces";
+import Link from "next/link";
+import React from "react";
+import { routeToEditProfile } from "routes";
+
+export interface ProfileIncompleteDialogProps {
+  open: boolean;
+  onClose: () => void;
+  attempted_action: "create_event" | "send_message" | "send_request";
+}
+
+export default function ProfileIncompleteDialog({
+  open,
+  onClose,
+  attempted_action,
+}: ProfileIncompleteDialogProps) {
+  const { t } = useTranslation([DASHBOARD]);
+
+  const action_text = t(
+    `dashboard:complete_profile_dialog.actions.${attempted_action}`
+  );
+
+  return (
+    <Dialog
+      aria-labelledby="profile-incomplete-dialog-title"
+      open={open}
+      onClose={onClose}
+    >
+      <DialogTitle id="profile-incomplete-dialog-title">
+        {t("dashboard:complete_profile_dialog.title")}
+      </DialogTitle>
+      <DialogContent>
+        <DialogContentText>
+          <Trans i18nKey="dashboard:complete_profile_dialog.description_1">
+            Before you can {{ action_name: action_text }}, you must{" "}
+            <strong>write a bit about yourself</strong> in your profile and{" "}
+            <strong>upload a profile photo</strong>.
+          </Trans>
+        </DialogContentText>
+        <DialogContentText>
+          <Trans i18nKey="dashboard:complete_profile_dialog.description_2">
+            This helps build a trusted community and reduce spam. For more
+            information,{" "}
+            <StyledLink href="https://help.couchers.org/hc/couchersorg-help-center/articles/1725919152-why-do-i-need-to-complete-my-profile-to-use-some-features">
+              please refer to this help page
+            </StyledLink>
+            . Thank you for your help!
+          </Trans>
+        </DialogContentText>
+      </DialogContent>
+      <DialogActions>
+        <Link href={routeToEditProfile()} passHref>
+          <Button>
+            {t("dashboard:complete_profile_dialog.edit_profile_button")}
+          </Button>
+        </Link>
+        <Button onClick={onClose} color="default">
+          {t("dashboard:complete_profile_dialog.cancel_button")}
+        </Button>
+      </DialogActions>
+    </Dialog>
+  );
+}
diff --git a/app/web/features/communities/events/CreateEventPage.tsx b/app/web/features/communities/events/CreateEventPage.tsx
index 7ba2ddabd7..7bbb57802d 100644
--- a/app/web/features/communities/events/CreateEventPage.tsx
+++ b/app/web/features/communities/events/CreateEventPage.tsx
@@ -1,6 +1,8 @@
 import { Typography } from "@material-ui/core";
 import Button from "components/Button";
 import HtmlMeta from "components/HtmlMeta";
+import ProfileIncompleteDialog from "components/ProfileIncompleteDialog/ProfileIncompleteDialog";
+import useAccountInfo from "features/auth/useAccountInfo";
 import { communityEventsBaseKey } from "features/queryKeys";
 import { RpcError } from "grpc-web";
 import { useTranslation } from "i18n";
@@ -8,7 +10,7 @@ import { COMMUNITIES, GLOBAL } from "i18n/namespaces";
 import { useRouter } from "next/router";
 import { Event } from "proto/events_pb";
 import { useMutation, useQueryClient } from "react-query";
-import { routeToEvent } from "routes";
+import { dashboardRoute, routeToEvent } from "routes";
 import { service } from "service";
 import type { CreateEventInput } from "service/events";
 import dayjs, { TIME_FORMAT } from "utils/dayjs";
@@ -114,9 +116,17 @@ export default function CreateEventPage() {
     }
   );
 
+  const { data: accountInfo, isLoading: isAccountInfoLoading } =
+    useAccountInfo();
+
   return (
     <>
       <HtmlMeta title={t("communities:create_event_page_title")} />
+      <ProfileIncompleteDialog
+        open={!isAccountInfoLoading && !accountInfo?.profileComplete}
+        onClose={() => router.push(dashboardRoute)}
+        attempted_action="create_event"
+      />
       <EventForm
         error={error}
         isMutationLoading={isLoading}
diff --git a/app/web/features/dashboard/locales/en.json b/app/web/features/dashboard/locales/en.json
index e0248bb147..0f8ad76724 100644
--- a/app/web/features/dashboard/locales/en.json
+++ b/app/web/features/dashboard/locales/en.json
@@ -5,6 +5,18 @@
   "upload_photo": "2. Upload a photo (by clicking on the avatar on the edit page)",
   "edit_profile_button_text": "Edit your profile",
   "complete_profile_explanation": "Don't you hate how other platforms have empty \"ghost\" profiles with no information. Please don't ghost us! 👻👻👻",
+  "complete_profile_dialog": {
+    "title": "Almost there: please complete your profile to continue!",
+    "description_1": "Before you can {{action_name}}, you must <4>write a bit about yourself</4> in your profile and <7>upload a profile photo</7>.",
+    "description_2": "This helps build a trusted community and reduce spam. For more information, <2>please refer to this help page</2>. Thank you for your help!",
+    "actions": {
+      "send_request": "send a request",
+      "send_message": "send a message",
+      "create_event": "create an event"
+    },
+    "edit_profile_button": "Edit your profile now",
+    "cancel_button": "Never mind"
+  },
   "your_communities_heading": "Your communities",
   "all_communities_heading": "All communities",
   "no_community": "You are not currently in a community.",
diff --git a/app/web/features/profile/actions/MessageUserButton.tsx b/app/web/features/profile/actions/MessageUserButton.tsx
index c4e9235aa0..777cc921f7 100644
--- a/app/web/features/profile/actions/MessageUserButton.tsx
+++ b/app/web/features/profile/actions/MessageUserButton.tsx
@@ -1,8 +1,11 @@
 import Button from "components/Button";
+import ProfileIncompleteDialog from "components/ProfileIncompleteDialog/ProfileIncompleteDialog";
+import useAccountInfo from "features/auth/useAccountInfo";
 import { useTranslation } from "i18n";
 import { PROFILE } from "i18n/namespaces";
 import { useRouter } from "next/router";
 import { User } from "proto/api_pb";
+import { useState } from "react";
 import { useMutation } from "react-query";
 import { routeToCreateMessage, routeToGroupChat } from "routes";
 import { service } from "service";
@@ -37,9 +40,34 @@ export default function MessageUserButton({
     }
   );
 
+  const [showCantMessageDialog, setShowCantMessageDialog] =
+    useState<boolean>(false);
+
+  const { data: accountInfo, isLoading: isAccountInfoLoading } =
+    useAccountInfo();
+
+  const onClick = () => {
+    if (!accountInfo?.profileComplete) {
+      setShowCantMessageDialog(true);
+    } else {
+      mutate();
+    }
+  };
+
   return (
-    <Button loading={isLoading} onClick={() => mutate()}>
-      {t("actions.message_label")}
-    </Button>
+    <>
+      <ProfileIncompleteDialog
+        open={showCantMessageDialog}
+        onClose={() => setShowCantMessageDialog(false)}
+        attempted_action="send_message"
+      />
+      <Button
+        loading={isLoading}
+        onClick={onClick}
+        disabled={isAccountInfoLoading}
+      >
+        {t("actions.message_label")}
+      </Button>
+    </>
   );
 }
diff --git a/app/web/features/profile/view/Overview.tsx b/app/web/features/profile/view/Overview.tsx
index 45fc723d71..9af3bb968d 100644
--- a/app/web/features/profile/view/Overview.tsx
+++ b/app/web/features/profile/view/Overview.tsx
@@ -1,6 +1,8 @@
 import Alert from "components/Alert";
 import Button from "components/Button";
+import ProfileIncompleteDialog from "components/ProfileIncompleteDialog/ProfileIncompleteDialog";
 import { useAuthContext } from "features/auth/AuthProvider";
+import useAccountInfo from "features/auth/useAccountInfo";
 import FlagButton from "features/FlagButton";
 import FriendActions from "features/profile/actions/FriendActions";
 import MessageUserButton from "features/profile/actions/MessageUserButton";
@@ -66,10 +68,31 @@ function DefaultActions({
     user.hostingStatus === HostingStatus.HOSTING_STATUS_CANT_HOST;
 
   const [mutationError, setMutationError] = useState("");
+  const [showCantRequestDialog, setShowCantRequestDialog] =
+    useState<boolean>(false);
+
+  const { data: accountInfo, isLoading: isAccountInfoLoading } =
+    useAccountInfo();
+
+  const requestButton = () => {
+    if (!accountInfo?.profileComplete) {
+      setShowCantRequestDialog(true);
+    } else {
+      setIsRequesting(true);
+    }
+  };
 
   return (
     <>
-      <Button onClick={() => setIsRequesting(true)} disabled={disableHosting}>
+      <ProfileIncompleteDialog
+        open={showCantRequestDialog}
+        onClose={() => setShowCantRequestDialog(false)}
+        attempted_action="send_request"
+      />
+      <Button
+        onClick={requestButton}
+        disabled={isAccountInfoLoading || disableHosting}
+      >
         {disableHosting
           ? t("global:hosting_status.cant_host")
           : t("profile:actions.request")}
diff --git a/app/web/pages/event/new.tsx b/app/web/pages/event/new.tsx
index f44927b4e8..9357487424 100644
--- a/app/web/pages/event/new.tsx
+++ b/app/web/pages/event/new.tsx
@@ -1,6 +1,6 @@
 import { appGetLayout } from "components/AppRoute";
 import CreateEventPage from "features/communities/events/CreateEventPage";
-import { COMMUNITIES, GLOBAL } from "i18n/namespaces";
+import { COMMUNITIES, DASHBOARD, GLOBAL } from "i18n/namespaces";
 import { GetStaticProps } from "next";
 import nextI18nextConfig from "next-i18next.config";
 import { serverSideTranslations } from "next-i18next/serverSideTranslations";
@@ -9,7 +9,7 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => ({
   props: {
     ...(await serverSideTranslations(
       locale ?? "en",
-      [GLOBAL, COMMUNITIES],
+      [GLOBAL, COMMUNITIES, DASHBOARD],
       nextI18nextConfig
     )),
   },
diff --git a/app/web/pages/user/[[...slug]].tsx b/app/web/pages/user/[[...slug]].tsx
index 51285388b3..649b019f00 100644
--- a/app/web/pages/user/[[...slug]].tsx
+++ b/app/web/pages/user/[[...slug]].tsx
@@ -17,7 +17,7 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => ({
   props: {
     ...(await serverSideTranslations(
       locale ?? "en",
-      ["global", "profile", "connections"],
+      ["global", "profile", "connections", "dashboard"],
       nextI18nextConfig
     )),
   },


Test patch:
diff --git a/app/web/features/communities/events/CreateEventPage.test.tsx b/app/web/features/communities/events/CreateEventPage.test.tsx
index 10d6e1080b..e309658323 100644
--- a/app/web/features/communities/events/CreateEventPage.test.tsx
+++ b/app/web/features/communities/events/CreateEventPage.test.tsx
@@ -6,7 +6,7 @@ import { service } from "service";
 import events from "test/fixtures/events.json";
 import wrapper, { getHookWrapperWithClient } from "test/hookWrapper";
 import { server } from "test/restMock";
-import { t } from "test/utils";
+import { MockedService, t } from "test/utils";
 
 import CreateEventPage from "./CreateEventPage";
 
@@ -16,6 +16,23 @@ const createEventMock = service.events.createEvent as jest.MockedFunction<
   typeof service.events.createEvent
 >;
 
+const getAccountInfoMock = service.account.getAccountInfo as MockedService<
+  typeof service.account.getAccountInfo
+>;
+
+const accountInfo = {
+  username: "tester",
+  email: "email@couchers.org",
+  profileComplete: true,
+  phone: "+46701740605",
+  phoneVerified: true,
+  timezone: "Australia/Broken_Hill",
+  hasStrongVerification: false,
+  birthdateVerificationStatus: 1,
+  genderVerificationStatus: 3,
+  doNotEmail: false,
+};
+
 describe("Create event page", () => {
   beforeAll(() => {
     server.listen();
@@ -29,6 +46,7 @@ describe("Create event page", () => {
     createEventMock.mockResolvedValue(events[0]);
     jest.useFakeTimers();
     jest.setSystemTime(new Date("2021-08-01 00:00"));
+    getAccountInfoMock.mockResolvedValue(accountInfo);
   });
 
   afterEach(() => {
diff --git a/app/web/features/profile/actions/MessageUserButton.test.tsx b/app/web/features/profile/actions/MessageUserButton.test.tsx
index cf6839fb4b..006aa79521 100644
--- a/app/web/features/profile/actions/MessageUserButton.test.tsx
+++ b/app/web/features/profile/actions/MessageUserButton.test.tsx
@@ -7,7 +7,7 @@ import { routeToCreateMessage, routeToGroupChat } from "routes";
 import { service } from "service";
 import users from "test/fixtures/users.json";
 import wrapper from "test/hookWrapper";
-import { MockedService } from "test/utils";
+import { MockedService, t } from "test/utils";
 
 const setErrorMock = jest.fn();
 const getDirectMessageMock = service.conversations
@@ -15,34 +15,92 @@ const getDirectMessageMock = service.conversations
   typeof service.conversations.getDirectMessage
 >;
 
+const getAccountInfoMock = service.account.getAccountInfo as MockedService<
+  typeof service.account.getAccountInfo
+>;
+
+const accountInfo = {
+  username: "tester",
+  email: "email@couchers.org",
+  profileComplete: true,
+  phone: "+46701740605",
+  phoneVerified: true,
+  timezone: "Australia/Broken_Hill",
+  hasStrongVerification: false,
+  birthdateVerificationStatus: 1,
+  genderVerificationStatus: 3,
+  doNotEmail: false,
+};
+
+const incompleteAccountInfo = { ...accountInfo, profileComplete: false };
+
 describe("MessageUserButton", () => {
   beforeEach(() => {
     setErrorMock.mockClear();
   });
 
   it("redirects to thread if dm exists", async () => {
+    getAccountInfoMock.mockResolvedValue(accountInfo);
     getDirectMessageMock.mockResolvedValueOnce(99);
     const user = users[0];
     render(<MessageUserButton user={user} setMutationError={setErrorMock} />, {
       wrapper,
     });
 
-    userEvent.click(screen.getByRole("button"));
+    const button = screen.getByRole("button");
+
+    await waitFor(() => {
+      expect(button).toBeEnabled();
+    });
+
+    userEvent.click(button);
 
     await waitFor(() => expect(mockRouter.pathname).toBe(routeToGroupChat(99)));
   });
 
   it("redirects to chat tab with state if dm doesn't exist", async () => {
+    getAccountInfoMock.mockResolvedValue(accountInfo);
     getDirectMessageMock.mockResolvedValueOnce(false);
     const user = users[0];
     render(<MessageUserButton user={user} setMutationError={setErrorMock} />, {
       wrapper,
     });
 
-    userEvent.click(screen.getByRole("button"));
+    const button = screen.getByRole("button");
+
+    await waitFor(() => {
+      expect(button).toBeEnabled();
+    });
+
+    userEvent.click(button);
 
     await waitFor(() =>
       expect(mockRouter.asPath).toBe(routeToCreateMessage(user.username))
     );
   });
+
+  it("pops up incomplete profile note if profile is incomplete", async () => {
+    getAccountInfoMock.mockResolvedValue(incompleteAccountInfo);
+    getDirectMessageMock.mockResolvedValueOnce(false);
+    const user = users[0];
+    render(<MessageUserButton user={user} setMutationError={setErrorMock} />, {
+      wrapper,
+    });
+
+    const button = screen.getByRole("button");
+
+    await waitFor(() => {
+      expect(button).toBeEnabled();
+    });
+
+    userEvent.click(button);
+
+    await waitFor(async () =>
+      expect(
+        await screen.findByLabelText(
+          t("dashboard:complete_profile_dialog.title")
+        )
+      ).toBeVisible()
+    );
+  });
 });
diff --git a/app/web/features/profile/view/UserOverview.test.tsx b/app/web/features/profile/view/UserOverview.test.tsx
index 50e1cbda9d..bcbb4596ca 100644
--- a/app/web/features/profile/view/UserOverview.test.tsx
+++ b/app/web/features/profile/view/UserOverview.test.tsx
@@ -108,6 +108,7 @@ describe("UserOverview", () => {
       );
       expect(screen.getByText("Edit profile")).toBeInTheDocument();
     });
+
     it("should not show badge when no strong verification", () => {
       render(
         <ProfileUserProvider
@@ -121,6 +122,7 @@ describe("UserOverview", () => {
       const verificationBadge = screen.queryByTestId("error-icon");
       expect(verificationBadge).toBeNull();
     });
+
     it("should show badge when user has strong verification", () => {
       render(
         <ProfileUserProvider