Skip to content

Commit

Permalink
Bepro 2237 notification subscribe to task (#479)
Browse files Browse the repository at this point in the history
* create notification settings table

* add notification icons

* add subscriptions column to table and model

* create endpoint and hooks to subscribe/unsubscribe

* fix model name

* fix condition

* create subscription button component

* add subscription button to task hero

* remove default value

* fix reading of null

* create task subscription hook and refactor subscription button to use it

* stop propagation to avoid redirection when button is clicked

* dont show subscribe button if task is cancelled or closed

* add tooltip to button

* adjust button design

* omit subscriptions

* update notifications text
  • Loading branch information
vhcsilva authored Jul 22, 2024
1 parent d49327f commit 1e28bc8
Show file tree
Hide file tree
Showing 22 changed files with 351 additions and 33 deletions.
14 changes: 2 additions & 12 deletions assets/icons/bell-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,8 @@ import { SVGProps } from "react";

export default function BellIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M13.8625 10.9963C13.5156 10.3988 13 8.70813 13 6.5C13 5.17392 12.4732 3.90215 11.5355 2.96447C10.5979 2.02678 9.32608 1.5 8 1.5C6.67392 1.5 5.40215 2.02678 4.46447 2.96447C3.52678 3.90215 3 5.17392 3 6.5C3 8.70875 2.48375 10.3988 2.13688 10.9963C2.04829 11.1482 2.00133 11.3207 2.00073 11.4966C2.00013 11.6724 2.0459 11.8453 2.13344 11.9978C2.22097 12.1503 2.34718 12.277 2.49932 12.3652C2.65146 12.4534 2.82416 12.4999 3 12.5H5.55063C5.66598 13.0645 5.97277 13.5718 6.41908 13.9361C6.8654 14.3004 7.42386 14.4994 8 14.4994C8.57614 14.4994 9.1346 14.3004 9.58092 13.9361C10.0272 13.5718 10.334 13.0645 10.4494 12.5H13C13.1758 12.4998 13.3484 12.4532 13.5005 12.365C13.6525 12.2768 13.7786 12.15 13.8661 11.9975C13.9536 11.845 13.9993 11.6722 13.9986 11.4964C13.998 11.3206 13.9511 11.1481 13.8625 10.9963ZM8 13.5C7.68989 13.4999 7.38743 13.4037 7.13425 13.2246C6.88107 13.0455 6.68962 12.7924 6.58625 12.5H9.41375C9.31038 12.7924 9.11893 13.0455 8.86575 13.2246C8.61257 13.4037 8.31011 13.4999 8 13.5ZM3 11.5C3.48125 10.6725 4 8.755 4 6.5C4 5.43913 4.42143 4.42172 5.17157 3.67157C5.92172 2.92143 6.93913 2.5 8 2.5C9.06087 2.5 10.0783 2.92143 10.8284 3.67157C11.5786 4.42172 12 5.43913 12 6.5C12 8.75313 12.5175 10.6706 13 11.5H3Z"
fill="#F1F1F4"
/>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M21.0002 28C21.0002 28.2652 20.8949 28.5196 20.7073 28.7071C20.5198 28.8946 20.2654 29 20.0002 29H12.0002C11.735 29 11.4807 28.8946 11.2931 28.7071C11.1056 28.5196 11.0002 28.2652 11.0002 28C11.0002 27.7348 11.1056 27.4804 11.2931 27.2929C11.4807 27.1054 11.735 27 12.0002 27H20.0002C20.2654 27 20.5198 27.1054 20.7073 27.2929C20.8949 27.4804 21.0002 27.7348 21.0002 28ZM27.7315 24C27.558 24.3056 27.3061 24.5595 27.0017 24.7353C26.6974 24.911 26.3517 25.0024 26.0002 25H6.00023C5.64865 24.9995 5.30341 24.9064 4.9993 24.73C4.69519 24.5535 4.44297 24.3 4.26804 23.9951C4.09312 23.6901 4.00168 23.3444 4.00294 22.9928C4.0042 22.6412 4.09812 22.2962 4.27523 21.9925C4.96898 20.7975 6.00023 17.4175 6.00023 13C6.00023 10.3478 7.0538 7.8043 8.92916 5.92893C10.8045 4.05357 13.3481 3 16.0002 3C18.6524 3 21.1959 4.05357 23.0713 5.92893C24.9467 7.8043 26.0002 10.3478 26.0002 13C26.0002 17.4163 27.0327 20.7975 27.7265 21.9925C27.9054 22.2966 28 22.6429 28.0006 22.9957C28.0013 23.3486 27.908 23.6952 27.7302 24H27.7315ZM26.0002 23C25.034 21.3412 24.0002 17.5063 24.0002 13C24.0002 10.8783 23.1574 8.84344 21.6571 7.34315C20.1568 5.84285 18.122 5 16.0002 5C13.8785 5 11.8437 5.84285 10.3434 7.34315C8.84309 8.84344 8.00023 10.8783 8.00023 13C8.00023 17.5075 6.96523 21.3425 6.00023 23H26.0002Z" fill="white" />
</svg>
);
}
9 changes: 9 additions & 0 deletions assets/icons/bell-slash-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SVGProps } from "react";

