Compare commits

..

2 Commits

Author SHA1 Message Date
L-Sun
ac76e5b949 chore: enable webview debugging for Android 2025-10-02 21:48:29 +08:00
L-Sun
0bc1005b96 fix(core): infinitied loop 2025-09-26 15:48:24 +08:00
375 changed files with 863 additions and 43919 deletions

View File

@@ -148,11 +148,6 @@
"description": "Whether allow new registrations.\n@default true",
"default": true
},
"allowSignupForOauth": {
"type": "boolean",
"description": "Whether allow new registrations via configured oauth.\n@default true",
"default": true
},
"requireEmailDomainVerification": {
"type": "boolean",
"description": "Whether require email domain record verification before accessing restricted resources.\n@default false",
@@ -195,11 +190,6 @@
"type": "object",
"description": "Configuration for mailer module",
"properties": {
"SMTP.name": {
"type": "string",
"description": "Name of the email server (e.g. your domain name)\n@default \"AFFiNE Server\"\n@environment `MAILER_SERVERNAME`",
"default": "AFFiNE Server"
},
"SMTP.host": {
"type": "string",
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"\n@environment `MAILER_HOST`",
@@ -235,11 +225,6 @@
"description": "The emails from these domains are always sent using the fallback SMTP server.\n@default []",
"default": []
},
"fallbackSMTP.name": {
"type": "string",
"description": "Name of the fallback email server (e.g. your domain name)\n@default \"AFFiNE Server\"",
"default": "AFFiNE Server"
},
"fallbackSMTP.host": {
"type": "string",
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"",
@@ -684,12 +669,12 @@
},
"scenarios": {
"type": "object",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"gemini-2.5-flash\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4@20250514\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"claude-sonnet-4@20250514\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4@20250514\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"default": {
"override_enabled": false,
"scenarios": {
"audio_transcribing": "gemini-2.5-flash",
"chat": "gemini-2.5-flash",
"chat": "claude-sonnet-4@20250514",
"embedding": "gemini-embedding-001",
"image": "gpt-image-1",
"rerank": "gpt-4.1",
@@ -1108,33 +1093,18 @@
},
"apiKey": {
"type": "string",
"description": "[Deprecated] Stripe API key. Use payment.stripe.apiKey instead.\n@default \"\"\n@environment `STRIPE_API_KEY`",
"description": "Stripe API key to enable payment service.\n@default \"\"\n@environment `STRIPE_API_KEY`",
"default": ""
},
"webhookKey": {
"type": "string",
"description": "[Deprecated] Stripe webhook key. Use payment.stripe.webhookKey instead.\n@default \"\"\n@environment `STRIPE_WEBHOOK_KEY`",
"description": "Stripe webhook key to enable payment service.\n@default \"\"\n@environment `STRIPE_WEBHOOK_KEY`",
"default": ""
},
"stripe": {
"type": "object",
"description": "Stripe sdk options and credentials\n@default {\"apiKey\":\"\",\"webhookKey\":\"\"}\n@link https://docs.stripe.com/api",
"default": {
"apiKey": "",
"webhookKey": ""
}
},
"revenuecat": {
"type": "object",
"description": "RevenueCat integration configs\n@default {\"enabled\":false,\"apiKey\":\"\",\"projectId\":\"\",\"webhookAuth\":\"\",\"environment\":\"production\",\"productMap\":{}}\n@link https://www.revenuecat.com/docs/",
"default": {
"enabled": false,
"apiKey": "",
"projectId": "",
"webhookAuth": "",
"environment": "production",
"productMap": {}
}
"description": "Stripe sdk options\n@default {}\n@link https://docs.stripe.com/api",
"default": {}
}
}
},

View File

@@ -81,7 +81,7 @@ Star us, and you will receive all release notifications from GitHub without any
**Multimodal AI partner ready to kick in any work**
- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, [AFFiNE AI](https://affine.pro/ai) pushes your creativity to the edge of your imagination, just like [Canvas AI](https://affine.pro/blog/best-canvas-ai) to generate mind map for brainstorming.
- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, [AFFiNE AI](https://affine.pro/ai) pushes your creativity to the edge of your imagination,just like [Canvas AI](https://affine.pro/blog/best-canvas-ai) to generate mind map for brainstorming.
**Local-first & Real-time collaborative**

View File

@@ -17,7 +17,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -10,7 +10,6 @@
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-inline-preset": "workspace:*",
@@ -19,7 +18,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@emoji-mart/data": "^1.2.1",

View File

@@ -1,54 +1,18 @@
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import { createLitPortal } from '@blocksuite/affine-components/portal';
import { DefaultInlineManagerExtension } from '@blocksuite/affine-inline-preset';
import { type CalloutBlockModel, DefaultTheme } from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { type CalloutBlockModel } from '@blocksuite/affine-model';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
DocModeProvider,
type IconData,
IconPickerServiceIdentifier,
IconType,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import * as icons from '@blocksuite/icons/lit';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import type { BlockComponent } from '@blocksuite/std';
import { type Signal, signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
import { flip, offset } from '@floating-ui/dom';
import { css, html } from 'lit';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
// Copy of renderUniLit and UniLit from affine-data-view
export const renderUniLit = <Props, Expose extends NonNullable<unknown>>(
uni: UniComponent<Props, Expose> | undefined,
props?: Props,
options?: {
ref?: Signal<Expose | undefined>;
style?: Readonly<StyleInfo>;
class?: string;
}
): TemplateResult => {
return html` <uni-lit
.uni="${uni}"
.props="${props}"
.ref="${options?.ref}"
style=${options?.style ? styleMap(options?.style) : ''}
></uni-lit>`;
};
const getIcon = (icon?: IconData) => {
console.log(icon);
if (!icon) {
return '💡';
}
if (icon.type === IconType.Emoji) {
return icon.unicode;
}
if (icon.type === IconType.AffineIcon) {
return (
icons as Record<string, (props: { style: string }) => TemplateResult>
)[`${icon.name}Icon`]?.({ style: `color:${icon.color}` });
}
return '💡';
};
import { query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockModel> {
static override styles = css`
:host {
@@ -58,12 +22,14 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
.affine-callout-block-container {
display: flex;
align-items: flex-start;
padding: 5px 10px;
border-radius: 8px;
background-color: ${unsafeCSSVarV2('block/callout/background/grey')};
}
.affine-callout-emoji-container {
margin-right: 10px;
margin-top: 14px;
user-select: none;
font-size: 1.2em;
width: 24px;
@@ -71,15 +37,6 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
margin-bottom: 10px;
flex-shrink: 0;
position: relative;
}
.affine-callout-emoji {
display: flex;
align-items: center;
justify-content: center;
}
.affine-callout-emoji:hover {
cursor: pointer;
@@ -91,92 +48,37 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
min-width: 0;
padding-left: 10px;
}
.icon-picker-container {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
background: white;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
width: 300px;
height: 400px;
}
`;
private readonly showIconPicker$ = signal(false);
private _closeEmojiMenu() {
this.showIconPicker$.value = false;
}
private _toggleIconPicker() {
this.showIconPicker$.value = !this.showIconPicker$.value;
}
private _renderIconPicker() {
if (!this.showIconPicker$.value) {
return html``;
private _emojiMenuAbortController: AbortController | null = null;
private readonly _toggleEmojiMenu = () => {
if (this._emojiMenuAbortController) {
this._emojiMenuAbortController.abort();
}
this._emojiMenuAbortController = new AbortController();
// Get IconPickerService from the framework
const iconPickerService = this.std.getOptional(IconPickerServiceIdentifier);
if (!iconPickerService) {
console.warn('IconPickerService not found');
return html``;
}
const theme = this.std.get(ThemeProvider).theme$.value;
// Get the uni-component from the service
const iconPickerComponent = iconPickerService.iconPickerComponent;
// Create props for the icon picker
const props = {
onSelect: (iconData?: IconData) => {
this.model.props.icon$.value = iconData;
this._closeEmojiMenu(); // Close the picker after selection
},
onClose: () => {
this._closeEmojiMenu();
},
};
return html`
<div
@click=${(e: MouseEvent) => {
e.stopPropagation();
createLitPortal({
template: html`<affine-emoji-menu
.theme=${theme}
.onEmojiSelect=${(data: any) => {
this.model.props.emoji = data.native;
}}
class="icon-picker-container"
>
${renderUniLit(iconPickerComponent, props)}
</div>
`;
}
private readonly _handleBlockClick = (event: MouseEvent) => {
// Check if the click target is emoji related element
const target = event.target as HTMLElement;
if (
target.closest('.affine-callout-emoji-container') ||
target.classList.contains('affine-callout-emoji')
) {
return;
}
// Only handle clicks when there are no children
if (this.model.children.length > 0) {
return;
}
// Prevent event bubbling
event.stopPropagation();
// Create a new paragraph block
const paragraphId = this.store.addBlock('affine:paragraph', {}, this.model);
// Focus the new paragraph
focusTextModel(this.std, paragraphId);
></affine-emoji-menu>`,
portalStyles: {
zIndex: 'var(--affine-z-index-popover)',
},
container: this.host,
computePosition: {
referenceElement: this._emojiButton,
placement: 'bottom-start',
middleware: [flip(), offset(4)],
autoUpdate: { animationFrame: true },
},
abortController: this._emojiMenuAbortController,
closeOnClickAway: true,
});
};
get attributeRenderer() {
@@ -195,6 +97,9 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
return this.std.get(DefaultInlineManagerExtension.identifier);
}
@query('.affine-callout-emoji')
private accessor _emojiButton!: HTMLElement;
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
@@ -205,32 +110,18 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
}
override renderBlock() {
const icon = this.model.props.icon$.value;
const background = this.model.props.background$.value;
const themeProvider = this.std.get(ThemeProvider);
const theme = themeProvider.theme$.value;
const backgroundColor = themeProvider.generateColorProperty(
background || DefaultTheme.NoteBackgroundColorMap.White,
DefaultTheme.NoteBackgroundColorMap.White,
theme
);
const emoji = this.model.props.emoji$.value;
return html`
<div
class="affine-callout-block-container"
@click=${this._handleBlockClick}
style=${styleMap({
backgroundColor: backgroundColor,
})}
>
<div class="affine-callout-block-container">
<div
@click=${this._toggleIconPicker}
@click=${this._toggleEmojiMenu}
contenteditable="false"
class="affine-callout-emoji-container"
style=${styleMap({
display: emoji.length === 0 ? 'none' : undefined,
})}
>
<span class="affine-callout-emoji">${getIcon(icon)}</span>
${this._renderIconPicker()}
<span class="affine-callout-emoji">${emoji}</span>
</div>
<div class="affine-callout-children">
${this.renderChildren(this.model)}

View File

@@ -1,7 +1,4 @@
import {
CalloutBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { CalloutBlockModel } from '@blocksuite/affine-model';
import { matchModels } from '@blocksuite/affine-shared/utils';
import {
BlockSelection,
@@ -9,46 +6,13 @@ import {
TextSelection,
} from '@blocksuite/std';
import { calloutToParagraphCommand } from './commands/callout-to-paragraph.js';
import { splitCalloutCommand } from './commands/split-callout.js';
export const CalloutKeymapExtension = KeymapExtension(std => {
return {
Enter: ctx => {
const text = std.selection.find(TextSelection);
if (!text) return false;
const currentBlock = std.store.getBlock(text.from.blockId);
if (!currentBlock) return false;
// Check if current block is a callout block
let calloutBlock = currentBlock;
if (!matchModels(currentBlock.model, [CalloutBlockModel])) {
// If not, check if the parent is a callout block
const parent = std.store.getParent(currentBlock.model);
if (!parent || !matchModels(parent, [CalloutBlockModel])) {
return false;
}
const parentBlock = std.store.getBlock(parent.id);
if (!parentBlock) return false;
calloutBlock = parentBlock;
}
ctx.get('keyboardState').raw.preventDefault();
std.command
.chain()
.pipe(splitCalloutCommand, {
blockId: calloutBlock.model.id,
inlineIndex: text.from.index,
currentBlockId: text.from.blockId,
})
.run();
return true;
},
Backspace: ctx => {
const text = std.selection.find(TextSelection);
if (text && text.isCollapsed() && text.from.index === 0) {
const event = ctx.get('defaultState').event;
event.preventDefault();
const block = std.store.getBlock(text.from.blockId);
if (!block) return false;
@@ -56,22 +20,6 @@ export const CalloutKeymapExtension = KeymapExtension(std => {
if (!parent) return false;
if (!matchModels(parent, [CalloutBlockModel])) return false;
// Check if current block is a paragraph inside callout
if (matchModels(block.model, [ParagraphBlockModel])) {
event.preventDefault();
std.command
.chain()
.pipe(calloutToParagraphCommand, {
id: block.model.id,
})
.run();
return true;
}
// Fallback to selecting the callout block
event.preventDefault();
std.selection.setGroup('note', [
std.selection.create(BlockSelection, {
blockId: parent.id,

View File

@@ -1,86 +0,0 @@
import {
CalloutBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { Command } from '@blocksuite/std';
import { BlockSelection } from '@blocksuite/std';
import { Text } from '@blocksuite/store';
export const calloutToParagraphCommand: Command<
{
id: string;
stopCapturing?: boolean;
},
{
success: boolean;
}
> = (ctx, next) => {
const { id, stopCapturing = true } = ctx;
const std = ctx.std;
const doc = std.store;
const model = doc.getBlock(id)?.model;
if (!model || !matchModels(model, [ParagraphBlockModel])) return false;
const parent = doc.getParent(model);
if (!parent || !matchModels(parent, [CalloutBlockModel])) return false;
if (stopCapturing) std.store.captureSync();
// Get current block index in callout
const currentIndex = parent.children.indexOf(model);
const hasText = model.text && model.text.length > 0;
// Find previous paragraph block in callout
let previousBlock = null;
for (let i = currentIndex - 1; i >= 0; i--) {
const sibling = parent.children[i];
if (matchModels(sibling, [ParagraphBlockModel])) {
previousBlock = sibling;
break;
}
}
if (previousBlock && hasText) {
// Clone current text content before any operations to prevent data loss
const currentText = model.text || new Text();
// Get previous block text and merge index
const previousText = previousBlock.text || new Text();
const mergeIndex = previousText.length;
// Apply each delta from cloned current text to previous block to preserve formatting
previousText.join(currentText);
// Remove current block after text has been merged
doc.deleteBlock(model, {
deleteChildren: false,
});
// Focus at merge point in previous block
focusTextModel(std, previousBlock.id, mergeIndex);
} else if (previousBlock && !hasText) {
// Move cursor to end of previous block
doc.deleteBlock(model, {
deleteChildren: false,
});
const previousText = previousBlock.text || new Text();
focusTextModel(std, previousBlock.id, previousText.length);
} else {
// No previous block, select the entire callout
doc.deleteBlock(model, {
deleteChildren: false,
});
std.selection.setGroup('note', [
std.selection.create(BlockSelection, {
blockId: parent.id,
}),
]);
}
return next({ success: true });
};

View File

@@ -1,85 +0,0 @@
import {
CalloutBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { Command, EditorHost } from '@blocksuite/std';
export const splitCalloutCommand: Command<{
blockId: string;
inlineIndex: number;
currentBlockId: string;
}> = (ctx, next) => {
const { blockId, inlineIndex, currentBlockId, std } = ctx;
const host = std.host as EditorHost;
const doc = host.store;
const calloutModel = doc.getBlock(blockId)?.model;
if (!calloutModel || !matchModels(calloutModel, [CalloutBlockModel])) {
console.error(`block ${blockId} is not a callout block`);
return;
}
const currentModel = doc.getBlock(currentBlockId)?.model;
if (!currentModel) {
console.error(`current block ${currentBlockId} not found`);
return;
}
doc.captureSync();
if (matchModels(currentModel, [ParagraphBlockModel])) {
// User is in a paragraph within the callout's children
const afterText = currentModel.props.text.split(inlineIndex);
// Update the current paragraph's text to keep only the part before cursor
doc.transact(() => {
currentModel.props.text.delete(
inlineIndex,
currentModel.props.text.length - inlineIndex
);
});
// Create a new paragraph block after the current one
const parent = doc.getParent(currentModel);
if (parent) {
const currentIndex = parent.children.indexOf(currentModel);
const newParagraphId = doc.addBlock(
'affine:paragraph',
{
text: afterText,
},
parent,
currentIndex + 1
);
if (newParagraphId) {
host.updateComplete
.then(() => {
focusTextModel(std, newParagraphId);
})
.catch(console.error);
}
}
} else {
// If current block is not a paragraph, create a new paragraph in callout
const newParagraphId = doc.addBlock(
'affine:paragraph',
{
text: new Text(),
},
calloutModel
);
if (newParagraphId) {
host.updateComplete
.then(() => {
focusTextModel(std, newParagraphId);
})
.catch(console.error);
}
}
next();
};

View File

@@ -1,12 +1,24 @@
import { CalloutBlockModel } from '@blocksuite/affine-model';
import { focusBlockEnd } from '@blocksuite/affine-shared/commands';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
import {
findAncestorModel,
isInsideBlockByFlavour,
matchModels,
} from '@blocksuite/affine-shared/utils';
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { FontIcon } from '@blocksuite/icons/lit';
import { calloutTooltip } from './tooltips';
export const calloutSlashMenuConfig: SlashMenuConfig = {
disableWhen: ({ model }) => {
return (
findAncestorModel(model, ancestor =>
matchModels(ancestor, [CalloutBlockModel])
) !== null
);
},
items: [
{
name: 'Callout',

View File

@@ -1,124 +0,0 @@
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
import { CalloutBlockModel, DefaultTheme } from '@blocksuite/affine-model';
import {
type ToolbarAction,
type ToolbarActionGroup,
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
import { PaletteIcon } from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
const colors = [
'default',
'red',
'orange',
'yellow',
'green',
'teal',
'blue',
'purple',
'grey',
] as const;
const backgroundColorAction = {
id: 'background-color',
label: 'Background Color',
tooltip: 'Change background color',
icon: PaletteIcon(),
run() {
// This will be handled by the content function
},
content(ctx) {
const model = ctx.getCurrentModelByType(CalloutBlockModel);
if (!model) return null;
const updateBackground = (color: string) => {
// Map text highlight colors to note background colors
const colorMap: Record<
string,
keyof typeof DefaultTheme.NoteBackgroundColorMap | null
> = {
default: null,
red: 'Red',
orange: 'Orange',
yellow: 'Yellow',
green: 'Green',
teal: 'Green', // Map teal to green as it's not available in NoteBackgroundColorMap
blue: 'Blue',
purple: 'Purple',
grey: 'White', // Map grey to white as it's the closest available
};
const mappedColor = colorMap[color];
const backgroundValue = mappedColor
? DefaultTheme.NoteBackgroundColorMap[mappedColor]
: null;
ctx.store.updateBlock(model, { background: backgroundValue });
};
return html`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="background"
.tooltip=${'Background Color'}
>
${PaletteIcon()} ${EditorChevronDown}
</editor-icon-button>
`}
>
<div data-size="large" data-orientation="vertical">
<div class="highlight-heading">Background</div>
${repeat(colors, color => {
const isDefault = color === 'default';
const value = isDefault
? null
: `var(--affine-text-highlight-${color})`;
const displayName = `${color} Background`;
return html`
<editor-menu-action
data-testid="background-${color}"
@click=${() => updateBackground(color)}
>
<affine-text-duotone-icon
style=${styleMap({
'--color': 'var(--affine-text-primary-color)',
'--background': value ?? 'transparent',
})}
></affine-text-duotone-icon>
<span class="label capitalize">${displayName}</span>
</editor-menu-action>
`;
})}
</div>
</editor-menu-button>
`;
},
} satisfies ToolbarAction;
const builtinToolbarConfig = {
actions: [
{
id: 'style',
actions: [backgroundColorAction],
} satisfies ToolbarActionGroup<ToolbarAction>,
],
} as const satisfies ToolbarModuleConfig;
export const createBuiltinToolbarConfigExtension = (
flavour: string
): ExtensionType[] => {
return [
ToolbarModuleExtension({
id: BlockFlavourIdentifier(flavour),
config: builtinToolbarConfig,
}),
];
};

View File

@@ -1,11 +1,14 @@
import { CalloutBlockComponent } from './callout-block';
import { EmojiMenu } from './emoji-menu';
export function effects() {
customElements.define('affine-callout', CalloutBlockComponent);
customElements.define('affine-emoji-menu', EmojiMenu);
}
declare global {
interface HTMLElementTagNameMap {
'affine-callout': CalloutBlockComponent;
'affine-emoji-menu': EmojiMenu;
}
}

View File

@@ -0,0 +1,34 @@
import { WithDisposable } from '@blocksuite/global/lit';
import data from '@emoji-mart/data';
import { Picker } from 'emoji-mart';
import { html, LitElement, type PropertyValues } from 'lit';
import { property, query } from 'lit/decorators.js';
export class EmojiMenu extends WithDisposable(LitElement) {
override firstUpdated(props: PropertyValues) {
const result = super.firstUpdated(props);
const picker = new Picker({
data,
onEmojiSelect: this.onEmojiSelect,
autoFocus: true,
theme: this.theme,
});
this.emojiMenu.append(picker as unknown as Node);
return result;
}
@property({ attribute: false })
accessor onEmojiSelect: (data: any) => void = () => {};
@property({ attribute: false })
accessor theme: 'light' | 'dark' = 'light';
@query('.affine-emoji-menu')
accessor emojiMenu!: HTMLElement;
override render() {
return html`<div class="affine-emoji-menu"></div>`;
}
}

View File

@@ -8,7 +8,6 @@ import { literal } from 'lit/static-html.js';
import { CalloutKeymapExtension } from './callout-keymap';
import { calloutSlashMenuConfig } from './configs/slash-menu';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { effects } from './effects';
export class CalloutViewExtension extends ViewExtensionProvider {
@@ -26,7 +25,6 @@ export class CalloutViewExtension extends ViewExtensionProvider {
BlockViewExtension('affine:callout', literal`affine-callout`),
CalloutKeymapExtension,
SlashMenuConfigExtension('affine:callout', calloutSlashMenuConfig),
...createBuiltinToolbarConfigExtension('affine:callout'),
]);
}
}

View File

@@ -22,7 +22,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -21,7 +21,7 @@
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@emotion/css": "^11.13.5",

View File

@@ -20,7 +20,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -20,7 +20,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -20,7 +20,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/affine-widget-frame-title": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -22,7 +22,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -38,7 +38,7 @@
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -20,7 +20,7 @@
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@emotion/css": "^11.13.5",

View File

@@ -13,7 +13,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@blocksuite/sync": "workspace:*",

View File

@@ -193,7 +193,6 @@ export const menuButtonItems = {
(config: {
name: string;
label?: () => TemplateResult;
info?: TemplateResult;
prefix?: TemplateResult;
postfix?: TemplateResult;
isSelected?: boolean;
@@ -212,7 +211,7 @@ export const menuButtonItems = {
return html`
${config.prefix}
<div class="affine-menu-action-text">
${config.label?.() ?? config.name} ${config.info}
${config.label?.() ?? config.name}
</div>
${config.postfix ?? (config.isSelected ? DoneIcon() : undefined)}
`;

View File

@@ -13,7 +13,7 @@
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@emotion/css": "^11.13.5",

View File

@@ -16,7 +16,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -19,7 +19,6 @@ const DOC_BLOCK_CHILD_PADDING = 24;
export class DocTitle extends WithDisposable(ShadowlessElement) {
static override styles = css`
.doc-icon-container,
.doc-title-container {
box-sizing: border-box;
font-family: var(--affine-font-family);
@@ -50,7 +49,6 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
/* Extra small devices (phones, 640px and down) */
@container viewport (width <= 640px) {
.doc-icon-container,
.doc-title-container {
padding-left: ${DOC_BLOCK_CHILD_PADDING}px;
padding-right: ${DOC_BLOCK_CHILD_PADDING}px;

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -21,7 +21,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -25,7 +25,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -21,7 +21,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -16,7 +16,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -17,7 +17,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -16,7 +16,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -22,7 +22,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -1,4 +1,3 @@
import type { IconData } from '@blocksuite/affine-shared/services';
import {
BlockModel,
BlockSchemaExtension,
@@ -6,22 +5,18 @@ import {
type Text,
} from '@blocksuite/store';
import type { Color } from '../../themes/index.js';
import { DefaultTheme } from '../../themes/index.js';
import type { BlockMeta } from '../../utils/types';
export type CalloutProps = {
icon?: IconData;
emoji: string;
text: Text;
background: Color;
} & BlockMeta;
export const CalloutBlockSchema = defineBlockSchema({
flavour: 'affine:callout',
props: (internal): CalloutProps => ({
icon: undefined,
emoji: '😀',
text: internal.Text(),
background: DefaultTheme.NoteBackgroundColorMap.White,
'meta:createdAt': undefined,
'meta:updatedAt': undefined,
'meta:createdBy': undefined,

View File

@@ -14,7 +14,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -12,7 +12,7 @@
"dependencies": {
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -1 +0,0 @@
export * from './icon-picker-service/index.js';

View File

@@ -1,37 +0,0 @@
import type { UniComponent } from '@blocksuite/affine-shared/types';
import { createIdentifier } from '@blocksuite/global/di';
import type { TemplateResult } from 'lit';
export enum IconType {
Emoji = 'emoji',
AffineIcon = 'affine-icon',
Blob = 'blob',
}
export type IconData =
| {
type: IconType.Emoji;
unicode: string;
}
| {
type: IconType.AffineIcon;
name: string;
color: string;
}
| {
type: IconType.Blob;
blob: Blob;
};
export interface IconPickerOptions {
onSelect?: (icon: IconData) => void;
onClose?: () => void;
currentIcon?: IconData;
}
export interface IconPickerService {
iconPickerComponent: UniComponent<{ onSelect?: (data?: IconData) => void }>;
renderIconPicker(options: IconPickerOptions): TemplateResult;
}
export const IconPickerServiceIdentifier =
createIdentifier<IconPickerService>('IconPickerService');

View File

@@ -13,7 +13,6 @@ export * from './feature-flag-service';
export * from './file-size-limit-service';
export * from './font-loader';
export * from './generate-url-service';
export * from './icon-picker-service';
export * from './link-preview-service';
export * from './native-clipboard-service';
export * from './notification-service';

View File

@@ -21,7 +21,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -17,7 +17,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.16",

View File

@@ -17,7 +17,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",

View File

@@ -21,7 +21,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",

View File

@@ -17,7 +17,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",

View File

@@ -16,7 +16,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",

View File

@@ -33,7 +33,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-selected-rect": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -16,7 +16,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.16",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",

View File

@@ -14,7 +14,7 @@
"license": "MIT",
"dependencies": {
"@blocksuite/affine": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.3",
"@lottiefiles/dotlottie-wc": "^0.5.0",

View File

@@ -14,7 +14,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/integration-test": "workspace:*",
"@preact/signals-core": "^1.8.0",
"@shoelace-style/shoelace": "2.20.1",

View File

@@ -53,7 +53,7 @@
"@affine-tools/cli": "workspace:*",
"@capacitor/cli": "^7.0.0",
"@eslint/js": "^9.16.0",
"@faker-js/faker": "^10.0.0",
"@faker-js/faker": "^9.3.0",
"@istanbuljs/schema": "^0.1.3",
"@magic-works/i18n-codegen": "^0.6.1",
"@playwright/test": "=1.52.0",

View File

@@ -1,12 +0,0 @@
-- CreateEnum
CREATE TYPE "Provider" AS ENUM ('stripe', 'revenuecat');
-- CreateEnum
CREATE TYPE "IapStore" AS ENUM ('app_store', 'play_store');
-- AlterTable
ALTER TABLE "subscriptions" ADD COLUMN "iap_store" "IapStore",
ADD COLUMN "provider" "Provider" NOT NULL DEFAULT 'stripe',
ADD COLUMN "rc_entitlement" VARCHAR,
ADD COLUMN "rc_external_ref" VARCHAR,
ADD COLUMN "rc_product_id" VARCHAR;

View File

@@ -127,8 +127,7 @@
"@affine-tools/cli": "workspace:*",
"@affine-tools/utils": "workspace:*",
"@affine/graphql": "workspace:*",
"@faker-js/faker": "^10.0.0",
"@nestjs/swagger": "^11.2.0",
"@faker-js/faker": "^9.6.0",
"@nestjs/testing": "patch:@nestjs/testing@npm%3A10.4.15#~/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^5.0.1",

View File

@@ -749,16 +749,6 @@ model Subscription {
stripeSubscriptionId String? @unique @map("stripe_subscription_id")
// stripe schedule id
stripeScheduleId String? @map("stripe_schedule_id") @db.VarChar
// subscription provider: stripe or revenuecat
provider Provider @default(stripe)
// iap store for revenuecat subscriptions
iapStore IapStore? @map("iap_store")
// revenuecat entitlement name like "Pro" / "AI"
rcEntitlement String? @map("rc_entitlement") @db.VarChar
// revenuecat product id like "app.affine.pro.Annual"
rcProductId String? @map("rc_product_id") @db.VarChar
// external reference, appstore originalTransactionId or play purchaseToken
rcExternalRef String? @map("rc_external_ref") @db.VarChar
// subscription.status, active/past_due/canceled/unpaid...
status String @db.VarChar(20)
// subscription.current_period_start
@@ -780,16 +770,6 @@ model Subscription {
@@map("subscriptions")
}
enum Provider {
stripe
revenuecat
}
enum IapStore {
app_store
play_store
}
model Invoice {
stripeInvoiceId String @id @map("stripe_invoice_id")
targetId String @map("target_id") @db.VarChar

View File

@@ -444,37 +444,3 @@ Generated by [AVA](https://avajs.dev).
},
],
}
## should resolve model correctly based on subscription status and prompt config
> should honor requested pro model
'gemini-2.5-pro'
> should fallback to default model
'gemini-2.5-flash'
> should fallback to default model when requesting pro model during trialing
'gemini-2.5-flash'
> should honor requested non-pro model during trialing
'gemini-2.5-flash'
> should pick default model when no requested model during trialing
'gemini-2.5-flash'
> should pick default model when no requested model during active
'gemini-2.5-flash'
> should honor requested pro model during active
'claude-sonnet-4@20250514'
> should fallback to default model when requesting non-optional model during active
'gemini-2.5-flash'

View File

@@ -60,9 +60,6 @@ import {
import { AutoRegisteredWorkflowExecutor } from '../plugins/copilot/workflow/executor/utils';
import { WorkflowGraphList } from '../plugins/copilot/workflow/graph';
import { CopilotWorkspaceService } from '../plugins/copilot/workspace';
import { PaymentModule } from '../plugins/payment';
import { SubscriptionService } from '../plugins/payment/service';
import { SubscriptionStatus } from '../plugins/payment/types';
import { MockCopilotProvider } from './mocks';
import { createTestingModule, TestingModule } from './utils';
import { WorkflowTestCases } from './utils/copilot';
@@ -85,7 +82,6 @@ type Context = {
storage: CopilotStorage;
workflow: CopilotWorkflowService;
cronJobs: CopilotCronJobs;
subscription: SubscriptionService;
executors: {
image: CopilotChatImageExecutor;
text: CopilotChatTextExecutor;
@@ -120,7 +116,6 @@ test.before(async t => {
},
},
}),
PaymentModule,
QuotaModule,
StorageModule,
CopilotModule,
@@ -129,13 +124,6 @@ test.before(async t => {
// use real JobQueue for testing
builder.overrideProvider(JobQueue).useClass(JobQueue);
builder.overrideProvider(OpenAIProvider).useClass(MockCopilotProvider);
builder.overrideProvider(SubscriptionService).useClass(
class {
select() {
return { getSubscription: async () => undefined };
}
}
);
},
});
@@ -157,7 +145,6 @@ test.before(async t => {
const transcript = module.get(CopilotTranscriptionService);
const workspaceEmbedding = module.get(CopilotWorkspaceService);
const cronJobs = module.get(CopilotCronJobs);
const subscription = module.get(SubscriptionService);
t.context.module = module;
t.context.auth = auth;
@@ -176,7 +163,6 @@ test.before(async t => {
t.context.transcript = transcript;
t.context.workspaceEmbedding = workspaceEmbedding;
t.context.cronJobs = cronJobs;
t.context.subscription = subscription;
t.context.executors = {
image: module.get(CopilotChatImageExecutor),
@@ -2061,90 +2047,3 @@ test('should handle copilot cron jobs correctly', async t => {
toBeGenerateStub.restore();
jobAddStub.restore();
});
test('should resolve model correctly based on subscription status and prompt config', async t => {
const { db, session, subscription } = t.context;
// 1) Seed a prompt that has optionalModels and proModels in config
const promptName = 'resolve-model-test';
await db.aiPrompt.create({
data: {
name: promptName,
model: 'gemini-2.5-flash',
messages: {
create: [{ idx: 0, role: 'system', content: 'test' }],
},
config: { proModels: ['gemini-2.5-pro', 'claude-sonnet-4@20250514'] },
optionalModels: [
'gemini-2.5-flash',
'gemini-2.5-pro',
'claude-sonnet-4@20250514',
],
},
});
// 2) Create a chat session with this prompt
const sessionId = await session.create({
promptName,
docId: 'test',
workspaceId: 'test',
userId,
pinned: false,
});
const s = (await session.get(sessionId))!;
const mockStatus = (status?: SubscriptionStatus) => {
Sinon.restore();
Sinon.stub(subscription, 'select').callsFake(() => ({
// @ts-expect-error mock
getSubscription: async () => (status ? { status } : null),
}));
};
// payment disabled -> allow requested if in optional; pro not blocked
{
const model1 = await s.resolveModel(false, 'gemini-2.5-pro');
t.snapshot(model1, 'should honor requested pro model');
const model2 = await s.resolveModel(false, 'not-in-optional');
t.snapshot(model2, 'should fallback to default model');
}
// payment enabled + trialing: requesting pro should fallback to default
{
mockStatus(SubscriptionStatus.Trialing);
const model3 = await s.resolveModel(true, 'gemini-2.5-pro');
t.snapshot(
model3,
'should fallback to default model when requesting pro model during trialing'
);
const model4 = await s.resolveModel(true, 'gemini-2.5-flash');
t.snapshot(model4, 'should honor requested non-pro model during trialing');
const model5 = await s.resolveModel(true);
t.snapshot(
model5,
'should pick default model when no requested model during trialing'
);
}
// payment enabled + active: without requested -> default model; requested pro should be honored
{
mockStatus(SubscriptionStatus.Active);
const model6 = await s.resolveModel(true);
t.snapshot(
model6,
'should pick default model when no requested model during active'
);
const model7 = await s.resolveModel(true, 'claude-sonnet-4@20250514');
t.snapshot(model7, 'should honor requested pro model during active');
const model8 = await s.resolveModel(true, 'not-in-optional');
t.snapshot(
model8,
'should fallback to default model when requesting non-optional model during active'
);
}
});

View File

@@ -1,253 +0,0 @@
# Snapshot report for `src/__tests__/payment/revenuecat.spec.ts`
The actual snapshot is saved in `revenuecat.spec.ts.snap`.
Generated by [AVA](https://avajs.dev).
## should resolve product mapping consistently (whitelist, override, unknown)
> should map product for whitelist/override/unknown
{
override: {
customMonthly: {
plan: 'pro',
recurring: 'monthly',
},
},
unknown: null,
whitelist: {
aiAnnual: {
plan: 'ai',
recurring: 'yearly',
},
proAnnual: {
plan: 'pro',
recurring: 'yearly',
},
proMonthly: {
plan: 'pro',
recurring: 'monthly',
},
},
}
## should standardize RC subscriber response and upsert subscription with observability fields
> should standardize payload and have events
{
activatedCount: 1,
canceledCount: 0,
dbObservability: {
iapStore: 'app_store',
provider: 'revenuecat',
rcEntitlement: 'Pro',
rcExternalRef: 'orig-tx-1',
rcProductId: 'app.affine.pro.Annual',
},
lastActivated: {
plan: 'pro',
recurring: 'yearly',
},
subscriberCount: 1,
}
## should process expiration/refund by deleting subscription and emitting canceled
> should process expiration/refund and emit canceled
{
activatedCount: 0,
canceledCount: 1,
finalDBCount: 0,
lastCanceled: {
plan: 'pro',
recurring: 'yearly',
},
subscriberCount: 1,
}
## should enqueue per-user reconciliation jobs for existing RC active/trialing/past_due subscriptions
> should enqueue per-user RC reconciliation jobs (deduplicated by userId)
{
queued: [
{
name: 'nightly.revenuecat.syncUser',
opts: {
attempts: 3,
backoff: {
delay: 60000,
type: 'exponential',
},
jobId: 'nightly-rc-sync-u1',
},
payload: {
userId: 'u1',
},
},
{
name: 'nightly.revenuecat.syncUser',
opts: {
attempts: 3,
backoff: {
delay: 60000,
type: 'exponential',
},
jobId: 'nightly-rc-sync-u2',
},
payload: {
userId: 'u2',
},
},
],
uniqueJobCount: 2,
}
## should activate subscriptions via webhook for whitelisted products across stores (iOS/Android)
> should activate subscriptions via webhook for whitelisted products across stores (iOS/Android)
{
results: [
{
activatedCount: 1,
name: 'Pro monthly on iOS',
rec: {
iapStore: 'app_store',
plan: 'pro',
provider: 'revenuecat',
rcEntitlement: 'Pro',
rcExternalRef: 'orig-ios-1',
rcProductId: 'app.affine.pro.Monthly',
recurring: 'monthly',
status: 'active',
},
},
{
activatedCount: 1,
name: 'AI annual on Android',
rec: {
iapStore: 'play_store',
plan: 'ai',
provider: 'revenuecat',
rcEntitlement: 'AI',
rcExternalRef: 'token-android-1',
rcProductId: 'app.affine.pro.ai.Annual',
recurring: 'yearly',
status: 'active',
},
},
],
}
## should keep active and advance period dates when a trialing subscription renews
> should keep active after trial renewal
{
activatedCount: 2,
canceledCount: 0,
status: 'active',
}
## should remove or cancel the record and revoke entitlement when a trialing subscription expires
> should remove record
{
canceledCount: 1,
finalDBCount: 0,
}
## should set canceledAt and keep active until expiration when will_renew is false (cancellation before period end)
> should keep active until period end when will_renew is false
{
activatedCount: 1,
canceledCount: 0,
hasCanceledAt: true,
status: 'active',
}
## should retain record as past_due (inactive but not expired) and NOT emit canceled event
> should retain past_due record and NOT emit canceled event
{
canceledCount: 0,
status: 'past_due',
}
## should skip RC upsert when Stripe active already exists for same plan
> should skip RC upsert when Stripe active already exists
{
activatedCount: 0,
hasRCRecord: false,
}
## should reconcile and fix missing or out-of-order states for revenuecat Active/Trialing/PastDue records
> should reconcile and fix missing or out-of-order states for revenuecat records
{
activatedCount: 1,
canceledCount: 0,
subscriberCount: 1,
}
## should treat refund as early expiration and revoke immediately
> should delete record and emit canceled on refund
{
canceledCount: 1,
finalDBCount: 0,
}
## should ignore non-whitelisted productId and not write to DB
> should ignore non-whitelisted productId and not write to DB
{
activatedCount: 0,
canceledCount: 0,
dbCount: 0,
}
## should map via entitlement+duration when productId not whitelisted (P1M/P1Y only)
> should map via entitlement+duration fallback and ignore unsupported durations
{
aiViaFallback: {
plan: 'ai',
provider: 'revenuecat',
recurring: 'yearly',
},
eventsCounts: {
afterFirst: {
a: 1,
c: 0,
},
afterSecond: {
a: 2,
c: 0,
},
afterThird: {
a: 2,
c: 0,
},
},
proViaFallback: {
plan: 'pro',
provider: 'revenuecat',
recurring: 'monthly',
},
totalCount: 2,
}

View File

@@ -1,929 +0,0 @@
import { PrismaClient, User } from '@prisma/client';
import ava, { TestFn } from 'ava';
import { omit } from 'lodash-es';
import Sinon from 'sinon';
import {
EventBus,
ManagedByAppStoreOrPlay,
SubscriptionAlreadyExists,
} from '../../base';
import { ConfigModule } from '../../base/config';
import { FeatureService } from '../../core/features';
import { Models } from '../../models';
import { PaymentModule } from '../../plugins/payment';
import { SubscriptionCronJobs } from '../../plugins/payment/cron';
import { UserSubscriptionManager } from '../../plugins/payment/manager';
import {
RcEvent,
resolveProductMapping,
RevenueCatService,
RevenueCatWebhookController,
RevenueCatWebhookHandler,
type Subscription,
} from '../../plugins/payment/revenuecat';
import { SubscriptionService } from '../../plugins/payment/service';
import {
SubscriptionPlan,
SubscriptionRecurring,
} from '../../plugins/payment/types';
import { createTestingApp, TestingApp } from '../utils';
type Ctx = {
module: TestingApp;
db: PrismaClient;
models: Models;
event: Sinon.SinonStubbedInstance<EventBus>;
service: SubscriptionService;
rc: RevenueCatService;
webhook: RevenueCatWebhookHandler;
controller: RevenueCatWebhookController;
mockSub: (subs: Subscription[]) => Sinon.SinonStub;
mockSubSeq: (sequences: Subscription[][]) => Sinon.SinonStub;
triggerWebhook: (
userId: string,
event: Omit<RcEvent, 'app_id' | 'environment'>
) => Promise<void>;
collectEvents: () => {
activatedCount: number;
canceledCount: number;
events: Record<string, any[]>;
};
};
const test = ava as TestFn<Ctx>;
let user: User;
test.beforeEach(async t => {
const app = await createTestingApp({
imports: [
ConfigModule.override({
payment: {
revenuecat: {
enabled: true,
webhookAuth: '42',
},
},
}),
PaymentModule,
],
tapModule: m => {
m.overrideProvider(FeatureService).useValue(
Sinon.createStubInstance(FeatureService)
);
m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus));
},
});
const db = app.get(PrismaClient);
const models = app.get(Models);
const event = app.get(EventBus) as Sinon.SinonStubbedInstance<EventBus>;
const service = app.get(SubscriptionService);
const rc = app.get(RevenueCatService);
const webhook = app.get(RevenueCatWebhookHandler);
const controller = app.get(RevenueCatWebhookController);
t.context.module = app;
t.context.db = db;
t.context.models = models;
t.context.event = event;
t.context.service = service;
t.context.rc = rc;
t.context.webhook = webhook;
t.context.controller = controller;
t.context.mockSub = subs => Sinon.stub(rc, 'getSubscriptions').resolves(subs);
t.context.mockSubSeq = sequences => {
const stub = Sinon.stub(rc, 'getSubscriptions');
sequences.forEach((seq, idx) => {
if (idx === 0) stub.onFirstCall().resolves(seq);
else if (idx === 1) stub.onSecondCall().resolves(seq);
else stub.onCall(idx).resolves(seq);
});
return stub;
};
t.context.triggerWebhook = async (appUserId, event) => {
await webhook.onWebhook({
appUserId,
event: {
...event,
app_id: 'app.affine.pro',
environment: 'SANDBOX',
} as RcEvent,
});
};
t.context.collectEvents = () => {
const events = event.emit.getCalls().reduce(
(acc, c) => {
const [key, value] = c.args;
acc[key] = acc[key] || [];
acc[key].push(value);
return acc;
},
{} as { [key: string]: any[] }
);
const activatedCount = events['user.subscription.activated']?.length || 0;
const canceledCount = events['user.subscription.canceled']?.length || 0;
return { activatedCount, canceledCount, events };
};
});
test.beforeEach(async t => {
await t.context.module.initTestingDB();
user = await t.context.models.user.create({
email: 'test@affine.pro',
});
});
test.afterEach.always(async t => {
Sinon.reset();
await t.context.module.close();
});
test('should resolve product mapping consistently (whitelist, override, unknown)', t => {
const override = {
'custom.sku.monthly': { plan: 'pro', recurring: 'monthly' },
} as Record<string, { plan: string; recurring: string }>;
const actual = {
whitelist: {
proMonthly: resolveProductMapping({
productId: 'app.affine.pro.Monthly',
}),
proAnnual: resolveProductMapping({ productId: 'app.affine.pro.Annual' }),
aiAnnual: resolveProductMapping({
productId: 'app.affine.pro.ai.Annual',
}),
},
override: {
customMonthly: resolveProductMapping(
{ productId: 'custom.sku.monthly' },
override
),
},
unknown: resolveProductMapping({ productId: 'unknown.sku' }),
};
t.snapshot(actual, 'should map product for whitelist/override/unknown');
});
test('should standardize RC subscriber response and upsert subscription with observability fields', async t => {
const { webhook, collectEvents, mockSub } = t.context;
const subscriber = mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-01-01T00:00:00.000Z'),
expirationDate: new Date('2026-01-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: true,
duration: null,
},
]);
await webhook.onWebhook({
appUserId: user.id,
event: {
id: 'evt_1',
environment: 'PRODUCTION',
app_id: 'app.affine.pro',
type: 'INITIAL_PURCHASE',
store: 'app_store',
original_transaction_id: 'orig-tx-1',
},
});
const { activatedCount, canceledCount, events } = collectEvents();
const record = await t.context.db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
select: {
provider: true,
iapStore: true,
rcEntitlement: true,
rcProductId: true,
rcExternalRef: true,
},
});
t.snapshot(
{
subscriberCount: subscriber.getCalls()?.length || 0,
activatedCount,
canceledCount,
lastActivated: omit(
events['user.subscription.activated']?.slice(-1)?.[0],
'userId'
),
dbObservability: record,
},
'should standardize payload and have events'
);
});
test('should process expiration/refund by deleting subscription and emitting canceled', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
await db.subscription.create({
data: {
targetId: user.id,
plan: 'pro',
status: 'active',
provider: 'revenuecat',
recurring: 'annual',
start: new Date('2025-01-01T00:00:00.000Z'),
},
});
const subscriber = mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: false,
latestPurchaseDate: new Date('2024-01-01T00:00:00.000Z'),
expirationDate: new Date('2024-02-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: false,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_2',
type: 'EXPIRATION',
store: 'app_store',
original_transaction_id: 'orig-tx-2',
});
const finalDBCount = await db.subscription.count({
where: { targetId: user.id, plan: 'pro' },
});
const { activatedCount, canceledCount, events } = collectEvents();
t.snapshot(
{
finalDBCount,
subscriberCount: subscriber.getCalls()?.length || 0,
activatedCount,
canceledCount,
lastCanceled: omit(
events['user.subscription.canceled']?.slice(-1)?.[0],
'userId'
),
},
'should process expiration/refund and emit canceled'
);
});
test('should enqueue per-user reconciliation jobs for existing RC active/trialing/past_due subscriptions', async t => {
const { module, db } = t.context;
const cron = module.get(SubscriptionCronJobs);
const common = { provider: 'revenuecat', start: new Date() } as const;
await db.subscription.createMany({
data: [
{
targetId: 'u1',
plan: 'pro',
status: 'active',
recurring: 'monthly',
...common,
},
{
targetId: 'u2',
plan: 'ai',
status: 'trialing',
recurring: 'annual',
...common,
},
{
targetId: 'u1',
plan: 'ai',
status: 'past_due',
recurring: 'monthly',
...common,
},
],
});
await cron.reconcileRevenueCatSubscriptions();
const calls = module.queue.add.getCalls().map(c => ({
name: c.args[0],
payload: c.args[1],
opts: c.args[2],
}));
t.snapshot(
{
queued: calls,
uniqueJobCount: calls.filter(
c => c.name === 'nightly.revenuecat.syncUser'
).length,
},
'should enqueue per-user RC reconciliation jobs (deduplicated by userId)'
);
});
test('should activate subscriptions via webhook for whitelisted products across stores (iOS/Android)', async t => {
const { db, event, collectEvents, mockSubSeq, triggerWebhook } = t.context;
const scenarios = [
{
name: 'Pro monthly on iOS',
stub: [
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-01-10T00:00:00.000Z'),
expirationDate: new Date('2025-02-10T00:00:00.000Z'),
productId: 'app.affine.pro.Monthly',
store: 'app_store' as const,
willRenew: true,
duration: null,
},
],
event: {
id: 'evt_ios_1',
type: 'INITIAL_PURCHASE',
store: 'app_store',
original_transaction_id: 'orig-ios-1',
},
expectedPlan: 'pro' as const,
},
{
name: 'AI annual on Android',
stub: [
{
identifier: 'AI',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-03-01T00:00:00.000Z'),
expirationDate: new Date('2026-03-01T00:00:00.000Z'),
productId: 'app.affine.pro.ai.Annual',
store: 'play_store' as const,
willRenew: true,
duration: null,
},
],
event: {
id: 'evt_android_1',
type: 'INITIAL_PURCHASE',
store: 'play_store',
purchase_token: 'token-android-1',
},
expectedPlan: 'ai' as const,
},
];
const results: any[] = [];
mockSubSeq(scenarios.map(s => s.stub));
for (const s of scenarios) {
// reset event history between scenarios for clean counts
event.emit.resetHistory?.();
await triggerWebhook(user.id, s.event);
const rec = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: s.expectedPlan } },
select: {
plan: true,
recurring: true,
status: true,
provider: true,
iapStore: true,
rcEntitlement: true,
rcProductId: true,
rcExternalRef: true,
},
});
const { activatedCount } = collectEvents();
results.push({ name: s.name, rec, activatedCount });
}
t.snapshot(
{ results },
'should activate subscriptions via webhook for whitelisted products across stores (iOS/Android)'
);
});
test('should keep active and advance period dates when a trialing subscription renews', async t => {
const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context;
mockSubSeq([
[
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-04-01T00:00:00.000Z'),
expirationDate: new Date('2025-04-08T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: true,
duration: null,
},
],
[
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-04-08T00:00:00.000Z'),
expirationDate: new Date('2026-04-08T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: true,
duration: null,
},
],
]);
await triggerWebhook(user.id, {
id: 'evt_trial',
type: 'INITIAL_PURCHASE',
period_type: 'trial',
store: 'app_store',
});
await triggerWebhook(user.id, {
id: 'evt_renew',
type: 'RENEWAL',
store: 'app_store',
});
const rec = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
select: { status: true, start: true, end: true },
});
const { activatedCount, canceledCount } = collectEvents();
t.snapshot(
{ status: rec?.status, activatedCount, canceledCount },
'should keep active after trial renewal'
);
});
test('should remove or cancel the record and revoke entitlement when a trialing subscription expires', async t => {
const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context;
mockSubSeq([
[
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-04-01T00:00:00.000Z'),
expirationDate: new Date('2025-04-08T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: false,
duration: null,
},
],
[
{
identifier: 'Pro',
isTrial: false,
isActive: false,
latestPurchaseDate: new Date('2025-04-01T00:00:00.000Z'),
expirationDate: new Date('2024-01-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: false,
duration: null,
},
],
]);
await triggerWebhook(user.id, {
id: 'evt_trial2',
type: 'INITIAL_PURCHASE',
period_type: 'trial',
store: 'app_store',
});
await triggerWebhook(user.id, {
id: 'evt_expire_trial',
type: 'EXPIRATION',
store: 'app_store',
});
const finalDBCount = await db.subscription.count({
where: { targetId: user.id, plan: 'pro' },
});
const { canceledCount } = collectEvents();
t.snapshot({ finalDBCount, canceledCount }, 'should remove record');
});
test('should set canceledAt and keep active until expiration when will_renew is false (cancellation before period end)', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-05-01T00:00:00.000Z'),
expirationDate: new Date('2025-06-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: false,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_cancel_before_end',
type: 'CANCELLATION',
store: 'app_store',
});
const rec = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
select: { status: true, canceledAt: true },
});
const { activatedCount, canceledCount } = collectEvents();
t.snapshot(
{
status: rec?.status,
hasCanceledAt: !!rec?.canceledAt,
activatedCount,
canceledCount,
},
'should keep active until period end when will_renew is false'
);
});
test('should retain record as past_due (inactive but not expired) and NOT emit canceled event', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: false,
latestPurchaseDate: new Date('2025-05-01T00:00:00.000Z'),
expirationDate: new Date('2999-01-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: true,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_pastdue',
type: 'BILLING_ISSUE',
store: 'app_store',
});
const rec = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
select: { status: true },
});
const { canceledCount } = collectEvents();
t.snapshot(
{ status: rec?.status, canceledCount },
'should retain past_due record and NOT emit canceled event'
);
});
test('should block checkout when an existing subscription of the same plan is active', async t => {
const { module, db } = t.context;
const manager = module.get(UserSubscriptionManager);
{
await db.subscription.create({
data: {
targetId: user.id,
plan: 'pro',
status: 'active',
provider: 'revenuecat',
recurring: 'monthly',
start: new Date('2025-01-01T00:00:00.000Z'),
},
});
await t.throwsAsync(
manager.checkout(
{
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
variant: null,
},
{
successCallbackLink: '/',
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
},
{ user: { id: user.id, email: user.email } }
),
{ instanceOf: ManagedByAppStoreOrPlay }
);
}
{
await db.subscription.update({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
data: { provider: 'stripe' },
});
await t.throwsAsync(
() =>
manager.checkout(
{
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
variant: null,
},
{
successCallbackLink: '/',
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
},
{ user: { id: user.id, email: user.email } }
),
{ instanceOf: SubscriptionAlreadyExists }
);
}
});
test('should skip RC upsert when Stripe active already exists for same plan', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
await db.subscription.create({
data: {
targetId: user.id,
plan: 'pro',
status: 'active',
provider: 'stripe',
recurring: 'monthly',
start: new Date('2025-01-01T00:00:00.000Z'),
},
});
mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-06-01T00:00:00.000Z'),
expirationDate: new Date('2025-07-01T00:00:00.000Z'),
productId: 'app.affine.pro.Monthly',
store: 'app_store',
willRenew: true,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_conflict',
type: 'INITIAL_PURCHASE',
store: 'app_store',
});
const rcRec = await db.subscription.findFirst({
where: { targetId: user.id, plan: 'pro', provider: 'revenuecat' },
});
const { activatedCount } = collectEvents();
t.snapshot(
{ hasRCRecord: !!rcRec, activatedCount },
'should skip RC upsert when Stripe active already exists'
);
});
test('should block read-write ops on revenuecat-managed record (cancel/resume/updateRecurring)', async t => {
const { db, service } = t.context;
await db.subscription.create({
data: {
targetId: user.id,
plan: 'pro',
status: 'active',
provider: 'revenuecat',
recurring: 'monthly',
start: new Date(),
},
});
// local helper used multiple times within this test
const expectManaged = async (fn: () => Promise<any>) =>
t.throwsAsync(() => fn(), { instanceOf: ManagedByAppStoreOrPlay });
await expectManaged(() =>
service.cancelSubscription({ plan: SubscriptionPlan.Pro, userId: user.id })
);
await expectManaged(() =>
service.resumeSubscription({ plan: SubscriptionPlan.Pro, userId: user.id })
);
await expectManaged(() =>
service.updateSubscriptionRecurring(
{ plan: SubscriptionPlan.Pro, userId: user.id },
SubscriptionRecurring.Yearly
)
);
});
test('should reconcile and fix missing or out-of-order states for revenuecat Active/Trialing/PastDue records', async t => {
const { webhook, collectEvents, mockSub } = t.context;
const subscriber = mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-03-01T00:00:00.000Z'),
expirationDate: new Date('2026-03-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'play_store',
willRenew: true,
duration: null,
},
]);
await webhook.syncAppUser(user.id);
const { activatedCount, canceledCount } = collectEvents();
const subscriberCount = subscriber.getCalls()?.length || 0;
t.snapshot(
{ subscriberCount, activatedCount, canceledCount },
'should reconcile and fix missing or out-of-order states for revenuecat records'
);
});
test('should treat refund as early expiration and revoke immediately', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
await db.subscription.create({
data: {
targetId: user.id,
plan: 'pro',
status: 'active',
provider: 'revenuecat',
recurring: 'monthly',
start: new Date('2025-01-01T00:00:00.000Z'),
},
});
mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: false,
latestPurchaseDate: new Date('2025-01-01T00:00:00.000Z'),
expirationDate: new Date('2025-01-15T00:00:00.000Z'),
productId: 'app.affine.pro.Monthly',
store: 'app_store',
willRenew: false,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_refund',
type: 'CANCELLATION',
store: 'app_store',
});
const count = await db.subscription.count({
where: { targetId: user.id, plan: 'pro' },
});
const { canceledCount } = collectEvents();
t.snapshot(
{ finalDBCount: count, canceledCount },
'should delete record and emit canceled on refund'
);
});
test('should ignore non-whitelisted productId and not write to DB', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
mockSub([
{
identifier: 'Weird',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-07-01T00:00:00.000Z'),
expirationDate: new Date('2026-07-01T00:00:00.000Z'),
productId: 'unknown.sku',
store: 'app_store',
willRenew: true,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_unknown',
type: 'INITIAL_PURCHASE',
store: 'app_store',
});
const dbCount = await db.subscription.count({ where: { targetId: user.id } });
const { activatedCount, canceledCount } = collectEvents();
t.snapshot(
{ dbCount, activatedCount, canceledCount },
'should ignore non-whitelisted productId and not write to DB'
);
});
test('should map via entitlement+duration when productId not whitelisted (P1M/P1Y only)', async t => {
const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context;
mockSubSeq([
[
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-08-01T00:00:00.000Z'),
expirationDate: new Date('2025-09-01T00:00:00.000Z'),
productId: 'unknown.sku',
store: 'app_store',
willRenew: true,
duration: 'P1M',
},
],
[
{
identifier: 'AI',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-10-01T00:00:00.000Z'),
expirationDate: new Date('2026-10-01T00:00:00.000Z'),
productId: 'unknown.sku',
store: 'play_store',
willRenew: true,
duration: 'P1Y',
},
],
[
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-11-01T00:00:00.000Z'),
expirationDate: new Date('2026-02-01T00:00:00.000Z'),
productId: 'unknown.sku',
store: 'app_store',
willRenew: true,
duration: 'P3M', // not supported -> ignore
},
],
]);
// pro monthly via fallback
await triggerWebhook(user.id, {
id: 'evt_fb1',
type: 'INITIAL_PURCHASE',
store: 'app_store',
});
const r1 = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
select: { plan: true, recurring: true, provider: true },
});
const s1 = collectEvents();
// ai yearly via fallback
await triggerWebhook(user.id, {
id: 'evt_fb2',
type: 'INITIAL_PURCHASE',
store: 'play_store',
});
const r2 = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'ai' } },
select: { plan: true, recurring: true, provider: true },
});
const s2 = collectEvents();
// unsupported duration ignored
await triggerWebhook(user.id, {
id: 'evt_fb3',
type: 'INITIAL_PURCHASE',
store: 'app_store',
});
const count = await db.subscription.count({ where: { targetId: user.id } });
const s3 = collectEvents();
t.snapshot(
{
proViaFallback: r1,
aiViaFallback: r2,
totalCount: count,
eventsCounts: {
afterFirst: { a: s1.activatedCount, c: s1.canceledCount },
afterSecond: { a: s2.activatedCount, c: s2.canceledCount },
afterThird: { a: s3.activatedCount, c: s3.canceledCount },
},
},
'should map via entitlement+duration fallback and ignore unsupported durations'
);
});
test('should not dispatch webhook event when authorization header is missing or mismatched', async t => {
const { controller, event } = t.context;
const before = event.emitAsync.getCalls()?.length || 0;
const e = { id: '42', type: 'INITIAL_PURCHASE', app_user_id: user.id };
await controller.handleWebhook({ body: { event: e } } as any, undefined);
const after = event.emitAsync.getCalls()?.length || 0;
t.is(after - before, 0, 'should not emit event');
});

View File

@@ -192,10 +192,8 @@ test.before(async t => {
payment: {
enabled: true,
showLifetimePrice: true,
stripe: {
apiKey: '1',
webhookKey: '1',
},
apiKey: '1',
webhookKey: '1',
},
}),
AppModule,

View File

@@ -637,11 +637,6 @@ export const USER_FRIENDLY_ERRORS = {
type: 'invalid_input',
message: 'Workspace id is required to update team subscription.',
},
managed_by_app_store_or_play: {
type: 'action_forbidden',
message:
'This subscription is managed by App Store or Google Play. Please manage it in the corresponding store.',
},
// Copilot errors
copilot_session_not_found: {

View File

@@ -651,12 +651,6 @@ export class WorkspaceIdRequiredToUpdateTeamSubscription extends UserFriendlyErr
}
}
export class ManagedByAppStoreOrPlay extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'managed_by_app_store_or_play', message);
}
}
export class CopilotSessionNotFound extends UserFriendlyError {
constructor(message?: string) {
super('resource_not_found', 'copilot_session_not_found', message);
@@ -1195,7 +1189,6 @@ export enum ErrorNames {
CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION,
WORKSPACE_ID_REQUIRED_FOR_TEAM_SUBSCRIPTION,
WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION,
MANAGED_BY_APP_STORE_OR_PLAY,
COPILOT_SESSION_NOT_FOUND,
COPILOT_SESSION_INVALID_INPUT,
COPILOT_SESSION_DELETED,

View File

@@ -8,7 +8,6 @@ import { Global, Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import type { Request, Response } from 'express';
import { NodeEnv } from '../../env';
import { Config } from '../config';
import { mapAnyError } from '../nestjs/exception';
import { GQLLoggerPlugin } from './logger-plugin';
@@ -31,7 +30,7 @@ export type GraphqlContext = {
numberScalarMode: 'integer',
},
useGlobalPrefix: true,
graphiql: env.NODE_ENV === NodeEnv.Development,
playground: true,
sortSchema: true,
autoSchemaFile: join(
env.projectRoot,

View File

@@ -50,13 +50,6 @@ export class AccessTokenResolver {
return await this.models.accessToken.list(user.id);
}
@Query(() => [RevealedAccessToken])
async revealedAccessTokens(
@CurrentUser() user: CurrentUser
): Promise<RevealedAccessToken[]> {
return await this.models.accessToken.list(user.id, true);
}
@Mutation(() => RevealedAccessToken)
async generateUserAccessToken(
@CurrentUser() user: CurrentUser,

View File

@@ -8,7 +8,6 @@ export interface AuthConfig {
ttr: number;
};
allowSignup: boolean;
allowSignupForOauth: boolean;
requireEmailDomainVerification: boolean;
requireEmailVerification: boolean;
passwordRequirements: ConfigItem<{
@@ -28,10 +27,6 @@ defineModuleConfig('auth', {
desc: 'Whether allow new registrations.',
default: true,
},
allowSignupForOauth: {
desc: 'Whether allow new registrations via configured oauth.',
default: true,
},
requireEmailDomainVerification: {
desc: 'Whether require email domain record verification before accessing restricted resources.',
default: false,

View File

@@ -6,7 +6,6 @@ declare global {
interface AppConfigSchema {
mailer: {
SMTP: {
name: string;
host: string;
port: number;
username: string;
@@ -17,7 +16,6 @@ declare global {
fallbackDomains: ConfigItem<string[]>;
fallbackSMTP: {
name: string;
host: string;
port: number;
username: string;
@@ -30,11 +28,6 @@ declare global {
}
defineModuleConfig('mailer', {
'SMTP.name': {
desc: 'Name of the email server (e.g. your domain name)',
default: 'AFFiNE Server',
env: 'MAILER_SERVERNAME',
},
'SMTP.host': {
desc: 'Host of the email server (e.g. smtp.gmail.com)',
default: '',
@@ -71,10 +64,6 @@ defineModuleConfig('mailer', {
default: [],
shape: z.array(z.string()),
},
'fallbackSMTP.name': {
desc: 'Name of the fallback email server (e.g. your domain name)',
default: 'AFFiNE Server',
},
'fallbackSMTP.host': {
desc: 'Host of the email server (e.g. smtp.gmail.com)',
default: '',

View File

@@ -20,7 +20,6 @@ function configToSMTPOptions(
config: AppConfig['mailer']['SMTP']
): SMTPTransport.Options {
return {
name: config.name,
host: config.host,
port: config.port,
tls: {

View File

@@ -15,14 +15,13 @@ export class AccessTokenModel extends BaseModel {
super();
}
async list(userId: string, revealed: boolean = false) {
async list(userId: string) {
return await this.db.accessToken.findMany({
select: {
id: true,
name: true,
createdAt: true,
expiresAt: true,
token: revealed,
},
where: {
userId,

View File

@@ -459,19 +459,27 @@ export class CopilotSessionModel extends BaseModel {
docId: options.docId,
deletedAt: null,
},
select: { id: true },
select: { id: true, prompt: true },
});
const sessionIds = sessions.map(({ id }) => id);
// cleanup all messages
await this.db.aiSessionMessage.deleteMany({
where: { sessionId: { in: sessionIds } },
});
await this.db.aiSession.updateMany({
where: { id: { in: sessionIds } },
data: { pinned: false, deletedAt: new Date() },
});
// only mark action session as deleted
// chat session always can be reuse
const actionIds = sessions
.filter(({ prompt }) => !!prompt.action)
.map(({ id }) => id);
// 标记 action session 为已删除
if (actionIds.length > 0) {
await this.db.aiSession.updateMany({
where: { id: { in: actionIds } },
data: { pinned: false, deletedAt: new Date() },
});
}
return sessionIds;
}

View File

@@ -51,7 +51,7 @@ defineModuleConfig('copilot', {
override_enabled: false,
scenarios: {
audio_transcribing: 'gemini-2.5-flash',
chat: 'gemini-2.5-flash',
chat: 'claude-sonnet-4@20250514',
embedding: 'gemini-embedding-001',
image: 'gpt-image-1',
rerank: 'gpt-4.1',

View File

@@ -44,7 +44,6 @@ import {
NoCopilotProviderAvailable,
UnsplashIsNotConfigured,
} from '../../base';
import { ServerFeature, ServerService } from '../../core';
import { CurrentUser, Public } from '../../core/auth';
import { CopilotContextService } from './context';
import {
@@ -76,7 +75,6 @@ export class CopilotController implements BeforeApplicationShutdown {
constructor(
private readonly config: Config,
private readonly server: ServerService,
private readonly chatSession: ChatSessionService,
private readonly context: CopilotContextService,
private readonly provider: CopilotProviderFactory,
@@ -114,10 +112,10 @@ export class CopilotController implements BeforeApplicationShutdown {
throw new CopilotSessionNotFound();
}
const model = await session.resolveModel(
this.server.features.includes(ServerFeature.Payment),
modelId
);
const model =
modelId && session.optionalModels.includes(modelId)
? modelId
: session.model;
const hasAttachment = messageId
? !!(await session.getMessageById(messageId)).attachments?.length

View File

@@ -1928,11 +1928,18 @@ Now apply the \`updates\` to the \`content\`, following the intent in \`op\`, an
];
const CHAT_PROMPT: Omit<Prompt, 'name'> = {
model: 'gemini-2.5-flash',
model: 'claude-sonnet-4@20250514',
optionalModels: [
'gpt-4.1',
'gpt-5',
'o3',
'o4-mini',
'gemini-2.5-flash',
'gemini-2.5-pro',
'claude-opus-4@20250514',
'claude-sonnet-4@20250514',
'claude-3-7-sonnet@20250219',
'claude-3-5-sonnet-v2@20241022',
],
messages: [
{
@@ -2092,7 +2099,6 @@ Below is the user's query. Please respond in the user's preferred language witho
'codeArtifact',
'blobRead',
],
proModels: ['gemini-2.5-pro', 'claude-sonnet-4@20250514'],
},
};

View File

@@ -21,7 +21,6 @@ export class AnthropicOfficialProvider extends AnthropicProvider<AnthropicOffici
override readonly models = [
{
name: 'Claude Opus 4',
id: 'claude-opus-4-20250514',
capabilities: [
{
@@ -31,7 +30,6 @@ export class AnthropicOfficialProvider extends AnthropicProvider<AnthropicOffici
],
},
{
name: 'Claude Sonnet 4',
id: 'claude-sonnet-4-20250514',
capabilities: [
{
@@ -41,7 +39,6 @@ export class AnthropicOfficialProvider extends AnthropicProvider<AnthropicOffici
],
},
{
name: 'Claude 3.7 Sonnet',
id: 'claude-3-7-sonnet-20250219',
capabilities: [
{
@@ -51,7 +48,6 @@ export class AnthropicOfficialProvider extends AnthropicProvider<AnthropicOffici
],
},
{
name: 'Claude 3.5 Sonnet',
id: 'claude-3-5-sonnet-20241022',
capabilities: [
{

View File

@@ -15,7 +15,6 @@ export class AnthropicVertexProvider extends AnthropicProvider<AnthropicVertexCo
override readonly models = [
{
name: 'Claude Opus 4',
id: 'claude-opus-4@20250514',
capabilities: [
{
@@ -25,7 +24,6 @@ export class AnthropicVertexProvider extends AnthropicProvider<AnthropicVertexCo
],
},
{
name: 'Claude Sonnet 4',
id: 'claude-sonnet-4@20250514',
capabilities: [
{
@@ -35,7 +33,6 @@ export class AnthropicVertexProvider extends AnthropicProvider<AnthropicVertexCo
],
},
{
name: 'Claude 3.7 Sonnet',
id: 'claude-3-7-sonnet@20250219',
capabilities: [
{
@@ -45,7 +42,6 @@ export class AnthropicVertexProvider extends AnthropicProvider<AnthropicVertexCo
],
},
{
name: 'Claude 3.5 Sonnet',
id: 'claude-3-5-sonnet-v2@20241022',
capabilities: [
{

Some files were not shown because too many files have changed in this diff Show More