update tip-tap packages

This commit is contained in:
Briant Diehl
2025-07-15 11:45:04 -06:00
parent ac68c1f7e6
commit 6c82da6289
14 changed files with 585 additions and 459 deletions
+343 -251
View File
File diff suppressed because it is too large Load Diff
+15 -18
View File
@@ -71,6 +71,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.10.4",
"@essentials/one-key-map": "^1.2.0",
"@floating-ui/dom": "^1.6.0",
"@google-cloud/recaptcha-enterprise": "^5.1.1",
"@headlessui/react": "2.2",
"@hookform/resolvers": "^5.1.1",
@@ -102,19 +103,19 @@
"@tabler/icons-react": "^3.7.0",
"@tanstack/react-query": "^4.12.0",
"@tanstack/react-query-devtools": "^4.13.0",
"@tiptap/extension-color": "^2.0.3",
"@tiptap/extension-heading": "^2.0.3",
"@tiptap/extension-image": "^2.0.0-beta.209",
"@tiptap/extension-link": "^2.0.0-beta.209",
"@tiptap/extension-mention": "^2.0.0-beta.209",
"@tiptap/extension-placeholder": "^2.0.0-beta.209",
"@tiptap/extension-text-style": "^2.0.3",
"@tiptap/extension-underline": "^2.0.0-beta.209",
"@tiptap/extension-youtube": "^2.0.0-beta.209",
"@tiptap/pm": "^2.3.0",
"@tiptap/react": "^2.0.0-beta.209",
"@tiptap/starter-kit": "^2.0.0-beta.209",
"@tiptap/suggestion": "^2.0.0-beta.209",
"@tiptap/extension-color": "3.0.3",
"@tiptap/extension-heading": "3.0.3",
"@tiptap/extension-image": "3.0.3",
"@tiptap/extension-link": "3.0.3",
"@tiptap/extension-mention": "3.0.3",
"@tiptap/extension-placeholder": "3.0.3",
"@tiptap/extension-text": "3.0.3",
"@tiptap/extension-underline": "3.0.3",
"@tiptap/extension-youtube": "3.0.3",
"@tiptap/pm": "3.0.3",
"@tiptap/react": "3.0.3",
"@tiptap/starter-kit": "3.0.3",
"@tiptap/suggestion": "3.0.3",
"@trpc/client": "^10.45.0",
"@trpc/next": "^10.45.0",
"@trpc/react-query": "^10.45.0",
@@ -210,7 +211,6 @@
"stream-to-blob": "^2.0.1",
"stripe": "^11.6.0",
"superjson": "1.9.1",
"tippy.js": "^6.3.7",
"trie-memoize": "^1.2.0",
"unfurl.js": "^6.4.0",
"unified": "^11.0.5",
@@ -283,8 +283,5 @@
},
"ct3aMetadata": {
"initVersion": "6.2.1"
},
"overrides": {
"@react-aria/interactions": "3.16.0"
}
}
}
+2 -1
View File
@@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker';
import dayjs from 'dayjs';
import { capitalize, pull, range, without } from 'lodash-es';
import format from 'pg-format';
import type { DatabaseError } from 'pg-protocol/src/messages';
// import type { DatabaseError } from 'pg-protocol/src/messages';
import { clickhouse } from '~/server/clickhouse/client';
import type { BaseModelType } from '~/server/common/constants';
import { constants } from '~/server/common/constants';
@@ -51,6 +51,7 @@ import {
insertRows,
randPrependBad,
} from './utils';
import type { DatabaseError } from 'pg';
// import { fetchBlob } from '~/utils/file-utils';
// Usage: npx tsx ./scripts/local-dev/gen_seed.ts --rows=1000
@@ -8,14 +8,16 @@ import { IconAlertTriangle } from '@tabler/icons-react';
import { Color } from '@tiptap/extension-color';
import Heading from '@tiptap/extension-heading';
import Mention from '@tiptap/extension-mention';
import Placeholder from '@tiptap/extension-placeholder';
import TextStyle from '@tiptap/extension-text-style';
import { TextStyleKit } from '@tiptap/extension-text-style';
import Underline from '@tiptap/extension-underline';
import Youtube from '@tiptap/extension-youtube';
import { Placeholder } from '@tiptap/extensions';
import type { Editor, Extensions } from '@tiptap/react';
import { BubbleMenu, Extension, mergeAttributes, nodePasteRule, useEditor } from '@tiptap/react';
import { Extension, mergeAttributes, nodePasteRule, useEditor } from '@tiptap/react';
import { BubbleMenu } from '@tiptap/react/menus';
import StarterKit from '@tiptap/starter-kit';
import React, { useEffect, useImperativeHandle, useRef } from 'react';
import React, { useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import type { CSSProperties } from 'react';
import slugify from 'slugify';
import { InsertInstagramEmbedControl } from '~/components/RichTextEditor/InsertInstagramEmbedControl';
@@ -138,137 +140,151 @@ export function RichTextEditor({
const addMentions = includeControls.includes('mentions');
const addPolls = includeControls.includes('polls');
const linkExtension = withLinkValidation ? LinkWithValidation : Link;
const { uploadToCF } = useCFImageUpload();
const extensions: Extensions = [
Placeholder.configure({ placeholder }),
StarterKit.configure({
// heading: !addHeading ? false : { levels: [1, 2, 3] },
heading: false,
bulletList: !addList ? false : undefined,
orderedList: !addList ? false : undefined,
bold: !addFormatting ? false : undefined,
italic: !addFormatting ? false : undefined,
strike: !addFormatting ? false : undefined,
code: !addFormatting ? false : undefined,
blockquote: !addFormatting ? false : undefined,
codeBlock: !addFormatting ? false : undefined,
dropcursor: !addMedia ? false : undefined,
}),
...(addHeading
? [
Heading.configure({
levels: [1, 2, 3],
}).extend({
addAttributes() {
return {
...this.parent?.(),
id: { default: null },
};
},
addOptions() {
return {
...this.parent?.(),
HTMLAttributes: {
id: null,
},
};
},
renderHTML({ node }) {
const hasLevel = this.options.levels.includes(node.attrs.level);
const level = hasLevel ? node.attrs.level : this.options.levels[0];
const id = `${slugify(node.textContent.toLowerCase())}-${getRandomId()}`;
return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, { id }), 0];
},
}),
]
: []),
...(onSuperEnter
? [
Extension.create({
name: 'onSubmitShortcut',
addKeyboardShortcuts: () => ({
'Mod-Enter': () => {
onSuperEnter();
return true; // Dunno why they want a boolean here
const extensions = useMemo(() => {
const arr: Extensions = [
Placeholder.configure({ placeholder }),
StarterKit.configure({
// heading: !addHeading ? false : { levels: [1, 2, 3] },
heading: false,
bulletList: !addList ? false : undefined,
orderedList: !addList ? false : undefined,
bold: !addFormatting ? false : undefined,
italic: !addFormatting ? false : undefined,
strike: !addFormatting ? false : undefined,
code: !addFormatting ? false : undefined,
blockquote: !addFormatting ? false : undefined,
codeBlock: !addFormatting ? false : undefined,
dropcursor: !addMedia ? false : undefined,
underline: !addFormatting ? false : undefined,
link: false,
}),
];
// if (addFormatting) arr.push(Underline);
if (addColors) arr.push(TextStyleKit, Color);
if (addLink) {
const linkExtension = withLinkValidation ? LinkWithValidation : Link;
arr.push(linkExtension);
}
if (addHeading)
arr.push(
Heading.configure({
levels: [1, 2, 3],
}).extend({
addAttributes() {
return {
...this.parent?.(),
id: { default: null },
};
},
addOptions() {
return {
...this.parent?.(),
HTMLAttributes: {
id: null,
},
}),
}),
]
: []),
...(addFormatting ? [Underline] : []),
...(addColors ? [TextStyle, Color] : []),
...(addLink ? [linkExtension] : []),
...(addMedia
? [
CustomImage.configure({
// To allow links on images
inline: true,
uploadImage: uploadToCF,
onUploadStart: () => {
showNotification({
id: UPLOAD_NOTIFICATION_ID,
loading: true,
withCloseButton: false,
autoClose: false,
message: 'Uploading images...',
});
},
onUploadEnd: () => {
hideNotification(UPLOAD_NOTIFICATION_ID);
},
}),
Youtube.configure({
addPasteHandler: false,
modestBranding: false,
}).extend({
renderHTML(input) {
const { HTMLAttributes } = input;
if (!HTMLAttributes.src || !this.parent) return ['div', { 'data-youtube-video': '' }];
};
},
renderHTML({ node }) {
const hasLevel = this.options.levels.includes(node.attrs.level);
const level: string | number = hasLevel ? node.attrs.level : this.options.levels[0];
const id = `${slugify(node.textContent.toLowerCase())}-${getRandomId()}`;
return this.parent(input);
},
addPasteRules() {
return [
nodePasteRule({
find: /^(https?:\/\/)?(www\.|music\.)?(youtube\.com|youtu\.be)(?!.*\/channel\/)(?!\/@)(.+)?$/g,
type: this.type,
getAttributes: (match) => ({ src: match.input }),
}),
];
return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, { id }), 0];
},
})
);
if (onSuperEnter)
arr.push(
Extension.create({
name: 'onSubmitShortcut',
addKeyboardShortcuts: () => ({
'Mod-Enter': () => {
onSuperEnter();
return true; // Dunno why they want a boolean here
},
}),
Instagram.configure({
HTMLAttributes: { class: classes.instagramEmbed },
height: 'auto',
}),
]
: []),
...(addMentions
? [
Mention.configure({
suggestion: getSuggestions({ defaultSuggestions }),
HTMLAttributes: {
class: classes.mention,
},
renderLabel({ options, node }) {
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;
},
}),
]
: []),
...(addPolls
? [
StrawPoll.configure({
HTMLAttributes: { class: classes.strawPollEmbed },
height: 'auto',
}),
]
: []),
];
})
);
if (addMedia) {
arr.push(
CustomImage.configure({
// To allow links on images
inline: true,
uploadImage: uploadToCF,
onUploadStart: () => {
showNotification({
id: UPLOAD_NOTIFICATION_ID,
loading: true,
withCloseButton: false,
autoClose: false,
message: 'Uploading images...',
});
},
onUploadEnd: () => {
hideNotification(UPLOAD_NOTIFICATION_ID);
},
}),
Youtube.configure({
addPasteHandler: false,
modestBranding: false,
}).extend({
renderHTML(input) {
const { HTMLAttributes } = input;
if (!HTMLAttributes.src || !this.parent) return ['div', { 'data-youtube-video': '' }];
return this.parent(input);
},
addPasteRules() {
return [
nodePasteRule({
find: /^(https?:\/\/)?(www\.|music\.)?(youtube\.com|youtu\.be)(?!.*\/channel\/)(?!\/@)(.+)?$/g,
type: this.type,
getAttributes: (match) => ({ src: match.input }),
}),
];
},
}),
Instagram.configure({
HTMLAttributes: { class: classes.instagramEmbed },
height: 'auto',
})
);
}
if (addMentions)
arr.push(
Mention.configure({
suggestion: getSuggestions({ defaultSuggestions }),
HTMLAttributes: {
class: classes.mention,
},
renderLabel({ options, node }) {
const label = node.attrs.label ?? node.attrs.id;
return `${options.suggestion.char ?? ''}${typeof label === 'string' ? label : ''}`;
},
})
);
if (addPolls)
arr.push(
StrawPoll.configure({
HTMLAttributes: { class: classes.strawPollEmbed },
height: 'auto',
})
);
return arr;
}, [
addList,
addFormatting,
addColors,
addLink,
withLinkValidation,
addHeading,
onSuperEnter,
addMedia,
addMentions,
addPolls,
]);
const editor = useEditor({
extensions,
+33 -23
View File
@@ -1,7 +1,6 @@
import { ReactRenderer } from '@tiptap/react';
import type { SuggestionOptions } from '@tiptap/suggestion';
import type { Instance as TippyInstance } from 'tippy.js';
import tippy from 'tippy.js';
import { computePosition, flip, shift } from '@floating-ui/dom';
import { posToDOMRect, ReactRenderer } from '@tiptap/react';
import type { MentionListRef } from '~/components/RichTextEditor/MentionList';
import { MentionList } from '~/components/RichTextEditor/MentionList';
@@ -17,8 +16,8 @@ export function getSuggestions(options?: Options) {
.filter((suggestion) => suggestion.label.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5),
render: () => {
let component: ReactRenderer<MentionListRef> | undefined;
let popup: TippyInstance[] | undefined;
let component: ReactRenderer<MentionListRef>;
// let popup: TippyInstance[] | undefined;
return {
onStart: (props) => {
@@ -27,40 +26,33 @@ export function getSuggestions(options?: Options) {
editor: props.editor,
});
if (!props.clientRect) return;
(component.element as HTMLElement).style.position = 'absolute';
popup = tippy('body', {
getReferenceClientRect: props.clientRect as () => DOMRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
});
document.body.appendChild(component.element);
updatePosition(props.editor, component.element);
},
onUpdate(props) {
component?.updateProps(props);
component.updateProps(props);
if (!props.clientRect) return;
popup?.[0].setProps({
getReferenceClientRect: props.clientRect as () => DOMRect,
});
updatePosition(props.editor, component.element);
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup?.[0].hide();
component.destroy();
return true;
}
if (!component?.ref) return false;
return component?.ref.onKeyDown(props);
return component.ref?.onKeyDown(props) ?? true;
},
onExit() {
popup?.[0].destroy();
component?.destroy();
component.element.remove();
component.destroy();
},
};
},
@@ -68,3 +60,21 @@ export function getSuggestions(options?: Options) {
return suggestion;
}
const updatePosition = (editor: any, element: any) => {
const virtualElement = {
getBoundingClientRect: () =>
posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
};
computePosition(virtualElement, element, {
placement: 'bottom-start',
strategy: 'absolute',
middleware: [shift(), flip()],
}).then(({ x, y, strategy }) => {
element.style.width = 'max-content';
element.style.position = strategy;
element.style.left = `${x}px`;
element.style.top = `${y}px`;
});
};
+2 -1
View File
@@ -8,9 +8,10 @@ import {
IconBrandReddit,
IconMail,
} from '@tabler/icons-react';
import type { BuiltInProviderType } from 'next-auth/providers';
import classes from './Social.module.css';
import clsx from 'clsx';
import type { BuiltInProviderType } from 'next-auth/providers/index';
type SocialProps = Partial<
Record<
+5 -1
View File
@@ -15,11 +15,15 @@ type CustomImageOptions = ImageOptions & {
export const CustomImage = ImageExtension.extend<CustomImageOptions>({
draggable: true,
addOptions() {
return {
inline: false,
allowBase64: false,
HTMLAttributes: {},
...this.parent?.(),
...constants.richTextEditor,
};
} as CustomImageOptions;
},
addProseMirrorPlugins() {
return [
+16 -10
View File
@@ -214,16 +214,22 @@ function ArticleDetailsPage({ id }: InferGetServerSidePropsType<typeof getServer
title={`${article.title} | Civitai`}
description={truncate(removeTags(article.content), { length: 150 })}
images={article?.coverImage}
links={[
{
href: `${env.NEXT_PUBLIC_BASE_URL}/articles/${article.id}/${slugit(article.title)}`,
rel: 'canonical',
},
{
href: `${env.NEXT_PUBLIC_BASE_URL}/articles/${article.id}`,
rel: 'alternate',
},
]}
links={
env.NEXT_PUBLIC_BASE_URL
? [
{
href: `${env.NEXT_PUBLIC_BASE_URL}/articles/${article.id}/${slugit(
article.title
)}`,
rel: 'canonical',
},
{
href: `${env.NEXT_PUBLIC_BASE_URL}/articles/${article.id}`,
rel: 'alternate',
},
]
: []
}
deIndex={!article?.publishedAt || article?.availability === Availability.Unsearchable}
/>
<SensitiveShield contentNsfwLevel={article.nsfwLevel}>
+1 -1
View File
@@ -10,7 +10,7 @@ import {
Title,
} from '@mantine/core';
import { IconCircleCheck, IconExclamationMark, IconHome } from '@tabler/icons-react';
import type { BuiltInProviderType } from 'next-auth/providers';
import type { BuiltInProviderType } from 'next-auth/providers/index';
import { getProviders, signIn } from 'next-auth/react';
import { AlertWithIcon } from '~/components/AlertWithIcon/AlertWithIcon';
import { NextLink as Link } from '~/components/NextLink/NextLink';
+2 -2
View File
@@ -64,7 +64,7 @@ export const getServerSideProps = createServerSideProps({
},
};
if (!features?.canBuyBuzz) {
if (!features?.canBuyBuzz && env.NEXT_PUBLIC_SERVER_DOMAIN_GREEN) {
return {
redirect: {
destination: `https://${env.NEXT_PUBLIC_SERVER_DOMAIN_GREEN}/user/membership?sync-account=blue`,
@@ -120,7 +120,7 @@ export default function UserMembership() {
title: 'Whoops!',
error:
error instanceof Error
? error.message
? error
: { message: 'An error occurred while refreshing your subscription' },
reason:
error instanceof Error
+3 -1
View File
@@ -3,7 +3,7 @@ import { CdpClient } from '@coinbase/cdp-sdk';
import { createPublicClient, erc20Abi, http, parseUnits } from 'viem';
import { baseSepolia } from 'viem/chains';
import { checkOnrampStatus, getOnrampUrl } from './onramp';
import type { SendUserOperationReturnType } from '@coinbase/cdp-sdk/_types/actions/evm/sendUserOperation';
import { dbWrite } from '~/server/db/client';
import { CryptoTransactionStatus } from '~/shared/utils/prisma/enums';
import { env } from '~/env/server';
@@ -21,6 +21,8 @@ export const cdp = new CdpClient({
walletSecret: env.CDP_WALLET_SECRET,
});
type SendUserOperationReturnType = AsyncReturnType<typeof cdp.evm.sendUserOperation>;
// Initialize the public client
// This is used to wait for the transaction receipt
const publicClient = createPublicClient({
+11 -6
View File
@@ -66,7 +66,7 @@ export type CsamReportProps = Omit<CsamReport, 'details' | 'images'> & {
images: CsamReportImage[];
};
const baseDir = `${isProd ? env.DIRNAME : process.cwd()}/csam`;
const baseDir = `${isProd && env.DIRNAME ? env.DIRNAME : process.cwd()}/csam`;
export async function getImageResources({ ids }: GetImageResourcesOutput) {
return await dbRead.imageResourceHelper.findMany({
@@ -328,11 +328,15 @@ async function constructReportPayload({
if (reportedUser) {
if (minorDepiction === 'non-real')
additionalInfo.push(
`${reportedUser.username} (${reportedUser.id}), appears to have used the following models' image/video generation and/or editing capabilities to produce sexual content depicting non-real minors.`
`${reportedUser.username as string} (${
reportedUser.id
}), appears to have used the following models' image/video generation and/or editing capabilities to produce sexual content depicting non-real minors.`
);
else if (minorDepiction === 'real')
additionalInfo.push(
`${reportedUser.username} (${reportedUser.id}), appears to have used the following models' image/video editing capabilities to modify images of real minors for the apparent purpose of sexualizing them.`
`${reportedUser.username as string} (${
reportedUser.id
}), appears to have used the following models' image/video editing capabilities to modify images of real minors for the apparent purpose of sexualizing them.`
);
additionalInfo.push(section2);
@@ -491,8 +495,9 @@ export async function processCsamReport(report: CsamReportProps) {
const additionalInfo: string[] = [];
if (modelId) additionalInfo.push(`model id: ${modelId}`);
if (modelVersionId) additionalInfo.push(`model version id: ${modelVersionId}`);
if (prompt) additionalInfo.push(`prompt: ${prompt}`);
if (negativePrompt) additionalInfo.push(`negativePrompt: ${negativePrompt}`);
if (prompt && typeof prompt === 'string') additionalInfo.push(`prompt: ${prompt}`);
if (negativePrompt && typeof negativePrompt === 'string')
additionalInfo.push(`negativePrompt: ${negativePrompt}`);
const fileAnnotations =
imageReportInfo?.fileAnnotations ?? ({} as Ncmec.FileAnnotationsSchema);
@@ -793,7 +798,7 @@ export async function archiveCsamDataForReport(data: CsamReportProps) {
? image.name.substring(0, image.name.lastIndexOf('.'))
: image.url;
const name = imageName.length ? imageName : image.url;
const filename = `${name}.${blob.type.split('/').pop()}`;
const filename = `${name}.${blob.type.split('/').pop() as string}`;
archive.append(buffer, { name: filename });
});
+1 -1
View File
@@ -14,7 +14,7 @@ const parsers = {
};
export async function ExifParser(file: File | string) {
let tags: ExifReader.Tags = {};
let tags: ExifReader.Tags = {} as ExifReader.Tags;
try {
tags = await ExifReader.load(file, { includeUnknown: true });
} catch (e) {
+6 -14
View File
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2018",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
// "types": ["offscreencanvas"],
"allowJs": true,
"skipLibCheck": true,
@@ -15,7 +11,7 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
@@ -23,19 +19,15 @@
"noUncheckedIndexedAccess": false, // TODO swap to true
"baseUrl": "src",
"paths": {
"~/*": [
"./*"
]
"~/*": ["./*"]
},
"typeRoots": [
"./types"
],
"typeRoots": ["./types"],
"noErrorTruncation": true,
"plugins": [
{
"name": "next"
}
],
]
},
"include": [
// "next-env.d.ts",
@@ -43,7 +35,7 @@
// "**/*.tsx",
// "**/*.cjs",
// "**/*.mjs",
"scripts/local-dev/*.ts",
"scripts/local-dev/*.ts",
"src",
"tests",
".next/types/**/*.ts"