diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/index.tsx
index 632fbb526..59bc0886b 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/index.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/index.tsx
@@ -17,6 +17,7 @@ import { ExportPanel } from './export';
import { LabelsPanel } from './labels';
import { MembersPanel } from './members';
import { ProfilePanel } from './profile';
+import { SharingPanel } from './sharing';
import type { WorkspaceSettingDetailProps } from './types';
export const WorkspaceSettingDetail = ({
@@ -67,6 +68,7 @@ export const WorkspaceSettingDetail = ({
+
{environment.isElectron && (
{
+ const workspace = useService(WorkspaceService).workspace;
+ if (workspace.flavour === WorkspaceFlavour.LOCAL) {
+ return null;
+ }
+ return ;
+};
+
+export const Sharing = () => {
+ const t = useI18n();
+ const shareSetting = useService(WorkspaceShareSettingService).sharePreview;
+ const enableUrlPreview = useLiveData(shareSetting.enableUrlPreview$);
+ const loading = useLiveData(shareSetting.isLoading$);
+
+ const handleCheck = useAsyncCallback(
+ async (checked: boolean) => {
+ await shareSetting.setEnableUrlPreview(checked);
+ },
+ [shareSetting]
+ );
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts
index a5ce02444..d2b260e72 100644
--- a/packages/frontend/core/src/modules/index.ts
+++ b/packages/frontend/core/src/modules/index.ts
@@ -19,6 +19,7 @@ import { configurePermissionsModule } from './permissions';
import { configureWorkspacePropertiesModule } from './properties';
import { configureQuickSearchModule } from './quicksearch';
import { configureShareDocsModule } from './share-doc';
+import { configureShareSettingModule } from './share-setting';
import { configureSystemFontFamilyModule } from './system-font-family';
import { configureTagModule } from './tag';
import { configureTelemetryModule } from './telemetry';
@@ -34,6 +35,7 @@ export function configureCommonModules(framework: Framework) {
configureQuotaModule(framework);
configurePermissionsModule(framework);
configureShareDocsModule(framework);
+ configureShareSettingModule(framework);
configureTelemetryModule(framework);
configureFindInPageModule(framework);
configurePeekViewModule(framework);
diff --git a/packages/frontend/core/src/modules/share-setting/entities/share-setting.ts b/packages/frontend/core/src/modules/share-setting/entities/share-setting.ts
new file mode 100644
index 000000000..9169ed226
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-setting/entities/share-setting.ts
@@ -0,0 +1,80 @@
+import { DebugLogger } from '@affine/debug';
+import type { GetEnableUrlPreviewQuery } from '@affine/graphql';
+import type { WorkspaceService } from '@toeverything/infra';
+import {
+ backoffRetry,
+ catchErrorInto,
+ effect,
+ Entity,
+ fromPromise,
+ LiveData,
+ mapInto,
+ onComplete,
+ onStart,
+} from '@toeverything/infra';
+import { exhaustMap } from 'rxjs';
+
+import { isBackendError, isNetworkError } from '../../cloud';
+import type { WorkspaceShareSettingStore } from '../stores/share-setting';
+
+type EnableUrlPreview =
+ GetEnableUrlPreviewQuery['workspace']['enableUrlPreview'];
+
+const logger = new DebugLogger('affine:workspace-permission');
+
+export class WorkspaceShareSetting extends Entity {
+ enableUrlPreview$ = new LiveData(null);
+ isLoading$ = new LiveData(false);
+ error$ = new LiveData(null);
+
+ constructor(
+ private readonly workspaceService: WorkspaceService,
+ private readonly store: WorkspaceShareSettingStore
+ ) {
+ super();
+ this.revalidate();
+ }
+
+ revalidate = effect(
+ exhaustMap(() => {
+ return fromPromise(signal =>
+ this.store.fetchWorkspaceEnableUrlPreview(
+ this.workspaceService.workspace.id,
+ signal
+ )
+ ).pipe(
+ backoffRetry({
+ when: isNetworkError,
+ count: Infinity,
+ }),
+ backoffRetry({
+ when: isBackendError,
+ count: 3,
+ }),
+ mapInto(this.enableUrlPreview$),
+ catchErrorInto(this.error$, error => {
+ logger.error('Failed to fetch enableUrlPreview', error);
+ }),
+ onStart(() => this.isLoading$.setValue(true)),
+ onComplete(() => this.isLoading$.setValue(false))
+ );
+ })
+ );
+
+ async waitForRevalidation(signal?: AbortSignal) {
+ this.revalidate();
+ await this.isLoading$.waitFor(isLoading => !isLoading, signal);
+ }
+
+ async setEnableUrlPreview(enableUrlPreview: EnableUrlPreview) {
+ await this.store.updateWorkspaceEnableUrlPreview(
+ this.workspaceService.workspace.id,
+ enableUrlPreview
+ );
+ await this.waitForRevalidation();
+ }
+
+ override dispose(): void {
+ this.revalidate.unsubscribe();
+ }
+}
diff --git a/packages/frontend/core/src/modules/share-setting/index.ts b/packages/frontend/core/src/modules/share-setting/index.ts
new file mode 100644
index 000000000..d751ba7cd
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-setting/index.ts
@@ -0,0 +1,23 @@
+export { WorkspaceShareSettingService } from './services/share-setting';
+
+import { GraphQLService } from '@affine/core/modules/cloud';
+import {
+ type Framework,
+ WorkspaceScope,
+ WorkspaceService,
+} from '@toeverything/infra';
+
+import { WorkspaceShareSetting } from './entities/share-setting';
+import { WorkspaceShareSettingService } from './services/share-setting';
+import { WorkspaceShareSettingStore } from './stores/share-setting';
+
+export function configureShareSettingModule(framework: Framework) {
+ framework
+ .scope(WorkspaceScope)
+ .service(WorkspaceShareSettingService)
+ .store(WorkspaceShareSettingStore, [GraphQLService])
+ .entity(WorkspaceShareSetting, [
+ WorkspaceService,
+ WorkspaceShareSettingStore,
+ ]);
+}
diff --git a/packages/frontend/core/src/modules/share-setting/services/share-setting.ts b/packages/frontend/core/src/modules/share-setting/services/share-setting.ts
new file mode 100644
index 000000000..43d74258f
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-setting/services/share-setting.ts
@@ -0,0 +1,7 @@
+import { Service } from '@toeverything/infra';
+
+import { WorkspaceShareSetting } from '../entities/share-setting';
+
+export class WorkspaceShareSettingService extends Service {
+ sharePreview = this.framework.createEntity(WorkspaceShareSetting);
+}
diff --git a/packages/frontend/core/src/modules/share-setting/stores/share-setting.ts b/packages/frontend/core/src/modules/share-setting/stores/share-setting.ts
new file mode 100644
index 000000000..7521303b4
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-setting/stores/share-setting.ts
@@ -0,0 +1,45 @@
+import type { GraphQLService } from '@affine/core/modules/cloud';
+import {
+ getEnableUrlPreviewQuery,
+ setEnableUrlPreviewMutation,
+} from '@affine/graphql';
+import { Store } from '@toeverything/infra';
+
+export class WorkspaceShareSettingStore extends Store {
+ constructor(private readonly graphqlService: GraphQLService) {
+ super();
+ }
+
+ async fetchWorkspaceEnableUrlPreview(
+ workspaceId: string,
+ signal?: AbortSignal
+ ) {
+ const data = await this.graphqlService.gql({
+ query: getEnableUrlPreviewQuery,
+ variables: {
+ id: workspaceId,
+ },
+ context: {
+ signal,
+ },
+ });
+ return data.workspace.enableUrlPreview;
+ }
+
+ async updateWorkspaceEnableUrlPreview(
+ workspaceId: string,
+ enableUrlPreview: boolean,
+ signal?: AbortSignal
+ ) {
+ await this.graphqlService.gql({
+ query: setEnableUrlPreviewMutation,
+ variables: {
+ id: workspaceId,
+ enableUrlPreview,
+ },
+ context: {
+ signal,
+ },
+ });
+ }
+}
diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts
index 47a97eb63..92c5c62af 100644
--- a/packages/frontend/graphql/src/graphql/index.ts
+++ b/packages/frontend/graphql/src/graphql/index.ts
@@ -1170,6 +1170,32 @@ mutation verifyEmail($token: String!) {
}`,
};
+export const getEnableUrlPreviewQuery = {
+ id: 'getEnableUrlPreviewQuery' as const,
+ operationName: 'getEnableUrlPreview',
+ definitionName: 'workspace',
+ containsFile: false,
+ query: `
+query getEnableUrlPreview($id: String!) {
+ workspace(id: $id) {
+ enableUrlPreview
+ }
+}`,
+};
+
+export const setEnableUrlPreviewMutation = {
+ id: 'setEnableUrlPreviewMutation' as const,
+ operationName: 'setEnableUrlPreview',
+ definitionName: 'updateWorkspace',
+ containsFile: false,
+ query: `
+mutation setEnableUrlPreview($id: ID!, $enableUrlPreview: Boolean!) {
+ updateWorkspace(input: {id: $id, enableUrlPreview: $enableUrlPreview}) {
+ id
+ }
+}`,
+};
+
export const enabledFeaturesQuery = {
id: 'enabledFeaturesQuery' as const,
operationName: 'enabledFeatures',
diff --git a/packages/frontend/graphql/src/graphql/workspace-enable-url-preview-get.gql b/packages/frontend/graphql/src/graphql/workspace-enable-url-preview-get.gql
new file mode 100644
index 000000000..df9ac4e97
--- /dev/null
+++ b/packages/frontend/graphql/src/graphql/workspace-enable-url-preview-get.gql
@@ -0,0 +1,5 @@
+query getEnableUrlPreview($id: String!) {
+ workspace(id: $id) {
+ enableUrlPreview
+ }
+}
diff --git a/packages/frontend/graphql/src/graphql/workspace-enable-url-preview-set.gql b/packages/frontend/graphql/src/graphql/workspace-enable-url-preview-set.gql
new file mode 100644
index 000000000..37788ba2c
--- /dev/null
+++ b/packages/frontend/graphql/src/graphql/workspace-enable-url-preview-set.gql
@@ -0,0 +1,5 @@
+mutation setEnableUrlPreview($id: ID!, $enableUrlPreview: Boolean!) {
+ updateWorkspace(input: { id: $id, enableUrlPreview: $enableUrlPreview }) {
+ id
+ }
+}
diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts
index b5dfe0ecb..be66ef315 100644
--- a/packages/frontend/graphql/src/schema.ts
+++ b/packages/frontend/graphql/src/schema.ts
@@ -2325,6 +2325,25 @@ export type VerifyEmailMutation = {
verifyEmail: boolean;
};
+export type GetEnableUrlPreviewQueryVariables = Exact<{
+ id: Scalars['String']['input'];
+}>;
+
+export type GetEnableUrlPreviewQuery = {
+ __typename?: 'Query';
+ workspace: { __typename?: 'WorkspaceType'; enableUrlPreview: boolean };
+};
+
+export type SetEnableUrlPreviewMutationVariables = Exact<{
+ id: Scalars['ID']['input'];
+ enableUrlPreview: Scalars['Boolean']['input'];
+}>;
+
+export type SetEnableUrlPreviewMutation = {
+ __typename?: 'Mutation';
+ updateWorkspace: { __typename?: 'WorkspaceType'; id: string };
+};
+
export type EnabledFeaturesQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
@@ -2619,6 +2638,11 @@ export type Queries =
variables: SubscriptionQueryVariables;
response: SubscriptionQuery;
}
+ | {
+ name: 'getEnableUrlPreviewQuery';
+ variables: GetEnableUrlPreviewQueryVariables;
+ response: GetEnableUrlPreviewQuery;
+ }
| {
name: 'enabledFeaturesQuery';
variables: EnabledFeaturesQueryVariables;
@@ -2831,6 +2855,11 @@ export type Mutations =
variables: VerifyEmailMutationVariables;
response: VerifyEmailMutation;
}
+ | {
+ name: 'setEnableUrlPreviewMutation';
+ variables: SetEnableUrlPreviewMutationVariables;
+ response: SetEnableUrlPreviewMutation;
+ }
| {
name: 'setWorkspaceExperimentalFeatureMutation';
variables: SetWorkspaceExperimentalFeatureMutationVariables;
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index 577be1e0f..2b8577751 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -1407,6 +1407,9 @@
"com.affine.settings.workspace.sharing.url-preview.description": "Allow URL unfurling by Slack & other social apps, even if a doc is only accessible by workspace members.",
"com.affine.settings.workspace.sharing.url-preview.title": "Always Enable URL Preview",
"com.affine.settings.workspace.storage.tip": "Click to move storage location.",
+ "com.affine.settings.workspace.sharing.title": "Sharing",
+ "com.affine.settings.workspace.sharing.url-preview.title": "Always Enable URL Preview",
+ "com.affine.settings.workspace.sharing.url-preview.description": "Allow URL unfurling by Slack & other social apps, even if a doc is only accessible by workspace members.",
"com.affine.share-menu.EnableCloudDescription": "Sharing doc requires AFFiNE Cloud.",
"com.affine.share-menu.ShareMode": "Share mode",
"com.affine.share-menu.SharePage": "Share doc",