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",