Skip to content

Commit

Permalink
Merge pull request #1821 from ResearchHub/remove-authorship
Browse files Browse the repository at this point in the history
"For You" Bounty notifications; Optional targeting based on expertise;
  • Loading branch information
yattias authored Aug 28, 2024
2 parents 7ca919e + 50c2bf7 commit 2626268
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 60 deletions.
166 changes: 163 additions & 3 deletions components/Bounty/BountyModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUndo } from "@fortawesome/pro-solid-svg-icons";
import { faUndo, faUserGroup, faUserPlus, faUsers } from "@fortawesome/pro-solid-svg-icons";
import { faClock } from "@fortawesome/pro-regular-svg-icons";
import { faInfoCircle } from "@fortawesome/pro-light-svg-icons";
import {
Expand All @@ -12,7 +12,7 @@ import { captureEvent } from "@sentry/browser";
import { connect, useSelector } from "react-redux";
import { css, StyleSheet } from "aphrodite";
import { MessageActions } from "~/redux/message";
import { ReactElement, useState, useEffect } from "react";
import { ReactElement, useState, useEffect, useContext } from "react";
import { trackEvent } from "~/config/utils/analytics";
import BaseModal from "../Modals/BaseModal";
import Bounty, { formatBountyAmount } from "~/config/types/bounty";
Expand All @@ -30,6 +30,51 @@ import { RootState } from "~/redux";
import { ID, parseUser } from "~/config/types/root_types";
import FormSelect from "../Form/FormSelect";
import { COMMENT_TYPES, COMMENT_TYPE_OPTIONS } from "../Comment/lib/types";
import { Hub, parseHub } from "~/config/types/hub";
import fetchReputationHubs from "../Hubs/api/fetchReputationHubs";
import { DocumentContext } from "../Document/lib/DocumentContext";
import HubTag from "../Hubs/HubTag";
import Select, { ValueType, OptionTypeBase, components } from "react-select";



const selectDropdownStyles = {
multiTagLabelStyle: {
color: colors.NEW_BLUE(1),
cursor: "pointer",
},
multiTagStyle: {
padding: "4px 12px",
borderRadius: 50,
fontSize: 15
},
option: {
textAlign: "left",
backgroundColor: "unset",
background: "unset",
":hover": {
backgroundColor: colors.NEW_BLUE(0.1),
},
},
menuList: {
},
valueContainer: {
padding: "7px 7px 7px 4px",
},
};

const _convertToSelectOption = (hubs: Hub[]): OptionTypeBase[] => {
const repHubDropdownOptions = hubs.map((h: any) => ({
label: h.name,
value: h.id,
name: h.name,
valueForApi: h.id,
}));

return repHubDropdownOptions;
}



type Props = {
isOpen: boolean;
Expand All @@ -44,6 +89,7 @@ type Props = {
setMessage: Function;
};


function BountyModal({
isOpen,
withPreview,
Expand Down Expand Up @@ -72,7 +118,10 @@ function BountyModal({
const [hasMaxRscAlert, setHasMaxRscAlert] = useState(false);
const [bountyType, setBountyType] = useState<COMMENT_TYPES | null>(null);
const [success, setSuccess] = useState(false);
const [reputationHubs, setReputationHubs] = useState<Array<Hub>>([]);
const [selectedReputationHubs, setSelectedReputationHubs] = useState<Array<OptionTypeBase>>([]);

const documentContext = useContext(DocumentContext);
const { rscToUSDDisplay } = useExchangeRate();

useEffect((): void => {
Expand All @@ -83,6 +132,35 @@ function BountyModal({
);
}, [currentUserBalance, offeredAmount]);

useEffect(() => {

if (originalBounty) {
// This flow is only applicable to "new bounties" not "contributions".
// If contribution is being made, we should not allow users to target based on expertise.
return;
}

if (isOpen && reputationHubs.length === 0) {
fetchReputationHubs().then((response) => {
const hubs = response.map((hub:any) => parseHub(hub));
setReputationHubs(hubs);

if (documentContext.metadata?.hubs) {
const repHubs = documentContext.metadata?.hubs.reduce((acc:Hub[], hub) => {
if (hub.isUsedForRep) {
acc.push(hub);
}
return acc;
}, []);

const preselectedOption = _convertToSelectOption(repHubs);
setSelectedReputationHubs(preselectedOption);
}

});
}
}, [isOpen, reputationHubs, originalBounty]);

const handleClose = () => {
closeModal();
setSuccess(false);
Expand Down Expand Up @@ -147,6 +225,7 @@ function BountyModal({
new Bounty({
amount: offeredAmount,
bounty_type: bountyType,
...(selectedReputationHubs.length > 0 && { target_hubs: selectedReputationHubs.map(h => h.value) }),
})
);
closeModal();
Expand Down Expand Up @@ -175,6 +254,10 @@ function BountyModal({
const showAlertNextToBtn = hasMinRscAlert || hasMaxRscAlert || withPreview;
const researchHubAmount = calcResearchHubAmount({ offeredAmount });
const totalAmount = calcTotalAmount({ offeredAmount });

const repHubDropdownOptions = _convertToSelectOption(reputationHubs);


return (
<BaseModal
closeModal={handleClose}
Expand Down Expand Up @@ -348,6 +431,45 @@ function BountyModal({
</div>
</div>
</div>

{!originalBounty && (
<div style={{ marginBottom: 0, padding: "0px 30px 30px 30px", }}>
<div
className={css(styles.lineItemText, styles.sectionLabel)}
>
<FontAwesomeIcon fontSize={18} style={{ marginRight: 4, }} icon={faUserGroup}></FontAwesomeIcon>
Target Audience <span className={css(styles.optionalLabel)}>- Optional</span>
</div>
<div className={css(styles.lineItemText, styles.sectionDescription)}>
Target specific users to help with your bounty. If non is selected, we will automatically match the bounty to relevant users.
</div>
<FormSelect
id={"Expertise"}
isMulti={true}
required={false}
value={selectedReputationHubs}
reactStyles={{}}
inputStyle={styles.inputStyle}
reactSelect={{ styles: selectDropdownStyles }}
showCountInsteadOfLabels={false}
options={repHubDropdownOptions}
containerStyle={[
styles.dropdownContainer,
]}
onChange={(id, value) => {
setSelectedReputationHubs(value);
}}
isSearchable={true}
placeholder={"Choose Expertise"}
multiTagStyle={null}
multiTagLabelStyle={null}
isClearable={false}
/>
</div>
)}



<div className={css(infoSectionStyles.bountyInfo)}>
{originalBounty && (
<div className={css(infoSectionStyles.infoRow)}>
Expand Down Expand Up @@ -413,7 +535,7 @@ function BountyModal({
You will have a chance to review and cancel before bounty
is created
</div>
) : null}
) : null}
<div className={css(styles.addBtnContainer)}>
<Button
label={originalBounty ? "Contribute" : "Add bounty"}
Expand Down Expand Up @@ -504,6 +626,44 @@ const infoSectionStyles = StyleSheet.create({
});

const styles = StyleSheet.create({
sectionLabel: {
fontSize: 18,
fontWeight: 500,
display: "flex",
columnGap: "5px",
alignItems: "center",
},
optionalLabel: {
fontSize: 18,
fontWeight: 400,
},
sectionDescription: {
marginTop: 8,
fontSize: 16,
color: colors.DARKER_GREY(),
marginBottom: 20,
textAlign: "left",
},
tagStyle: {
fontSize: 13,
},
inputStyle: {

},
dropdownContainer: {
width: "100%",
minHeight: "unset",
marginTop: 0,
marginBottom: 0,
marginRight: 20,
// [`@media only screen and (max-width: ${breakpoints.small.str})`]: {
// marginRight: 0,
// },
},
dropdownInput: {
width: "100%",
minHeight: "unset",
},
rootContainer: {
fontSize: 18,
width: "100%",
Expand Down
1 change: 1 addition & 0 deletions components/Comment/CommentEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ const CommentEditor = ({
...(interimBounty && {
bountyAmount: interimBounty.amount,
bountyType: interimBounty.bountyType,
targetHubs: interimBounty.targetHubs,
}),
});

Expand Down
3 changes: 3 additions & 0 deletions components/Comment/CommentFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,14 @@ const CommentFeed = ({
bountyAmount,
bountyType,
mentions,
targetHubs
}: {
content: any;
commentType: COMMENT_TYPES;
bountyAmount?: number;
mentions?: Array<string>;
bountyType: COMMENT_TYPES;
targetHubs?: Array<ID>;
}) => {
let comment: CommentType;
try {
Expand All @@ -264,6 +266,7 @@ const CommentFeed = ({
bountyAmount,
bountyType,
mentions,
targetHubs,
});

setRootLevelCommentCount(rootLevelCommentCount + 1);
Expand Down
3 changes: 3 additions & 0 deletions components/Comment/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export const createCommentAPI = async ({
anchor = null,
organizationId,
bountyType,
targetHubs,
}: {
content: any;
commentType?: COMMENT_TYPES;
Expand All @@ -164,6 +165,7 @@ export const createCommentAPI = async ({
anchor?: null | SerializedAnchorPosition;
organizationId?: ID;
bountyType?: COMMENT_TYPES;
targetHubs?: Array<ID>;
}): Promise<Comment> => {
const _url = generateApiUrl(
`${documentType}/${documentId}/comments/` +
Expand All @@ -182,6 +184,7 @@ export const createCommentAPI = async ({
...(bountyAmount && { amount: bountyAmount, bounty_type: bountyType }),
...(anchor && { anchor }),
...(threadId && { thread_id: threadId }),
...(targetHubs && { target_hub_ids: targetHubs }),
},
undefined,
organizationId ? { "x-organization-id": organizationId } : undefined
Expand Down
14 changes: 14 additions & 0 deletions components/Hubs/api/fetchReputationHubs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import API, { generateApiUrl, buildQueryString } from "~/config/api";
import { Helpers } from "@quantfive/js-web-config";

const fetchReputationHubs = async () => {
const url = generateApiUrl(`hub/rep_hubs`);

const response = await fetch(url, API.GET_CONFIG()).then((res): any =>
Helpers.parseJSON(res)
);

return response;
}

export default fetchReputationHubs;
Loading

0 comments on commit 2626268

Please sign in to comment.