/* INCLUDE: "common.jsx" */ const nearNFDevsContractAccountId = props.nearNFDevsContractAccountId || (context.widgetSrc ?? "near-analytics.near").split("/", 1)[0]; const nearNFDevsWidgetsAccountId = props.nearNFDevsWidgetsAccountId || (context.widgetSrc ?? "near-analytics.near").split("/", 1)[0]; function widget(widgetName, widgetProps, key) { widgetProps = { ...widgetProps, nearNFDevsContractAccountId: props.nearNFDevsContractAccountId, nearNFDevsWidgetsAccountId: props.nearNFDevsWidgetsAccountId, referral: props.referral, }; return ( <Widget src={`${nearNFDevsWidgetsAccountId}/widget/townhall-board.${widgetName}`} props={widgetProps} key={key} /> ); } function href(widgetName, linkProps) { linkProps = { ...linkProps }; if (props.nearNFDevsContractAccountId) { linkProps.nearNFDevsContractAccountId = props.nearNFDevsContractAccountId; } if (props.nearNFDevsWidgetsAccountId) { linkProps.nearNFDevsWidgetsAccountId = props.nearNFDevsWidgetsAccountId; } if (props.referral) { linkProps.referral = props.referral; } const linkPropsQuery = Object.entries(linkProps) .filter(([_key, nullable]) => (nullable ?? null) !== null) .map(([key, value]) => `${key}=${value}`) .join("&"); return `/#/${nearNFDevsWidgetsAccountId}/widget/townhall-board.pages.${widgetName}${ linkPropsQuery ? "?" : "" }${linkPropsQuery}`; } /* END_INCLUDE: "common.jsx" */ const parentId = props.parentId ?? null; const postId = props.postId ?? null; const mode = props.mode ?? "Create"; const referralLabels = props.referral ? [`referral:${props.referral}`] : []; const labelStrings = (props.labels ? props.labels.split(",") : []).concat( referralLabels ); const labels = labelStrings.map((s) => { return { name: s }; }); initState({ seekingFunding: false, // author_id: context.accountId, // Should be a list of objects with field "name". labels, // Should be a list of labels as strings. // Both of the label structures should be modified together. labelStrings, postType: "Idea", name: props.name ?? "", description: props.description ?? "", amount: props.amount ?? "", token: props.token ?? "NEAR", supervisor: props.supervisor ?? "neardevgov.near", githubLink: props.githubLink ?? "", warning: "", }); // This must be outside onClick, because Near.view returns null at first, and when the view call finished, it returns true/false. // If checking this inside onClick, it will give `null` and we cannot tell the result is true or false. let grantNotify = Near.view("social.near", "is_write_permission_granted", { predecessor_id: nearNFDevsContractAccountId, key: context.accountId + "/index/notify", }); if (grantNotify === null) { return; } const onSubmit = () => { let labels = state.labelStrings; let body = { name: state.name, description: generateDescription( state.description, state.amount, state.token, state.supervisor ), }; if (state.postType === "Solution") { body = { ...body, post_type: "Submission", submission_version: "V1", }; } else { // Idea body = { ...body, post_type: "Idea", idea_version: "V1", }; } if (!context.accountId) return; let txn = []; if (mode == "Create") { txn.push({ contractName: nearNFDevsContractAccountId, methodName: "add_post", args: { parent_id: parentId, labels, body: body, }, deposit: Big(10).pow(21).mul(2), gas: Big(10).pow(12).mul(100), }); } else if (mode == "Edit") { txn.push({ contractName: nearNFDevsContractAccountId, methodName: "edit_post", args: { id: postId, labels, body: body, }, deposit: Big(10).pow(21).mul(2), gas: Big(10).pow(12).mul(100), }); } if (mode == "Create" || mode == "Edit") { if (grantNotify === false) { txn.unshift({ contractName: "social.near", methodName: "grant_write_permission", args: { predecessor_id: nearNFDevsContractAccountId, keys: [context.accountId + "/index/notify"], }, deposit: Big(10).pow(23), gas: Big(10).pow(12).mul(30), }); } Near.call(txn); } }; const onIdeaClick = () => { State.update({ postType: "Idea", seekingFunding: false }); }; const onSolutionClick = () => { State.update({ postType: "Solution" }); }; const normalizeLabel = (label) => label .replaceAll(/[- \.]/g, "_") .replaceAll(/[^\w]+/g, "") .replaceAll(/_+/g, "-") .replace(/^-+/, "") .replace(/-+$/, "") .toLowerCase() .trim("-"); const checkLabel = (label) => { Near.asyncView(nearNFDevsContractAccountId, "is_allowed_to_use_labels", { editor: context.accountId, labels: [label], }).then((allowed) => { if (allowed) { State.update({ warning: "" }); } else { State.update({ warning: 'The label "' + label + '" is protected and can only be added by moderators', }); return; } }); }; const setLabels = (labels) => { labels = labels.map((o) => { o.name = normalizeLabel(o.name); return o; }); if (labels.length < state.labels.length) { let oldLabels = new Set(state.labels.map((label) => label.name)); for (let label of labels) { oldLabels.delete(label.name); } let removed = oldLabels.values().next().value; Near.asyncView(nearNFDevsContractAccountId, "is_allowed_to_use_labels", { editor: context.accountId, labels: [removed], }).then((allowed) => { if (allowed) { let labelStrings = labels.map(({ name }) => name); State.update({ labels, labelStrings }); } else { State.update({ warning: 'The label "' + removed + '" is protected and can only be updated by moderators', }); return; } }); } else { let labelStrings = labels.map((o) => { return o.name; }); State.update({ labels, labelStrings }); } }; const existingLabelStrings = Near.view(nearNFDevsContractAccountId, "get_all_allowed_labels", { editor: context.accountId, }) ?? []; const existingLabelSet = new Set(existingLabelStrings); const existingLabels = existingLabelStrings.map((s) => { return { name: s }; }); const labelEditor = ( <div className="col-lg-12 mb-2"> <p className="fs-6 fw-bold mb-1">Labels</p> <Typeahead multiple labelKey="name" onInputChange={checkLabel} onChange={setLabels} options={existingLabels} placeholder="near.social, widget, NEP, standard, protocol, tool" selected={state.labels} positionFixed allowNew={(results, props) => { return ( !existingLabelSet.has(props.text) && props.selected.filter((selected) => selected.name === props.text) .length == 0 && Near.view(nearNFDevsContractAccountId, "is_allowed_to_use_labels", { editor: context.accountId, labels: [props.text], }) ); }} /> </div> ); const nameDiv = ( <div className="col-lg-6 mb-2"> <p className="fs-6 fw-bold mb-1">Title</p> <input type="text" value={state.name} onChange={(event) => State.update({ name: event.target.value })} /> </div> ); const descriptionDiv = ( <div className="col-lg-12 mb-2"> <p className="fs-6 fw-bold mb-1">Description</p> <textarea value={state.description} type="text" rows={6} className="form-control" onChange={(event) => State.update({ description: event.target.value })} /> </div> ); const isFundraisingDiv = ( // This is jank with just btns and not radios. But the radios were glitchy af <> <div class="mb-2"> <p class="fs-6 fw-bold mb-1"> Are you seeking funding for your solution? <span class="text-muted fw-normal">(Optional)</span> </p> <div class="form-check form-check-inline"> <label class="form-check-label"> <button className="btn btn-light p-0" style={{ backgroundColor: state.seekingFunding ? "#0C7283" : "inherit", color: "#f3f3f3", border: "solid #D9D9D9", borderRadius: "100%", height: "20px", width: "20px", }} onClick={() => State.update({ seekingFunding: true })} /> Yes </label> </div> <div class="form-check form-check-inline"> <label class="form-check-label"> <button className="btn btn-light p-0" style={{ backgroundColor: !state.seekingFunding ? "#0C7283" : "inherit", color: "#f3f3f3", border: "solid #D9D9D9", borderRadius: "100%", height: "20px", width: "20px", }} onClick={() => State.update({ seekingFunding: false })} /> No </label> </div> </div> </> );