Merge remote-tracking branch 'origin/main' into kill-daisy

This commit is contained in:
Sinclair Chen 2022-10-13 10:58:36 -07:00
commit 69ce2095eb
49 changed files with 1114 additions and 683 deletions

View File

@ -16,7 +16,6 @@ export const DEV_CONFIG: EnvConfig = {
cloudRunId: 'w3txbmd3ba', cloudRunId: 'w3txbmd3ba',
cloudRunRegion: 'uc', cloudRunRegion: 'uc',
amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3', amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3',
// this is Phil's deployment twitchBotEndpoint: 'https://dev-twitch-bot.manifold.markets',
twitchBotEndpoint: 'https://king-prawn-app-5btyw.ondigitalocean.app',
sprigEnvironmentId: 'Tu7kRZPm7daP', sprigEnvironmentId: 'Tu7kRZPm7daP',
} }

View File

@ -8,11 +8,13 @@
}, },
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@tiptap/core": "2.0.0-beta.182", "@tiptap/core": "2.0.0-beta.199",
"@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-image": "2.0.0-beta.199",
"@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-link": "2.0.0-beta.199",
"@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/extension-mention": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.191", "@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"lodash": "4.17.21" "lodash": "4.17.21"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,4 +1,5 @@
import { generateText, JSONContent } from '@tiptap/core' import { generateText, JSONContent, Node } from '@tiptap/core'
import { generateJSON } from '@tiptap/html'
// Tiptap starter extensions // Tiptap starter extensions
import { Blockquote } from '@tiptap/extension-blockquote' import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold' import { Bold } from '@tiptap/extension-bold'
@ -51,6 +52,26 @@ export function parseMentions(data: JSONContent): string[] {
return uniq(mentions) return uniq(mentions)
} }
// TODO: this is a hack to get around the fact that tiptap doesn't have a
// way to add a node view without bundling in tsx
function skippableComponent(name: string): Node<any, any> {
return Node.create({
name,
group: 'block',
content: 'inline*',
parseHTML() {
return [
{
tag: 'grid-cards-component',
},
]
},
})
}
const stringParseExts = [ const stringParseExts = [
// StarterKit extensions // StarterKit extensions
Blockquote, Blockquote,
@ -78,6 +99,7 @@ const stringParseExts = [
renderText: ({ node }) => renderText: ({ node }) =>
'[embed]' + node.attrs.src ? `(${node.attrs.src})` : '', '[embed]' + node.attrs.src ? `(${node.attrs.src})` : '',
}), }),
skippableComponent('gridCardsComponent'),
TiptapTweet.extend({ renderText: () => '[tweet]' }), TiptapTweet.extend({ renderText: () => '[tweet]' }),
TiptapSpoiler.extend({ renderHTML: () => ['span', '[spoiler]', 0] }), TiptapSpoiler.extend({ renderHTML: () => ['span', '[spoiler]', 0] }),
] ]
@ -86,3 +108,7 @@ export function richTextToString(text?: JSONContent) {
if (!text) return '' if (!text) return ''
return generateText(text, stringParseExts) return generateText(text, stringParseExts)
} }
export function htmlToRichText(html: string) {
return generateJSON(html, stringParseExts)
}

View File

@ -680,6 +680,17 @@ $ curl https://manifold.markets/api/v0/market/{marketId}/sell -X POST \
--data-raw '{"outcome": "YES", "shares": 10}' --data-raw '{"outcome": "YES", "shares": 10}'
``` ```
### `POST /v0/comment`
Creates a comment in the specified market. Only supports top-level comments for now.
Parameters:
- `contractId`: Required. The ID of the market to comment on.
- `content`: The comment to post, formatted as [TipTap json](https://tiptap.dev/guide/output#option-1-json), OR
- `html`: The comment to post, formatted as an HTML string, OR
- `markdown`: The comment to post, formatted as a markdown string.
### `GET /v0/bets` ### `GET /v0/bets`
Gets a list of bets, ordered by creation date descending. Gets a list of bets, ordered by creation date descending.

View File

@ -110,7 +110,7 @@ service cloud.firestore {
match /contracts/{contractId} { match /contracts/{contractId} {
allow read; allow read;
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks']); .hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks', 'flaggedByUsernames']);
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['description', 'closeTime', 'question', 'visibility', 'unlistedById']) .hasOnly(['description', 'closeTime', 'question', 'visibility', 'unlistedById'])
&& resource.data.creatorId == request.auth.uid; && resource.data.creatorId == request.auth.uid;

View File

@ -15,9 +15,9 @@
"dev": "nodemon src/serve.ts", "dev": "nodemon src/serve.ts",
"localDbScript": "firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export", "localDbScript": "firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export",
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export", "serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export",
"db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export", "db:update-local-from-remote": "yarn db:backup-remote && gsutil -m rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
"db:backup-local": "firebase emulators:export --force ./firestore_export", "db:backup-local": "firebase emulators:export --force ./firestore_export",
"db:rename-remote-backup-folder": "gsutil mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)", "db:rename-remote-backup-folder": "gsutil -m mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)",
"db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/", "db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/",
"verify": "(cd .. && yarn verify)", "verify": "(cd .. && yarn verify)",
"verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty" "verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty"
@ -26,11 +26,13 @@
"dependencies": { "dependencies": {
"@amplitude/node": "1.10.0", "@amplitude/node": "1.10.0",
"@google-cloud/functions-framework": "3.1.2", "@google-cloud/functions-framework": "3.1.2",
"@tiptap/core": "2.0.0-beta.182", "@tiptap/core": "2.0.0-beta.199",
"@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-image": "2.0.0-beta.199",
"@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-link": "2.0.0-beta.199",
"@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/extension-mention": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.191", "@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"cors": "2.8.5", "cors": "2.8.5",
"dayjs": "1.11.4", "dayjs": "1.11.4",
"express": "4.18.1", "express": "4.18.1",
@ -38,6 +40,7 @@
"firebase-functions": "3.21.2", "firebase-functions": "3.21.2",
"lodash": "4.17.21", "lodash": "4.17.21",
"mailgun-js": "0.22.0", "mailgun-js": "0.22.0",
"marked": "4.1.1",
"module-alias": "2.2.2", "module-alias": "2.2.2",
"node-fetch": "2", "node-fetch": "2",
"stripe": "8.194.0", "stripe": "8.194.0",
@ -45,6 +48,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/mailgun-js": "0.22.12", "@types/mailgun-js": "0.22.12",
"@types/marked": "4.0.7",
"@types/module-alias": "2.0.1", "@types/module-alias": "2.0.1",
"@types/node-fetch": "2.6.2", "@types/node-fetch": "2.6.2",
"firebase-functions-test": "0.3.3", "firebase-functions-test": "0.3.3",

View File

@ -0,0 +1,105 @@
import * as admin from 'firebase-admin'
import { getContract, getUser, log } from './utils'
import { APIError, newEndpoint, validate } from './api'
import { JSONContent } from '@tiptap/core'
import { z } from 'zod'
import { removeUndefinedProps } from '../../common/util/object'
import { htmlToRichText } from '../../common/util/parse'
import { marked } from 'marked'
const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection(
z.record(z.any()),
z.object({
type: z.string().optional(),
attrs: z.record(z.any()).optional(),
content: z.array(contentSchema).optional(),
marks: z
.array(
z.intersection(
z.record(z.any()),
z.object({
type: z.string(),
attrs: z.record(z.any()).optional(),
})
)
)
.optional(),
text: z.string().optional(),
})
)
)
const postSchema = z.object({
contractId: z.string(),
content: contentSchema.optional(),
html: z.string().optional(),
markdown: z.string().optional(),
})
const MAX_COMMENT_JSON_LENGTH = 20000
// For now, only supports creating a new top-level comment on a contract.
// Replies, posts, chats are not supported yet.
export const createcomment = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore()
const { contractId, content, html, markdown } = validate(postSchema, req.body)
const creator = await getUser(auth.uid)
const contract = await getContract(contractId)
if (!creator) {
throw new APIError(400, 'No user exists with the authenticated user ID.')
}
if (!contract) {
throw new APIError(400, 'No contract exists with the given ID.')
}
let contentJson = null
if (content) {
contentJson = content
} else if (html) {
console.log('html', html)
contentJson = htmlToRichText(html)
} else if (markdown) {
const markedParse = marked.parse(markdown)
log('parsed', markedParse)
contentJson = htmlToRichText(markedParse)
log('json', contentJson)
}
if (!contentJson) {
throw new APIError(400, 'No comment content provided.')
}
if (JSON.stringify(contentJson).length > MAX_COMMENT_JSON_LENGTH) {
throw new APIError(
400,
`Comment is too long; should be less than ${MAX_COMMENT_JSON_LENGTH} as a JSON string.`
)
}
const ref = firestore.collection(`contracts/${contractId}/comments`).doc()
const comment = removeUndefinedProps({
id: ref.id,
content: contentJson,
createdTime: Date.now(),
userId: creator.id,
userName: creator.name,
userUsername: creator.username,
userAvatarUrl: creator.avatarUrl,
// OnContract fields
commentType: 'contract',
contractId: contractId,
contractSlug: contract.slug,
contractQuestion: contract.question,
})
await ref.set(comment)
return { status: 'success', comment }
})

View File