export default function BellSlashIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M6.74002 4.32933C6.6522 4.23034 6.54561 4.14976 6.42642 4.09227C6.30723 4.03478 6.17781 4.00151 6.04567 3.9944C5.91353 3.9873 5.7813 4.00648 5.65663 4.05086C5.53196 4.09523 5.41734 4.16391 5.3194 4.25291C5.22147 4.34191 5.14218 4.44946 5.08612 4.56933C5.03006 4.6892 4.99835 4.819 4.99282 4.95122C4.9873 5.08343 5.00807 5.21543 5.05394 5.33956C5.0998 5.46369 5.16985 5.57748 5.26002 5.67433L7.35252 7.97683C6.46384 9.50217 5.99704 11.2365 6.00002 13.0018C6.00002 17.4193 4.96752 20.7993 4.27377 21.9943C4.0966 22.2981 4.00268 22.6433 4.00148 22.995C4.00027 23.3467 4.09182 23.6924 4.26689 23.9974C4.44196 24.3025 4.69437 24.5559 4.99865 24.7323C5.30293 24.9086 5.64833 25.0016 6.00002 25.0018H22.83L25.26 27.6743C25.3478 27.7733 25.4544 27.8539 25.5736 27.9114C25.6928 27.9689 25.8222 28.0022 25.9544 28.0093C26.0865 28.0164 26.2187 27.9972 26.3434 27.9528C26.4681 27.9084 26.5827 27.8398 26.6806 27.7508C26.7786 27.6618 26.8579 27.5542 26.9139 27.4343C26.97 27.3145 27.0017 27.1847 27.0072 27.0525C27.0127 26.9202 26.992 26.7882 26.9461 26.6641C26.9002 26.54 26.8302 26.4262 26.74 26.3293L6.74002 4.32933ZM6.00002 23.0018C6.96252 21.3468 8.00002 17.5118 8.00002 13.0018C7.99826 11.8068 8.26572 10.6268 8.78252 9.54933L21.0113 23.0018H6.00002ZM21 28.0018C21 28.267 20.8947 28.5214 20.7071 28.7089C20.5196 28.8965 20.2652 29.0018 20 29.0018H12C11.7348 29.0018 11.4804 28.8965 11.2929 28.7089C11.1054 28.5214 11 28.267 11 28.0018C11 27.7366 11.1054 27.4823 11.2929 27.2947C11.4804 27.1072 11.7348 27.0018 12 27.0018H20C20.2652 27.0018 20.5196 27.1072 20.7071 27.2947C20.8947 27.4823 21 27.7366 21 28.0018ZM26.75 22.4081C26.6332 22.4534 26.509 22.4767 26.3838 22.4768C26.1829 22.4766 25.9868 22.4159 25.8209 22.3027C25.655 22.1894 25.5271 22.0288 25.4538 21.8418C24.5438 19.5256 24 16.2206 24 13.0018C24.0004 11.6026 23.6338 10.2277 22.9368 9.01445C22.2398 7.80117 21.2367 6.79193 20.0278 6.08745C18.8188 5.38297 17.4462 5.0079 16.047 4.99968C14.6478 4.99146 13.2709 5.35037 12.0538 6.04058C11.8236 6.16529 11.5537 6.19478 11.3021 6.12272C11.0505 6.05067 10.8371 5.88282 10.7078 5.6552C10.5785 5.42759 10.5437 5.15836 10.6107 4.90533C10.6777 4.65229 10.8413 4.43562 11.0663 4.30183C12.5876 3.4389 14.3087 2.99007 16.0577 3.00017C17.8067 3.01026 19.5225 3.47891 21.0338 4.35935C22.5451 5.23979 23.7991 6.50122 24.6705 8.01773C25.5419 9.53424 26.0003 11.2528 26 13.0018C26 17.4206 27.0063 20.3256 27.315 21.1118C27.412 21.3587 27.4069 21.6339 27.3009 21.877C27.195 22.12 26.9968 22.3111 26.75 22.4081Z" fill="white" />
</svg>
);
}
12 changes: 6 additions & 6 deletions components/bounty/bounty-hero/bounty-settings/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,16 @@ export default function BountySettingsView({
useEffect(loadOutsideClick, [show]);

return (
<>
<div className="col-auto">
<div className="position-relative d-flex justify-content-end" ref={node}>
<div
className={`cursor-pointer hover-white border ${
(show && "border-primary") || "border-gray-850"
} border-radius-8 d-flex`}
className={`cursor-pointer hover-white px-2 border align-items-center ${
(show && "border-primary") || "border-gray-800"
} border-radius-4 d-flex`}
onClick={() => setShow(!show)}
data-testid="task-options"
>
<span className="mx-2 my-1">{t("common:misc.options")}</span>
<span className="text-gray-400 mb-2">. . .</span>
</div>

<div
Expand Down Expand Up @@ -174,7 +174,7 @@ export default function BountySettingsView({
<Translation ns="common" label="modals.hard-cancel.content" />
</h5>
</Modal>
</>
</div>
);
}

20 changes: 13 additions & 7 deletions components/bounty/bounty-hero/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { UserProfileLink } from "components/common/user-profile-link/user-profil
import CustomContainer from "components/custom-container";
import If from "components/If";
import OriginLinkWarningModal from "components/modals/origin-link-warning/view";
import { SubscriptionTaskButton }
from "components/notifications/subscription-task-button/subscription-task-button.controller";
import TaskStatusInfo from "components/task-status-info";

import { IssueBigNumberData, IssueState } from "interfaces/issue-data";
Expand Down Expand Up @@ -52,7 +54,7 @@ export default function BountyHeroView({
}

return (
<div className="mt-2 border-bottom border-gray-850 pb">
<div className="mt-3 border-bottom border-gray-850 pb">
<CustomContainer>
<div className="row d-flex flex-row justify-content-center">
<div className="col-12 min-w-bounty-hero justify-content-center">
Expand All @@ -66,14 +68,18 @@ export default function BountyHeroView({
</span>
</div>

<div className="col-auto">
<BountySettings
currentBounty={bounty}
updateBountyData={updateBountyData}
isEditIssue={isEditIssue}
onEditIssue={handleEditIssue}
<div className="col-auto px-0 px-md-1">
<SubscriptionTaskButton
taskId={+bounty?.id}
/>
</div>

<BountySettings
currentBounty={bounty}
updateBountyData={updateBountyData}
isEditIssue={isEditIssue}
onEditIssue={handleEditIssue}
/>
</div>

<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ import ChainIcon from "components/chain-icon";
import MarketplaceWithNetworkLogo
from "components/common/marketplace-with-network-logo/marketplace-with-network-logo.view";
import If from "components/If";
import { SubscriptionTaskButton }
from "components/notifications/subscription-task-button/subscription-task-button.controller";

import { TaskListItemVariantProps } from "types/components";

export default function TaskListItemDefault ({
export default function TaskListItemDefault({
task,
isMarketplaceList,
onClick,
}: TaskListItemVariantProps) {
const { t } = useTranslation(["bounty", "common", "custom-network"]);

const isSubscribable = !["closed", "canceled"].includes(task?.state);

return(
<div
className="row align-items-center mx-0 p-3 bg-gray-900 border border-gray-800 border-radius-8 cursor-pointer"
Expand Down Expand Up @@ -52,6 +56,15 @@ export default function TaskListItemDefault ({
<div className="col sm-regular text-white two-lines-text capitalize-first px-0">
{task?.title}
</div>

<If condition={isSubscribable}>
<div className="col-auto">
<SubscriptionTaskButton
taskId={+task?.id}
variant="icon"
/>
</div>
</If>
</div>

<div className="row align-items-center border-sm-bottom border-gray-850 mx-0 gap-2 gap-sm-3 pb-sm-3">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { MouseEvent } from "react";

import { SubscriptionTaskButtonView }
from "components/notifications/subscription-task-button/subscription-task-button.view";

import { useUserStore } from "x-hooks/stores/user/user.store";
import { useTaskSubscription } from "x-hooks/use-task-subscription";

interface SubscriptionTaskButtonProps {
taskId: number;
variant?: "icon" | "text";
}

export function SubscriptionTaskButton({
taskId,
variant = "text",
}: SubscriptionTaskButtonProps) {
const { currentUser } = useUserStore();
const { isSubscribed, subscribe, unsubscribe, isSubscribing, isUnsubscribing } = useTaskSubscription();

const isConnected = !!currentUser?.walletAddress;

function onClick(e: MouseEvent<HTMLButtonElement>) {
e?.stopPropagation();

if (isSubscribed(taskId))
unsubscribe(taskId);
else
subscribe(taskId);
}

return(
<SubscriptionTaskButtonView
isSubscribed={isSubscribed(taskId)}
isDisabled={isSubscribing || isUnsubscribing}
isConnected={isConnected}
variant={variant}
onClick={onClick}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { MouseEvent } from "react";

import { useTranslation } from "next-i18next";

import BellIcon from "assets/icons/bell-icon";
import BellSlashIcon from "assets/icons/bell-slash-icon";

import Button from "components/button";
import { Tooltip } from "components/common/tooltip/tooltip.view";
import If from "components/If";
import { ResponsiveEle } from "components/responsive-wrapper";

interface SubscriptionTaskButtonViewProps {
isSubscribed: boolean;
isDisabled: boolean;
isConnected: boolean;
variant?: "icon" | "text";
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
}

export function SubscriptionTaskButtonView({
isSubscribed,
isDisabled,
isConnected,
variant = "text",
onClick,
}: SubscriptionTaskButtonViewProps) {
const { t } = useTranslation("bounty");

const isTextVariant = variant === "text";
const icon = isSubscribed ? <BellSlashIcon height={18} width={18} /> : <BellIcon height={18} width={18} />;
const text = isSubscribed ? t("actions.unsubscribe") : t("actions.subscribe");

if (!isConnected)
return <></>;

return(
<Tooltip tip={text}>
<div>
<Button
onClick={onClick}
disabled={isDisabled}
color="gray-900"
className="border-radius-4 py-1 px-2 border-gray-800 not-svg d-flex align-items-center font-weight-normal"
transparent
>
<span className="d-block m-1">
{icon}
</span>

<If condition={isTextVariant}>
<ResponsiveEle
tabletView={<span>{text}</span>}
/>
</If>
</Button>
</div>
</Tooltip>
);
}
2 changes: 1 addition & 1 deletion components/profile/notification-form/controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function NotificationForm() {
if (!settings)
return null;

const { id, userId, ...rest } = settings;
const { id, userId, subscriptions, ...rest } = settings;

return rest;
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.addColumn("notification_settings", "subscriptions", {
type: Sequelize.ARRAY(Sequelize.INTEGER),
allowNull: true
});
},

async down (queryInterface, Sequelize) {
await queryInterface.removeColumn("notification_settings", "subscriptions");
}
};
7 changes: 6 additions & 1 deletion db/models/notification-settings.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ class NotificationSettings extends Model {
type: DataTypes.BOOLEAN,
allowNull: true,
defaultValue: true
},
subscriptions: {
type: DataTypes.ARRAY(DataTypes.INTEGER),
allowNull: true,
defaultValue: []
}
}, {
sequelize,
Expand All @@ -68,4 +73,4 @@ class NotificationSettings extends Model {
}
}

module.exports = NotificationSettings;
module.exports = NotificationSettings;
1 change: 1 addition & 0 deletions interfaces/user-notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ export interface NotificationSettings {
commentsOnTasks: boolean;
commentsOnProposals: boolean;
commentsOnDeliverables: boolean;
subscriptions: number[];
}
20 changes: 20 additions & 0 deletions pages/api/task/[id]/subscribe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { NextApiRequest, NextApiResponse } from "next";

import { UserRoute } from "middleware";

import { subscribeToTask } from "server/common/task/subscribe";

async function handler(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "PUT":
res.status(200).json(await subscribeToTask(req));
break;

default:
res.status(405);
}

res.end();
}

export default UserRoute(handler);
20 changes: 20 additions & 0 deletions pages/api/task/[id]/unsubscribe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { NextApiRequest, NextApiResponse } from "next";

import { UserRoute } from "middleware";

import { unsubscribeOfTask } from "server/common/task/unsubscribe";

async function handler(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "PUT":
res.status(200).json(await unsubscribeOfTask(req));
break;

default:
res.status(405);
}

res.end();
}

export default UserRoute(handler);
8 changes: 6 additions & 2 deletions public/locales/en/bounty.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@
"start-working": "As a worker, click here to show the community you're working on this task",
"create-deliverable": "As a worker, show your deliverable to the task owner and community",
"create-proposal": "As curator, propose a deliverable as acceptable for the task"
}
},
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe"
},
"management": {
"label": "Management",
Expand Down Expand Up @@ -143,7 +145,9 @@
"banned-domain": "This domain is banned and cannot be used to open tasks.",
"failed-to-cancel": "Task could not be canceled",
"invalid-link": "Invalid link provided. Please check if the link has the protocol (http/https) or the link is correct.",
"failed-to-edit": "Failed to edit bounty"
"failed-to-edit": "Failed to edit bounty",
"failed-to-subscribe": "Failed to subscribe",
"failed-to-unsubscribe": "Failed to unsubscribe"
},
"title": "Create new Task",
"fields": {
Expand Down
2 changes: 1 addition & 1 deletion public/locales/en/profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
},
"notifications-form": {
"title": "Notifications",
"message": "Allow Bepro Marketplace to send you updates via email. These can include updates to tasks, app updates, etc.",
"message": "Allow Bepro Marketplace to send you updates in-app and via email. These can include updates to tasks, app updates, etc.",
"invalid-email": "Invalid email",
"re-send-email": "A confirmation email was sent, please check your inbox and spam box.",
"re-send": "Re-send",
Expand Down
Loading

0 comments on commit 1e28bc8

Please sign in to comment.