diff --git a/common/charity.ts b/common/charity.ts index ab769655..ecc9f89b 100644 --- a/common/charity.ts +++ b/common/charity.ts @@ -19,8 +19,7 @@ export const charities: Charity[] = [ website: 'https://www.1daysooner.org/', preview: 'Accelerating the development of each vaccine by even a couple of days via COVID-19 human challenge trials could save thousands of lives.', - photo: - 'https://images.squarespace-cdn.com/content/v1/5f5f8496d1d7713486b6075a/666cbb5a-5335-4323-b9ea-b764edc826e1/OFFICIAL+1Day+Sooner+Logo.png', + photo: 'https://i.imgur.com/bUDdzUE.png', description: `1Day Sooner is a non-profit that advocates on behalf of COVID-19 challenge trial volunteers. After a vaccine candidate is created in a lab, it is developed through a combination of pre-clinical evaluation and three phases of clinical trials that test its safety and efficacy. In traditional Phase III trials, participants receive the vaccine candidate or a placebo/active comparator, and efficacy is judged by comparing the prevalence of infection in the vaccine group and the placebo/comparator group, to test the hypothesis that significantly fewer participants in the vaccine group get infected. In these traditional trials, after receiving the treatment, participants return to their homes and their normal daily lives so as to test the treatment under real world conditions. Since only a small proportion of these participants may encounter the disease, it may take a large number of participants and a good deal of time for these trials to reveal differences between the vaccine and placebo groups. @@ -35,8 +34,7 @@ export const charities: Charity[] = [ website: 'https://quantifieduncertainty.org/', preview: 'The Quantified Uncertainty Research Institute advances forecasting and epistemics to improve the long-term future of humanity.', - photo: - 'https://quantifieduncertainty.org/_next/image?url=https%3A%2F%2Fsuper-static-assets.s3.amazonaws.com%2F09bb1362-5e3f-4724-8ffd-f3235f67356f%2Fimages%2F6151ac3e-aed7-44c7-9827-399fe6e9222b.png&w=1920&q=80', + photo: 'https://i.imgur.com/ZsSXPjH.png', description: `QURI researches systematic practices to specify and estimate the most important parameters for the most important or scalable decisions. Research areas include forecasting, epistemics, evaluations, ontology, and estimation. We emphasize technological solutions that can heavily scale in the next 5 to 30 years. @@ -47,7 +45,7 @@ export const charities: Charity[] = [ { name: 'Long-Term Future Fund', website: 'https://funds.effectivealtruism.org/funds/far-future', - photo: 'https://app.effectivealtruism.org/logo-funds.svg', + photo: 'https://i.imgur.com/C2qka9g.png', preview: 'Positively influence the long-term trajectory of civilization by making grants that address global catastrophic risks.', description: `The Fund has a broad remit to make grants that promote, implement and advocate for longtermist ideas. Many of our grants aim to address potential risks from advanced artificial intelligence and to build infrastructure and advocate for longtermist projects. However, we welcome applications related to long-term institutional reform or other global catastrophic risks (e.g., pandemics or nuclear conflict). @@ -58,10 +56,39 @@ export const charities: Charity[] = [ - Promoting long-term thinking`, tags: ['Featured'] as CharityTag[], }, + { + name: 'Nonlinear', + website: 'https://www.nonlinear.org/', + photo: 'https://i.imgur.com/Muifc1l.png', + preview: + 'Incubate longtermist nonprofits by connecting founders with ideas, funding, and mentorship.', + description: `Problem: There are tens of thousands of people working full time to make AI powerful, but around one hundred working to make AI safe. This needs to change. + + Longtermism is held back by two bottlenecks: + 1. Lots of funding, but few charities to deploy it. + 2. Lots of talent, but few charities creating jobs. + + Solution: Longtermism needs more charities to deploy funding and create jobs. Our goal is to 10x the number of talented people working on longtermism by launching dozens of high impact charities. + + This helps solve the bottlenecks because entrepreneurs “unlock” latent EA talent - if one person starts an organization that employs 100 people who weren’t previously working on AI safety, that doubles the number of people working on the problem. + + Our process: + 1. Research the highest leverage ideas + 2. Find the right founders + 3. Connect them with mentors and funding + + We will be announcing more details about our incubation program soon. + + A few of the ideas we’ve incubated so far: + - The Nonlinear Library: Listen to top EA content on your podcast player. We use text-to-speech software to create an automatically updating repository of audio content from the EA Forum, Alignment Forum, and LessWrong. You can find it on all major podcast players here. + - EA Hiring Agency: Helping EA orgs scalably hire talent. + - EA Houses: EA's Airbnb - Connecting EAs who have extra space with EAs who need space here.`, + tags: ['Featured'] as CharityTag[], + }, { name: 'GiveWell Maximum Impact Fund', website: 'https://www.givewell.org/maximum-impact-fund', - photo: 'https://www.givewell.org/sites/all/themes/gw_basic/logo.png', + photo: 'https://i.imgur.com/xikuDMZ.png', preview: 'We search for the charities that save or improve lives the most per dollar.', description: ` @@ -98,8 +125,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Give Directly', website: 'https://www.givedirectly.org/', ein: '27-1661997', - photo: - 'https://www.givewell.org/sites/default/files/charity_logos/GiveDirectly.jpg', + photo: 'https://i.imgur.com/lrdxSyd.jpg', preview: 'Send money directly to people living in poverty.', description: 'GiveDirectly is a nonprofit that lets donors like you send money directly to the world’s poorest households. We believe people living in poverty deserve the dignity to choose for themselves how best to improve their lives — cash enables that choice. Since 2009, we’ve delivered $500M+ in cash directly into the hands of over 1 million families living in poverty. We currently have operations in Kenya, Rwanda, Liberia, Malawi, Morocco, Mozambique, DRC, Uganda, and the United States.', @@ -108,8 +134,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Hellen Keller International', website: 'https://www.hki.org/', ein: '13-5562162', - photo: - 'https://www.ntd-ngonetwork.org/sites/nnn/files/content/organisation/logos/2020-01-28/v2_HKLogo_Primary_RGB.jpg', + photo: 'https://i.imgur.com/Dl97Abk.jpg', preview: 'We envision a world where no one is deprived of the opportunity to live a healthy life – and reach their true potential.', description: @@ -119,8 +144,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Against Malaria Foundation', website: 'https://www.againstmalaria.com/', ein: '20-3069841', - photo: - 'https://media-exp1.licdn.com/dms/image/C4D0BAQFvdcum9KBNfg/company-logo_200_200/0?e=2159024400&v=beta&t=hxjJCKQkMp2irTOcuJEceW7x4l3c4PD7gYCQ6ulgYlg', + photo: 'https://i.imgur.com/F3JoZi9.png', preview: 'We help protect people from malaria.', description: 'AMF (againstmalaria.com) provides funding for long-lasting insecticide-treated net (LLIN) distributions (for protection against malaria) in developing countries. There is strong evidence that distributing LLINs reduces child mortality and malaria cases. AMF conducts post-distribution surveys of completed distributions to determine whether LLINs have reached their intended destinations and how long they remain in good condition.', @@ -128,8 +152,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un { name: 'Rethink Charity', website: 'https://rethink.charity/', - photo: - 'https://process.filestackapi.com/resize=width:600,height:315,fit:max/quality=value:90/jvYvq1JFQkOqo3J8hVcJ', + photo: 'https://i.imgur.com/Go7N7As.png', preview: 'Providing vital support to high-impact charities and charitable projects.', description: `At Rethink Charity, we’re excited about improving the world by providing vital support to high-impact charities and charitable projects. We equip them with tools to boost their impact, through our projects that empower their donors with tax-efficient giving options and strategically coordinated matching opportunities. @@ -143,8 +166,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Malaria Consortium', website: 'https://www.malariaconsortium.org/', ein: '98-0627052', - photo: - 'https://www.malariaconsortium.org/website-2013/images_template/malaria_consortium_logo.png', + photo: 'https://i.imgur.com/LGwy9d8.png ', preview: 'We specialise in the prevention, control and treatment of malaria and other communicable diseases.', description: @@ -153,7 +175,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un { name: 'The Center for the Study of Partisanship and Ideology', website: 'https://cspicenter.org/', - photo: 'https://cspicenter.org/wp-content/uploads/2020/02/CSPI.png', + photo: 'https://i.imgur.com/O88tkOW.png', preview: 'Support and fund research on how ideology and government policy contribute to scientific, technological, and social progress.', description: `Over the last few decades, scientific and technological progress have stagnated. Scientists conduct more research than ever before, but groundbreaking innovation is scarce. At the same time, identity politics and political polarization have reached new extremes, and social trends such as family stability and crime are worse than in previous decades and in some cases moving in the wrong direction. What explains these trends, and how can we reverse them? @@ -170,8 +192,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Faunalytics', website: 'https://faunalytics.org/', ein: '01-0686889', - photo: - 'https://animalcharityevaluators.org/wp-content/uploads/2016/08/logo-faunalytics2400x2400-200x200@2x.jpg', + photo: 'https://i.imgur.com/3JXhuXl.jpg', preview: 'Faunalytics conducts research and shares knowledge to help advocates help animals effectively.', description: @@ -181,8 +202,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'The Humane League', website: 'https://thehumaneleague.org/', ein: '04-3817491', - photo: - 'https://animalcharityevaluators.org/wp-content/uploads/2019/03/thl-mended-heart-logo@2x-200x200@2x.jpg', + photo: 'https://i.imgur.com/za9Rwon.jpg', preview: 'We exist to end the abuse of animals raised for food by influencing the policies of the world’s biggest companies, demanding legislation, and empowering others to take action and leave animals off their plates', description: @@ -192,8 +212,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Wild Animal Initiative', website: 'https://www.wildanimalinitiative.org/', ein: '82-2281466', - photo: - 'https://animalcharityevaluators.org/wp-content/uploads/2020/11/WAI-logo_square-gray-on-teal-1-630x630.png', + photo: 'https://i.imgur.com/bOVUnDm.png', preview: 'We want to make life better for wild animals.', description: 'Wild Animal Initiative (WAI) currently operates in the U.S., where they work to strengthen the animal advocacy movement through creating an academic field dedicated to wild animal welfare. They compile literature reviews, write theoretical and opinion articles, and publish research results on their website and/or in peer-reviewed journals. WAI focuses on identifying and sharing possible research avenues and connecting with more established fields. They also work with researchers from various academic and non-academic institutions to identify potential collaborators, and they recently launched a grant assistance program.', @@ -202,8 +221,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'New Incentives', website: 'https://www.newincentives.org/', ein: '45-2368993', - photo: - 'https://uploads-ssl.webflow.com/5f7c51bf9fac9b5ed62aa37b/5f7c51bf9fac9b85c42aa3df_Group%20344%20(1).svg', + photo: 'https://i.imgur.com/bYl4tk3.png', preview: 'Cash incentives to boost vaccination rates and save lives.', description: 'New Incentives (newincentives.org) runs a conditional cash transfer (CCT) program in North West Nigeria which seeks to increase uptake of routine immunizations through cash transfers, raising public awareness of the benefits of vaccination and reducing the frequency of vaccine stockouts.', @@ -212,8 +230,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'SCI foundation', website: 'https://schistosomiasiscontrolinitiative.org/', ein: '', - photo: - 'https://images.easyfundraising.org.uk/cause/cropped/cause-logo-e99e0632a8a9572150fdcf3abf08ad45.png', + photo: 'https://i.imgur.com/sWD8zM5.png', preview: 'SCI works with governments in sub-Saharan Africa to create or scale up programs that treat schistosomiasis and soil-transmitted helminthiasis ("deworming").', description: @@ -223,8 +240,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Wikimedia Foundation', website: 'https://wikimediafoundation.org/', ein: '20-0049703', - photo: - 'https://2.bp.blogspot.com/-jVseU39DW0s/VjmXVMOEEEI/AAAAAAAACK8/dwUP6sLqy-Q/s1600/wikimedia.png', + photo: 'https://i.imgur.com/klEzUbR.png', preview: 'We help everyone share in the sum of all knowledge.', description: 'We are the people who keep knowledge free. There is an amazing community of people around the world that makes great projects like Wikipedia. We help them do that work. We take care of the technical infrastructure, the legal challenges, and the growing pains.', @@ -233,8 +249,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Rainforest Trust', website: 'https://www.rainforesttrust.org/', ein: '13-3500609', - photo: - 'https://ww1.prweb.com/prfiles/2019/05/29/16344590/Rrainforest%20Trust%20new%20logo%20tall-1%20copy.png', + photo: 'https://i.imgur.com/6MzS530.png', preview: 'Rainforest Trust saves endangered wildlife and protects our planet by creating rainforest reserves through partnerships, community engagement and donor support.', description: @@ -244,8 +259,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'The Nature Conservancy', website: 'https://www.nature.org/en-us/', ein: '53-0242652', - photo: - 'https://mma.prnewswire.com/media/1140905/The_Nature_Conservancy_Logo.jpg?p=facebook', + photo: 'https://i.imgur.com/vjxkoGo.jpg', preview: 'A Future Where People and Nature Thrive', description: 'The Nature Conservancy is a global environmental nonprofit working to create a world where people and nature can thrive. Founded in the U.S. through grassroots action in 1951, The Nature Conservancy has grown to become one of the most effective and wide-reaching environmental organizations in the world. Thanks to more than a million members and the dedicated efforts of our diverse staff and over 400 scientists, we impact conservation in 76 countries and territories: 37 by direct conservation impact and 39 through partners.', @@ -254,7 +268,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Doctors Without Borders', website: 'https://www.doctorswithoutborders.org/', ein: '13-3433452', - photo: 'https://www.doctorswithoutborders.org/themes/custom/msf/logo.svg', + photo: 'https://i.imgur.com/xqhH9FE.png', preview: 'We provide independent, impartial medical humanitarian assistance to the people who need it most.', description: @@ -264,8 +278,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'World Wildlife Fund', website: 'https://www.worldwildlife.org/', ein: '52-1693387', - photo: - 'https://www.worldwildlife.org/assets/structure/unique/logo-c562409bb6158bf64e5f8b1be066dbd5983d75f5ce7c9935a5afffbcc03f8e5d.png', + photo: 'https://i.imgur.com/hDADuqW.png', preview: 'WWF works to sustain the natural world for the benefit of people and wildlife, collaborating with partners from local to global levels in nearly 100 countries.', description: @@ -274,7 +287,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un { name: 'UNICEF USA', website: 'https://www.unicefusa.org/', - photo: 'https://www.unicefusa.org/sites/default/files/UNICEFUSA_DIG_C.svg', + photo: 'https://i.imgur.com/9cxuvZi.png', ein: '13-1760110', preview: "UNICEF USA helps save and protect the world's most vulnerable children.", @@ -285,8 +298,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Vitamin Angels', website: 'https://www.vitaminangels.org/', ein: '77-0485881', - photo: - 'https://www.newhope.com/sites/newhope360.com/files/styles/article_featured_retina/public/vitamin-angels-logo.jpg?itok=pfNCPLE0', + photo: 'https://i.imgur.com/Mf35IOu.jpg', preview: 'By improving access to vital nutrition, everyone gets an equal chance to grow, thrive, and prosper.', description: @@ -296,7 +308,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Free Software Foundation', website: 'https://www.fsf.org/', ein: '04-2888848', - photo: 'https://www.gnu.org/graphics/logo-fsf.org.png', + photo: 'https://i.imgur.com/z87sFDE.png', preview: 'The Free Software Foundation (FSF) is a nonprofit with a worldwide mission to promote computer user freedom.', description: @@ -306,8 +318,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Direct Relief', website: 'https://www.directrelief.org/', ein: '95-1831116', - photo: - 'https://www.ngoadvisor.net/wp-content/uploads/2016/02/DirectRelief_Logo_RGB-2-1920x576.png', + photo: 'https://i.imgur.com/QS7kHAU.png', preview: 'Direct Relief is a humanitarian aid organization, active in all 50 states and more than 80 countries, with a mission to improve the health and lives of people affected by poverty or emergencies – without regard to politics, religion, or ability to pay.', description: @@ -317,8 +328,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'World Resources Institute', website: 'https://www.wri.org/', ein: '52-1257057', - photo: - 'https://www.americansecurityproject.org/wp-content/uploads/2016/11/WRI_logo_4c.png', + photo: 'https://i.imgur.com/Bi6MgYI.png', preview: 'WRI is a global nonprofit organization that works with leaders in government, business and civil society to research, design, and carry out practical solutions that simultaneously improve people’s lives and ensure nature can thrive.', description: @@ -328,8 +338,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'ProPublica', website: 'https://www.propublica.org/', ein: '14-2007220', - photo: - 'https://seekvectorlogo.com/wp-content/uploads/2018/09/propublica-vector-logo.png', + photo: 'https://i.imgur.com/R5Vt3Pb.png', preview: 'The mission: to expose abuses of power and betrayals of the public trust by government, business, and other institutions, using the moral force of investigative journalism to spur reform through the sustained spotlighting of wrongdoing.', description: @@ -339,8 +348,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Dana-Farber Cancer Institute', website: 'https://www.dana-farber.org/', ein: '04-2263040', - photo: - 'https://www.danafarbermasterclass.com/assets/images/DFCI-logo-lens-stacked.png', + photo: 'https://i.imgur.com/SQNn97p.png', preview: "For over 70 years, we've led the world by making life-changing breakthroughs in cancer research and patient care, providing the most advanced treatments available.", description: @@ -350,8 +358,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'Save The Children', website: 'https://www.savethechildren.org/', ein: '06-0726487', - photo: - 'https://www.thisisclapham.co.uk/wp-content/uploads/2016/08/savethechildren.png', + photo: 'https://i.imgur.com/GngYPBI.png', preview: 'Through the decades, Save the Children has continued to work to save children’s lives, and that’s still what we do today.', description: @@ -361,8 +368,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'World Central Kitchen Incorporated', website: 'https://wck.org/', ein: '27-3521132', - photo: - 'https://res.cloudinary.com/dktp1ybbx/image/upload/f_auto,fl_lossy,q_auto/v1560203222/organization/prod/924457/M0oxO9vaxO.png', + photo: 'https://i.imgur.com/te93MaY.png', preview: 'WCK is first to the frontlines, providing meals in response to humanitarian, climate, and community crises. We build resilient food systems with locally led solutions.', description: @@ -372,8 +378,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un name: 'The Johns Hopkins Center for Health Security', website: 'https://www.centerforhealthsecurity.org/', ein: '', - photo: - 'https://www.centerforhealthsecurity.org/sebin/d/d/CHS.logo.horizontal.blue.png', + photo: 'https://i.imgur.com/gKZE2Xs.png', preview: 'Our mission: to protect people’s health from epidemics and disasters and ensure that communities are resilient to major challenges.', description: @@ -382,8 +387,7 @@ Future plans: We expect to focus on similar theoretical problems in alignment un { name: 'ALLFED', website: 'https://allfed.info/', - photo: - 'https://images1.the-dots.com/1860424/allfed-logo-1.png?p=projectImageFullJpg', + photo: 'https://i.imgur.com/p235vwF.jpg', ein: '27-6601178', preview: 'Feeding everyone no matter what.', description: diff --git a/common/comment.ts b/common/comment.ts index 5daeb37e..15cfbcb5 100644 --- a/common/comment.ts +++ b/common/comment.ts @@ -4,6 +4,7 @@ export type Comment = { id: string contractId: string betId?: string + answerOutcome?: string userId: string text: string diff --git a/common/util/format.ts b/common/util/format.ts index e4ac0bd7..3310d902 100644 --- a/common/util/format.ts +++ b/common/util/format.ts @@ -9,9 +9,7 @@ const formatter = new Intl.NumberFormat('en-US', { export function formatMoney(amount: number) { const newAmount = Math.round(amount) === 0 ? 0 : Math.floor(amount) // handle -0 case - return ( - ENV_CONFIG.moneyMoniker + ' ' + formatter.format(newAmount).replace('$', '') - ) + return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '') } export function formatWithCommas(amount: number) { diff --git a/functions/src/add-liquidity.ts b/functions/src/add-liquidity.ts index e37804d3..2f6d2446 100644 --- a/functions/src/add-liquidity.ts +++ b/functions/src/add-liquidity.ts @@ -1,11 +1,11 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { Contract } from '../../common/contract' -import { User } from '../../common/user' -import { removeUndefinedProps } from '../../common/util/object' +import { Contract } from 'common/contract' +import { User } from 'common/user' +import { removeUndefinedProps } from 'common/util/object' import { redeemShares } from './redeem-shares' -import { getNewLiquidityProvision } from '../../common/add-liquidity' +import { getNewLiquidityProvision } from 'common/add-liquidity' export const addLiquidity = functions.runWith({ minInstances: 1 }).https.onCall( async ( diff --git a/functions/src/change-user-info.ts b/functions/src/change-user-info.ts index ab15eb70..f85d45b3 100644 --- a/functions/src/change-user-info.ts +++ b/functions/src/change-user-info.ts @@ -2,12 +2,12 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { getUser } from './utils' -import { Contract } from '../../common/contract' -import { Comment } from '../../common/comment' -import { User } from '../../common/user' -import { cleanUsername } from '../../common/util/clean-username' -import { removeUndefinedProps } from '../../common/util/object' -import { Answer } from '../../common/answer' +import { Contract } from 'common/contract' +import { Comment } from 'common/comment' +import { User } from 'common/user' +import { cleanUsername } from 'common/util/clean-username' +import { removeUndefinedProps } from 'common/util/object' +import { Answer } from 'common/answer' export const changeUserInfo = functions .runWith({ minInstances: 1 }) diff --git a/functions/src/create-answer.ts b/functions/src/create-answer.ts index 1da8f350..55211585 100644 --- a/functions/src/create-answer.ts +++ b/functions/src/create-answer.ts @@ -1,18 +1,13 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { - Contract, - DPM, - FreeResponse, - FullContract, -} from '../../common/contract' -import { User } from '../../common/user' -import { getLoanAmount, getNewMultiBetInfo } from '../../common/new-bet' -import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer' +import { Contract, DPM, FreeResponse, FullContract } from 'common/contract' +import { User } from 'common/user' +import { getLoanAmount, getNewMultiBetInfo } from 'common/new-bet' +import { Answer, MAX_ANSWER_LENGTH } from 'common/answer' import { getContract, getValues } from './utils' import { sendNewAnswerEmail } from './emails' -import { Bet } from '../../common/bet' +import { Bet } from 'common/bet' export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( async ( diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index ae7b0ad8..5345469e 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -13,10 +13,10 @@ import { MAX_QUESTION_LENGTH, MAX_TAG_LENGTH, outcomeType, -} from '../../common/contract' -import { slugify } from '../../common/util/slugify' -import { randomString } from '../../common/util/random' -import { getNewContract } from '../../common/new-contract' +} from 'common/contract' +import { slugify } from 'common/util/slugify' +import { randomString } from 'common/util/random' +import { getNewContract } from 'common/new-contract' import { FIXED_ANTE, getAnteBets, @@ -24,8 +24,8 @@ import { getFreeAnswerAnte, HOUSE_LIQUIDITY_PROVIDER_ID, MINIMUM_ANTE, -} from '../../common/antes' -import { getNoneAnswer } from '../../common/answer' +} from 'common/antes' +import { getNoneAnswer } from 'common/answer' export const createContract = functions .runWith({ minInstances: 1 }) @@ -72,7 +72,6 @@ export const createContract = functions ) return { status: 'error', message: 'Invalid initial probability' } - const ante = FIXED_ANTE // data.ante // uses utc time on server: const today = new Date().setHours(0, 0, 0, 0) const userContractsCreatedTodaySnapshot = await firestore @@ -82,6 +81,8 @@ export const createContract = functions .get() const isFree = userContractsCreatedTodaySnapshot.size === 0 + const ante = FIXED_ANTE // data.ante + if ( ante === undefined || ante < MINIMUM_ANTE || diff --git a/functions/src/create-fold.ts b/functions/src/create-fold.ts index 36d1d269..5cf40b22 100644 --- a/functions/src/create-fold.ts +++ b/functions/src/create-fold.ts @@ -3,10 +3,10 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' import { getUser } from './utils' -import { Contract } from '../../common/contract' -import { slugify } from '../../common/util/slugify' -import { randomString } from '../../common/util/random' -import { Fold } from '../../common/fold' +import { Contract } from 'common/contract' +import { slugify } from 'common/util/slugify' +import { randomString } from 'common/util/random' +import { Fold } from 'common/fold' export const createFold = functions.runWith({ minInstances: 1 }).https.onCall( async ( diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index f73b868b..dd2b5275 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -6,15 +6,12 @@ import { STARTING_BALANCE, SUS_STARTING_BALANCE, User, -} from '../../common/user' +} from 'common/user' import { getUser, getUserByUsername } from './utils' -import { randomString } from '../../common/util/random' -import { - cleanDisplayName, - cleanUsername, -} from '../../common/util/clean-username' +import { randomString } from 'common/util/random' +import { cleanDisplayName, cleanUsername } from 'common/util/clean-username' import { sendWelcomeEmail } from './emails' -import { isWhitelisted } from '../../common/envs/constants' +import { isWhitelisted } from 'common/envs/constants' export const createUser = functions .runWith({ minInstances: 1 }) diff --git a/functions/src/emails.ts b/functions/src/emails.ts index c3b70734..4c434bcf 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -1,14 +1,14 @@ import * as _ from 'lodash' -import { DOMAIN, PROJECT_ID } from '../../common/envs/constants' -import { Answer } from '../../common/answer' -import { Bet } from '../../common/bet' -import { getProbability } from '../../common/calculate' -import { Comment } from '../../common/comment' -import { Contract, FreeResponseContract } from '../../common/contract' -import { CREATOR_FEE } from '../../common/fees' -import { PrivateUser, User } from '../../common/user' -import { formatMoney, formatPercent } from '../../common/util/format' +import { DOMAIN, PROJECT_ID } from 'common/envs/constants' +import { Answer } from 'common/answer' +import { Bet } from 'common/bet' +import { getProbability } from 'common/calculate' +import { Comment } from 'common/comment' +import { Contract, FreeResponseContract } from 'common/contract' +import { CREATOR_FEE } from 'common/fees' +import { PrivateUser, User } from 'common/user' +import { formatMoney, formatPercent } from 'common/util/format' import { sendTemplateEmail, sendTextEmail } from './send-email' import { getPrivateUser, getUser } from './utils' diff --git a/functions/src/market-close-emails.ts b/functions/src/market-close-emails.ts index bb144600..3adc5b6d 100644 --- a/functions/src/market-close-emails.ts +++ b/functions/src/market-close-emails.ts @@ -1,7 +1,7 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { Contract } from '../../common/contract' +import { Contract } from 'common/contract' import { getPrivateUser, getUserByUsername } from './utils' import { sendMarketCloseEmail } from './emails' diff --git a/functions/src/on-create-bet.ts b/functions/src/on-create-bet.ts index deaa4c4a..e3cf0ece 100644 --- a/functions/src/on-create-bet.ts +++ b/functions/src/on-create-bet.ts @@ -3,7 +3,7 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' import { getContract } from './utils' -import { Bet } from '../../common/bet' +import { Bet } from 'common/bet' const firestore = admin.firestore() diff --git a/functions/src/on-create-comment.ts b/functions/src/on-create-comment.ts index 18fc6757..654f9055 100644 --- a/functions/src/on-create-comment.ts +++ b/functions/src/on-create-comment.ts @@ -3,10 +3,10 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' import { getContract, getUser, getValues } from './utils' -import { Comment } from '../../common/comment' +import { Comment } from 'common/comment' import { sendNewCommentEmail } from './emails' -import { Bet } from '../../common/bet' -import { Answer } from '../../common/answer' +import { Bet } from 'common/bet' +import { Answer } from 'common/answer' const firestore = admin.firestore() diff --git a/functions/src/on-view.ts b/functions/src/on-view.ts index d2f746d5..6e08ae76 100644 --- a/functions/src/on-view.ts +++ b/functions/src/on-view.ts @@ -1,6 +1,6 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { View } from '../../common/tracking' +import { View } from 'common/tracking' const firestore = admin.firestore() diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 74487126..083c8bc2 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -1,18 +1,18 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { Contract } from '../../common/contract' -import { User } from '../../common/user' +import { Contract } from 'common/contract' +import { User } from 'common/user' import { getNewBinaryCpmmBetInfo, getNewBinaryDpmBetInfo, getNewMultiBetInfo, getLoanAmount, -} from '../../common/new-bet' -import { addObjects, removeUndefinedProps } from '../../common/util/object' -import { Bet } from '../../common/bet' +} from 'common/new-bet' +import { addObjects, removeUndefinedProps } from 'common/util/object' +import { Bet } from 'common/bet' import { redeemShares } from './redeem-shares' -import { Fees } from '../../common/fees' +import { Fees } from 'common/fees' export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( diff --git a/functions/src/redeem-shares.ts b/functions/src/redeem-shares.ts index 08d87a8b..a43aa509 100644 --- a/functions/src/redeem-shares.ts +++ b/functions/src/redeem-shares.ts @@ -1,12 +1,12 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Bet } from '../../common/bet' -import { getProbability } from '../../common/calculate' +import { Bet } from 'common/bet' +import { getProbability } from 'common/calculate' -import { Binary, CPMM, FullContract } from '../../common/contract' -import { noFees } from '../../common/fees' -import { User } from '../../common/user' +import { Binary, CPMM, FullContract } from 'common/contract' +import { noFees } from 'common/fees' +import { User } from 'common/user' export const redeemShares = async (userId: string, contractId: string) => { return await firestore.runTransaction(async (transaction) => { diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 5a2edec2..5ee82ff5 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -2,14 +2,14 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { Contract } from '../../common/contract' -import { User } from '../../common/user' -import { Bet } from '../../common/bet' +import { Contract } from 'common/contract' +import { User } from 'common/user' +import { Bet } from 'common/bet' import { getUser, isProd, payUser } from './utils' import { sendMarketResolutionEmail } from './emails' -import { getLoanPayouts, getPayouts } from '../../common/payouts' -import { removeUndefinedProps } from '../../common/util/object' -import { LiquidityProvision } from '../../common/liquidity-provision' +import { getLoanPayouts, getPayouts } from 'common/payouts' +import { removeUndefinedProps } from 'common/util/object' +import { LiquidityProvision } from 'common/liquidity-provision' export const resolveMarket = functions .runWith({ minInstances: 1 }) diff --git a/functions/src/scripts/cache-views.ts b/functions/src/scripts/cache-views.ts index c7145a1e..c7ed661f 100644 --- a/functions/src/scripts/cache-views.ts +++ b/functions/src/scripts/cache-views.ts @@ -5,9 +5,9 @@ import { initAdmin } from './script-init' initAdmin() import { getValues } from '../utils' -import { View } from '../../../common/tracking' -import { User } from '../../../common/user' -import { batchedWaitAll } from '../../../common/util/promise' +import { View } from 'common/tracking' +import { User } from 'common/user' +import { batchedWaitAll } from 'common/util/promise' const firestore = admin.firestore() diff --git a/functions/src/scripts/correct-bet-probability.ts b/functions/src/scripts/correct-bet-probability.ts index 3b57dbeb..e65b4ddf 100644 --- a/functions/src/scripts/correct-bet-probability.ts +++ b/functions/src/scripts/correct-bet-probability.ts @@ -4,9 +4,9 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { Bet } from '../../../common/bet' -import { getDpmProbability } from '../../../common/calculate-dpm' -import { Binary, Contract, DPM, FullContract } from '../../../common/contract' +import { Bet } from 'common/bet' +import { getDpmProbability } from 'common/calculate-dpm' +import { Binary, Contract, DPM, FullContract } from 'common/contract' type DocRef = admin.firestore.DocumentReference const firestore = admin.firestore() diff --git a/functions/src/scripts/create-private-users.ts b/functions/src/scripts/create-private-users.ts index 8051a447..a83bb53e 100644 --- a/functions/src/scripts/create-private-users.ts +++ b/functions/src/scripts/create-private-users.ts @@ -4,7 +4,7 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { PrivateUser, STARTING_BALANCE, User } from '../../../common/user' +import { PrivateUser, STARTING_BALANCE, User } from 'common/user' const firestore = admin.firestore() diff --git a/functions/src/scripts/denormalize-avatar-urls.ts b/functions/src/scripts/denormalize-avatar-urls.ts new file mode 100644 index 00000000..23b7dfc9 --- /dev/null +++ b/functions/src/scripts/denormalize-avatar-urls.ts @@ -0,0 +1,125 @@ +// Script for lining up users and contracts/comments to make sure the denormalized avatar URLs in the contracts and +// comments match the user avatar URLs. + +import * as admin from 'firebase-admin' +import { initAdmin } from './script-init' +import { + DocumentCorrespondence, + findDiffs, + describeDiff, + applyDiff, +} from './denormalize' +import { DocumentSnapshot, Transaction } from 'firebase-admin/firestore' + +initAdmin() +const firestore = admin.firestore() + +async function getUsersById(transaction: Transaction) { + const results = new Map() + const users = await transaction.get(firestore.collection('users')) + users.forEach((doc) => { + results.set(doc.get('id'), doc) + }) + console.log(`Found ${results.size} unique users.`) + return results +} + +async function getContractsByUserId(transaction: Transaction) { + let n = 0 + const results = new Map() + const contracts = await transaction.get(firestore.collection('contracts')) + contracts.forEach((doc) => { + const creatorId = doc.get('creatorId') + const creatorContracts = results.get(creatorId) || [] + creatorContracts.push(doc) + results.set(creatorId, creatorContracts) + n++ + }) + console.log(`Found ${n} contracts from ${results.size} unique users.`) + return results +} + +async function getCommentsByUserId(transaction: Transaction) { + let n = 0 + const results = new Map() + const comments = await transaction.get(firestore.collectionGroup('comments')) + comments.forEach((doc) => { + const userId = doc.get('userId') + const userComments = results.get(userId) || [] + userComments.push(doc) + results.set(userId, userComments) + n++ + }) + console.log(`Found ${n} comments from ${results.size} unique users.`) + return results +} + +async function getAnswersByUserId(transaction: Transaction) { + let n = 0 + const results = new Map() + const answers = await transaction.get(firestore.collectionGroup('answers')) + answers.forEach((doc) => { + const userId = doc.get('userId') + const userAnswers = results.get(userId) || [] + userAnswers.push(doc) + results.set(userId, userAnswers) + n++ + }) + console.log(`Found ${n} answers from ${results.size} unique users.`) + return results +} + +if (require.main === module) { + admin.firestore().runTransaction(async (transaction) => { + const [usersById, contractsByUserId, commentsByUserId, answersByUserId] = + await Promise.all([ + getUsersById(transaction), + getContractsByUserId(transaction), + getCommentsByUserId(transaction), + getAnswersByUserId(transaction), + ]) + + const usersContracts = Array.from( + usersById.entries(), + ([id, doc]): DocumentCorrespondence => { + return [doc, contractsByUserId.get(id) || []] + } + ) + const contractDiffs = findDiffs( + usersContracts, + 'avatarUrl', + 'creatorAvatarUrl' + ) + console.log(`Found ${contractDiffs.length} contracts with mismatches.`) + contractDiffs.forEach((d) => { + console.log(describeDiff(d)) + applyDiff(transaction, d) + }) + + const usersComments = Array.from( + usersById.entries(), + ([id, doc]): DocumentCorrespondence => { + return [doc, commentsByUserId.get(id) || []] + } + ) + const commentDiffs = findDiffs(usersComments, 'avatarUrl', 'userAvatarUrl') + console.log(`Found ${commentDiffs.length} comments with mismatches.`) + commentDiffs.forEach((d) => { + console.log(describeDiff(d)) + applyDiff(transaction, d) + }) + + const usersAnswers = Array.from( + usersById.entries(), + ([id, doc]): DocumentCorrespondence => { + return [doc, answersByUserId.get(id) || []] + } + ) + const answerDiffs = findDiffs(usersAnswers, 'avatarUrl', 'avatarUrl') + console.log(`Found ${answerDiffs.length} answers with mismatches.`) + answerDiffs.forEach((d) => { + console.log(describeDiff(d)) + applyDiff(transaction, d) + }) + }) +} diff --git a/functions/src/scripts/denormalize.ts b/functions/src/scripts/denormalize.ts new file mode 100644 index 00000000..ca4111b0 --- /dev/null +++ b/functions/src/scripts/denormalize.ts @@ -0,0 +1,48 @@ +// Helper functions for maintaining the relationship between fields in one set of documents and denormalized copies in +// another set of documents. + +import { DocumentSnapshot, Transaction } from 'firebase-admin/firestore' + +export type DocumentValue = { + doc: DocumentSnapshot + field: string + val: any +} +export type DocumentCorrespondence = [DocumentSnapshot, DocumentSnapshot[]] +export type DocumentDiff = { + src: DocumentValue + dest: DocumentValue +} + +export function findDiffs( + docs: DocumentCorrespondence[], + srcPath: string, + destPath: string +) { + const diffs: DocumentDiff[] = [] + for (let [srcDoc, destDocs] of docs) { + const srcVal = srcDoc.get(srcPath) + for (let destDoc of destDocs) { + const destVal = destDoc.get(destPath) + if (destVal !== srcVal) { + diffs.push({ + src: { doc: srcDoc, field: srcPath, val: srcVal }, + dest: { doc: destDoc, field: destPath, val: destVal }, + }) + } + } + } + return diffs +} + +export function describeDiff(diff: DocumentDiff) { + function describeDocVal(x: DocumentValue): string { + return `${x.doc.ref.path}.${x.field}: ${x.val}` + } + return `${describeDocVal(diff.src)} -> ${describeDocVal(diff.dest)}` +} + +export function applyDiff(transaction: Transaction, diff: DocumentDiff) { + const { src, dest } = diff + transaction.update(dest.doc.ref, dest.field, src.val) +} diff --git a/functions/src/scripts/get-json-dump.ts b/functions/src/scripts/get-json-dump.ts index b9909132..3027ce45 100644 --- a/functions/src/scripts/get-json-dump.ts +++ b/functions/src/scripts/get-json-dump.ts @@ -5,10 +5,10 @@ import * as fs from 'fs' import { initAdmin } from './script-init' initAdmin() -import { Bet } from '../../../common/bet' -import { Contract } from '../../../common/contract' +import { Bet } from 'common/bet' +import { Contract } from 'common/contract' import { getValues } from '../utils' -import { Comment } from '../../../common/comment' +import { Comment } from 'common/comment' const firestore = admin.firestore() diff --git a/functions/src/scripts/lowercase-fold-tags.ts b/functions/src/scripts/lowercase-fold-tags.ts index 80b79a33..f5d01bfe 100644 --- a/functions/src/scripts/lowercase-fold-tags.ts +++ b/functions/src/scripts/lowercase-fold-tags.ts @@ -5,7 +5,7 @@ import { initAdmin } from './script-init' initAdmin() import { getValues } from '../utils' -import { Fold } from '../../../common/fold' +import { Fold } from 'common/fold' async function lowercaseFoldTags() { const firestore = admin.firestore() diff --git a/functions/src/scripts/make-contracts-public.ts b/functions/src/scripts/make-contracts-public.ts index 19d2e196..5d958f13 100644 --- a/functions/src/scripts/make-contracts-public.ts +++ b/functions/src/scripts/make-contracts-public.ts @@ -4,7 +4,7 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { Contract } from '../../../common/contract' +import { Contract } from 'common/contract' const firestore = admin.firestore() diff --git a/functions/src/scripts/migrate-contract.ts b/functions/src/scripts/migrate-contract.ts index 718cf62e..7127f371 100644 --- a/functions/src/scripts/migrate-contract.ts +++ b/functions/src/scripts/migrate-contract.ts @@ -4,8 +4,8 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { Bet } from '../../../common/bet' -import { Contract } from '../../../common/contract' +import { Bet } from 'common/bet' +import { Contract } from 'common/contract' type DocRef = admin.firestore.DocumentReference diff --git a/functions/src/scripts/migrate-to-cfmm.ts b/functions/src/scripts/migrate-to-cfmm.ts index 874011ca..9dd8c63e 100644 --- a/functions/src/scripts/migrate-to-cfmm.ts +++ b/functions/src/scripts/migrate-to-cfmm.ts @@ -4,22 +4,13 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { - Binary, - Contract, - CPMM, - DPM, - FullContract, -} from '../../../common/contract' -import { Bet } from '../../../common/bet' -import { - calculateDpmPayout, - getDpmProbability, -} from '../../../common/calculate-dpm' -import { User } from '../../../common/user' -import { getCpmmInitialLiquidity } from '../../../common/antes' -import { noFees } from '../../../common/fees' -import { addObjects } from '../../../common/util/object' +import { Binary, Contract, CPMM, DPM, FullContract } from 'common/contract' +import { Bet } from 'common/bet' +import { calculateDpmPayout, getDpmProbability } from 'common/calculate-dpm' +import { User } from 'common/user' +import { getCpmmInitialLiquidity } from 'common/antes' +import { noFees } from 'common/fees' +import { addObjects } from 'common/util/object' type DocRef = admin.firestore.DocumentReference diff --git a/functions/src/scripts/migrate-to-dpm-2.ts b/functions/src/scripts/migrate-to-dpm-2.ts index 2c6f066f..81e99d98 100644 --- a/functions/src/scripts/migrate-to-dpm-2.ts +++ b/functions/src/scripts/migrate-to-dpm-2.ts @@ -4,14 +4,11 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { Binary, Contract, DPM, FullContract } from '../../../common/contract' -import { Bet } from '../../../common/bet' -import { - calculateDpmShares, - getDpmProbability, -} from '../../../common/calculate-dpm' -import { getSellBetInfo } from '../../../common/sell-bet' -import { User } from '../../../common/user' +import { Binary, Contract, DPM, FullContract } from 'common/contract' +import { Bet } from 'common/bet' +import { calculateDpmShares, getDpmProbability } from 'common/calculate-dpm' +import { getSellBetInfo } from 'common/sell-bet' +import { User } from 'common/user' type DocRef = admin.firestore.DocumentReference diff --git a/functions/src/scripts/pay-out-contract-again.ts b/functions/src/scripts/pay-out-contract-again.ts index 7672bf7b..0e56429f 100644 --- a/functions/src/scripts/pay-out-contract-again.ts +++ b/functions/src/scripts/pay-out-contract-again.ts @@ -4,10 +4,10 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { Bet } from '../../../common/bet' -import { Contract } from '../../../common/contract' -import { getLoanPayouts, getPayouts } from '../../../common/payouts' -import { filterDefined } from '../../../common/util/array' +import { Bet } from 'common/bet' +import { Contract } from 'common/contract' +import { getLoanPayouts, getPayouts } from 'common/payouts' +import { filterDefined } from 'common/util/array' type DocRef = admin.firestore.DocumentReference diff --git a/functions/src/scripts/recalculate-contract-totals.ts b/functions/src/scripts/recalculate-contract-totals.ts index 39942542..91165781 100644 --- a/functions/src/scripts/recalculate-contract-totals.ts +++ b/functions/src/scripts/recalculate-contract-totals.ts @@ -4,8 +4,8 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { Bet } from '../../../common/bet' -import { Contract } from '../../../common/contract' +import { Bet } from 'common/bet' +import { Contract } from 'common/contract' type DocRef = admin.firestore.DocumentReference diff --git a/functions/src/scripts/remove-answer-ante.ts b/functions/src/scripts/remove-answer-ante.ts index eb49af6c..8b026174 100644 --- a/functions/src/scripts/remove-answer-ante.ts +++ b/functions/src/scripts/remove-answer-ante.ts @@ -4,8 +4,8 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { Bet } from '../../../common/bet' -import { Contract } from '../../../common/contract' +import { Bet } from 'common/bet' +import { Contract } from 'common/contract' import { getValues } from '../utils' async function removeAnswerAnte() { diff --git a/functions/src/scripts/rename-user-contracts.ts b/functions/src/scripts/rename-user-contracts.ts index 9b0f569b..bcb4fea6 100644 --- a/functions/src/scripts/rename-user-contracts.ts +++ b/functions/src/scripts/rename-user-contracts.ts @@ -4,7 +4,7 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { Contract } from '../../../common/contract' +import { Contract } from 'common/contract' import { getValues } from '../utils' const firestore = admin.firestore() diff --git a/functions/src/scripts/update-contract-tags.ts b/functions/src/scripts/update-contract-tags.ts index 1dda5615..7e671c9f 100644 --- a/functions/src/scripts/update-contract-tags.ts +++ b/functions/src/scripts/update-contract-tags.ts @@ -4,8 +4,8 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { Contract } from '../../../common/contract' -import { parseTags } from '../../../common/util/parse' +import { Contract } from 'common/contract' +import { parseTags } from 'common/util/parse' import { getValues } from '../utils' async function updateContractTags() { diff --git a/functions/src/scripts/update-feed.ts b/functions/src/scripts/update-feed.ts index 25a0b14f..f98631dd 100644 --- a/functions/src/scripts/update-feed.ts +++ b/functions/src/scripts/update-feed.ts @@ -5,9 +5,9 @@ import { initAdmin } from './script-init' initAdmin() import { getValues } from '../utils' -import { User } from '../../../common/user' -import { batchedWaitAll } from '../../../common/util/promise' -import { Contract } from '../../../common/contract' +import { User } from 'common/user' +import { batchedWaitAll } from 'common/util/promise' +import { Contract } from 'common/contract' import { updateWordScores } from '../update-recommendations' import { getFeedContracts, doUserFeedUpdate } from '../update-feed' diff --git a/functions/src/scripts/update-last-comment-time.ts b/functions/src/scripts/update-last-comment-time.ts index ae950fbe..99d7f52d 100644 --- a/functions/src/scripts/update-last-comment-time.ts +++ b/functions/src/scripts/update-last-comment-time.ts @@ -4,9 +4,9 @@ import * as _ from 'lodash' import { initAdmin } from './script-init' initAdmin() -import { Contract } from '../../../common/contract' +import { Contract } from 'common/contract' import { getValues } from '../utils' -import { Comment } from '../../../common/comment' +import { Comment } from 'common/comment' async function updateLastCommentTime() { const firestore = admin.firestore() diff --git a/functions/src/sell-bet.ts b/functions/src/sell-bet.ts index fff88716..c685498b 100644 --- a/functions/src/sell-bet.ts +++ b/functions/src/sell-bet.ts @@ -1,12 +1,12 @@ import * as admin from 'firebase-admin' import * as functions from 'firebase-functions' -import { Contract } from '../../common/contract' -import { User } from '../../common/user' -import { Bet } from '../../common/bet' -import { getSellBetInfo } from '../../common/sell-bet' -import { addObjects, removeUndefinedProps } from '../../common/util/object' -import { Fees } from '../../common/fees' +import { Contract } from 'common/contract' +import { User } from 'common/user' +import { Bet } from 'common/bet' +import { getSellBetInfo } from 'common/sell-bet' +import { addObjects, removeUndefinedProps } from 'common/util/object' +import { Fees } from 'common/fees' export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall( async ( diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts index e4dbcbc9..158a5f6a 100644 --- a/functions/src/sell-shares.ts +++ b/functions/src/sell-shares.ts @@ -2,12 +2,12 @@ import * as _ from 'lodash' import * as admin from 'firebase-admin' import * as functions from 'firebase-functions' -import { Binary, CPMM, FullContract } from '../../common/contract' -import { User } from '../../common/user' -import { getCpmmSellBetInfo } from '../../common/sell-bet' -import { addObjects, removeUndefinedProps } from '../../common/util/object' +import { Binary, CPMM, FullContract } from 'common/contract' +import { User } from 'common/user' +import { getCpmmSellBetInfo } from 'common/sell-bet' +import { addObjects, removeUndefinedProps } from 'common/util/object' import { getValues } from './utils' -import { Bet } from '../../common/bet' +import { Bet } from 'common/bet' export const sellShares = functions.runWith({ minInstances: 1 }).https.onCall( async ( diff --git a/functions/src/transact.ts b/functions/src/transact.ts index 79b5ccb8..77323638 100644 --- a/functions/src/transact.ts +++ b/functions/src/transact.ts @@ -1,9 +1,9 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { User } from '../../common/user' -import { Txn } from '../../common/txn' -import { removeUndefinedProps } from '../../common/util/object' +import { User } from 'common/user' +import { Txn } from 'common/txn' +import { removeUndefinedProps } from 'common/util/object' export const transact = functions .runWith({ minInstances: 1 }) diff --git a/functions/src/unsubscribe.ts b/functions/src/unsubscribe.ts index c6edee92..7c9442d7 100644 --- a/functions/src/unsubscribe.ts +++ b/functions/src/unsubscribe.ts @@ -2,7 +2,7 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import * as _ from 'lodash' import { getUser } from './utils' -import { PrivateUser } from '../../common/user' +import { PrivateUser } from 'common/user' export const unsubscribe = functions .runWith({ minInstances: 1 }) diff --git a/functions/src/update-contract-metrics.ts b/functions/src/update-contract-metrics.ts index c3801df6..9214d2dc 100644 --- a/functions/src/update-contract-metrics.ts +++ b/functions/src/update-contract-metrics.ts @@ -3,9 +3,9 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' import { getValues } from './utils' -import { Contract } from '../../common/contract' -import { Bet } from '../../common/bet' -import { batchedWaitAll } from '../../common/util/promise' +import { Contract } from 'common/contract' +import { Bet } from 'common/bet' +import { batchedWaitAll } from 'common/util/promise' const firestore = admin.firestore() diff --git a/functions/src/update-feed.ts b/functions/src/update-feed.ts index accd48e8..3bfa7949 100644 --- a/functions/src/update-feed.ts +++ b/functions/src/update-feed.ts @@ -3,21 +3,21 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { getValue, getValues } from './utils' -import { Contract } from '../../common/contract' -import { logInterpolation } from '../../common/util/math' -import { DAY_MS } from '../../common/util/time' +import { Contract } from 'common/contract' +import { logInterpolation } from 'common/util/math' +import { DAY_MS } from 'common/util/time' import { getProbability, getOutcomeProbability, getTopAnswer, -} from '../../common/calculate' -import { Bet } from '../../common/bet' -import { Comment } from '../../common/comment' -import { User } from '../../common/user' +} from 'common/calculate' +import { Bet } from 'common/bet' +import { Comment } from 'common/comment' +import { User } from 'common/user' import { getContractScore, MAX_FEED_CONTRACTS, -} from '../../common/recommended-contracts' +} from 'common/recommended-contracts' import { callCloudFunction } from './call-cloud-function' const firestore = admin.firestore() diff --git a/functions/src/update-recommendations.ts b/functions/src/update-recommendations.ts index 4e656dda..e18e7c0e 100644 --- a/functions/src/update-recommendations.ts +++ b/functions/src/update-recommendations.ts @@ -3,12 +3,12 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' import { getValue, getValues } from './utils' -import { Contract } from '../../common/contract' -import { Bet } from '../../common/bet' -import { User } from '../../common/user' -import { ClickEvent } from '../../common/tracking' -import { getWordScores } from '../../common/recommended-contracts' -import { batchedWaitAll } from '../../common/util/promise' +import { Contract } from 'common/contract' +import { Bet } from 'common/bet' +import { User } from 'common/user' +import { ClickEvent } from 'common/tracking' +import { getWordScores } from 'common/recommended-contracts' +import { batchedWaitAll } from 'common/util/promise' import { callCloudFunction } from './call-cloud-function' const firestore = admin.firestore() diff --git a/functions/src/update-user-metrics.ts b/functions/src/update-user-metrics.ts index 70fd1bc5..6f755622 100644 --- a/functions/src/update-user-metrics.ts +++ b/functions/src/update-user-metrics.ts @@ -3,11 +3,11 @@ import * as admin from 'firebase-admin' import * as _ from 'lodash' import { getValues } from './utils' -import { Contract } from '../../common/contract' -import { Bet } from '../../common/bet' -import { User } from '../../common/user' -import { batchedWaitAll } from '../../common/util/promise' -import { calculatePayout } from '../../common/calculate' +import { Contract } from 'common/contract' +import { Bet } from 'common/bet' +import { User } from 'common/user' +import { batchedWaitAll } from 'common/util/promise' +import { calculatePayout } from 'common/calculate' const firestore = admin.firestore() diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 28ef5445..d0ab8c5d 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -1,7 +1,7 @@ import * as admin from 'firebase-admin' -import { Contract } from '../../common/contract' -import { PrivateUser, User } from '../../common/user' +import { Contract } from 'common/contract' +import { PrivateUser, User } from 'common/user' export const isProd = admin.instanceId().app.options.projectId === 'mantic-markets' diff --git a/functions/tsconfig.json b/functions/tsconfig.json index c836df11..e183bb44 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "baseUrl": "../", "module": "commonjs", "noImplicitReturns": true, "outDir": "lib", diff --git a/og-image/api/_lib/template.ts b/og-image/api/_lib/template.ts index 00d47394..a6b0336c 100644 --- a/og-image/api/_lib/template.ts +++ b/og-image/api/_lib/template.ts @@ -93,7 +93,8 @@ export function getHtml(parsedReq: ParsedRequest) { creatorAvatarUrl, } = parsedReq const MAX_QUESTION_CHARS = 100 - const truncatedQuestion = question.length > MAX_QUESTION_CHARS + const truncatedQuestion = + question.length > MAX_QUESTION_CHARS ? question.slice(0, MAX_QUESTION_CHARS) + '...' : question const hideAvatar = creatorAvatarUrl ? '' : 'hidden' diff --git a/web/components/add-funds-button.tsx b/web/components/add-funds-button.tsx index 7ca154ea..566f4716 100644 --- a/web/components/add-funds-button.tsx +++ b/web/components/add-funds-button.tsx @@ -1,8 +1,8 @@ import clsx from 'clsx' import { useEffect, useState } from 'react' -import { useUser } from '../hooks/use-user' -import { checkoutURL } from '../lib/service/stripe' +import { useUser } from 'web/hooks/use-user' +import { checkoutURL } from 'web/lib/service/stripe' import { FundsSelector } from './yes-no-selector' export function AddFundsButton(props: { className?: string }) { diff --git a/web/components/add-liquidity-panel.tsx b/web/components/add-liquidity-panel.tsx index f04c2b0a..240bde7d 100644 --- a/web/components/add-liquidity-panel.tsx +++ b/web/components/add-liquidity-panel.tsx @@ -1,10 +1,10 @@ import clsx from 'clsx' import { useState } from 'react' -import { Contract } from '../../common/contract' -import { formatMoney } from '../../common/util/format' -import { useUser } from '../hooks/use-user' -import { addLiquidity } from '../lib/firebase/api-call' +import { Contract } from 'common/contract' +import { formatMoney } from 'common/util/format' +import { useUser } from 'web/hooks/use-user' +import { addLiquidity } from 'web/lib/firebase/api-call' import { AmountInput } from './amount-input' import { Row } from './layout/row' diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index 783d8f19..ccd668d9 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -1,13 +1,14 @@ import clsx from 'clsx' import _ from 'lodash' -import { useUser } from '../hooks/use-user' -import { formatMoney, formatWithCommas } from '../../common/util/format' +import { useUser } from 'web/hooks/use-user' +import { formatMoney, formatWithCommas } from 'common/util/format' import { Col } from './layout/col' import { Row } from './layout/row' -import { Bet } from '../../common/bet' +import { Bet } from 'common/bet' import { Spacer } from './layout/spacer' -import { calculateCpmmSale } from '../../common/calculate-cpmm' -import { Binary, CPMM, FullContract } from '../../common/contract' +import { calculateCpmmSale } from 'common/calculate-cpmm' +import { Binary, CPMM, FullContract } from 'common/contract' +import { SiteLink } from './site-link' export function AmountInput(props: { amount: number | undefined @@ -65,7 +66,16 @@ export function AmountInput(props: { {error && (
- {error} + {error === 'Insufficient balance' ? ( + <> + Not enough funds. + + Buy more? + + + ) : ( + error + )}
)} @@ -168,9 +178,10 @@ export function SellAmountInput(props: { ] const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined - const shares = yesShares || noShares + const shares = Math.round(yesShares) || Math.round(noShares) + + const sharesSold = Math.min(amount ?? 0, shares) - const sharesSold = Math.min(amount ?? 0, yesShares || noShares) const { saleValue } = calculateCpmmSale( contract, sharesSold, diff --git a/web/components/analytics/charts.tsx b/web/components/analytics/charts.tsx index 4bf8d52b..44360c97 100644 --- a/web/components/analytics/charts.tsx +++ b/web/components/analytics/charts.tsx @@ -1,7 +1,7 @@ import { ResponsiveLine } from '@nivo/line' import dayjs from 'dayjs' import _ from 'lodash' -import { useWindowSize } from '../../hooks/use-window-size' +import { useWindowSize } from 'web/hooks/use-window-size' export function DailyCountChart(props: { startDate: number diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 9d84b2f2..e0984f6a 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -3,28 +3,28 @@ import _ from 'lodash' import { useEffect, useRef, useState } from 'react' import { XIcon } from '@heroicons/react/solid' -import { Answer } from '../../../common/answer' -import { DPM, FreeResponse, FullContract } from '../../../common/contract' +import { Answer } from 'common/answer' +import { DPM, FreeResponse, FullContract } from 'common/contract' import { BuyAmountInput } from '../amount-input' import { Col } from '../layout/col' -import { placeBet } from '../../lib/firebase/api-call' +import { placeBet } from 'web/lib/firebase/api-call' import { Row } from '../layout/row' import { Spacer } from '../layout/spacer' import { formatMoney, formatPercent, formatWithCommas, -} from '../../../common/util/format' +} from 'common/util/format' import { InfoTooltip } from '../info-tooltip' -import { useUser } from '../../hooks/use-user' +import { useUser } from 'web/hooks/use-user' import { getDpmOutcomeProbability, calculateDpmShares, calculateDpmPayoutAfterCorrectBet, getDpmOutcomeProbabilityAfterBet, -} from '../../../common/calculate-dpm' -import { firebaseLogin } from '../../lib/firebase/users' -import { Bet } from '../../../common/bet' +} from 'common/calculate-dpm' +import { firebaseLogin } from 'web/lib/firebase/users' +import { Bet } from 'common/bet' export function AnswerBetPanel(props: { answer: Answer @@ -174,7 +174,7 @@ export function AnswerBetPanel(props: { className="btn self-stretch whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600" onClick={firebaseLogin} > - Sign up to trade! + Sign up to bet! )} diff --git a/web/components/answers/answer-item.tsx b/web/components/answers/answer-item.tsx index fdeafea0..55351083 100644 --- a/web/components/answers/answer-item.tsx +++ b/web/components/answers/answer-item.tsx @@ -1,15 +1,15 @@ import clsx from 'clsx' import _ from 'lodash' -import { Answer } from '../../../common/answer' -import { DPM, FreeResponse, FullContract } from '../../../common/contract' +import { Answer } from 'common/answer' +import { DPM, FreeResponse, FullContract } from 'common/contract' import { Col } from '../layout/col' import { Row } from '../layout/row' import { Avatar } from '../avatar' import { SiteLink } from '../site-link' -import { formatPercent } from '../../../common/util/format' -import { getDpmOutcomeProbability } from '../../../common/calculate-dpm' -import { tradingAllowed } from '../../lib/firebase/contracts' +import { formatPercent } from 'common/util/format' +import { getDpmOutcomeProbability } from 'common/calculate-dpm' +import { tradingAllowed } from 'web/lib/firebase/contracts' import { Linkify } from '../linkify' export function AnswerItem(props: { @@ -68,7 +68,7 @@ export function AnswerItem(props: { {/* TODO: Show total pool? */} -
#{number}
+
{showChoice && '#' + number}
diff --git a/web/components/answers/answer-resolve-panel.tsx b/web/components/answers/answer-resolve-panel.tsx index 41aa90b2..70eb1299 100644 --- a/web/components/answers/answer-resolve-panel.tsx +++ b/web/components/answers/answer-resolve-panel.tsx @@ -2,13 +2,13 @@ import clsx from 'clsx' import _ from 'lodash' import { useState } from 'react' -import { DPM, FreeResponse, FullContract } from '../../../common/contract' +import { DPM, FreeResponse, FullContract } from 'common/contract' import { Col } from '../layout/col' -import { resolveMarket } from '../../lib/firebase/api-call' +import { resolveMarket } from 'web/lib/firebase/api-call' import { Row } from '../layout/row' import { ChooseCancelSelector } from '../yes-no-selector' import { ResolveConfirmationButton } from '../confirmation-button' -import { removeUndefinedProps } from '../../../common/util/object' +import { removeUndefinedProps } from 'common/util/object' export function AnswerResolvePanel(props: { contract: FullContract diff --git a/web/components/answers/answers-graph.tsx b/web/components/answers/answers-graph.tsx index df56ab93..5853f4a9 100644 --- a/web/components/answers/answers-graph.tsx +++ b/web/components/answers/answers-graph.tsx @@ -4,11 +4,11 @@ import dayjs from 'dayjs' import _ from 'lodash' import { memo } from 'react' -import { Bet } from '../../../common/bet' -import { DPM, FreeResponse, FullContract } from '../../../common/contract' -import { getOutcomeProbability } from '../../../common/calculate' -import { useBets } from '../../hooks/use-bets' -import { useWindowSize } from '../../hooks/use-window-size' +import { Bet } from 'common/bet' +import { DPM, FreeResponse, FullContract } from 'common/contract' +import { getOutcomeProbability } from 'common/calculate' +import { useBets } from 'web/hooks/use-bets' +import { useWindowSize } from 'web/hooks/use-window-size' const NUM_LINES = 6 diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 3af2c286..3d9d066b 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -1,21 +1,21 @@ import _ from 'lodash' import { useLayoutEffect, useState } from 'react' -import { DPM, FreeResponse, FullContract } from '../../../common/contract' +import { DPM, FreeResponse, FullContract } from 'common/contract' import { Col } from '../layout/col' -import { useUser } from '../../hooks/use-user' -import { getDpmOutcomeProbability } from '../../../common/calculate-dpm' -import { useAnswers } from '../../hooks/use-answers' -import { tradingAllowed } from '../../lib/firebase/contracts' +import { useUser } from 'web/hooks/use-user' +import { getDpmOutcomeProbability } from 'common/calculate-dpm' +import { useAnswers } from 'web/hooks/use-answers' +import { tradingAllowed } from 'web/lib/firebase/contracts' import { AnswerItem } from './answer-item' import { CreateAnswerPanel } from './create-answer-panel' import { AnswerResolvePanel } from './answer-resolve-panel' import { Spacer } from '../layout/spacer' import { FeedItems } from '../feed/feed-items' import { ActivityItem } from '../feed/activity-items' -import { User } from '../../../common/user' -import { getOutcomeProbability } from '../../../common/calculate' -import { Answer } from '../../../common/answer' +import { User } from 'common/user' +import { getOutcomeProbability } from 'common/calculate' +import { Answer } from 'common/answer' export function AnswersPanel(props: { contract: FullContract @@ -24,7 +24,7 @@ export function AnswersPanel(props: { const { creatorId, resolution, resolutions, totalBets } = contract const answers = useAnswers(contract.id) ?? contract.answers - const [winningAnswers, otherAnswers] = _.partition( + const [winningAnswers, losingAnswers] = _.partition( answers.filter( (answer) => answer.id !== '0' && totalBets[answer.id] > 0.000000001 ), @@ -36,7 +36,7 @@ export function AnswersPanel(props: { resolutions ? -1 * resolutions[answer.id] : 0 ), ..._.sortBy( - resolution ? [] : otherAnswers, + resolution ? [] : losingAnswers, (answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id) ), ] @@ -52,7 +52,11 @@ export function AnswersPanel(props: { const chosenTotal = _.sum(Object.values(chosenAnswers)) - const answerItems = getAnswers(contract, user) + const answerItems = getAnswerItems( + contract, + losingAnswers.length > 0 ? losingAnswers : sortedAnswers, + user + ) const onChoose = (answerId: string, prob: number) => { if (resolveOption === 'CHOOSE') { @@ -89,9 +93,7 @@ export function AnswersPanel(props: { return ( - {(resolveOption === 'CHOOSE' || - resolveOption === 'CHOOSE_MULTIPLE' || - resolution === 'MKT') && + {(resolveOption || resolution) && sortedAnswers.map((answer) => ( ))} - {sortedAnswers.length === 0 && ( -
No answers yet...
- )} - - {!resolveOption && sortedAnswers.length > 0 && ( + {!resolveOption && ( )} + {answers.length <= 1 && ( +
No answers yet...
+ )} + {tradingAllowed(contract) && (!resolveOption || resolveOption === 'CANCEL') && ( @@ -138,12 +140,11 @@ export function AnswersPanel(props: { ) } -function getAnswers( +function getAnswerItems( contract: FullContract, + answers: Answer[], user: User | undefined | null ) { - const { answers } = contract - let outcomes = _.uniq( answers.map((answer) => answer.number.toString()) ).filter((outcome) => getOutcomeProbability(contract, outcome) > 0.0001) diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx index 6a3dd8c6..2075b60d 100644 --- a/web/components/answers/create-answer-panel.tsx +++ b/web/components/answers/create-answer-panel.tsx @@ -3,26 +3,26 @@ import _ from 'lodash' import { useState } from 'react' import Textarea from 'react-expanding-textarea' -import { DPM, FreeResponse, FullContract } from '../../../common/contract' +import { DPM, FreeResponse, FullContract } from 'common/contract' import { BuyAmountInput } from '../amount-input' import { Col } from '../layout/col' -import { createAnswer } from '../../lib/firebase/api-call' +import { createAnswer } from 'web/lib/firebase/api-call' import { Row } from '../layout/row' import { formatMoney, formatPercent, formatWithCommas, -} from '../../../common/util/format' +} from 'common/util/format' import { InfoTooltip } from '../info-tooltip' -import { useUser } from '../../hooks/use-user' +import { useUser } from 'web/hooks/use-user' import { calculateDpmShares, calculateDpmPayoutAfterCorrectBet, getDpmOutcomeProbabilityAfterBet, -} from '../../../common/calculate-dpm' -import { firebaseLogin } from '../../lib/firebase/users' -import { Bet } from '../../../common/bet' -import { MAX_ANSWER_LENGTH } from '../../../common/answer' +} from 'common/calculate-dpm' +import { firebaseLogin } from 'web/lib/firebase/users' +import { Bet } from 'common/bet' +import { MAX_ANSWER_LENGTH } from 'common/answer' export function CreateAnswerPanel(props: { contract: FullContract diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 677e0197..2f9064c7 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -1,8 +1,8 @@ import clsx from 'clsx' import React, { useEffect, useState } from 'react' -import { useUser } from '../hooks/use-user' -import { Binary, CPMM, DPM, FullContract } from '../../common/contract' +import { useUser } from 'web/hooks/use-user' +import { Binary, CPMM, DPM, FullContract } from 'common/contract' import { Col } from './layout/col' import { Row } from './layout/row' import { Spacer } from './layout/spacer' @@ -11,11 +11,11 @@ import { formatMoney, formatPercent, formatWithCommas, -} from '../../common/util/format' +} from 'common/util/format' import { Title } from './title' -import { firebaseLogin, User } from '../lib/firebase/users' -import { Bet } from '../../common/bet' -import { placeBet, sellShares } from '../lib/firebase/api-call' +import { firebaseLogin, User } from 'web/lib/firebase/users' +import { Bet } from 'common/bet' +import { placeBet, sellShares } from 'web/lib/firebase/api-call' import { BuyAmountInput, SellAmountInput } from './amount-input' import { InfoTooltip } from './info-tooltip' import { BinaryOutcomeLabel } from './outcome-label' @@ -24,13 +24,10 @@ import { calculateShares, getProbability, getOutcomeProbabilityAfterBet, -} from '../../common/calculate' -import { useFocus } from '../hooks/use-focus' -import { useUserContractBets } from '../hooks/use-user-bets' -import { - calculateCpmmSale, - getCpmmProbability, -} from '../../common/calculate-cpmm' +} from 'common/calculate' +import { useFocus } from 'web/hooks/use-focus' +import { useUserContractBets } from 'web/hooks/use-user-bets' +import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm' import { SellRow } from './sell-row' import { useSaveShares } from './use-save-shares' @@ -72,7 +69,7 @@ export function BetPanel(props: { className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600" onClick={firebaseLogin} > - Sign up to trade! + Sign up to bet! )} @@ -187,7 +184,7 @@ export function BetPanelSwitcher(props: { className="btn flex-1 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600" onClick={firebaseLogin} > - Sign up to trade! + Sign up to bet! )} @@ -432,7 +429,13 @@ export function SellPanel(props: { sellBet({}).catch(), 5000 /* ms */) + function SellButton(props: { contract: Contract; bet: Bet }) { useEffect(() => { - // warm up cloud function - sellBet({}).catch() + warmUpSellBet() }, []) const { contract, bet } = props diff --git a/web/components/charity/charity-card.tsx b/web/components/charity/charity-card.tsx index bd95db73..92fa56d2 100644 --- a/web/components/charity/charity-card.tsx +++ b/web/components/charity/charity-card.tsx @@ -1,8 +1,9 @@ import { StarIcon } from '@heroicons/react/solid' import _ from 'lodash' import Link from 'next/link' -import { Charity } from '../../../common/charity' -import { useCharityTxns } from '../../hooks/use-charity-txns' +import Image from 'next/image' +import { Charity } from 'common/charity' +import { useCharityTxns } from 'web/hooks/use-charity-txns' import { manaToUSD } from '../../pages/charity/[charitySlug]' import { Row } from '../layout/row' @@ -15,17 +16,18 @@ export function CharityCard(props: { charity: Charity }) { return (
- + {tags?.includes('Featured') && } - -
- {photo ? ( - - ) : ( -
- )} -
+
+
+ {photo ? ( + + ) : ( +
+ )} +
+
{/*

{name}

*/}
{preview}
diff --git a/web/components/charity/feed-items.tsx b/web/components/charity/feed-items.tsx index 9216e776..368854c9 100644 --- a/web/components/charity/feed-items.tsx +++ b/web/components/charity/feed-items.tsx @@ -1,9 +1,9 @@ -import { Txn } from '../../../common/txn' +import { Txn } from 'common/txn' import { Avatar } from '../avatar' -import { useUserById } from '../../hooks/use-users' +import { useUserById } from 'web/hooks/use-users' import { UserLink } from '../user-page' import { manaToUSD } from '../../pages/charity/[charitySlug]' -import { RelativeTimestamp } from '../feed/feed-items' +import { RelativeTimestamp } from '../relative-timestamp' export function Donation(props: { txn: Txn }) { const { txn } = props diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx new file mode 100644 index 00000000..bceb2d59 --- /dev/null +++ b/web/components/comments-list.tsx @@ -0,0 +1,65 @@ +import { Comment } from 'common/comment' +import { Contract } from 'common/contract' +import { contractPath } from 'web/lib/firebase/contracts' +import { SiteLink } from './site-link' +import { Row } from './layout/row' +import { Avatar } from './avatar' +import { RelativeTimestamp } from './relative-timestamp' +import { UserLink } from './user-page' +import { User } from 'common/user' +import { Col } from './layout/col' +import { Linkify } from './linkify' + +export function UserCommentsList(props: { + user: User + commentsByUniqueContracts: Map +}) { + const { commentsByUniqueContracts } = props + + return ( + + {Array.from(commentsByUniqueContracts).map(([contract, comments]) => ( +
+
+ + {contract.question} + +
+ {comments.map((comment) => ( +
+
+ +
+
+ ))} +
+ ))} + + ) +} + +function ProfileComment(props: { comment: Comment }) { + const { comment } = props + const { text, userUsername, userName, userAvatarUrl, createdTime } = comment + // TODO: find and attach relevant bets by comment betId at some point + return ( +
+ + +
+
+

+ {' '} + +

+
+ +
+
+
+ ) +} diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index 41373679..d1c7dd31 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -2,12 +2,12 @@ import clsx from 'clsx' import Link from 'next/link' import _ from 'lodash' import { Row } from '../layout/row' -import { formatPercent } from '../../../common/util/format' +import { formatPercent } from 'common/util/format' import { Contract, contractPath, getBinaryProbPercent, -} from '../../lib/firebase/contracts' +} from 'web/lib/firebase/contracts' import { Col } from '../layout/col' import { Spacer } from '../layout/spacer' import { @@ -17,13 +17,13 @@ import { FreeResponse, FreeResponseContract, FullContract, -} from '../../../common/contract' +} from 'common/contract' import { AnswerLabel, BinaryContractOutcomeLabel, FreeResponseOutcomeLabel, } from '../outcome-label' -import { getOutcomeProbability, getTopAnswer } from '../../../common/calculate' +import { getOutcomeProbability, getTopAnswer } from 'common/calculate' import { AbbrContractDetails } from './contract-details' export function ContractCard(props: { diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx index dea08c6a..86331601 100644 --- a/web/components/contract/contract-description.tsx +++ b/web/components/contract/contract-description.tsx @@ -3,10 +3,10 @@ import dayjs from 'dayjs' import { useState } from 'react' import Textarea from 'react-expanding-textarea' -import { Contract } from '../../../common/contract' -import { parseTags } from '../../../common/util/parse' -import { useAdmin } from '../../hooks/use-admin' -import { updateContract } from '../../lib/firebase/contracts' +import { Contract } from 'common/contract' +import { parseTags } from 'common/util/parse' +import { useAdmin } from 'web/hooks/use-admin' +import { updateContract } from 'web/lib/firebase/contracts' import { Row } from '../layout/row' import { Linkify } from '../linkify' diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 8cc27496..21849cc4 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -3,21 +3,21 @@ import _ from 'lodash' import { ClockIcon, DatabaseIcon, PencilIcon } from '@heroicons/react/outline' import { TrendingUpIcon } from '@heroicons/react/solid' import { Row } from '../layout/row' -import { formatMoney } from '../../../common/util/format' +import { formatMoney } from 'common/util/format' import { UserLink } from '../user-page' import { Contract, contractMetrics, updateContract, -} from '../../lib/firebase/contracts' +} from 'web/lib/firebase/contracts' import { Col } from '../layout/col' import dayjs from 'dayjs' import { DateTimeTooltip } from '../datetime-tooltip' -import { fromNow } from '../../lib/util/time' +import { fromNow } from 'web/lib/util/time' import { Avatar } from '../avatar' import { useState } from 'react' import { ContractInfoDialog } from './contract-info-dialog' -import { Bet } from '../../../common/bet' +import { Bet } from 'common/bet' import NewContractBadge from '../new-contract-badge' export function AbbrContractDetails(props: { diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index 7d94c64a..dca03507 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -3,14 +3,11 @@ import clsx from 'clsx' import dayjs from 'dayjs' import _ from 'lodash' import { useState } from 'react' -import { Bet } from '../../../common/bet' +import { Bet } from 'common/bet' -import { Contract } from '../../../common/contract' -import { formatMoney } from '../../../common/util/format' -import { - contractPath, - getBinaryProbPercent, -} from '../../lib/firebase/contracts' +import { Contract } from 'common/contract' +import { formatMoney } from 'common/util/format' +import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts' import { AddLiquidityPanel } from '../add-liquidity-panel' import { CopyLinkButton } from '../copy-link-button' import { Col } from '../layout/col' diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index 524a1962..7ebdf09d 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -1,8 +1,8 @@ -import { Contract, tradingAllowed } from '../../lib/firebase/contracts' +import { Contract, tradingAllowed } from 'web/lib/firebase/contracts' import { Col } from '../layout/col' import { Spacer } from '../layout/spacer' import { ContractProbGraph } from './contract-prob-graph' -import { useUser } from '../../hooks/use-user' +import { useUser } from 'web/hooks/use-user' import { Row } from '../layout/row' import { Linkify } from '../linkify' import clsx from 'clsx' @@ -10,11 +10,11 @@ import { FreeResponseResolutionOrChance, BinaryResolutionOrChance, } from './contract-card' -import { Bet } from '../../../common/bet' -import { Comment } from '../../../common/comment' +import { Bet } from 'common/bet' +import { Comment } from 'common/comment' import BetRow from '../bet-row' import { AnswersGraph } from '../answers/answers-graph' -import { DPM, FreeResponse, FullContract } from '../../../common/contract' +import { DPM, FreeResponse, FullContract } from 'common/contract' import { ContractDescription } from './contract-description' import { ContractDetails } from './contract-details' import { ShareMarket } from '../share-market' diff --git a/web/components/contract/contract-prob-graph.tsx b/web/components/contract/contract-prob-graph.tsx index 18374f0e..e3743e84 100644 --- a/web/components/contract/contract-prob-graph.tsx +++ b/web/components/contract/contract-prob-graph.tsx @@ -2,11 +2,11 @@ import { DatumValue } from '@nivo/core' import { ResponsiveLine } from '@nivo/line' import dayjs from 'dayjs' import { memo } from 'react' -import { Bet } from '../../../common/bet' -import { getInitialProbability } from '../../../common/calculate' -import { Binary, CPMM, DPM, FullContract } from '../../../common/contract' -import { useBetsWithoutAntes } from '../../hooks/use-bets' -import { useWindowSize } from '../../hooks/use-window-size' +import { Bet } from 'common/bet' +import { getInitialProbability } from 'common/calculate' +import { Binary, CPMM, DPM, FullContract } from 'common/contract' +import { useBetsWithoutAntes } from 'web/hooks/use-bets' +import { useWindowSize } from 'web/hooks/use-window-size' export const ContractProbGraph = memo(function ContractProbGraph(props: { contract: FullContract diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 2ba6cde8..c59a834d 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -1,12 +1,13 @@ -import { Bet } from '../../../common/bet' -import { Contract } from '../../../common/contract' -import { Comment } from '../../lib/firebase/comments' -import { User } from '../../../common/user' -import { useBets } from '../../hooks/use-bets' +import { Bet } from 'common/bet' +import { Contract } from 'common/contract' +import { Comment } from 'web/lib/firebase/comments' +import { User } from 'common/user' +import { useBets } from 'web/hooks/use-bets' import { ContractActivity } from '../feed/contract-activity' import { ContractBetsTable, MyBetsSummary } from '../bets-list' import { Spacer } from '../layout/spacer' import { Tabs } from '../layout/tabs' +import { Col } from '../layout/col' export function ContractTabs(props: { contract: Contract @@ -33,14 +34,34 @@ export function ContractTabs(props: { ) const commentActivity = ( - + <> + + {contract.outcomeType === 'FREE_RESPONSE' && ( + +
General Comments
+
+ + + )} + ) const yourTrades = ( diff --git a/web/components/copy-link-button.tsx b/web/components/copy-link-button.tsx index d63d3ff2..6ad22893 100644 --- a/web/components/copy-link-button.tsx +++ b/web/components/copy-link-button.tsx @@ -2,10 +2,10 @@ import { Fragment } from 'react' import { LinkIcon } from '@heroicons/react/outline' import { Menu, Transition } from '@headlessui/react' import clsx from 'clsx' -import { Contract } from '../../common/contract' -import { copyToClipboard } from '../lib/util/copy' -import { contractPath } from '../lib/firebase/contracts' -import { ENV_CONFIG } from '../../common/envs/constants' +import { Contract } from 'common/contract' +import { copyToClipboard } from 'web/lib/util/copy' +import { contractPath } from 'web/lib/firebase/contracts' +import { ENV_CONFIG } from 'common/envs/constants' function copyContractUrl(contract: Contract) { copyToClipboard(`https://${ENV_CONFIG.domain}${contractPath(contract)}`) diff --git a/web/components/feed-create.tsx b/web/components/feed-create.tsx index f4094852..6d192336 100644 --- a/web/components/feed-create.tsx +++ b/web/components/feed-create.tsx @@ -4,54 +4,49 @@ import { Avatar } from './avatar' import { useEffect, useRef, useState } from 'react' import { Spacer } from './layout/spacer' import { NewContract } from '../pages/create' -import { firebaseLogin, User } from '../lib/firebase/users' +import { firebaseLogin, User } from 'web/lib/firebase/users' import { ContractsGrid } from './contract/contracts-list' -import { Contract, MAX_QUESTION_LENGTH } from '../../common/contract' +import { Contract, MAX_QUESTION_LENGTH } from 'common/contract' import { Col } from './layout/col' import clsx from 'clsx' import { Row } from './layout/row' import { ENV_CONFIG } from '../../common/envs/constants' +import { SiteLink } from './site-link' export function FeedPromo(props: { hotContracts: Contract[] }) { const { hotContracts } = props return ( <> - +

-
- A{' '} - - market{' '} - - for -
-
- every{' '} - - question - -
+
A market for
+ + every question +

-
- Find markets on any topic imaginable. Or create your own! +
+ Bet on any topic imaginable. Or create your own market!
- Sign up to get M$ 1,000 and start trading. + Sign up and get M$1,000 - worth $10 to your{' '} + + favorite charity. +
{' '} , + bets: Bet[], + comments: Comment[], + user: User | undefined | null +) { + let outcomes = _.uniq(bets.map((bet) => bet.outcome)).filter( + (outcome) => getOutcomeProbability(contract, outcome) > 0.0001 + ) + outcomes = _.sortBy(outcomes, (outcome) => + getOutcomeProbability(contract, outcome) + ) + + function collateCommentsSectionForOutcome(outcome: string) { + const answerBets = bets.filter((bet) => bet.outcome === outcome) + const answerComments = comments.filter( + (comment) => + comment.answerOutcome === outcome || + answerBets.some((bet) => bet.id === comment.betId) + ) + let items = [] + items.push({ + type: 'commentInput' as const, + id: 'commentInputFor' + outcome, + contract, + betsByCurrentUser: user + ? bets.filter((bet) => bet.userId === user.id) + : [], + comments: comments, + answerOutcome: outcome, + }) + items.push( + ...getCommentsWithPositions( + answerBets, + answerComments, + contract + ).reverse() + ) + return items + } + + const answerGroups = outcomes + .map((outcome) => { + const answer = contract.answers?.find( + (answer) => answer.id === outcome + ) as Answer + + const items = collateCommentsSectionForOutcome(outcome) + + return { + id: outcome, + type: 'answergroup' as const, + contract, + answer, + items, + user, + } + }) + .filter((group) => group.answer) as ActivityItem[] + return answerGroups +} + function groupBetsAndComments( bets: Bet[], comments: Comment[], @@ -382,7 +441,7 @@ export function getAllContractActivityItems( ) ) items.push({ - type: 'commentInput', + type: 'commentInput' as const, id: 'commentInput', contract, betsByCurrentUser: [], @@ -408,7 +467,7 @@ export function getAllContractActivityItems( if (outcomeType === 'BINARY') { items.push({ - type: 'commentInput', + type: 'commentInput' as const, id: 'commentInput', contract, betsByCurrentUser: [], @@ -479,7 +538,7 @@ export function getSpecificContractActivityItems( comments: Comment[], user: User | null | undefined, options: { - mode: 'comments' | 'bets' + mode: 'comments' | 'bets' | 'free-response-comment-answer-groups' } ) { const { mode } = options @@ -501,18 +560,39 @@ export function getSpecificContractActivityItems( break case 'comments': - items.push(...getCommentsWithPositions(bets, comments, contract)) + const nonFreeResponseComments = comments.filter( + (comment) => comment.answerOutcome === undefined + ) + const nonFreeResponseBets = + contract.outcomeType === 'FREE_RESPONSE' ? [] : bets + items.push( + ...getCommentsWithPositions( + nonFreeResponseBets, + nonFreeResponseComments, + contract + ) + ) items.push({ type: 'commentInput', id: 'commentInput', contract, betsByCurrentUser: user - ? bets.filter((bet) => bet.userId === user.id) + ? nonFreeResponseBets.filter((bet) => bet.userId === user.id) : [], - comments: comments, + comments: nonFreeResponseComments, }) break + case 'free-response-comment-answer-groups': + items.push( + ...getAnswerAndCommentInputGroups( + contract as FullContract, + bets, + comments, + user + ) + ) + break } return items.reverse() diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index f65c6716..40b2dd09 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -1,22 +1,28 @@ -import { Contract } from '../../lib/firebase/contracts' -import { Comment } from '../../lib/firebase/comments' -import { Bet } from '../../../common/bet' -import { useBets } from '../../hooks/use-bets' -import { useComments } from '../../hooks/use-comments' +import { Contract } from 'web/lib/firebase/contracts' +import { Comment } from 'web/lib/firebase/comments' +import { Bet } from 'common/bet' +import { useBets } from 'web/hooks/use-bets' +import { useComments } from 'web/hooks/use-comments' import { getAllContractActivityItems, getRecentContractActivityItems, getSpecificContractActivityItems, } from './activity-items' import { FeedItems } from './feed-items' -import { User } from '../../../common/user' +import { User } from 'common/user' export function ContractActivity(props: { contract: Contract bets: Bet[] comments: Comment[] user: User | null | undefined - mode: 'only-recent' | 'abbreviated' | 'all' | 'comments' | 'bets' + mode: + | 'only-recent' + | 'abbreviated' + | 'all' + | 'comments' + | 'bets' + | 'free-response-comment-answer-groups' contractPath?: string className?: string betRowClassName?: string @@ -38,7 +44,9 @@ export function ContractActivity(props: { ? getRecentContractActivityItems(contract, bets, comments, user, { contractPath, }) - : mode === 'comments' || mode === 'bets' + : mode === 'comments' || + mode === 'bets' || + mode === 'free-response-comment-answer-groups' ? getSpecificContractActivityItems(contract, bets, comments, user, { mode, }) diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index e21db3e5..cb089dd7 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -19,43 +19,38 @@ import { Contract, contractPath, tradingAllowed, -} from '../../lib/firebase/contracts' -import { useUser } from '../../hooks/use-user' +} from 'web/lib/firebase/contracts' +import { useUser } from 'web/hooks/use-user' import { Linkify } from '../linkify' import { Row } from '../layout/row' -import { createComment, MAX_COMMENT_LENGTH } from '../../lib/firebase/comments' -import { formatMoney, formatPercent } from '../../../common/util/format' -import { Comment } from '../../../common/comment' +import { createComment, MAX_COMMENT_LENGTH } from 'web/lib/firebase/comments' +import { formatMoney, formatPercent } from 'common/util/format' +import { Comment } from 'common/comment' import { BinaryResolutionOrChance } from '../contract/contract-card' import { SiteLink } from '../site-link' import { Col } from '../layout/col' import { UserLink } from '../user-page' import { DateTimeTooltip } from '../datetime-tooltip' -import { Bet } from '../../lib/firebase/bets' +import { Bet } from 'web/lib/firebase/bets' import { JoinSpans } from '../join-spans' -import { fromNow } from '../../lib/util/time' +import { fromNow } from 'web/lib/util/time' import BetRow from '../bet-row' import { Avatar } from '../avatar' -import { Answer } from '../../../common/answer' -import { ActivityItem } from './activity-items' -import { - Binary, - CPMM, - DPM, - FreeResponse, - FullContract, -} from '../../../common/contract' +import { Answer } from 'common/answer' +import { ActivityItem, GENERAL_COMMENTS_OUTCOME_ID } from './activity-items' +import { Binary, CPMM, DPM, FreeResponse, FullContract } from 'common/contract' import { BuyButton } from '../yes-no-selector' -import { getDpmOutcomeProbability } from '../../../common/calculate-dpm' +import { getDpmOutcomeProbability } from 'common/calculate-dpm' import { AnswerBetPanel } from '../answers/answer-bet-panel' -import { useSaveSeenContract } from '../../hooks/use-seen-contracts' -import { User } from '../../../common/user' +import { useSaveSeenContract } from 'web/hooks/use-seen-contracts' +import { User } from 'common/user' import { Modal } from '../layout/modal' -import { trackClick } from '../../lib/firebase/tracking' -import { firebaseLogin } from '../../lib/firebase/users' -import { DAY_MS } from '../../../common/util/time' +import { trackClick } from 'web/lib/firebase/tracking' +import { firebaseLogin } from 'web/lib/firebase/users' +import { DAY_MS } from 'common/util/time' import NewContractBadge from '../new-contract-badge' -import { calculateCpmmSale } from '../../../common/calculate-cpmm' +import { RelativeTimestamp } from '../relative-timestamp' +import { calculateCpmmSale } from 'common/calculate-cpmm' export function FeedItems(props: { contract: Contract @@ -222,52 +217,64 @@ export function CommentInput(props: { contract: Contract betsByCurrentUser: Bet[] comments: Comment[] + // Only for free response comment inputs + answerOutcome?: string }) { - const { contract, betsByCurrentUser, comments } = props + const { contract, betsByCurrentUser, comments, answerOutcome } = props const user = useUser() const [comment, setComment] = useState('') - - async function submitComment() { - if (!comment) return - if (!user) { - return await firebaseLogin() - } - await createComment(contract.id, comment, user) - setComment('') - } + const [focused, setFocused] = useState(false) // Should this be oldest bet or most recent bet? const mostRecentCommentableBet = betsByCurrentUser - .filter( - (bet) => - canCommentOnBet(bet.userId, bet.createdTime, user) && + .filter((bet) => { + if ( + canCommentOnBet(bet, user) && + // The bet doesn't already have a comment !comments.some((comment) => comment.betId == bet.id) - ) + ) { + if (!answerOutcome) return true + // If we're in free response, don't allow commenting on ante bet + return ( + bet.outcome !== GENERAL_COMMENTS_OUTCOME_ID && + answerOutcome === bet.outcome + ) + } + return false + }) .sort((b1, b2) => b1.createdTime - b2.createdTime) .pop() - if (mostRecentCommentableBet) { - return ( - - ) + const { id } = mostRecentCommentableBet || { id: undefined } + + async function submitComment(betId: string | undefined) { + if (!user) { + return await firebaseLogin() + } + if (!comment) return + await createComment(contract.id, comment, user, betId, answerOutcome) + setComment('') } + const { userPosition, userPositionMoney, yesFloorShares, noFloorShares } = getBettorsPosition(contract, Date.now(), betsByCurrentUser) return ( <> - +
-
+
- {user && userPosition > 0 && ( + {mostRecentCommentableBet && ( + + )} + {!mostRecentCommentableBet && user && userPosition > 0 && ( <> {'You have ' + userPositionMoney + ' '} <> @@ -280,47 +287,71 @@ export function CommentInput(props: { )} -
-