@ -197,6 +197,7 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async (
return await notificationRef.set(removeUndefinedProps(notification)) return await notificationRef.set(removeUndefinedProps(notification))
} }
const needNotFollowContractReasons = ['tagged_user']
const stillFollowingContract = (userId: string) => { const stillFollowingContract = (userId: string) => {
return contractFollowersIds.includes(userId) return contractFollowersIds.includes(userId)
} }
@ -205,7 +206,12 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async (
userId: string, userId: string,
reason: notification_reason_types reason: notification_reason_types
) => { ) => {
if (!stillFollowingContract(userId) || sourceUser.id == userId) return if (
(!stillFollowingContract(userId) &&
!needNotFollowContractReasons.includes(reason)) ||
sourceUser.id == userId
)
return
const privateUser = await getPrivateUser(userId) const privateUser = await getPrivateUser(userId)
if (!privateUser) return if (!privateUser) return
const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser( const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser(

View File

@ -65,6 +65,7 @@ import { sellbet } from './sell-bet'
import { sellshares } from './sell-shares' import { sellshares } from './sell-shares'
import { claimmanalink } from './claim-manalink' import { claimmanalink } from './claim-manalink'
import { createmarket } from './create-market' import { createmarket } from './create-market'
import { createcomment } from './create-comment'
import { addcommentbounty, awardcommentbounty } from './update-comment-bounty' import { addcommentbounty, awardcommentbounty } from './update-comment-bounty'
import { creategroup } from './create-group' import { creategroup } from './create-group'
import { resolvemarket } from './resolve-market' import { resolvemarket } from './resolve-market'
@ -94,6 +95,7 @@ const claimManalinkFunction = toCloudFunction(claimmanalink)
const createMarketFunction = toCloudFunction(createmarket) const createMarketFunction = toCloudFunction(createmarket)
const addSubsidyFunction = toCloudFunction(addsubsidy) const addSubsidyFunction = toCloudFunction(addsubsidy)
const addCommentBounty = toCloudFunction(addcommentbounty) const addCommentBounty = toCloudFunction(addcommentbounty)
const createCommentFunction = toCloudFunction(createcomment)
const awardCommentBounty = toCloudFunction(awardcommentbounty) const awardCommentBounty = toCloudFunction(awardcommentbounty)
const createGroupFunction = toCloudFunction(creategroup) const createGroupFunction = toCloudFunction(creategroup)
const resolveMarketFunction = toCloudFunction(resolvemarket) const resolveMarketFunction = toCloudFunction(resolvemarket)
@ -130,6 +132,7 @@ export {
acceptChallenge as acceptchallenge, acceptChallenge as acceptchallenge,
createPostFunction as createpost, createPostFunction as createpost,
saveTwitchCredentials as savetwitchcredentials, saveTwitchCredentials as savetwitchcredentials,
createCommentFunction as createcomment,
addCommentBounty as addcommentbounty, addCommentBounty as addcommentbounty,
awardCommentBounty as awardcommentbounty, awardCommentBounty as awardcommentbounty,
updateMetricsFunction as updatemetrics, updateMetricsFunction as updatemetrics,

View File

@ -1,6 +1,6 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod' import { z } from 'zod'
import { mapValues, groupBy, sumBy } from 'lodash' import { mapValues, groupBy, sumBy, uniqBy } from 'lodash'
import { import {
Contract, Contract,
@ -15,14 +15,14 @@ import {
getValues, getValues,
isProd, isProd,
log, log,
payUser, payUsers,
payUsersMultipleTransactions,
revalidateStaticProps, revalidateStaticProps,
} from './utils' } from './utils'
import { import {
getLoanPayouts, getLoanPayouts,
getPayouts, getPayouts,
groupPayoutsByUser, groupPayoutsByUser,
Payout,
} from '../../common/payouts' } from '../../common/payouts'
import { isAdmin, isManifoldId } from '../../common/envs/constants' import { isAdmin, isManifoldId } from '../../common/envs/constants'
import { removeUndefinedProps } from '../../common/util/object' import { removeUndefinedProps } from '../../common/util/object'
@ -36,6 +36,7 @@ import {
DEV_HOUSE_LIQUIDITY_PROVIDER_ID, DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
HOUSE_LIQUIDITY_PROVIDER_ID, HOUSE_LIQUIDITY_PROVIDER_ID,
} from '../../common/antes' } from '../../common/antes'
import { User } from 'common/user'
const bodySchema = z.object({ const bodySchema = z.object({
contractId: z.string(), contractId: z.string(),
@ -89,13 +90,10 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => {
if (!contractSnap.exists) if (!contractSnap.exists)
throw new APIError(404, 'No contract exists with the provided ID') throw new APIError(404, 'No contract exists with the provided ID')
const contract = contractSnap.data() as Contract const contract = contractSnap.data() as Contract
const { creatorId, closeTime } = contract const { creatorId } = contract
const firebaseUser = await admin.auth().getUser(auth.uid) const firebaseUser = await admin.auth().getUser(auth.uid)
const { value, resolutions, probabilityInt, outcome } = getResolutionParams( const resolutionParams = getResolutionParams(contract, req.body)
contract,
req.body
)
if ( if (
creatorId !== auth.uid && creatorId !== auth.uid &&
@ -109,6 +107,16 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => {
const creator = await getUser(creatorId) const creator = await getUser(creatorId)
if (!creator) throw new APIError(500, 'Creator not found') if (!creator) throw new APIError(500, 'Creator not found')
return await resolveMarket(contract, creator, resolutionParams)
})
export const resolveMarket = async (
contract: Contract,
creator: User,
{ value, resolutions, probabilityInt, outcome }: ResolutionParams
) => {
const { creatorId, closeTime, id: contractId } = contract
const resolutionProbability = const resolutionProbability =
probabilityInt !== undefined ? probabilityInt / 100 : undefined probabilityInt !== undefined ? probabilityInt / 100 : undefined
@ -131,15 +139,19 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => {
(doc) => doc.data() as LiquidityProvision (doc) => doc.data() as LiquidityProvision
) )
const { payouts, creatorPayout, liquidityPayouts, collectedFees } = const {
getPayouts( payouts: traderPayouts,
outcome, creatorPayout,
contract, liquidityPayouts,
bets, collectedFees,
liquidities, } = getPayouts(
resolutions, outcome,
resolutionProbability contract,
) bets,
liquidities,
resolutions,
resolutionProbability
)
const updatedContract = { const updatedContract = {
...contract, ...contract,
@ -156,30 +168,44 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => {
subsidyPool: 0, subsidyPool: 0,
} }
await contractDoc.update(updatedContract)
console.log('contract ', contractId, 'resolved to:', outcome)
const openBets = bets.filter((b) => !b.isSold && !b.sale) const openBets = bets.filter((b) => !b.isSold && !b.sale)
const loanPayouts = getLoanPayouts(openBets) const loanPayouts = getLoanPayouts(openBets)
const payouts = [
{ userId: creatorId, payout: creatorPayout, deposit: creatorPayout },
...liquidityPayouts.map((p) => ({ ...p, deposit: p.payout })),
...traderPayouts,
...loanPayouts,
]
if (!isProd()) if (!isProd())
console.log( console.log(
'payouts:', 'trader payouts:',
payouts, traderPayouts,
'creator payout:', 'creator payout:',
creatorPayout, creatorPayout,
'liquidity payout:' 'liquidity payout:',
liquidityPayouts,
'loan payouts:',
loanPayouts
) )
if (creatorPayout) const userCount = uniqBy(payouts, 'userId').length
await processPayouts([{ userId: creatorId, payout: creatorPayout }], true) const contractDoc = firestore.doc(`contracts/${contractId}`)
await processPayouts(liquidityPayouts, true) if (userCount <= 499) {
await firestore.runTransaction(async (transaction) => {
payUsers(transaction, payouts)
transaction.update(contractDoc, updatedContract)
})
} else {
await payUsersMultipleTransactions(payouts)
await contractDoc.update(updatedContract)
}
console.log('contract ', contractId, 'resolved to:', outcome)
await processPayouts([...payouts, ...loanPayouts])
await undoUniqueBettorRewardsIfCancelResolution(contract, outcome) await undoUniqueBettorRewardsIfCancelResolution(contract, outcome)
await revalidateStaticProps(getContractPath(contract)) await revalidateStaticProps(getContractPath(contract))
const userPayoutsWithoutLoans = groupPayoutsByUser(payouts) const userPayoutsWithoutLoans = groupPayoutsByUser(payouts)
@ -209,18 +235,6 @@ export const resolvemarket = newEndpoint(opts, async (req, auth) => {
) )
return updatedContract return updatedContract
})
const processPayouts = async (payouts: Payout[], isDeposit = false) => {
const userPayouts = groupPayoutsByUser(payouts)
const payoutPromises = Object.entries(userPayouts).map(([userId, payout]) =>
payUser(userId, payout, isDeposit)
)
return await Promise.all(payoutPromises)
.catch((e) => ({ status: 'error', message: e }))
.then(() => ({ status: 'success' }))
} }
function getResolutionParams(contract: Contract, body: string) { function getResolutionParams(contract: Contract, body: string) {
@ -287,6 +301,8 @@ function getResolutionParams(contract: Contract, body: string) {
throw new APIError(500, `Invalid outcome type: ${outcomeType}`) throw new APIError(500, `Invalid outcome type: ${outcomeType}`)
} }
type ResolutionParams = ReturnType<typeof getResolutionParams>
function validateAnswer( function validateAnswer(
contract: FreeResponseContract | MultipleChoiceContract, contract: FreeResponseContract | MultipleChoiceContract,
answer: number answer: number

View File

@ -0,0 +1,24 @@
import * as admin from 'firebase-admin'
import { initAdmin } from './script-init'
initAdmin()
const firestore = admin.firestore()
if (require.main === module) {
const contractsRef = firestore.collection('contracts')
contractsRef.get().then(async (contractsSnaps) => {
console.log(`Loaded ${contractsSnaps.size} contracts.`)
const needsFilling = contractsSnaps.docs.filter((ct) => {
return !('subsidyPool' in ct.data())
})
console.log(`Found ${needsFilling.length} contracts to update.`)
await Promise.all(
needsFilling.map((ct) => ct.ref.update({ subsidyPool: 0 }))
)
console.log(`Updated all contracts.`)
})
}

View File

@ -0,0 +1,59 @@
import { initAdmin } from './script-init'
initAdmin()
import { zip } from 'lodash'
import { filterDefined } from 'common/util/array'
import { resolveMarket } from '../resolve-market'
import { getContract, getUser } from '../utils'
if (require.main === module) {
const contractIds = process.argv.slice(2)
if (contractIds.length === 0) {
throw new Error('No contract ids provided')
}
resolveMarketsAgain(contractIds).then(() => process.exit(0))
}
async function resolveMarketsAgain(contractIds: string[]) {
const maybeContracts = await Promise.all(contractIds.map(getContract))
if (maybeContracts.some((c) => !c)) {
throw new Error('Invalid contract id')
}
const contracts = filterDefined(maybeContracts)
const maybeCreators = await Promise.all(
contracts.map((c) => getUser(c.creatorId))
)
if (maybeCreators.some((c) => !c)) {
throw new Error('No creator found')
}
const creators = filterDefined(maybeCreators)
if (
!contracts.every((c) => c.resolution === 'YES' || c.resolution === 'NO')
) {
throw new Error('Only YES or NO resolutions supported')
}
const resolutionParams = contracts.map((c) => ({
outcome: c.resolution as string,
value: undefined,
probabilityInt: undefined,
resolutions: undefined,
}))
const params = zip(contracts, creators, resolutionParams)
for (const [contract, creator, resolutionParams] of params) {
if (contract && creator && resolutionParams) {
console.log('Resolving', contract.question)
try {
await resolveMarket(contract, creator, resolutionParams)
} catch (e) {
console.log(e)
}
}
}
console.log(`Resolved all contracts.`)
}

View File

@ -19,6 +19,7 @@ import { sellbet } from './sell-bet'
import { sellshares } from './sell-shares' import { sellshares } from './sell-shares'
import { claimmanalink } from './claim-manalink' import { claimmanalink } from './claim-manalink'
import { createmarket } from './create-market' import { createmarket } from './create-market'
import { createcomment } from './create-comment'
import { creategroup } from './create-group' import { creategroup } from './create-group'
import { resolvemarket } from './resolve-market' import { resolvemarket } from './resolve-market'
import { unsubscribe } from './unsubscribe' import { unsubscribe } from './unsubscribe'
@ -53,6 +54,7 @@ addJsonEndpointRoute('/transact', transact)
addJsonEndpointRoute('/changeuserinfo', changeuserinfo) addJsonEndpointRoute('/changeuserinfo', changeuserinfo)
addJsonEndpointRoute('/createuser', createuser) addJsonEndpointRoute('/createuser', createuser)
addJsonEndpointRoute('/createanswer', createanswer) addJsonEndpointRoute('/createanswer', createanswer)
addJsonEndpointRoute('/createcomment', createcomment)
addJsonEndpointRoute('/placebet', placebet) addJsonEndpointRoute('/placebet', placebet)
addJsonEndpointRoute('/cancelbet', cancelbet) addJsonEndpointRoute('/cancelbet', cancelbet)
addJsonEndpointRoute('/sellbet', sellbet) addJsonEndpointRoute('/sellbet', sellbet)

View File

@ -1,7 +1,8 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import { FieldValue, Transaction } from 'firebase-admin/firestore'
import { chunk, groupBy, mapValues, sumBy } from 'lodash'
import { chunk } from 'lodash'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { PrivateUser, User } from '../../common/user' import { PrivateUser, User } from '../../common/user'
import { Group } from '../../common/group' import { Group } from '../../common/group'
@ -128,38 +129,29 @@ export const getUserByUsername = async (username: string) => {
return snap.empty ? undefined : (snap.docs[0].data() as User) return snap.empty ? undefined : (snap.docs[0].data() as User)
} }
const firestore = admin.firestore()
const updateUserBalance = ( const updateUserBalance = (
transaction: Transaction,
userId: string, userId: string,
delta: number, balanceDelta: number,
isDeposit = false depositDelta: number
) => { ) => {
const firestore = admin.firestore() const userDoc = firestore.doc(`users/${userId}`)
return firestore.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${userId}`)
const userSnap = await transaction.get(userDoc)
if (!userSnap.exists) return
const user = userSnap.data() as User
const newUserBalance = user.balance + delta // Note: Balance is allowed to go negative.
transaction.update(userDoc, {
// if (newUserBalance < 0) balance: FieldValue.increment(balanceDelta),
// throw new Error( totalDeposits: FieldValue.increment(depositDelta),
// `User (${userId}) balance cannot be negative: ${newUserBalance}`
// )
if (isDeposit) {
const newTotalDeposits = (user.totalDeposits || 0) + delta
transaction.update(userDoc, { totalDeposits: newTotalDeposits })
}
transaction.update(userDoc, { balance: newUserBalance })
}) })
} }
export const payUser = (userId: string, payout: number, isDeposit = false) => { export const payUser = (userId: string, payout: number, isDeposit = false) => {
if (!isFinite(payout)) throw new Error('Payout is not finite: ' + payout) if (!isFinite(payout)) throw new Error('Payout is not finite: ' + payout)
return updateUserBalance(userId, payout, isDeposit) return firestore.runTransaction(async (transaction) => {
updateUserBalance(transaction, userId, payout, isDeposit ? payout : 0)
})
} }
export const chargeUser = ( export const chargeUser = (
@ -170,7 +162,67 @@ export const chargeUser = (
if (!isFinite(charge) || charge <= 0) if (!isFinite(charge) || charge <= 0)
throw new Error('User charge is not positive: ' + charge) throw new Error('User charge is not positive: ' + charge)
return updateUserBalance(userId, -charge, isAnte) return payUser(userId, -charge, isAnte)
}
const checkAndMergePayouts = (
payouts: {
userId: string
payout: number
deposit?: number
}[]
) => {
for (const { payout, deposit } of payouts) {
if (!isFinite(payout)) {
throw new Error('Payout is not finite: ' + payout)
}
if (deposit !== undefined && !isFinite(deposit)) {
throw new Error('Deposit is not finite: ' + deposit)
}
}
const groupedPayouts = groupBy(payouts, 'userId')
return Object.values(
mapValues(groupedPayouts, (payouts, userId) => ({
userId,
payout: sumBy(payouts, 'payout'),
deposit: sumBy(payouts, (p) => p.deposit ?? 0),
}))
)
}
// Max 500 users in one transaction.
export const payUsers = (
transaction: Transaction,
payouts: {
userId: string
payout: number
deposit?: number
}[]
) => {
const mergedPayouts = checkAndMergePayouts(payouts)
for (const { userId, payout, deposit } of mergedPayouts) {
updateUserBalance(transaction, userId, payout, deposit)
}
}
export const payUsersMultipleTransactions = async (
payouts: {
userId: string
payout: number
deposit?: number
}[]
) => {
const mergedPayouts = checkAndMergePayouts(payouts)
const payoutChunks = chunk(mergedPayouts, 500)
for (const payoutChunk of payoutChunks) {
await firestore.runTransaction(async (transaction) => {
for (const { userId, payout, deposit } of payoutChunk) {
updateUserBalance(transaction, userId, payout, deposit)
}
})
}
} }
export const getContractPath = (contract: Contract) => { export const getContractPath = (contract: Contract) => {

View File

@ -227,10 +227,7 @@ function OpenAnswer(props: {
username={username} username={username}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
/> />
<Linkify <Linkify className="text-md whitespace-pre-line" text={text} />
className="text-md cursor-pointer whitespace-pre-line"
text={text}
/>
</Row> </Row>
<Row className="gap-2"> <Row className="gap-2">
<div className="my-auto text-xl">{probPercent}</div> <div className="my-auto text-xl">{probPercent}</div>

View File

@ -5,7 +5,6 @@ import { awardCommentBounty } from 'web/lib/firebase/api'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { Row } from './layout/row' import { Row } from './layout/row'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { TextButton } from 'web/components/text-button'
import { COMMENT_BOUNTY_AMOUNT } from 'common/economy' import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
@ -37,10 +36,17 @@ export function AwardBountyButton(prop: {
const canUp = me && me.id !== comment.userId && contract.creatorId === me.id const canUp = me && me.id !== comment.userId && contract.creatorId === me.id
if (!canUp) return <div /> if (!canUp) return <div />
return ( return (
<Row className={clsx('-ml-2 items-center gap-0.5', !canUp ? '-ml-6' : '')}> <Row
<TextButton className={'font-bold'} onClick={submit}> className={clsx('my-auto items-center gap-0.5', !canUp ? '-ml-6' : '')}
>
<button
className={
'rounded-full border border-indigo-400 bg-indigo-50 py-0.5 px-2 text-xs text-indigo-400 transition-colors hover:bg-indigo-400 hover:text-white'
}
onClick={submit}
>
Award {formatMoney(COMMENT_BOUNTY_AMOUNT)} Award {formatMoney(COMMENT_BOUNTY_AMOUNT)}
</TextButton> </button>
</Row> </Row>
) )
} }

View File

@ -206,7 +206,7 @@ export function BetsList(props: { user: User }) {
value={filter} value={filter}
onChange={(e) => setFilter(e.target.value as BetFilter)} onChange={(e) => setFilter(e.target.value as BetFilter)}
> >
<option value="open">Open</option> <option value="open">Active</option>
<option value="limit_bet">Limit orders</option> <option value="limit_bet">Limit orders</option>
<option value="sold">Sold</option> <option value="sold">Sold</option>
<option value="closed">Closed</option> <option value="closed">Closed</option>

View File

@ -82,3 +82,39 @@ export function Button(props: {
</button> </button>
) )
} }
export function IconButton(props: {
className?: string
onClick?: MouseEventHandler<any> | undefined
children?: ReactNode
size?: SizeType
type?: 'button' | 'reset' | 'submit'
disabled?: boolean
loading?: boolean
}) {
const {
children,
className,
onClick,
size = 'md',
type = 'button',
disabled = false,
loading,
} = props
return (
<button
type={type}
className={clsx(
'inline-flex items-center justify-center transition-colors disabled:cursor-not-allowed',
sizeClasses[size],
'disabled:text-greyscale-2 text-greyscale-6 hover:text-indigo-600',
className
)}
disabled={disabled || loading}
onClick={onClick}
>
{children}
</button>
)
}

View File

@ -35,7 +35,7 @@ import { trackCallback } from 'web/lib/service/analytics'
import { getMappedValue } from 'common/pseudo-numeric' import { getMappedValue } from 'common/pseudo-numeric'
import { Tooltip } from '../tooltip' import { Tooltip } from '../tooltip'
import { SiteLink } from '../site-link' import { SiteLink } from '../site-link'
import { ProbChange } from './prob-change-table' import { ProbOrNumericChange } from './prob-change-table'
import { Card } from '../card' import { Card } from '../card'
import { ProfitBadgeMana } from '../profit-badge' import { ProfitBadgeMana } from '../profit-badge'
import { floatingEqual } from 'common/util/math' import { floatingEqual } from 'common/util/math'
@ -396,12 +396,17 @@ export function ContractCardProbChange(props: {
className?: string className?: string
}) { }) {
const { noLinkAvatar, showPosition, className } = props const { noLinkAvatar, showPosition, className } = props
const yesOutcomeLabel =
props.contract.outcomeType === 'PSEUDO_NUMERIC' ? 'HIGHER' : 'YES'
const noOutcomeLabel =
props.contract.outcomeType === 'PSEUDO_NUMERIC' ? 'LOWER' : 'NO'
const contract = useContractWithPreload(props.contract) as CPMMBinaryContract const contract = useContractWithPreload(props.contract) as CPMMBinaryContract
const user = useUser() const user = useUser()
const metrics = useUserContractMetrics(user?.id, contract.id) const metrics = useUserContractMetrics(user?.id, contract.id)
const dayMetrics = metrics && metrics.from && metrics.from.day const dayMetrics = metrics && metrics.from && metrics.from.day
const outcome = const binaryOutcome =
metrics && floatingEqual(metrics.totalShares.NO ?? 0, 0) ? 'YES' : 'NO' metrics && floatingEqual(metrics.totalShares.NO ?? 0, 0) ? 'YES' : 'NO'
return ( return (
@ -418,7 +423,7 @@ export function ContractCardProbChange(props: {
> >
<span className="line-clamp-3">{contract.question}</span> <span className="line-clamp-3">{contract.question}</span>
</SiteLink> </SiteLink>
<ProbChange className="py-2 pr-4" contract={contract} /> <ProbOrNumericChange className="py-2 pr-4" contract={contract} />
</Row> </Row>
{showPosition && metrics && metrics.hasShares && ( {showPosition && metrics && metrics.hasShares && (
<Row <Row
@ -426,9 +431,9 @@ export function ContractCardProbChange(props: {
'items-center justify-between gap-4 pl-6 pr-4 pb-2 text-sm' 'items-center justify-between gap-4 pl-6 pr-4 pb-2 text-sm'
)} )}
> >
<Row className="gap-1 text-gray-700"> <Row className="gap-1 text-gray-500">
<div className="text-gray-500">Position</div> {Math.floor(metrics.totalShares[binaryOutcome])}{' '}
{Math.floor(metrics.totalShares[outcome])} {outcome} {binaryOutcome === 'YES' ? yesOutcomeLabel : noOutcomeLabel} shares
</Row> </Row>
{dayMetrics && ( {dayMetrics && (

View File

@ -45,13 +45,11 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) {
const { contract, isAdmin } = props const { contract, isAdmin } = props
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
const [editingQ, setEditingQ] = useState(false) const [editingQ, setEditingQ] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
// key: `description ${contract.id}`, // key: `description ${contract.id}`,
max: MAX_DESCRIPTION_LENGTH, max: MAX_DESCRIPTION_LENGTH,
defaultValue: contract.description, defaultValue: contract.description,
disabled: isSubmitting,
}) })
async function saveDescription() { async function saveDescription() {
@ -66,10 +64,8 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) {
<Row className="gap-2"> <Row className="gap-2">
<Button <Button
onClick={async () => { onClick={async () => {
setIsSubmitting(true)
await saveDescription() await saveDescription()
setEditing(false) setEditing(false)
setIsSubmitting(false)
}} }}
> >
Save Save

View File

@ -34,6 +34,7 @@ import { ExtraContractActionsRow } from './extra-contract-actions-row'
import { GroupLink } from 'common/group' import { GroupLink } from 'common/group'
import { Subtitle } from '../subtitle' import { Subtitle } from '../subtitle'
import { useIsMobile } from 'web/hooks/use-is-mobile' import { useIsMobile } from 'web/hooks/use-is-mobile'
import { useIsClient } from 'web/hooks/use-is-client'
import { import {
BountiedContractBadge, BountiedContractBadge,
BountiedContractSmallBadge, BountiedContractSmallBadge,
@ -52,22 +53,23 @@ export function MiscDetails(props: {
const { volume, closeTime, isResolved, createdTime, resolutionTime } = const { volume, closeTime, isResolved, createdTime, resolutionTime } =
contract contract
const isClient = useIsClient()
const isNew = createdTime > Date.now() - DAY_MS && !isResolved const isNew = createdTime > Date.now() - DAY_MS && !isResolved
const groupToDisplay = getGroupLinkToDisplay(contract) const groupToDisplay = getGroupLinkToDisplay(contract)
return ( return (
<Row className="items-center gap-3 truncate text-sm text-gray-400"> <Row className="items-center gap-3 truncate text-sm text-gray-400">
{showTime === 'close-date' ? ( {isClient && showTime === 'close-date' ? (
<Row className="gap-0.5 whitespace-nowrap"> <Row className="gap-0.5 whitespace-nowrap">
<ClockIcon className="h-5 w-5" /> <ClockIcon className="h-5 w-5" />
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '} {(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
{fromNow(closeTime || 0)} {fromNow(closeTime || 0)}
</Row> </Row>
) : showTime === 'resolve-date' && resolutionTime !== undefined ? ( ) : isClient && showTime === 'resolve-date' && resolutionTime ? (
<Row className="gap-0.5"> <Row className="gap-0.5">
<ClockIcon className="h-5 w-5" /> <ClockIcon className="h-5 w-5" />
{'Resolved '} {'Resolved '}
{fromNow(resolutionTime || 0)} {fromNow(resolutionTime)}
</Row> </Row>
) : (contract?.featuredOnHomeRank ?? 0) > 0 ? ( ) : (contract?.featuredOnHomeRank ?? 0) > 0 ? (
<FeaturedContractBadge /> <FeaturedContractBadge />
@ -390,6 +392,7 @@ function EditableCloseDate(props: {
}) { }) {
const { closeTime, contract, isCreator, disabled } = props const { closeTime, contract, isCreator, disabled } = props
const isClient = useIsClient()
const dayJsCloseTime = dayjs(closeTime) const dayJsCloseTime = dayjs(closeTime)
const dayJsNow = dayjs() const dayJsNow = dayjs()
@ -452,7 +455,7 @@ function EditableCloseDate(props: {
className="w-full shrink-0 sm:w-fit" className="w-full shrink-0 sm:w-fit"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onChange={(e) => setCloseDate(e.target.value)} onChange={(e) => setCloseDate(e.target.value)}
min={Date.now()} min={isClient ? Date.now() : undefined}
value={closeDate} value={closeDate}
/> />
<Input <Input
@ -479,14 +482,18 @@ function EditableCloseDate(props: {
</Col> </Col>
</Modal> </Modal>
<DateTimeTooltip <DateTimeTooltip
text={closeTime > Date.now() ? 'Trading ends:' : 'Trading ended:'} text={
isClient && closeTime <= Date.now()
? 'Trading ended:'
: 'Trading ends:'
}
time={closeTime} time={closeTime}
> >
<Row <Row
className={clsx(!disabled && isCreator ? 'cursor-pointer' : '')} className={clsx(!disabled && isCreator ? 'cursor-pointer' : '')}
onClick={() => !disabled && isCreator && setIsEditingCloseTime(true)} onClick={() => !disabled && isCreator && setIsEditingCloseTime(true)}
> >
{isSameDay ? ( {isSameDay && isClient ? (
<span className={'capitalize'}> {fromNow(closeTime)}</span> <span className={'capitalize'}> {fromNow(closeTime)}</span>
) : isSameYear ? ( ) : isSameYear ? (
dayJsCloseTime.format('MMM D') dayJsCloseTime.format('MMM D')

View File

@ -19,13 +19,11 @@ import ShortToggle from '../widgets/short-toggle'
import { DuplicateContractButton } from '../duplicate-contract-button' import { DuplicateContractButton } from '../duplicate-contract-button'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { BETTORS, User } from 'common/user' import { BETTORS, User } from 'common/user'
import { Button } from '../button' import { IconButton } from '../button'
import { AddLiquidityButton } from './add-liquidity-button' import { AddLiquidityButton } from './add-liquidity-button'
import { Tooltip } from '../tooltip'
import { Table } from '../table' import { Table } from '../table'
export const contractDetailsButtonClassName =
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
export function ContractInfoDialog(props: { export function ContractInfoDialog(props: {
contract: Contract contract: Contract
user: User | null | undefined user: User | null | undefined
@ -85,170 +83,173 @@ export function ContractInfoDialog(props: {
return ( return (
<> <>
<Button <Tooltip text="Market details" placement="bottom" noTap noFade>
size="sm" <IconButton
color="gray-white" size="2xs"
className={clsx(contractDetailsButtonClassName, className)} className={clsx(className)}
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
> >
<DotsHorizontalIcon <DotsHorizontalIcon
className={clsx('h-5 w-5 flex-shrink-0')} className={clsx('h-5 w-5 flex-shrink-0')}
aria-hidden="true" aria-hidden="true"
/> />
</Button> </IconButton>
<Modal open={open} setOpen={setOpen}> <Modal open={open} setOpen={setOpen}>
<Col className="gap-4 rounded bg-white p-6"> <Col className="gap-4 rounded bg-white p-6">
<Title className="!mt-0 !mb-0" text="This Market" /> <Title className="!mt-0 !mb-0" text="This Market" />
<Table>
<tbody>
<tr>
<td>Type</td>
<td>{typeDisplay}</td>
</tr>
<tr> <Table>
<td>Payout</td> <tbody>
<td className="flex gap-1">
{mechanism === 'cpmm-1' ? (
<>
Fixed{' '}
<InfoTooltip text="Each YES share is worth M$1 if YES wins." />
</>
) : (
<>
Parimutuel{' '}
<InfoTooltip text="Each share is a fraction of the pool. " />
</>
)}
</td>
</tr>
<tr>
<td>Market created</td>
<td>{formatTime(createdTime)}</td>
</tr>
{closeTime && (
<tr> <tr>
<td>Market close{closeTime > Date.now() ? 's' : 'd'}</td> <td>Type</td>
<td>{formatTime(closeTime)}</td> <td>{typeDisplay}</td>
</tr> </tr>
)}
{resolutionTime && (
<tr> <tr>
<td>Market resolved</td> <td>Payout</td>
<td>{formatTime(resolutionTime)}</td> <td className="flex gap-1">
</tr> {mechanism === 'cpmm-1' ? (
)} <>
Fixed{' '}
<tr> <InfoTooltip text="Each YES share is worth M$1 if YES wins." />
<td> </>
<span className="mr-1">Volume</span> ) : (
<InfoTooltip text="Total amount bought or sold" /> <>
</td> Parimutuel{' '}
<td>{formatMoney(contract.volume)}</td> <InfoTooltip text="Each share is a fraction of the pool. " />
</tr> </>
)}
<tr>
<td>{capitalize(BETTORS)}</td>
<td>{uniqueBettorCount ?? '0'}</td>
</tr>
<tr>
<td>
<Row>
<span className="mr-1">Elasticity</span>
<InfoTooltip
text={
mechanism === 'cpmm-1'
? 'Probability change between a M$50 bet on YES and NO'
: 'Probability change from a M$100 bet'
}
/>
</Row>
</td>
<td>{formatPercent(elasticity)}</td>
</tr>
<tr>
<td>Liquidity subsidies</td>
<td>
{mechanism === 'cpmm-1'
? formatMoney(contract.totalLiquidity)
: formatMoney(100)}
</td>
</tr>
<tr>
<td>Pool</td>
<td>
{mechanism === 'cpmm-1' && outcomeType === 'BINARY'
? `${Math.round(pool.YES)} YES, ${Math.round(pool.NO)} NO`
: mechanism === 'cpmm-1' && outcomeType === 'PSEUDO_NUMERIC'
? `${Math.round(pool.YES)} HIGHER, ${Math.round(
pool.NO
)} LOWER`
: contractPool(contract)}
</td>
</tr>
{/* Show a path to Firebase if user is an admin, or we're on localhost */}
{(isAdmin || isDev) && (
<tr>
<td>[ADMIN] Firestore</td>
<td>
<SiteLink href={firestoreConsolePath(id)}>
Console link
</SiteLink>
</td> </td>
</tr> </tr>
)}
{isAdmin && (
<tr>
<td>[ADMIN] Featured</td>
<td>
<ShortToggle
on={featured}
setOn={setFeatured}
onChange={onFeaturedToggle}
/>
</td>
</tr>
)}
{user && (
<tr>
<td>{isAdmin ? '[ADMIN]' : ''} Unlisted</td>
<td>
<ShortToggle
disabled={
isUnlisted
? !(isAdmin || (isCreator && wasUnlistedByCreator))
: !(isCreator || isAdmin)
}
on={contract.visibility === 'unlisted'}
setOn={(b) =>
updateContract(id, {
visibility: b ? 'unlisted' : 'public',
unlistedById: b ? user.id : '',
})
}
/>
</td>
</tr>
)}
</tbody>
</Table>
<Row className="flex-wrap"> <tr>
{mechanism === 'cpmm-1' && ( <td>Market created</td>
<AddLiquidityButton contract={contract} className="mr-2" /> <td>{formatTime(createdTime)}</td>
)} </tr>
<DuplicateContractButton contract={contract} />
</Row> {closeTime && (
</Col> <tr>
</Modal> <td>Market close{closeTime > Date.now() ? 's' : 'd'}</td>
<td>{formatTime(closeTime)}</td>
</tr>
)}
{resolutionTime && (
<tr>
<td>Market resolved</td>
<td>{formatTime(resolutionTime)}</td>
</tr>
)}
<tr>
<td>
<span className="mr-1">Volume</span>
<InfoTooltip text="Total amount bought or sold" />
</td>
<td>{formatMoney(contract.volume)}</td>
</tr>
<tr>
<td>{capitalize(BETTORS)}</td>
<td>{uniqueBettorCount ?? '0'}</td>
</tr>
<tr>
<td>
<Row>
<span className="mr-1">Elasticity</span>
<InfoTooltip
text={
mechanism === 'cpmm-1'
? 'Probability change between a M$50 bet on YES and NO'
: 'Probability change from a M$100 bet'
}
/>
</Row>
</td>
<td>{formatPercent(elasticity)}</td>
</tr>
<tr>
<td>Liquidity subsidies</td>
<td>
{mechanism === 'cpmm-1'
? formatMoney(contract.totalLiquidity)
: formatMoney(100)}
</td>
</tr>
<tr>
<td>Pool</td>
<td>
{mechanism === 'cpmm-1' && outcomeType === 'BINARY'
? `${Math.round(pool.YES)} YES, ${Math.round(pool.NO)} NO`
: mechanism === 'cpmm-1' &&
outcomeType === 'PSEUDO_NUMERIC'
? `${Math.round(pool.YES)} HIGHER, ${Math.round(
pool.NO
)} LOWER`
: contractPool(contract)}
</td>
</tr>
{/* Show a path to Firebase if user is an admin, or we're on localhost */}
{(isAdmin || isDev) && (
<tr>
<td>[ADMIN] Firestore</td>
<td>
<SiteLink href={firestoreConsolePath(id)}>
Console link
</SiteLink>
</td>
</tr>
)}
{isAdmin && (
<tr>
<td>[ADMIN] Featured</td>
<td>
<ShortToggle
on={featured}
setOn={setFeatured}
onChange={onFeaturedToggle}
/>
</td>
</tr>
)}
{user && (
<tr>
<td>{isAdmin ? '[ADMIN]' : ''} Unlisted</td>
<td>
<ShortToggle
disabled={
isUnlisted
? !(isAdmin || (isCreator && wasUnlistedByCreator))
: !(isCreator || isAdmin)
}
on={contract.visibility === 'unlisted'}
setOn={(b) =>
updateContract(id, {
visibility: b ? 'unlisted' : 'public',
unlistedById: b ? user.id : '',
})
}
/>
</td>
</tr>
)}
</tbody>
</Table>
<Row className="flex-wrap">
{mechanism === 'cpmm-1' && (
<AddLiquidityButton contract={contract} className="mr-2" />
)}
<DuplicateContractButton contract={contract} />
</Row>
</Col>
</Modal>
</Tooltip>
</> </>
) )
} }

View File

@ -6,17 +6,19 @@ import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts'
import { fromNow } from 'web/lib/util/time' import { fromNow } from 'web/lib/util/time'
import { BinaryContractOutcomeLabel } from '../outcome-label' import { BinaryContractOutcomeLabel } from '../outcome-label'
import { getColor } from './quick-bet' import { getColor } from './quick-bet'
import { useIsClient } from 'web/hooks/use-is-client'
export function ContractMention(props: { contract: Contract }) { export function ContractMention(props: { contract: Contract }) {
const { contract } = props const { contract } = props
const { outcomeType, resolution } = contract const { outcomeType, resolution } = contract
const probTextColor = `text-${getColor(contract)}` const probTextColor = `text-${getColor(contract)}`
const isClient = useIsClient()
return ( return (
<Link href={contractPath(contract)}> <Link href={contractPath(contract)}>
<a <a
className="group inline whitespace-nowrap rounded-sm hover:bg-indigo-50 focus:bg-indigo-50" className="group inline whitespace-nowrap rounded-sm hover:bg-indigo-50 focus:bg-indigo-50"
title={tooltipLabel(contract)} title={isClient ? tooltipLabel(contract) : undefined}
> >
<span className="break-anywhere mr-0.5 whitespace-normal font-normal text-indigo-700"> <span className="break-anywhere mr-0.5 whitespace-normal font-normal text-indigo-700">
{contract.question} {contract.question}

View File

@ -2,7 +2,7 @@ import { ShareIcon } from '@heroicons/react/outline'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { Contract } from 'web/lib/firebase/contracts' import { Contract } from 'web/lib/firebase/contracts'
import React, { useState } from 'react' import React, { useState } from 'react'
import { Button } from 'web/components/button' import { IconButton } from 'web/components/button'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { ShareModal } from './share-modal' import { ShareModal } from './share-modal'
import { FollowMarketButton } from 'web/components/follow-market-button' import { FollowMarketButton } from 'web/components/follow-market-button'
@ -16,15 +16,14 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
const [isShareOpen, setShareOpen] = useState(false) const [isShareOpen, setShareOpen] = useState(false)
return ( return (
<Row> <Row className="gap-1">
<FollowMarketButton contract={contract} user={user} /> <FollowMarketButton contract={contract} user={user} />
<LikeMarketButton contract={contract} user={user} /> <LikeMarketButton contract={contract} user={user} />
<Tooltip text="Share" placement="bottom" noTap noFade> <Tooltip text="Share" placement="bottom" noTap noFade>
<Button <IconButton
size="sm" size="2xs"
color="gray-white"
className={'flex'} className={'flex'}
onClick={() => setShareOpen(true)} onClick={() => setShareOpen(true)}
> >
@ -35,7 +34,7 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
contract={contract} contract={contract}
user={user} user={user}
/> />
</Button> </IconButton>
</Tooltip> </Tooltip>
<ContractInfoDialog contract={contract} user={user} /> <ContractInfoDialog contract={contract} user={user} />

View File

@ -7,6 +7,7 @@ import { formatPercent } from 'common/util/format'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { LoadingIndicator } from '../loading-indicator' import { LoadingIndicator } from '../loading-indicator'
import { ContractCardProbChange } from './contract-card' import { ContractCardProbChange } from './contract-card'
import { formatNumericProbability } from 'common/pseudo-numeric'
export function ProfitChangeTable(props: { export function ProfitChangeTable(props: {
contracts: CPMMBinaryContract[] contracts: CPMMBinaryContract[]
@ -118,7 +119,7 @@ export function ProbChangeTable(props: {
) )
} }
export function ProbChange(props: { export function ProbOrNumericChange(props: {
contract: CPMMContract contract: CPMMContract
className?: string className?: string
}) { }) {
@ -127,13 +128,17 @@ export function ProbChange(props: {
prob, prob,
probChanges: { day: change }, probChanges: { day: change },
} = contract } = contract
const number =
contract.outcomeType === 'PSEUDO_NUMERIC'
? formatNumericProbability(prob, contract)
: null
const color = change >= 0 ? 'text-teal-500' : 'text-red-400' const color = change >= 0 ? 'text-teal-500' : 'text-red-400'
return ( return (
<Col className={clsx('flex flex-col items-end', className)}> <Col className={clsx('flex flex-col items-end', className)}>
<div className="mb-0.5 mr-0.5 text-2xl"> <div className="mb-0.5 mr-0.5 text-2xl">
{formatPercent(Math.round(100 * prob) / 100)} {number ? number : formatPercent(Math.round(100 * prob) / 100)}
</div> </div>
<div className={clsx('text-base', color)}> <div className={clsx('text-base', color)}>
{(change > 0 ? '+' : '') + (change * 100).toFixed(0) + '%'} {(change > 0 ? '+' : '') + (change * 100).toFixed(0) + '%'}

View File

@ -1,10 +1,9 @@
import clsx from 'clsx' import clsx from 'clsx'
import { HeartIcon } from '@heroicons/react/outline'
import { Button } from 'web/components/button'
import { formatMoney, shortFormatNumber } from 'common/util/format' import { formatMoney, shortFormatNumber } from 'common/util/format'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { Tooltip } from '../tooltip' import { Tooltip } from '../tooltip'
import TipJar from 'web/public/custom-components/tipJar'
import { useState } from 'react'
export function TipButton(props: { export function TipButton(props: {
tipAmount: number tipAmount: number
@ -14,11 +13,12 @@ export function TipButton(props: {
isCompact?: boolean isCompact?: boolean
disabled?: boolean disabled?: boolean
}) { }) {
const { tipAmount, totalTipped, userTipped, isCompact, onClick, disabled } = const { tipAmount, totalTipped, userTipped, onClick, disabled } = props
props
const tipDisplay = shortFormatNumber(Math.ceil(totalTipped / 10)) const tipDisplay = shortFormatNumber(Math.ceil(totalTipped / 10))
const [hover, setHover] = useState(false)
return ( return (
<Tooltip <Tooltip
text={ text={
@ -30,39 +30,40 @@ export function TipButton(props: {
noTap noTap
noFade noFade
> >
<Button <button
size={'sm'}
className={clsx(
'max-w-xs self-center',
isCompact && 'px-0 py-0',
disabled && 'hover:bg-inherit'
)}
color={'gray-white'}
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
className={clsx(
'px-2 py-1 text-xs', //2xs button
'text-greyscale-6 transition-transform hover:text-indigo-600 disabled:cursor-not-allowed',
!disabled ? 'hover:rotate-12' : ''
)}
onMouseOver={() => setHover(true)}
onMouseLeave={() => setHover(false)}
> >
<Col className={'relative items-center sm:flex-row'}> <Col className={clsx('relative', disabled ? 'opacity-30' : '')}>
<HeartIcon <TipJar
className={clsx( size={18}
'h-5 w-5', color={userTipped || (hover && !disabled) ? '#4f46e5' : '#66667C'}
totalTipped > 0 ? 'mr-2' : '', fill={userTipped ? '#4f46e5' : 'none'}
userTipped ? 'fill-teal-500 text-teal-500' : ''
)}
/> />
{totalTipped > 0 && ( <div
<div className={clsx(
className={clsx( ' absolute top-[2px] text-[0.5rem]',
'bg-greyscale-5 absolute ml-3.5 mt-2 h-4 w-4 rounded-full align-middle text-white sm:mt-3 sm:h-5 sm:w-5 sm:px-1', userTipped ? 'text-white' : '',
tipDisplay.length > 2 tipDisplay.length === 1
? 'text-[0.4rem] sm:text-[0.5rem]' ? 'left-[7px]'
: 'sm:text-2xs text-[0.5rem]' : tipDisplay.length === 2
)} ? 'left-[4.5px]'
> : tipDisplay.length > 2
{tipDisplay} ? 'left-[4px] top-[2.5px] text-[0.35rem]'
</div> : ''
)} )}
>
{totalTipped > 0 ? tipDisplay : ''}
</div>
</Col> </Col>
</Button> </button>
</Tooltip> </Tooltip>
) )
} }

View File

@ -22,7 +22,6 @@ export function CreatePost(props: { group?: Group }) {
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
key: `post ${group?.id || ''}`, key: `post ${group?.id || ''}`,
disabled: isSubmitting,
}) })
const isValid = const isValid =

View File

@ -90,18 +90,17 @@ export const editorExtensions = (simple = false): Extensions => [
const proseClass = clsx( const proseClass = clsx(
'prose prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', 'prose prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed',
'font-light prose-a:font-light prose-blockquote:font-light' 'font-light prose-a:font-light prose-blockquote:font-light prose-sm'
) )
export function useTextEditor(props: { export function useTextEditor(props: {
placeholder?: string placeholder?: string
max?: number max?: number
defaultValue?: Content defaultValue?: Content
disabled?: boolean
simple?: boolean simple?: boolean
key?: string // unique key for autosave. If set, plz call `clearContent(true)` on submit to clear autosave key?: string // unique key for autosave. If set, plz call `clearContent(true)` on submit to clear autosave
}) { }) {
const { placeholder, max, defaultValue, disabled, simple, key } = props const { placeholder, max, defaultValue, simple, key } = props
const [content, saveContent] = usePersistentState<JSONContent | undefined>( const [content, saveContent] = usePersistentState<JSONContent | undefined>(
undefined, undefined,
@ -169,10 +168,6 @@ export function useTextEditor(props: {
}, },
}) })
useEffect(() => {
editor?.setEditable(!disabled)
}, [editor, disabled])
return { editor, upload } return { editor, upload }
} }

View File

@ -5,6 +5,7 @@ import Link from 'next/link'
import { fromNow } from 'web/lib/util/time' import { fromNow } from 'web/lib/util/time'
import { ToastClipboard } from 'web/components/toast-clipboard' import { ToastClipboard } from 'web/components/toast-clipboard'
import { LinkIcon } from '@heroicons/react/outline' import { LinkIcon } from '@heroicons/react/outline'
import { useIsClient } from 'web/hooks/use-is-client'
export function CopyLinkDateTimeComponent(props: { export function CopyLinkDateTimeComponent(props: {
prefix: string prefix: string
@ -14,6 +15,7 @@ export function CopyLinkDateTimeComponent(props: {
className?: string className?: string
}) { }) {
const { prefix, slug, elementId, createdTime, className } = props const { prefix, slug, elementId, createdTime, className } = props
const isClient = useIsClient()
const [showToast, setShowToast] = useState(false) const [showToast, setShowToast] = useState(false)
function copyLinkToComment( function copyLinkToComment(
@ -36,7 +38,7 @@ export function CopyLinkDateTimeComponent(props: {
'text-greyscale-4 hover:bg-greyscale-1.5 mx-1 whitespace-nowrap rounded-sm px-1 text-xs transition-colors' 'text-greyscale-4 hover:bg-greyscale-1.5 mx-1 whitespace-nowrap rounded-sm px-1 text-xs transition-colors'
} }
> >
{fromNow(createdTime)} {isClient && fromNow(createdTime)}
{showToast && <ToastClipboard />} {showToast && <ToastClipboard />}
<LinkIcon className="ml-1 mb-0.5 inline" height={13} /> <LinkIcon className="ml-1 mb-0.5 inline" height={13} />
</a> </a>

View File

@ -24,7 +24,7 @@ import { UserLink } from 'web/components/user-link'
import { CommentInput } from '../comment-input' import { CommentInput } from '../comment-input'
import { AwardBountyButton } from 'web/components/award-bounty-button' import { AwardBountyButton } from 'web/components/award-bounty-button'
import { ReplyIcon } from '@heroicons/react/solid' import { ReplyIcon } from '@heroicons/react/solid'
import { Button } from '../button' import { IconButton } from '../button'
import { ReplyToggle } from '../comments/reply-toggle' import { ReplyToggle } from '../comments/reply-toggle'
export type ReplyTo = { id: string; username: string } export type ReplyTo = { id: string; username: string }
@ -37,7 +37,7 @@ export function FeedCommentThread(props: {
}) { }) {
const { contract, threadComments, tips, parentComment } = props const { contract, threadComments, tips, parentComment } = props
const [replyTo, setReplyTo] = useState<ReplyTo>() const [replyTo, setReplyTo] = useState<ReplyTo>()
const [seeReplies, setSeeReplies] = useState(false) const [seeReplies, setSeeReplies] = useState(true)
const user = useUser() const user = useUser()
const onSubmitComment = useEvent(() => setReplyTo(undefined)) const onSubmitComment = useEvent(() => setReplyTo(undefined))
@ -154,36 +154,46 @@ export function ParentFeedComment(props: {
numComments={numComments} numComments={numComments}
onClick={onSeeReplyClick} onClick={onSeeReplyClick}
/> />
<Row className="grow justify-end gap-2"> <CommentActions
{onReplyClick && ( onReplyClick={onReplyClick}
<Button comment={comment}
size={'sm'} showTip={showTip}
className={clsx( myTip={myTip}
'hover:bg-greyscale-2 mt-0 mb-1 max-w-xs px-0 py-0' totalTip={totalTip}
)} contract={contract}
color={'gray-white'} />
onClick={() => onReplyClick(comment)}
>
<ReplyIcon className="h-5 w-5" />
</Button>
)}
{showTip && (
<Tipper
comment={comment}
myTip={myTip ?? 0}
totalTip={totalTip ?? 0}
/>
)}
{(contract.openCommentBounties ?? 0) > 0 && (
<AwardBountyButton comment={comment} contract={contract} />
)}
</Row>
</Row> </Row>
</Col> </Col>
</Row> </Row>
) )
} }
export function CommentActions(props: {
onReplyClick?: (comment: ContractComment) => void
comment: ContractComment
showTip?: boolean
myTip?: number
totalTip?: number
contract: Contract
}) {
const { onReplyClick, comment, showTip, myTip, totalTip, contract } = props
return (
<Row className="grow justify-end">
{onReplyClick && (
<IconButton size={'xs'} onClick={() => onReplyClick(comment)}>
<ReplyIcon className="h-5 w-5" />
</IconButton>
)}
{showTip && (
<Tipper comment={comment} myTip={myTip ?? 0} totalTip={totalTip ?? 0} />
)}
{(contract.openCommentBounties ?? 0) > 0 && (
<AwardBountyButton comment={comment} contract={contract} />
)}
</Row>
)
}
export const FeedComment = memo(function FeedComment(props: { export const FeedComment = memo(function FeedComment(props: {
contract: Contract contract: Contract
comment: ContractComment comment: ContractComment
@ -233,30 +243,14 @@ export const FeedComment = memo(function FeedComment(props: {
content={content || text} content={content || text}
smallImage smallImage
/> />
<Row className="grow justify-end gap-2"> <CommentActions
{onReplyClick && ( onReplyClick={onReplyClick}
<Button comment={comment}
size={'sm'} showTip={showTip}
className={clsx( myTip={myTip}
'hover:bg-greyscale-2 mt-0 mb-1 max-w-xs px-0 py-0' totalTip={totalTip}
)} contract={contract}
color={'gray-white'} />
onClick={() => onReplyClick(comment)}
>
<ReplyIcon className="h-5 w-5" />
</Button>
)}
{showTip && (
<Tipper
comment={comment}
myTip={myTip ?? 0}
totalTip={totalTip ?? 0}
/>
)}
{(contract.openCommentBounties ?? 0) > 0 && (
<AwardBountyButton comment={comment} contract={contract} />
)}
</Row>
</Col> </Col>
</Row> </Row>
) )

View File

@ -1,4 +1,4 @@
import { Button } from 'web/components/button' import { IconButton } from 'web/components/button'
import { import {
Contract, Contract,
followContract, followContract,
@ -33,9 +33,8 @@ export const FollowMarketButton = (props: {
noTap noTap
noFade noFade
> >
<Button <IconButton
size={'sm'} size="2xs"
color={'gray-white'}
onClick={async () => { onClick={async () => {
if (!user) return firebaseLogin() if (!user) return firebaseLogin()
if (followers?.includes(user.id)) { if (followers?.includes(user.id)) {
@ -65,18 +64,12 @@ export const FollowMarketButton = (props: {
> >
{watching ? ( {watching ? (
<Col className={'items-center gap-x-2 sm:flex-row'}> <Col className={'items-center gap-x-2 sm:flex-row'}>
<EyeOffIcon <EyeOffIcon className={clsx('h-5 w-5')} aria-hidden="true" />
className={clsx('h-5 w-5 sm:h-6 sm:w-6')}
aria-hidden="true"
/>
{/* Unwatch */} {/* Unwatch */}
</Col> </Col>
) : ( ) : (
<Col className={'items-center gap-x-2 sm:flex-row'}> <Col className={'items-center gap-x-2 sm:flex-row'}>
<EyeIcon <EyeIcon className={clsx('h-5 w-5')} aria-hidden="true" />
className={clsx('h-5 w-5 sm:h-6 sm:w-6')}
aria-hidden="true"
/>
{/* Watch */} {/* Watch */}
</Col> </Col>
)} )}
@ -87,7 +80,7 @@ export const FollowMarketButton = (props: {
followers?.includes(user?.id ?? 'nope') ? 'watched' : 'unwatched' followers?.includes(user?.id ?? 'nope') ? 'watched' : 'unwatched'
} a question!`} } a question!`}
/> />
</Button> </IconButton>
</Tooltip> </Tooltip>
) )
} }

View File

@ -33,11 +33,9 @@ export function GroupOverviewPost(props: {
function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) { function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {
const { group, post } = props const { group, post } = props
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
defaultValue: post?.content, defaultValue: post?.content,
disabled: isSubmitting,
}) })
async function savePost() { async function savePost() {
@ -76,10 +74,8 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {
<Row className="gap-2"> <Row className="gap-2">
<Button <Button
onClick={async () => { onClick={async () => {
setIsSubmitting(true)
await savePost() await savePost()
setEditing(false) setEditing(false)
setIsSubmitting(false)
}} }}
> >
Save Save

View File

@ -264,7 +264,7 @@ export function PinnedItems(props: {
</div> </div>
)} )}
{pinned.map((element, index) => ( {pinned.map((element, index) => (
<div className="relative mb-4"> <div className="relative mb-4" key={element.key}>
{element} {element}
{editMode && <CrossIcon onClick={() => onDeleteClicked(index)} />} {editMode && <CrossIcon onClick={() => onDeleteClicked(index)} />}

View File

@ -9,9 +9,9 @@ import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { Claim, Manalink } from 'common/manalink' import { Claim, Manalink } from 'common/manalink'
import { ShareIconButton } from './share-icon-button' import { ShareIconButton } from './share-icon-button'
import { contractDetailsButtonClassName } from './contract/contract-info-dialog'
import { useUserById } from 'web/hooks/use-user' import { useUserById } from 'web/hooks/use-user'
import getManalinkUrl from 'web/get-manalink-url' import getManalinkUrl from 'web/get-manalink-url'
import { IconButton } from './button'
export type ManalinkInfo = { export type ManalinkInfo = {
expiresTime: number | null expiresTime: number | null
@ -123,7 +123,7 @@ export function ManalinkCardFromView(props: {
src="/logo-white.svg" src="/logo-white.svg"
/> />
</Col> </Col>
<Row className="relative w-full gap-1 rounded-b-lg bg-white px-4 py-2 text-lg"> <Row className="relative w-full rounded-b-lg bg-white px-4 py-2 align-middle text-lg">
<div <div
className={clsx( className={clsx(
'my-auto mb-1 w-full', 'my-auto mb-1 w-full',
@ -133,32 +133,23 @@ export function ManalinkCardFromView(props: {
{formatMoney(amount)} {formatMoney(amount)}
</div> </div>
<button <IconButton size="2xs" onClick={() => (window.location.href = qrUrl)}>
onClick={() => (window.location.href = qrUrl)}
className={clsx(contractDetailsButtonClassName)}
>
<QrcodeIcon className="h-6 w-6" /> <QrcodeIcon className="h-6 w-6" />
</button> </IconButton>
<ShareIconButton <ShareIconButton
toastClassName={'-left-48 min-w-[250%]'} toastClassName={'-left-48 min-w-[250%]'}
buttonClassName={'transition-colors'}
onCopyButtonClassName={
'bg-gray-200 text-gray-600 transition-none hover:bg-gray-200 hover:text-gray-600'
}
copyPayload={getManalinkUrl(link.slug)} copyPayload={getManalinkUrl(link.slug)}
/> />
<button <IconButton
size="xs"
onClick={() => setShowDetails(!showDetails)} onClick={() => setShowDetails(!showDetails)}
className={clsx( className={clsx(
contractDetailsButtonClassName, showDetails ? ' text-indigo-600 hover:text-indigo-700' : ''
showDetails
? 'bg-gray-200 text-gray-600 hover:bg-gray-200 hover:text-gray-600'
: ''
)} )}
> >
<DotsHorizontalIcon className="h-[24px] w-5" /> <DotsHorizontalIcon className="h-5 w-5" />
</button> </IconButton>
</Row> </Row>
</Col> </Col>
<div className="mt-2 mb-4 text-xs text-gray-500 md:text-sm"> <div className="mt-2 mb-4 text-xs text-gray-500 md:text-sm">

View File

@ -54,47 +54,48 @@ export default function Welcome() {
if (isTwitch || !user || (!user.shouldShowWelcome && !groupSelectorOpen)) if (isTwitch || !user || (!user.shouldShowWelcome && !groupSelectorOpen))
return <></> return <></>
return ( if (groupSelectorOpen)
<> return (
<GroupSelectorDialog <GroupSelectorDialog
open={groupSelectorOpen} open={groupSelectorOpen}
setOpen={() => setGroupSelectorOpen(false)} setOpen={() => setGroupSelectorOpen(false)}
/> />
)
<Modal open={open} setOpen={toggleOpen}> return (
<Col className="h-[32rem] place-content-between rounded-md bg-white px-8 py-6 text-sm font-light md:h-[40rem] md:text-lg"> <Modal open={open} setOpen={toggleOpen}>
{page === 0 && <Page0 />} <Col className="h-[32rem] place-content-between rounded-md bg-white px-8 py-6 text-sm font-light md:h-[40rem] md:text-lg">
{page === 1 && <Page1 />} {page === 0 && <Page0 />}
{page === 2 && <Page2 />} {page === 1 && <Page1 />}
{page === 3 && <Page3 />} {page === 2 && <Page2 />}
<Col> {page === 3 && <Page3 />}
<Row className="place-content-between"> <Col>
<ChevronLeftIcon <Row className="place-content-between">
className={clsx( <ChevronLeftIcon
'h-10 w-10 text-gray-400 hover:text-gray-500', className={clsx(
page === 0 ? 'disabled invisible' : '' 'h-10 w-10 text-gray-400 hover:text-gray-500',
)} page === 0 ? 'disabled invisible' : ''
onClick={decreasePage} )}
/> onClick={decreasePage}
<PageIndicator page={page} totalpages={TOTAL_PAGES} /> />
<ChevronRightIcon <PageIndicator page={page} totalpages={TOTAL_PAGES} />
className={clsx( <ChevronRightIcon
'h-10 w-10 text-indigo-500 hover:text-indigo-600', className={clsx(
page === TOTAL_PAGES - 1 ? 'disabled invisible' : '' 'h-10 w-10 text-indigo-500 hover:text-indigo-600',
)} page === TOTAL_PAGES - 1 ? 'disabled invisible' : ''
onClick={increasePage} )}
/> onClick={increasePage}
</Row> />
<u </Row>
className="self-center text-xs text-gray-500" <u
onClick={() => toggleOpen(false)} className="self-center text-xs text-gray-500"
> onClick={() => toggleOpen(false)}
I got the gist, exit welcome >
</u> I got the gist, exit welcome
</Col> </u>
</Col> </Col>
</Modal> </Col>
</> </Modal>
) )
} }
@ -117,6 +118,7 @@ function PageIndicator(props: { page: number; totalpages: number }) {
<Row> <Row>
{[...Array(totalpages)].map((e, i) => ( {[...Array(totalpages)].map((e, i) => (
<div <div
key={i}
className={clsx( className={clsx(
'mx-1.5 my-auto h-1.5 w-1.5 rounded-full', 'mx-1.5 my-auto h-1.5 w-1.5 rounded-full',
i === page ? 'bg-indigo-500' : 'bg-gray-300' i === page ? 'bg-indigo-500' : 'bg-gray-300'

View File

@ -1,22 +1,16 @@
import { DateTimeTooltip } from './datetime-tooltip' import { DateTimeTooltip } from './datetime-tooltip'
import React, { useEffect, useState } from 'react'
import { fromNow } from 'web/lib/util/time' import { fromNow } from 'web/lib/util/time'
import { useIsClient } from 'web/hooks/use-is-client'
export function RelativeTimestamp(props: { time: number }) { export function RelativeTimestamp(props: { time: number }) {
const { time } = props const { time } = props
const [isClient, setIsClient] = useState(false) const isClient = useIsClient()
useEffect(() => {
// Only render on client to prevent difference from server.
setIsClient(true)
}, [])
return ( return (
<DateTimeTooltip <DateTimeTooltip
className="ml-1 whitespace-nowrap text-gray-400" className="ml-1 whitespace-nowrap text-gray-400"
time={time} time={time}
> >
{isClient ? fromNow(time) : ''} {isClient && fromNow(time)}
</DateTimeTooltip> </DateTimeTooltip>
) )
} }

View File

@ -5,34 +5,22 @@ import clsx from 'clsx'
import { copyToClipboard } from 'web/lib/util/copy' import { copyToClipboard } from 'web/lib/util/copy'
import { ToastClipboard } from 'web/components/toast-clipboard' import { ToastClipboard } from 'web/components/toast-clipboard'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { contractDetailsButtonClassName } from 'web/components/contract/contract-info-dialog' import { IconButton } from './button'
export function ShareIconButton(props: { export function ShareIconButton(props: {
buttonClassName?: string
onCopyButtonClassName?: string
toastClassName?: string toastClassName?: string
children?: React.ReactNode children?: React.ReactNode
iconClassName?: string iconClassName?: string
copyPayload: string copyPayload: string
}) { }) {
const { const { toastClassName, children, iconClassName, copyPayload } = props
buttonClassName,
onCopyButtonClassName,
toastClassName,
children,
iconClassName,
copyPayload,
} = props
const [showToast, setShowToast] = useState(false) const [showToast, setShowToast] = useState(false)
return ( return (
<div className="relative z-10 flex-shrink-0"> <div className="relative z-10 flex-shrink-0">
<button <IconButton
className={clsx( size="2xs"
contractDetailsButtonClassName, className={clsx('mt-1', showToast ? 'text-indigo-600' : '')}
buttonClassName,
showToast ? onCopyButtonClassName : ''
)}
onClick={() => { onClick={() => {
copyToClipboard(copyPayload) copyToClipboard(copyPayload)
track('copy share link') track('copy share link')
@ -41,11 +29,11 @@ export function ShareIconButton(props: {
}} }}
> >
<LinkIcon <LinkIcon
className={clsx(iconClassName ? iconClassName : 'h-[24px] w-5')} className={clsx(iconClassName ? iconClassName : 'h-5 w-5')}
aria-hidden="true" aria-hidden="true"
/> />
{children} {children}
</button> </IconButton>
{showToast && <ToastClipboard className={toastClassName} />} {showToast && <ToastClipboard className={toastClassName} />}
</div> </div>

View File

@ -291,7 +291,7 @@ export function ProfilePrivateStats(props: {
<Row className={'justify-between gap-4 sm:justify-end'}> <Row className={'justify-between gap-4 sm:justify-end'}>
<Col className={'text-greyscale-4 text-md sm:text-lg'}> <Col className={'text-greyscale-4 text-md sm:text-lg'}>
<span <span
className={clsx(profit >= 0 ? 'text-green-600' : 'text-red-400')} className={clsx(profit >= 0 ? 'text-teal-600' : 'text-red-400')}
> >
{formatMoney(profit)} {formatMoney(profit)}
</span> </span>

View File

@ -1,12 +1,17 @@
import { GlobalConfig } from 'common/globalConfig' import { GlobalConfig } from 'common/globalConfig'
import { useEffect, useState } from 'react' import { useEffect } from 'react'
import { listenForGlobalConfig } from 'web/lib/firebase/globalConfig' import { listenForGlobalConfig } from 'web/lib/firebase/globalConfig'
import { inMemoryStore, usePersistentState } from './use-persistent-state'
export const useGlobalConfig = () => { export const useGlobalConfig = () => {
const [globalConfig, setGlobalConfig] = useState<GlobalConfig | null>(null) const [globalConfig, setGlobalConfig] =
usePersistentState<GlobalConfig | null>(null, {
store: inMemoryStore(),
key: 'globalConfig',
})
useEffect(() => { useEffect(() => {
listenForGlobalConfig(setGlobalConfig) return listenForGlobalConfig(setGlobalConfig)
}, []) }, [setGlobalConfig])
return globalConfig return globalConfig
} }

View File

@ -0,0 +1,7 @@
import { useEffect, useState } from 'react'
export const useIsClient = () => {
const [isClient, setIsClient] = useState(false)
useEffect(() => setIsClient(true), [])
return isClient
}

View File

@ -89,6 +89,17 @@ export const historyStore = <T>(prefix = '__manifold'): PersistentStore<T> => ({
}, },
}) })
const store: Record<string, any> = {}
export const inMemoryStore = <T>(): PersistentStore<T> => ({
get: (k: string) => {
return store[k]
},
set: (k: string, v: T | undefined) => {
store[k] = v
},
})
export const usePersistentState = <T>( export const usePersistentState = <T>(
initial: T, initial: T,
persist?: PersistenceOptions<T> persist?: PersistenceOptions<T>

View File

@ -26,14 +26,16 @@
"@hello-pangea/dnd": "16.0.0", "@hello-pangea/dnd": "16.0.0",
"@heroicons/react": "1.0.6", "@heroicons/react": "1.0.6",
"@react-query-firebase/firestore": "0.4.2", "@react-query-firebase/firestore": "0.4.2",
"@tiptap/core": "2.0.0-beta.182", "@tiptap/core": "2.0.0-beta.199",
"@tiptap/extension-character-count": "2.0.0-beta.31", "@tiptap/extension-character-count": "2.0.0-beta.199",
"@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-image": "2.0.0-beta.199",
"@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-link": "2.0.0-beta.199",
"@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/extension-mention": "2.0.0-beta.199",
"@tiptap/extension-placeholder": "2.0.0-beta.53", "@tiptap/extension-placeholder": "2.0.0-beta.199",
"@tiptap/react": "2.0.0-beta.114", "@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.191", "@tiptap/react": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"algoliasearch": "4.13.0", "algoliasearch": "4.13.0",
"browser-image-compression": "2.0.0", "browser-image-compression": "2.0.0",
"clsx": "1.1.1", "clsx": "1.1.1",

View File

@ -0,0 +1,23 @@
import { NextApiRequest, NextApiResponse } from 'next'
import {
CORS_ORIGIN_MANIFOLD,
CORS_ORIGIN_LOCALHOST,
} from 'common/envs/constants'
import { applyCorsHeaders } from 'web/lib/api/cors'
import { fetchBackend, forwardResponse } from 'web/lib/api/proxy'
export const config = { api: { bodyParser: true } }
export default async function route(req: NextApiRequest, res: NextApiResponse) {
await applyCorsHeaders(req, res, {
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
methods: 'POST',
})
try {
const backendRes = await fetchBackend(req, 'createcomment')
await forwardResponse(res, backendRes)
} catch (err) {
console.error('Error talking to cloud function: ', err)
res.status(500).json({ message: 'Error communicating with backend.' })
}
}

View File

@ -227,7 +227,6 @@ export function NewContract(props: {
key: 'create market', key: 'create market',
max: MAX_DESCRIPTION_LENGTH, max: MAX_DESCRIPTION_LENGTH,
placeholder: descriptionPlaceholder, placeholder: descriptionPlaceholder,
disabled: isSubmitting,
defaultValue: params?.description defaultValue: params?.description
? JSON.parse(params.description) ? JSON.parse(params.description)
: undefined, : undefined,

View File

@ -35,9 +35,7 @@ export default function CreateDateDocPage() {
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({})
disabled: isSubmitting,
})
const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined
const isValid = const isValid =

View File

@ -1,4 +1,4 @@
import React, { ReactNode, useEffect, useState } from 'react' import React, { ReactNode, useEffect } from 'react'
import Router from 'next/router' import Router from 'next/router'
import { import {
AdjustmentsIcon, AdjustmentsIcon,
@ -63,6 +63,10 @@ import { useAllPosts } from 'web/hooks/use-post'
import { useGlobalConfig } from 'web/hooks/use-global-config' import { useGlobalConfig } from 'web/hooks/use-global-config'
import { useAdmin } from 'web/hooks/use-admin' import { useAdmin } from 'web/hooks/use-admin'
import { GlobalConfig } from 'common/globalConfig' import { GlobalConfig } from 'common/globalConfig'
import {
inMemoryStore,
usePersistentState,
} from 'web/hooks/use-persistent-state'
export default function Home() { export default function Home() {
const user = useUser() const user = useUser()
@ -105,7 +109,10 @@ export default function Home() {
groups?.map((g) => g.slug) groups?.map((g) => g.slug)
) )
const [pinned, setPinned] = useState<JSX.Element[] | null>(null) const [pinned, setPinned] = usePersistentState<JSX.Element[] | null>(null, {
store: inMemoryStore(),
key: 'home-pinned',
})
useEffect(() => { useEffect(() => {
const pinnedItems = globalConfig?.pinnedItems const pinnedItems = globalConfig?.pinnedItems
@ -139,7 +146,7 @@ export default function Home() {
} }
} }
getPinned() getPinned()
}, [globalConfig]) }, [globalConfig, setPinned])
const isLoading = const isLoading =
!user || !user ||
@ -277,6 +284,7 @@ function renderSections(
if (id === 'featured') { if (id === 'featured') {
return ( return (
<FeaturedSection <FeaturedSection
key={id}
globalConfig={globalConfig} globalConfig={globalConfig}
pinned={pinned} pinned={pinned}
isAdmin={isAdmin} isAdmin={isAdmin}

View File

@ -171,11 +171,9 @@ export function PostCommentsActivity(props: {
export function RichEditPost(props: { post: Post }) { export function RichEditPost(props: { post: Post }) {
const { post } = props const { post } = props
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
defaultValue: post.content, defaultValue: post.content,
disabled: isSubmitting,
}) })
async function savePost() { async function savePost() {
@ -193,10 +191,8 @@ export function RichEditPost(props: { post: Post }) {
<Row className="gap-2"> <Row className="gap-2">
<Button <Button
onClick={async () => { onClick={async () => {
setIsSubmitting(true)
await savePost() await savePost()
setEditing(false) setEditing(false)
setIsSubmitting(false)
}} }}
> >
Save Save

View File

@ -0,0 +1,23 @@
export default function TipJar({
size = 18,
color = '#66667C',
strokeWidth = 1.5,
fill = 'none',
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 18 18"
width={size}
height={size}
fill={fill}
stroke={color}
strokeWidth={strokeWidth}
opacity={50}
>
<path d="M15.5,8.1v5.8c0,1.43-1.16,2.6-2.6,2.6H5.1c-1.44,0-2.6-1.16-2.6-2.6v-5.8c0-1.04,.89-3.25,1.5-4.1h0v-2c0-.55,.45-1,1-1H13c.55,0,1,.45,1,1v2h0c.61,.85,1.5,3.06,1.5,4.1Z" />
<line x1="4" y1="4" x2="9" y2="4" />
<line x1="11.26" y1="4" x2="14" y2="4" />
</svg>
)
}

427
yarn.lock
View File

@ -2862,219 +2862,227 @@
lodash.isplainobject "^4.0.6" lodash.isplainobject "^4.0.6"
lodash.merge "^4.6.2" lodash.merge "^4.6.2"
"@tiptap/core@2.0.0-beta.182", "@tiptap/core@^2.0.0-beta.182": "@tiptap/core@2.0.0-beta.199", "@tiptap/core@^2.0.0-beta.199":
version "2.0.0-beta.182" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.182.tgz#d2001e9b765adda95e15d171479860a3349e2d04" resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.199.tgz#77c3d8df10a4594cf5860b8a73d5007b0a020d01"
integrity sha512-MZGkMGnVnWhBzjvpBNwQ9zBz38ndi3Irbf90uCTSArR0kaCVkW4vmyuPuOXd+0SO8Yv/l5oyDdOCpaG3rnQYfw== integrity sha512-34GaXcBEmNFjW1R7nf1LSmOHo3Q81YjKqvLAXjDLLG7MTx+YTrQ4yWwUvMsZtmi4o/FchUzrs1NVCfr571Zxzg==
dependencies: dependencies:
prosemirror-commands "1.3.0" prosemirror-commands "^1.3.1"
prosemirror-keymap "1.2.0" prosemirror-keymap "^1.2.0"
prosemirror-model "1.18.1" prosemirror-model "^1.18.1"
prosemirror-schema-list "1.2.0" prosemirror-schema-list "^1.2.2"
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
prosemirror-transform "1.6.0" prosemirror-transform "^1.7.0"
prosemirror-view "1.26.2" prosemirror-view "^1.28.2"
"@tiptap/extension-blockquote@^2.0.0-beta.29": "@tiptap/extension-blockquote@^2.0.0-beta.199":
version "2.0.0-beta.29" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.0.0-beta.29.tgz#6f1c4b17efa6457c7776f32d0807e96d848d4389" resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.0.0-beta.199.tgz#f3957a638d515f6e38cd91eacb59cbedef06af6c"
integrity sha512-zMYT5TtpKWav9VhTn4JLyMvXmhEdbD6on0MdhcTjRm0I5ugyR4ZbJwh2aelM7G9DZVYzB8jZU18OSDJmo7Af7w== integrity sha512-BbHKaIkVYgJCV5giJC3/bdXMZWxFylLKiAbOGSGwIsnnS5/oL+V4XN6hqcIDBxlcj3MQ/d9zG0+mvFyjRssAkg==
"@tiptap/extension-bold@^2.0.0-beta.28": "@tiptap/extension-bold@^2.0.0-beta.199":
version "2.0.0-beta.28" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.0.0-beta.28.tgz#cf67c264a80434ffb2368f3dd37cf357ae0c2064" resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.0.0-beta.199.tgz#edb3dca9ca49ec128611629fbb5d17eaca62c506"
integrity sha512-DY8GOzw9xjmTFrnvTbgHUNxTnDfKrkDgrhe0SUvdkT2udntWp8umPdhPiD3vczLgHOJw6tX68qMRjbsR1ZPcHQ== integrity sha512-l513jgGLmt8C69Yuh5Et7a46Tn8QpW4q1HhZK6ih0ajNT+L5Xk0CSxEK/K5EmHSACPhwqjsJztLpGjAdoOn0mA==
"@tiptap/extension-bubble-menu@^2.0.0-beta.61": "@tiptap/extension-bubble-menu@^2.0.0-beta.199":
version "2.0.0-beta.61" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.61.tgz#cc61ce8b094fdbcec58f44f0fa39172a726c024c" resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.199.tgz#23a01c4c57f5af2197299d836e18ffe498fd8d67"
integrity sha512-T3Yx+y1sUnXAJjK1CUfsQewSxOpDca9KzKqN2H9c9RZ9UlorR9XmZg6YYW7m9a7adeihj+o3cCO9jRd8dV+nnA== integrity sha512-T3K8xoDbX6J62lhIUpclQoW/1XFt7yfI5DCoxtVWUeKaF+pG6kdsB3CPG5C/+AQVlz2jSIJmQuPf8RQFpQs+yg==
dependencies: dependencies:
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
prosemirror-view "1.26.2" prosemirror-view "^1.28.2"
tippy.js "^6.3.7" tippy.js "^6.3.7"
"@tiptap/extension-bullet-list@^2.0.0-beta.29": "@tiptap/extension-bullet-list@^2.0.0-beta.199":
version "2.0.0-beta.29" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.0-beta.29.tgz#640883e4fffc1a86c7cbd78792688e7edee5ee41" resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.0-beta.199.tgz#72df0c94c6a9a5bce97ee1e0180657f1a63ffe18"
integrity sha512-R8VB2l1ZB6VeGWx/t/04nBS5Wg3qjIDEZCpPihj2fccJOw99Lu0Ub2UJg/SfdGmeNNpBh4ZYYFv1g/XjyzlXKg== integrity sha512-gGRQRqdQqCZQstB3ztSy8yzIdm5/5IIYxhCuFNb3Z9c9p/CzyRmaNqa7XkRLrXSajp4lS0OH8RkFUJqL6U+/9w==
"@tiptap/extension-character-count@2.0.0-beta.31": "@tiptap/extension-character-count@2.0.0-beta.199":
version "2.0.0-beta.31" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-character-count/-/extension-character-count-2.0.0-beta.31.tgz#fac9ba809ddc38cf67c8a05a42d94e062a1967d2" resolved "https://registry.yarnpkg.com/@tiptap/extension-character-count/-/extension-character-count-2.0.0-beta.199.tgz#cd6b008a8e242240c30dda7ee5fe515fc39a8d78"
integrity sha512-NNA9MN1IjZe+yYQLuYVAg9RNG/3RonYrHiM5mL6vsegd+PF4uMqyZLgsM0/9dMhxh9K/pDPaCRxhuDoZC8V1wA== integrity sha512-7QEyLZeTVHRi7XaI97n+yF/R9cs7Xo1pf/cpV+wn/QSkfr5YtYvy33LCN74Vlkw9FJ45KrROEXfawRE/UgM/Cw==
dependencies: dependencies:
prosemirror-model "1.18.1" prosemirror-model "^1.18.1"
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
"@tiptap/extension-code-block@^2.0.0-beta.42": "@tiptap/extension-code-block@^2.0.0-beta.199":
version "2.0.0-beta.42" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.0.0-beta.42.tgz#2abfd92eb22399fa542aafb3b76dddfb41d87ab5" resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.0.0-beta.199.tgz#145baa37276601bce75ddad55ab4926bc78b28fc"
integrity sha512-4wzLup4mI8w9ypIceekUV/8g41cQIPn31qs1iC9u1/JuTkjMj/tA+TFUyp6IMugLxoI/P2DlTztU6/6m7n9DyQ== integrity sha512-ZfftYE1kHA2pD46hXDkeYd1vuxp3bJLS854B2yHfw1cp3JVDjMXzm4Mzg7zLfr+YV1dT/N/fUfdCg38fqEUCyA==
dependencies: dependencies:
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
"@tiptap/extension-code@^2.0.0-beta.28": "@tiptap/extension-code@^2.0.0-beta.199":
version "2.0.0-beta.28" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.0.0-beta.28.tgz#a22c0e873497ac0bbcd77e4a855322f8591f954e" resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.0.0-beta.199.tgz#ab6e2355297b3faddf6ac6e9e02a52eca38c16f6"
integrity sha512-QPJ2Gwb1+3NgcC1ZIhvVcb+FsnWWDu5VZXTKXM4mz892i9V2x48uHg5anPiUV6pcolXsW1F5VNbXIHGTUUO6CQ== integrity sha512-P1U/xYD0MLT7JU2OHb3QoW7+JiPZXizFG/gTYmAHQV/gLH87cmflI7pPnloBdTkeIF0Q/cd6sSd75V9FxR4XJA==
"@tiptap/extension-document@^2.0.0-beta.17": "@tiptap/extension-document@^2.0.0-beta.199":
version "2.0.0-beta.17" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.0-beta.17.tgz#ded4182dd860762bcf41c588f712d83908c472a3" resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.0-beta.199.tgz#4a9432ab239cc951b4d9903a90d3a7d5597d3318"
integrity sha512-L6sg0FNchbtIpQkCSjMmItVGs3/vep8Fq56WRtDc1wBSGUSmtHaxQG7F2FZLnNIUMuvzVMRD81m2vYG73WkY6A== integrity sha512-l/3k9N2O4wIMQoN/SM3aIBwOhZ2KRxQoqGJfsbAUUwBURBDiT4N2VZaNiJC/w3xCVQXIxHSIlqtm9ZBcZeiH/Q==
"@tiptap/extension-dropcursor@^2.0.0-beta.29": "@tiptap/extension-dropcursor@^2.0.0-beta.199":
version "2.0.0-beta.29" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.0-beta.29.tgz#9ccc9d82cb9f8fa28a59ffc061c4c83ee059a12c" resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.0-beta.199.tgz#93d04db4ca56614b7b180632456cb9f2098c6156"
integrity sha512-I+joyoFB8pfdXUPLMqdNO08nlB5m2lbu0VQ5dpqdi/HzgVThMZPZA1cW0X8vAUvrALs5/JFRiFoR9hrLN5R5ng== integrity sha512-RhdYm0yBJxVLECaHWsZcBIwRJUoUqZ79jvs+kUVodxHW4+IxRAgEA+lImr0GD+kk8aX5Mrk8YhWuUUeu5nzpTg==
dependencies: dependencies:
prosemirror-dropcursor "1.5.0" prosemirror-dropcursor "1.5.0"
"@tiptap/extension-floating-menu@^2.0.0-beta.56": "@tiptap/extension-floating-menu@^2.0.0-beta.199":
version "2.0.0-beta.56" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.56.tgz#c7428d9109d215bdbd9033f69782c4aadb2aabec" resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.199.tgz#f0b0acb64e9b9bfe4935a0bd6b6d1863ea2d39cd"
integrity sha512-j/evHE/6UPGkIgXny9IGcAh0IrcnQmg0b2NBYebs2mqx9xYKYoe+0jVgNdLp/0M3MRgQCzyWTyatBDBFOUR2mw== integrity sha512-ELjqnNbxW66uqg54zlP2b4EVYUWvT2WvHmeOXALzoLlNzbqUopIl3XNRsvU2Dv1W88C1UjKgnRZIkHKFE1X3CA==
dependencies: dependencies:
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
prosemirror-view "1.26.2" prosemirror-view "^1.28.2"
tippy.js "^6.3.7" tippy.js "^6.3.7"
"@tiptap/extension-gapcursor@^2.0.0-beta.39": "@tiptap/extension-gapcursor@^2.0.0-beta.199":
version "2.0.0-beta.39" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.0-beta.39.tgz#b8585d2936df7ca90446758c3af90b46d552a1fb" resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.0-beta.199.tgz#ec801e30690d1b30183b9751d64615724311be97"
integrity sha512-oCyz5WEeQXrEIoa1WXaD52yf1EwMFCXaK1cVzFgUj8lkXJ+nJj+O/Zp0Mg+9/MVR0LYu/kifqVorKNXM4AFA/g== integrity sha512-0TDpDfDyay+IbD+wJMsBJ2c0Cq0NtllUOxbi0NPjjWW94Jrvs1yqUSzX4Qp9m5MW8qP24IV6krgZBM1JyQc6ng==
dependencies: dependencies:
prosemirror-gapcursor "1.3.0" prosemirror-gapcursor "^1.3.1"
"@tiptap/extension-hard-break@^2.0.0-beta.33": "@tiptap/extension-hard-break@^2.0.0-beta.199":
version "2.0.0-beta.33" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.0-beta.33.tgz#e2f355a22aaaec6e831cf2880c52aa5b0b860573" resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.0-beta.199.tgz#6ac23806c4e7e13f15e56cc2e1db7c12248e8091"
integrity sha512-41xf0vSV9hcyTFd01ItLq/CjhjgmOFLCrO3UWN/P2E/cIxuDTyXcvjTE/KXeqRCOV3OYd9fVr0wO91hc8Ij1Yg== integrity sha512-DF2wDo/+gSYRhzGowCvZJk3/j/zYJ22BHxZpkAEmLJ69mWSIqZv3S2/brujnNmnji9c3/+JN7ppPSeVykz0b9Q==
"@tiptap/extension-heading@^2.0.0-beta.29": "@tiptap/extension-heading@^2.0.0-beta.199":
version "2.0.0-beta.29" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.0.0-beta.29.tgz#d017d216c0fd1962c266f6f61a335093f9749862" resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.0.0-beta.199.tgz#b766de44fd09843364280a03d1e180db346f1ccd"
integrity sha512-q92jYcsT5bPhvuQaB0h44Z9r+Ii22tDYo082KMVnR4+tknHT/3xx+p4JC8KHjh+/5W8Quyafqy6mS8L8VX0zsQ== integrity sha512-WGQ7ET2TBpldrD8JX37OXHXq05LU3OWItIVBs9nKGh4otZTUwPtwfOyMlFfA+IMfQif+ilwLGvUC6EHOw/LwxQ==
"@tiptap/extension-history@^2.0.0-beta.26": "@tiptap/extension-history@^2.0.0-beta.199":
version "2.0.0-beta.26" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.0-beta.26.tgz#ae4c0ee8d19b3530e72d99cb5d0f69aefcf96d04" resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.0-beta.199.tgz#ff83184b6f7e0ed9a5c6362cfbc15319a451b960"
integrity sha512-ly19uwvdmXG8Fw1KcavXIHi3Qx6JBASOR7394zghOEpW3atpY8nd/8I373rZ8eDUcGOClfaF7bCx2xvIotAAnw== integrity sha512-oZMjKHFqqZuUuf0+IG5+OoKw9DIGilG+v8cm2JK9XnxF5CxF6HIXNDWl3552wRIA+Ro7fBRJEJ//hfJzp0Uhjw==
dependencies: dependencies:
prosemirror-history "1.3.0" prosemirror-history "^1.3.0"
"@tiptap/extension-horizontal-rule@^2.0.0-beta.36": "@tiptap/extension-horizontal-rule@^2.0.0-beta.199":
version "2.0.0-beta.36" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.0-beta.36.tgz#daf8e2d0f30b210a90fdb8f015646653661cfa04" resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.0-beta.199.tgz#6037b11f5a300ce0921a483d7fbbeea6a5b58f58"
integrity sha512-o+Zp7dcn3zAQhtlhZiFB69mTHuH3ZRbGEF7Cbf1D3uX1izotni5zIZbPaFFUT4r6OmVe/vDDt/nopfcGc10ktQ== integrity sha512-ISQndGiC6Y3+Ds3OJHKa2iB7s4FkRQxn8US/Hhj4yK7DOifoykLOrgDghwLu0H0dSM8KNb9caYEtmj64vDogNg==
dependencies: dependencies:
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
"@tiptap/extension-image@2.0.0-beta.30": "@tiptap/extension-image@2.0.0-beta.199":
version "2.0.0-beta.30" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.0-beta.30.tgz#60c6cfd09bfd017a3d8b1feaf0931462ffd71a60" resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.0-beta.199.tgz#48704c12e0cfc3da06a17cd44d2d8a8b9fd33de7"
integrity sha512-VhEmgiKkZMiKR7hbpJgIlIUS/QNjSGI5ER7mKDAbuV1IB5yb6nGjZ6o3Exrr2/CaTaW5hQarBC1z2Xgdu05EGg== integrity sha512-prmFRXdJucUeWVhSprrPfcn9pAtYkzm57N8WOUlVad5ocdeR9aMz9+0u0akb5v6XhaVAQgWqwqNb0Kx8lSuqhA==
"@tiptap/extension-italic@^2.0.0-beta.28": "@tiptap/extension-italic@^2.0.0-beta.199":
version "2.0.0-beta.28" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.28.tgz#bf88ecae64c8f2f69f1f508b802c1efd7454a84e" resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.199.tgz#db24dbdd0d47fdfaa22dde8ba35e2c08b7162e82"
integrity sha512-/pKRiCfewh7nqiXRD3N4hQHfGrGNOiWPFYZfY35bSpvTms7PDb/MF7xT1CWW23hSpY31BBS+R/a66vlR/gqu7Q== integrity sha512-jaYJr5ZMxU2swK6h1XJr6Wb1LlWOWbvsX/wo59iZ9KVv1AHiKZlCMcWGThy4aoAs/CUT11pB8qbzyOO163LHZg==
"@tiptap/extension-link@2.0.0-beta.43": "@tiptap/extension-link@2.0.0-beta.199":
version "2.0.0-beta.43" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.0-beta.43.tgz#c123a2170dd50d075b9fe7fb91d86d23f778ffb0" resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.0-beta.199.tgz#57865aa0211d7d9972ed5fee9a92fedbf0083b37"
integrity sha512-AYueqfTW713KGVfWSWhVbj4ObeWudgawikm3m0uYcKSdsAz/CfEvOD2/NA0uyQzlxmYLA6Pf8HMxoKGN+O4Cmg== integrity sha512-zwXDg+zsHhn2a4rJkFd/pND4zUfJ2RCgyrkBqAL+nimSiknaSsIw4NpnZTZFWze9i3NDcc2BNngDNovoEIEukg==
dependencies: dependencies:
linkifyjs "^3.0.5" linkifyjs "^3.0.5"
prosemirror-model "1.18.1" prosemirror-model "^1.18.1"
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
"@tiptap/extension-list-item@^2.0.0-beta.23": "@tiptap/extension-list-item@^2.0.0-beta.199":
version "2.0.0-beta.23" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.0-beta.23.tgz#6d1ac7235462b0bcee196f42bb1871669480b843" resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.0-beta.199.tgz#2e667f0ea5d9314307427625345e915edf91b989"
integrity sha512-AkzvdELz3ZnrlZM0r9+ritBDOnAjXHR/8zCZhW0ZlWx4zyKPMsNG5ygivY+xr4QT65NEGRT8P8b2zOhXrMjjMQ== integrity sha512-rzcz5MJgoX1M9M9e1iruyRxcwYyYmdCXsl9gB8hhJYh4R+AW1peRmHJ3vVX5oPZXg/tXOMTv/or2x8v30c9tJw==
"@tiptap/extension-mention@2.0.0-beta.102": "@tiptap/extension-mention@2.0.0-beta.199":
version "2.0.0-beta.102" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.0.0-beta.102.tgz#a80036b0a4481efc4f69b788af3f5c76428624cc" resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.0.0-beta.199.tgz#15f021411d48809ffc4c908204e6e262b0b9160b"
integrity sha512-QTBBpWnRnoV7/ZW31HwhPvZL3HiwnlehlHSLeMioVxAQPF5WrRtlOpxK/SRu7+KuwdCb7ZA1eWW/yjbXI3oktg== integrity sha512-zNCZbU03GNPaO9Aga/3AKUQBv0EridVdIFxUY3GVb5uixoxMeXDf/OI0GqnQ2KyW7ufmD5VdlZS0mP/9QCo+DA==
dependencies: dependencies:
"@tiptap/suggestion" "^2.0.0-beta.97" prosemirror-model "^1.18.1"
prosemirror-model "1.18.1" prosemirror-state "^1.4.1"
prosemirror-state "1.4.1"
"@tiptap/extension-ordered-list@^2.0.0-beta.30": "@tiptap/extension-ordered-list@^2.0.0-beta.199":
version "2.0.0-beta.30" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.0-beta.30.tgz#1f656b664302d90272c244b2e478d7056203f2a8" resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.0-beta.199.tgz#cdc6eb58e94d095013b5182683ce1e31b8733083"
integrity sha512-GRxGQdq1u0Rp5N8TjthCqoZ//460m343A0HCN7UwfQOnX7Ipv0UJemwNkSHWrl7Pexym9vy3yPWgrn7oRRmgEw== integrity sha512-ciQhBRtNUudQyCgvQKRZ1WbV7Q9IZP82GHEsk+wScZgI0SsrGY8pnfJT7CyF8aPIjkQkccozKVTbyMrjBOqWSw==
"@tiptap/extension-paragraph@^2.0.0-beta.26": "@tiptap/extension-paragraph@^2.0.0-beta.199":
version "2.0.0-beta.26" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.0-beta.26.tgz#5199c8cedb9c076347a2e15cc67442ef7c3c3fbb" resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.0-beta.199.tgz#34213e6594a1183a77bb33ced49502bafb0a3d1c"
integrity sha512-WcYsuUa7LLfk0vi7I1dVjdMRu53B52FMMqd+UL1qPdDKVkU3DBsZVwPj+yyfQyqN8Mc/xyg9VacGaiKFLmWNDg== integrity sha512-+BoMCaxlsHqw065zTUNd+ywkvFJzNKbTY461/AlKX2dgHeaO8doXHDQK+9icOpibQvrKaMhOJmuBTgGlJlUUgw==
"@tiptap/extension-placeholder@2.0.0-beta.53": "@tiptap/extension-placeholder@2.0.0-beta.199":
version "2.0.0-beta.53" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.0.0-beta.53.tgz#df29d813044da9a0e30bf8409335e77f6857c2b2" resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.0.0-beta.199.tgz#0208c42f8b92a88e66b726353d07b652f09fd823"
integrity sha512-NGU/a+GvcJVBjFqb2vI45+rNa3Cjsq/M+R/2xg9olb1w/HBr17NKf/5WSoqcc1S2cdnmMH6rB0/mVhG7Ciur+Q== integrity sha512-Tdq0r9XQ6hcu4ASvw2Xko6h8uS/xONmMmOFiTkK/54REB3RRQpkdCtXrhFn/T4DunJVBf6FUOLTjYN3SONhuew==
dependencies: dependencies:
prosemirror-model "1.18.1" prosemirror-model "^1.18.1"
prosemirror-state "1.4.1" prosemirror-state "^1.4.1"
prosemirror-view "1.26.2" prosemirror-view "^1.28.2"
"@tiptap/extension-strike@^2.0.0-beta.29": "@tiptap/extension-strike@^2.0.0-beta.199":
version "2.0.0-beta.29" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.0-beta.29.tgz#7004d0c5d126b0517fa78efc5a333a4b8e3334bf" resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.0-beta.199.tgz#5fc6e067728009d92027e58a042f18449f2fa264"
integrity sha512-zqFuY7GfNmZ/KClt6kxQ+msGo3syqucP/Xnlihxi+/h/G+oTvEwyOIXCtDOltvxcsWH/TUsdr5vzLp0j+Mdc6Q== integrity sha512-KyN5+d9o9FGvrSiSuh81oo4+XjMDsZVY4UHc9lBY0nAzaGAkJOwkCjk40RfyO5ZJ2GdEEQ6Nh/3YqVMcJTY+rA==
"@tiptap/extension-text@^2.0.0-beta.17": "@tiptap/extension-text@^2.0.0-beta.199":
version "2.0.0-beta.17" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.0-beta.17.tgz#4fdd1bdf62c82c1af6feef91c689906a8f5b171e" resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.0-beta.199.tgz#0204f3e50622e39b6fd08b5ef72fe7d0199117f5"
integrity sha512-OyKL+pqWJEtjyd9/mrsuY1kZh2b3LWpOQDWKtd4aWR4EA0efmQG+7FPwcIeAVEh7ZoqM+/ABCnPjN6IjzIrSfg== integrity sha512-ntOqEhkBjDHrdzxvpPe4U1JB5GgE9/yyWqWdgzSL9lpSndRTJN1xQLOmyuv0qsLqOgBHn1YITHvaxPb3t8FrFw==
"@tiptap/react@2.0.0-beta.114": "@tiptap/html@2.0.0-beta.199":
version "2.0.0-beta.114" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.0.0-beta.114.tgz#fa2b3fcdf379bf7ee25388c0eddbda49249977d5" resolved "https://registry.yarnpkg.com/@tiptap/html/-/html-2.0.0-beta.199.tgz#cb33319ad00b31ebf68dcc9cbb758c0aff78403c"
integrity sha512-9JbRE+16WM6RxbBxzY74SrJtLodvjeRBnEbWxuhxVgGKxMunRj6r8oED87ODJgqLmkpofwE0KFHTPGdEXfdcKA== integrity sha512-/Ow/LOCXZrNXR6cYJMN8O4ZMm7Jl/t6L0K6MCJ1p+pKu11xtf1mI73t8WYFA5J8t7cL7eWUgDPtqlOE+DJTvHg==
dependencies: dependencies:
"@tiptap/extension-bubble-menu" "^2.0.0-beta.61" "@tiptap/core" "^2.0.0-beta.199"
"@tiptap/extension-floating-menu" "^2.0.0-beta.56" prosemirror-model "^1.18.1"
prosemirror-view "1.26.2" zeed-dom "^0.9.19"
"@tiptap/starter-kit@2.0.0-beta.191": "@tiptap/react@2.0.0-beta.199":
version "2.0.0-beta.191" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.0.0-beta.191.tgz#3f549367f6dbb8cf83f63aa0941722d91d0fd8e7" resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.0.0-beta.199.tgz#98f9fb2134fdc385648ed06ea1ec78902c0e99cf"
integrity sha512-YRrBCi9W4jiH/xLTJJOCdD7pL4Wb98Ip8qCJ94RElShDj0O1i5tT9wWlgVWoGIU+CRAds5XENRwZ97sJ+YfYyg== integrity sha512-AjBtoavcJ7WOoEXdJlrVEdEv6xuI5UFnqB88w8NlORSkWbfQ3uuOm3A0LUZ92/SsBz6NISZbsFahMy0DYgGbIA==
dependencies: dependencies:
"@tiptap/core" "^2.0.0-beta.182" "@tiptap/extension-bubble-menu" "^2.0.0-beta.199"
"@tiptap/extension-blockquote" "^2.0.0-beta.29" "@tiptap/extension-floating-menu" "^2.0.0-beta.199"
"@tiptap/extension-bold" "^2.0.0-beta.28" prosemirror-view "^1.28.2"
"@tiptap/extension-bullet-list" "^2.0.0-beta.29"
"@tiptap/extension-code" "^2.0.0-beta.28"
"@tiptap/extension-code-block" "^2.0.0-beta.42"
"@tiptap/extension-document" "^2.0.0-beta.17"
"@tiptap/extension-dropcursor" "^2.0.0-beta.29"
"@tiptap/extension-gapcursor" "^2.0.0-beta.39"
"@tiptap/extension-hard-break" "^2.0.0-beta.33"
"@tiptap/extension-heading" "^2.0.0-beta.29"
"@tiptap/extension-history" "^2.0.0-beta.26"
"@tiptap/extension-horizontal-rule" "^2.0.0-beta.36"
"@tiptap/extension-italic" "^2.0.0-beta.28"
"@tiptap/extension-list-item" "^2.0.0-beta.23"
"@tiptap/extension-ordered-list" "^2.0.0-beta.30"
"@tiptap/extension-paragraph" "^2.0.0-beta.26"
"@tiptap/extension-strike" "^2.0.0-beta.29"
"@tiptap/extension-text" "^2.0.0-beta.17"
"@tiptap/suggestion@^2.0.0-beta.97": "@tiptap/starter-kit@2.0.0-beta.199":
version "2.0.0-beta.97" version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.0-beta.97.tgz#2e3dc20deebc2c37c5d39c848e61e9c837e7188a" resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.0.0-beta.199.tgz#b0d48784abe711afc2973592467f213f86794c82"
integrity sha512-3NWG+HE7v2w97Ek6z1tUosoZKpCDH+oAtIG9XoNkK1PmlaVV/H4d6HT9uPX+Y6SeN7fSAqlcXFUGLXcDi9d+Zw== integrity sha512-ToPhccdSAixNhAujBa5VEOg+G6Nkc+JJGaUVEEGH2EV2ICzbhFhcdbAHqI6lNtCFleaX9NULJBGHXEwLR5T83Q==
dependencies: dependencies:
prosemirror-model "1.18.1" "@tiptap/core" "^2.0.0-beta.199"
prosemirror-state "1.4.1" "@tiptap/extension-blockquote" "^2.0.0-beta.199"
prosemirror-view "1.26.2" "@tiptap/extension-bold" "^2.0.0-beta.199"
"@tiptap/extension-bullet-list" "^2.0.0-beta.199"
"@tiptap/extension-code" "^2.0.0-beta.199"
"@tiptap/extension-code-block" "^2.0.0-beta.199"
"@tiptap/extension-document" "^2.0.0-beta.199"
"@tiptap/extension-dropcursor" "^2.0.0-beta.199"
"@tiptap/extension-gapcursor" "^2.0.0-beta.199"
"@tiptap/extension-hard-break" "^2.0.0-beta.199"
"@tiptap/extension-heading" "^2.0.0-beta.199"
"@tiptap/extension-history" "^2.0.0-beta.199"
"@tiptap/extension-horizontal-rule" "^2.0.0-beta.199"
"@tiptap/extension-italic" "^2.0.0-beta.199"
"@tiptap/extension-list-item" "^2.0.0-beta.199"
"@tiptap/extension-ordered-list" "^2.0.0-beta.199"
"@tiptap/extension-paragraph" "^2.0.0-beta.199"
"@tiptap/extension-strike" "^2.0.0-beta.199"
"@tiptap/extension-text" "^2.0.0-beta.199"
"@tiptap/suggestion@2.0.0-beta.199":
version "2.0.0-beta.199"
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.0-beta.199.tgz#8bf8029a25826bc41266bb9fffe8b33195bf9a40"
integrity sha512-FwkaMW0fX1Xlsv4n5GPlVkHwwMSMvIXgZ6LZnwy4Mxo+Njz7YgHw8X8YQo927lMbYsVHYhNcsk726HaxudDMEw==
dependencies:
prosemirror-model "^1.18.1"
prosemirror-state "^1.4.1"
prosemirror-view "^1.28.2"
"@tootallnate/once@2": "@tootallnate/once@2":
version "2.0.0" version "2.0.0"
@ -3490,6 +3498,11 @@
"@types/node" "*" "@types/node" "*"
form-data "^2.5.0" form-data "^2.5.0"
"@types/marked@4.0.7":
version "4.0.7"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.7.tgz#400a76809fd08c2bbd9e25f3be06ea38c8e0a1d3"
integrity sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw==
"@types/mdast@^3.0.0": "@types/mdast@^3.0.0":
version "3.0.10" version "3.0.10"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
@ -4968,9 +4981,9 @@ commander@^8.0.0, commander@^8.3.0:
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
commander@^9.4.0: commander@^9.4.0:
version "9.4.1" version "9.4.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.1.tgz#d1dd8f2ce6faf93147295c0df13c7c21141cfbdd" resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.0.tgz#bc4a40918fefe52e22450c111ecd6b7acce6f11c"
integrity sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw== integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==
commondir@^1.0.1: commondir@^1.0.1:
version "1.0.1" version "1.0.1"
@ -5846,9 +5859,9 @@ dot-prop@^5.2.0:
is-obj "^2.0.0" is-obj "^2.0.0"
dotenv@^16.0.2: dotenv@^16.0.2:
version "16.0.3" version "16.0.2"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.2.tgz#0b0f8652c016a3858ef795024508cddc4bffc5bf"
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== integrity sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==
duplexer3@^0.1.4: duplexer3@^0.1.4:
version "0.1.4" version "0.1.4"
@ -7081,9 +7094,9 @@ google-p12-pem@^3.1.3:
node-forge "^1.3.1" node-forge "^1.3.1"
google-protobuf@^3.21.0: google-protobuf@^3.21.0:
version "3.21.2" version "3.21.0"
resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.2.tgz#4580a2bea8bbb291ee579d1fefb14d6fa3070ea4" resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.0.tgz#8dfa3fca16218618d373d414d3c1139e28034d6e"
integrity sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA== integrity sha512-byR7MBTK4tZ5PZEb+u5ZTzpt4SfrTxv5682MjPlHN16XeqgZE2/8HOIWeiXe8JKnT9OVbtBGhbq8mtvkK8cd5g==
got@^9.6.0: got@^9.6.0:
version "9.6.0" version "9.6.0"
@ -8583,6 +8596,11 @@ markdown-escapes@^1.0.0:
resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
marked@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/marked/-/marked-4.1.1.tgz#2f709a4462abf65a283f2453dc1c42ab177d302e"
integrity sha512-0cNMnTcUJPxbA6uWmCmjWz4NJRe/0Xfk2NhXCUHjew9qJzFN20krFnsUe7QynwqOwa5m1fZ4UDg0ycKFVC0ccw==
match-sorter@^6.0.2: match-sorter@^6.0.2:
version "6.3.1" version "6.3.1"
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda" resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda"
@ -9895,10 +9913,10 @@ property-information@^5.0.0, property-information@^5.3.0:
dependencies: dependencies:
xtend "^4.0.0" xtend "^4.0.0"
prosemirror-commands@1.3.0: prosemirror-commands@^1.3.1:
version "1.3.0" version "1.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.3.0.tgz#361b2e2b2a347ce7453386459f97c3f549a1113b" resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.3.1.tgz#926c88801eebaa50363d4658850b41406d375a31"
integrity sha512-BwBbZ5OAScPcm0x7H8SPbqjuEJnCU2RJT9LDyOiiIl/3NbL1nJZI4SFNHwU2e/tRr2Xe7JsptpzseqvZvToLBQ== integrity sha512-XTporPgoECkOQACVw0JTe3RZGi+fls3/byqt+tXwGTkD7qLuB4KdVrJamDMJf4kfKga3uB8hZ+kUUyZ5oWpnfg==
dependencies: dependencies:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
@ -9913,17 +9931,17 @@ prosemirror-dropcursor@1.5.0:
prosemirror-transform "^1.1.0" prosemirror-transform "^1.1.0"
prosemirror-view "^1.1.0" prosemirror-view "^1.1.0"
prosemirror-gapcursor@1.3.0: prosemirror-gapcursor@^1.3.1:
version "1.3.0" version "1.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.0.tgz#e07c22ad959b86ec0c4cfc590cc5f484dd984d56" resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.1.tgz#8cfd874592e4504d63720e14ed680c7866e64554"
integrity sha512-9Tdx83xB2W4Oqchm12FtCkSizbqvi64cjs1I9TRPblqdA5TUWoVZ4ZI+t71Jh6HSEh4cDMPzx3UwfryJtKlb/w== integrity sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA==
dependencies: dependencies:
prosemirror-keymap "^1.0.0" prosemirror-keymap "^1.0.0"
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
prosemirror-view "^1.0.0" prosemirror-view "^1.0.0"
prosemirror-history@1.3.0: prosemirror-history@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.0.tgz#bf5a1ff7759aca759ddf0c722c2fa5b14fb0ddc1" resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.0.tgz#bf5a1ff7759aca759ddf0c722c2fa5b14fb0ddc1"
integrity sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA== integrity sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA==
@ -9932,7 +9950,7 @@ prosemirror-history@1.3.0:
prosemirror-transform "^1.0.0" prosemirror-transform "^1.0.0"
rope-sequence "^1.3.0" rope-sequence "^1.3.0"
prosemirror-keymap@1.2.0, prosemirror-keymap@^1.0.0: prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz#d5cc9da9b712020690a994b50b92a0e448a60bf5" resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz#d5cc9da9b712020690a994b50b92a0e448a60bf5"
integrity sha512-TdSfu+YyLDd54ufN/ZeD1VtBRYpgZnTPnnbY+4R08DDgs84KrIPEPbJL8t1Lm2dkljFx6xeBE26YWH3aIzkPKg== integrity sha512-TdSfu+YyLDd54ufN/ZeD1VtBRYpgZnTPnnbY+4R08DDgs84KrIPEPbJL8t1Lm2dkljFx6xeBE26YWH3aIzkPKg==
@ -9940,23 +9958,23 @@ prosemirror-keymap@1.2.0, prosemirror-keymap@^1.0.0:
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
w3c-keyname "^2.2.0" w3c-keyname "^2.2.0"
prosemirror-model@1.18.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0: prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1:
version "1.18.1" version "1.18.1"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd"
integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw== integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==
dependencies: dependencies:
orderedmap "^2.0.0" orderedmap "^2.0.0"
prosemirror-schema-list@1.2.0: prosemirror-schema-list@^1.2.2:
version "1.2.0" version "1.2.2"
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.2.0.tgz#1932268593a7396c0ac168cbe31f28187406ce24" resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.2.2.tgz#bafda37b72367d39accdcaf6ddf8fb654a16e8e5"
integrity sha512-8PT/9xOx1HHdC7fDNNfhQ50Z8Mzu7nKyA1KCDltSpcZVZIbB0k7KtsHrnXyuIhbLlScoymBiLZ00c5MH6wdFsA== integrity sha512-rd0pqSDp86p0MUMKG903g3I9VmElFkQpkZ2iOd3EOVg1vo5Cst51rAsoE+5IPy0LPXq64eGcCYlW1+JPNxOj2w==
dependencies: dependencies:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0" prosemirror-transform "^1.0.0"
prosemirror-state@1.4.1, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2: prosemirror-state@1.4.1, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.1.tgz#f6e26c7b6a7e11206176689eb6ebbf91870953e1" resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.1.tgz#f6e26c7b6a7e11206176689eb6ebbf91870953e1"
integrity sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg== integrity sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg==
@ -9964,14 +9982,21 @@ prosemirror-state@1.4.1, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0" prosemirror-transform "^1.0.0"
prosemirror-transform@1.6.0, prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0: prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.6.0.tgz#8162dbfaf124f9253a7ab28605a9460411a96a53" resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.6.0.tgz#8162dbfaf124f9253a7ab28605a9460411a96a53"
integrity sha512-MAp7AjsjEGEqQY0sSMufNIUuEyB1ZR9Fqlm8dTwwWwpEJRv/plsKjWXBbx52q3Ml8MtaMcd7ic14zAHVB3WaMw== integrity sha512-MAp7AjsjEGEqQY0sSMufNIUuEyB1ZR9Fqlm8dTwwWwpEJRv/plsKjWXBbx52q3Ml8MtaMcd7ic14zAHVB3WaMw==
dependencies: dependencies:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-view@1.26.2, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0: prosemirror-transform@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.7.0.tgz#a8a0768f3ee6418d26ebef435beda9d43c65e472"
integrity sha512-O4T697Cqilw06Zvc3Wm+e237R6eZtJL/xGMliCi+Uo8VL6qHk6afz1qq0zNjT3eZMuYwnP8ZS0+YxX/tfcE9TQ==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0:
version "1.26.2" version "1.26.2"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.26.2.tgz#e673894ecf26aea330b727622d561c51b41d31eb" resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.26.2.tgz#e673894ecf26aea330b727622d561c51b41d31eb"
integrity sha512-CGKw+GadkfSBEwRAJTHCEKJ4DlV6/3IhAdjpwGyZHUHtbP7jX4Ol4zmi7xa2c6GOabDlIJLYXJydoNYLX7lNeQ== integrity sha512-CGKw+GadkfSBEwRAJTHCEKJ4DlV6/3IhAdjpwGyZHUHtbP7jX4Ol4zmi7xa2c6GOabDlIJLYXJydoNYLX7lNeQ==
@ -9980,6 +10005,15 @@ prosemirror-view@1.26.2, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0:
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0" prosemirror-transform "^1.1.0"
prosemirror-view@^1.28.2:
version "1.28.3"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.28.3.tgz#51131ede4d3852380be9a5e0e4ba7662725dac1f"
integrity sha512-YnJxLRzIaCNEt3VKiy+PBxtpwsCbjrfiBKIgHJeqbKhdeP8bU2qL4ngdGmxp9K4+06cZG5bE9vipuhP+KUl+BQ==
dependencies:
prosemirror-model "^1.16.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
proto3-json-serializer@^0.1.8: proto3-json-serializer@^0.1.8:
version "0.1.9" version "0.1.9"
resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-0.1.9.tgz#705ddb41b009dd3e6fcd8123edd72926abf65a34" resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-0.1.9.tgz#705ddb41b009dd3e6fcd8123edd72926abf65a34"
@ -12648,6 +12682,13 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zeed-dom@^0.9.19:
version "0.9.26"
resolved "https://registry.yarnpkg.com/zeed-dom/-/zeed-dom-0.9.26.tgz#f0127d1024b34a1233a321bd6d0275b3ba998b30"
integrity sha512-HWjX8rA3Y/RI32zby3KIN1D+mgskce+She4K7kRyyx62OiVxJ5FnYm8vWq0YVAja3Tf2S1M0XAc6O2lRFcMgcQ==
dependencies:
css-what "^6.1.0"
zod@3.17.2: zod@3.17.2:
version "3.17.2" version "3.17.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.2.tgz#d20b32146a3b5068f8f71768b4f9a4bfe52cddb0" resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.2.tgz#d20b32146a3b5068f8f71768b4f9a4bfe52cddb0"