mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
fix(core): fix create workspace force cloud (#9382)
This commit is contained in:
@@ -1,12 +1,8 @@
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
import { MenuItem } from '@affine/component/ui/menu';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import {
|
||||
type WorkspaceMetadata,
|
||||
WorkspacesService,
|
||||
} from '@affine/core/modules/workspace';
|
||||
import { type WorkspaceMetadata } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { Logo1Icon } from '@blocksuite/icons/rc';
|
||||
@@ -119,9 +115,6 @@ const UserWithWorkspaceListInner = ({
|
||||
globalDialogService.open('sign-in', { step: 'addSelfhosted' });
|
||||
}, [globalDialogService]);
|
||||
|
||||
const workspaceManager = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspaceManager.list.workspaces$);
|
||||
|
||||
return (
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
<AFFiNEWorkspaceList
|
||||
@@ -130,7 +123,6 @@ const UserWithWorkspaceListInner = ({
|
||||
showEnableCloudButton={showEnableCloudButton}
|
||||
showSettingsButton={showSettingsButton}
|
||||
/>
|
||||
{workspaces.length > 0 ? <Divider size="thinner" /> : null}
|
||||
<AddWorkspace
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
|
||||
@@ -138,17 +138,17 @@ const CloudWorkSpaceList = ({
|
||||
items={[
|
||||
server.id !== 'affine-cloud' && (
|
||||
<MenuItem key="delete-server" onClick={handleDeleteServer}>
|
||||
Delete Server
|
||||
{t['com.affine.server.delete']()}
|
||||
</MenuItem>
|
||||
),
|
||||
accountStatus === 'authenticated' && (
|
||||
<MenuItem key="sign-out" onClick={handleSignOut}>
|
||||
Sign Out
|
||||
{t['com.affine.sign.out']()}
|
||||
</MenuItem>
|
||||
),
|
||||
accountStatus === 'unauthenticated' && (
|
||||
<MenuItem key="sign-in" onClick={handleSignIn}>
|
||||
Sign In
|
||||
{t['com.affine.sign.in']()}
|
||||
</MenuItem>
|
||||
),
|
||||
]}
|
||||
@@ -206,6 +206,7 @@ const LocalWorkspaces = ({
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
onEnableCloudClick={onClickEnableCloud}
|
||||
/>
|
||||
<Divider size="thinner" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Avatar, ConfirmModal, Input, Switch } from '@affine/component';
|
||||
import { Avatar, ConfirmModal, Input, notify, Switch } from '@affine/component';
|
||||
import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
||||
import { CloudSvg } from '@affine/core/components/affine/share-page-modal/cloud-svg';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { AuthService, ServersService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type DialogComponentProps,
|
||||
type GLOBAL_DIALOG_SCHEMA,
|
||||
@@ -12,7 +12,7 @@ import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { WorkspacesService } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { buildShowcaseWorkspace } from '../../../utils/first-app-data';
|
||||
@@ -60,8 +60,18 @@ const NameWorkspaceContent = ({
|
||||
);
|
||||
|
||||
const handleCreateWorkspace = useCallback(() => {
|
||||
if (loginStatus !== 'authenticated' && enable) {
|
||||
return openSignInModal();
|
||||
}
|
||||
onConfirmName(workspaceName, enable ? serverId || 'affine-cloud' : 'local');
|
||||
}, [enable, onConfirmName, serverId, workspaceName]);
|
||||
}, [
|
||||
enable,
|
||||
loginStatus,
|
||||
onConfirmName,
|
||||
openSignInModal,
|
||||
serverId,
|
||||
workspaceName,
|
||||
]);
|
||||
|
||||
const onEnter = useCallback(() => {
|
||||
if (workspaceName) {
|
||||
@@ -109,37 +119,41 @@ const NameWorkspaceContent = ({
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.affineCloudWrapper}>
|
||||
<div className={styles.subTitle}>{t['AFFiNE Cloud']()}</div>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.cardText}>
|
||||
<div className={styles.cardTitle}>
|
||||
<span>{t['com.affine.nameWorkspace.affine-cloud.title']()}</span>
|
||||
<Switch
|
||||
checked={enable}
|
||||
onChange={onSwitchChange}
|
||||
disabled={forcedCloud}
|
||||
/>
|
||||
{!serverId || serverId === 'affine-cloud' ? (
|
||||
<div className={styles.affineCloudWrapper}>
|
||||
<div className={styles.subTitle}>{t['AFFiNE Cloud']()}</div>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.cardText}>
|
||||
<div className={styles.cardTitle}>
|
||||
<span>
|
||||
{t['com.affine.nameWorkspace.affine-cloud.title']()}
|
||||
</span>
|
||||
<Switch
|
||||
checked={enable}
|
||||
onChange={onSwitchChange}
|
||||
disabled={forcedCloud}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.cardDescription}>
|
||||
{t['com.affine.nameWorkspace.affine-cloud.description']()}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cardDescription}>
|
||||
{t['com.affine.nameWorkspace.affine-cloud.description']()}
|
||||
<div className={styles.cloudSvgContainer}>
|
||||
<CloudSvg />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cloudSvgContainer}>
|
||||
<CloudSvg />
|
||||
</div>
|
||||
{forcedCloud && BUILD_CONFIG.isWeb ? (
|
||||
<a
|
||||
className={styles.cloudTips}
|
||||
href={BUILD_CONFIG.downloadUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t['com.affine.nameWorkspace.affine-cloud.web-tips']()}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
{forcedCloud ? (
|
||||
<a
|
||||
className={styles.cloudTips}
|
||||
href={BUILD_CONFIG.downloadUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t['com.affine.nameWorkspace.affine-cloud.web-tips']()}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
@@ -150,10 +164,14 @@ export const CreateWorkspaceDialog = ({
|
||||
close,
|
||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['create-workspace']>) => {
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const serversService = useService(ServersService);
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const enableLocalWorkspace = useLiveData(
|
||||
featureFlagService.flags.enable_local_workspace.$
|
||||
);
|
||||
const server = useLiveData(
|
||||
serverId ? serversService.server$(serverId) : null
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onConfirmName = useAsyncCallback(
|
||||
@@ -164,14 +182,22 @@ export const CreateWorkspaceDialog = ({
|
||||
|
||||
// this will be the last step for web for now
|
||||
// fix me later
|
||||
const { meta, defaultDocId } = await buildShowcaseWorkspace(
|
||||
workspacesService,
|
||||
workspaceFlavour,
|
||||
name
|
||||
);
|
||||
|
||||
close({ metadata: meta, defaultDocId });
|
||||
setLoading(false);
|
||||
try {
|
||||
const { meta, defaultDocId } = await buildShowcaseWorkspace(
|
||||
workspacesService,
|
||||
workspaceFlavour,
|
||||
name
|
||||
);
|
||||
close({ metadata: meta, defaultDocId });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
notify.error({
|
||||
title: 'Failed to create workspace',
|
||||
message: 'please try again later.',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[loading, workspacesService, close]
|
||||
);
|
||||
@@ -186,13 +212,15 @@ export const CreateWorkspaceDialog = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<NameWorkspaceContent
|
||||
loading={loading}
|
||||
open
|
||||
serverId={serverId}
|
||||
forcedCloud={forcedCloud || !enableLocalWorkspace}
|
||||
onOpenChange={onOpenChange}
|
||||
onConfirmName={onConfirmName}
|
||||
/>
|
||||
<FrameworkScope scope={server?.scope}>
|
||||
<NameWorkspaceContent
|
||||
loading={loading}
|
||||
serverId={serverId}
|
||||
open
|
||||
forcedCloud={forcedCloud || !enableLocalWorkspace}
|
||||
onOpenChange={onOpenChange}
|
||||
onConfirmName={onConfirmName}
|
||||
/>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,16 +2,21 @@ import { Loading, Scrollable } from '@affine/component';
|
||||
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
|
||||
import type { ModalProps } from '@affine/component/ui/modal';
|
||||
import { Modal } from '@affine/component/ui/modal';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
AuthService,
|
||||
DefaultServerService,
|
||||
ServersService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type {
|
||||
DialogComponentProps,
|
||||
GLOBAL_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import type { WorkspaceMetadata } from '@affine/core/modules/workspace';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { ContactWithUsIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
|
||||
import { debounce } from 'lodash-es';
|
||||
import {
|
||||
Suspense,
|
||||
@@ -57,7 +62,21 @@ const SettingModalInner = ({
|
||||
activeWorkspaceMetadata: initialWorkspaceMetadata,
|
||||
scrollAnchor: undefined,
|
||||
});
|
||||
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
||||
const globalContextService = useService(GlobalContextService);
|
||||
|
||||
const currentServerId = useLiveData(
|
||||
globalContextService.globalContext.serverId.$
|
||||
);
|
||||
console.log(currentServerId);
|
||||
const serversService = useService(ServersService);
|
||||
const defaultServerService = useService(DefaultServerService);
|
||||
const currentServer =
|
||||
useLiveData(
|
||||
currentServerId ? serversService.server$(currentServerId) : null
|
||||
) ?? defaultServerService.server;
|
||||
const loginStatus = useLiveData(
|
||||
currentServer.scope.get(AuthService).session.status$
|
||||
);
|
||||
|
||||
const modalContentRef = useRef<HTMLDivElement>(null);
|
||||
const modalContentWrapperRef = useRef<HTMLDivElement>(null);
|
||||
@@ -120,7 +139,7 @@ const SettingModalInner = ({
|
||||
}, [setOpenStarAFFiNEModal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FrameworkScope scope={currentServer.scope}>
|
||||
<SettingSidebar
|
||||
activeTab={settingState.activeTab}
|
||||
onTabChange={onTabChange}
|
||||
@@ -188,7 +207,7 @@ const SettingModalInner = ({
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Viewport>
|
||||
</Scrollable.Root>
|
||||
</>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '@affine/component/setting-components';
|
||||
import { useWorkspace } from '@affine/core/components/hooks/use-workspace';
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
@@ -31,6 +32,7 @@ export const WorkspaceSettingDetail = ({
|
||||
// useWorkspace hook is a vary heavy operation here, but we need syncing name and avatar changes here,
|
||||
// we don't have a better way to do this now
|
||||
const workspace = useWorkspace(workspaceMetadata);
|
||||
const server = workspace?.scope.get(WorkspaceServerService).server;
|
||||
|
||||
const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
|
||||
|
||||
@@ -50,53 +52,55 @@ export const WorkspaceSettingDetail = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<SettingHeader
|
||||
title={t[`Workspace Settings with name`]({
|
||||
name: workspaceInfo?.name ?? UNTITLED_WORKSPACE_NAME,
|
||||
})}
|
||||
subtitle={t['com.affine.settings.workspace.description']()}
|
||||
/>
|
||||
<SettingWrapper title={t['Info']()}>
|
||||
<SettingRow
|
||||
name={t['Workspace Profile']()}
|
||||
desc={t['com.affine.settings.workspace.not-owner']()}
|
||||
spreadCol={false}
|
||||
>
|
||||
<ProfilePanel />
|
||||
<LabelsPanel />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
|
||||
<EnableCloudPanel onCloseSetting={onCloseSetting} />
|
||||
<WorkspaceQuotaPanel />
|
||||
<MembersPanel onChangeSettingState={onChangeSettingState} />
|
||||
</SettingWrapper>
|
||||
<SharingPanel />
|
||||
{BUILD_CONFIG.isElectron && (
|
||||
<SettingWrapper title={t['Storage and Export']()}>
|
||||
<DesktopExportPanel
|
||||
workspace={workspace}
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
/>
|
||||
<FrameworkScope scope={server?.scope}>
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<SettingHeader
|
||||
title={t[`Workspace Settings with name`]({
|
||||
name: workspaceInfo?.name ?? UNTITLED_WORKSPACE_NAME,
|
||||
})}
|
||||
subtitle={t['com.affine.settings.workspace.description']()}
|
||||
/>
|
||||
<SettingWrapper title={t['Info']()}>
|
||||
<SettingRow
|
||||
name={t['Workspace Profile']()}
|
||||
desc={t['com.affine.settings.workspace.not-owner']()}
|
||||
spreadCol={false}
|
||||
>
|
||||
<ProfilePanel />
|
||||
<LabelsPanel />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
)}
|
||||
<SettingWrapper>
|
||||
<DeleteLeaveWorkspace onCloseSetting={onCloseSetting} />
|
||||
<SettingRow
|
||||
name={
|
||||
<span style={{ color: 'var(--affine-text-secondary-color)' }}>
|
||||
{t['com.affine.resetSyncStatus.button']()}
|
||||
</span>
|
||||
}
|
||||
desc={t['com.affine.resetSyncStatus.description']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={handleResetSyncStatus}
|
||||
data-testid="reset-sync-status"
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
|
||||
<EnableCloudPanel onCloseSetting={onCloseSetting} />
|
||||
<WorkspaceQuotaPanel />
|
||||
<MembersPanel onChangeSettingState={onChangeSettingState} />
|
||||
</SettingWrapper>
|
||||
<SharingPanel />
|
||||
{BUILD_CONFIG.isElectron && (
|
||||
<SettingWrapper title={t['Storage and Export']()}>
|
||||
<DesktopExportPanel
|
||||
workspace={workspace}
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
/>
|
||||
</SettingWrapper>
|
||||
)}
|
||||
<SettingWrapper>
|
||||
<DeleteLeaveWorkspace onCloseSetting={onCloseSetting} />
|
||||
<SettingRow
|
||||
name={
|
||||
<span style={{ color: 'var(--affine-text-secondary-color)' }}>
|
||||
{t['com.affine.resetSyncStatus.button']()}
|
||||
</span>
|
||||
}
|
||||
desc={t['com.affine.resetSyncStatus.description']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={handleResetSyncStatus}
|
||||
data-testid="reset-sync-status"
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NotificationCenter } from '@affine/component';
|
||||
import { DefaultServerService } from '@affine/core/modules/cloud';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { FrameworkScope, useService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
@@ -11,7 +10,6 @@ import { FindInPageModal } from './find-in-page/find-in-page-modal';
|
||||
|
||||
export const RootWrapper = () => {
|
||||
const defaultServerService = useService(DefaultServerService);
|
||||
const globalContextService = useService(GlobalContextService);
|
||||
const [isServerReady, setIsServerReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,15 +30,6 @@ export const RootWrapper = () => {
|
||||
};
|
||||
}, [defaultServerService, isServerReady]);
|
||||
|
||||
useEffect(() => {
|
||||
globalContextService.globalContext.serverId.set(
|
||||
defaultServerService.server.id
|
||||
);
|
||||
return () => {
|
||||
globalContextService.globalContext.serverId.set(null);
|
||||
};
|
||||
}, [defaultServerService, globalContextService]);
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={defaultServerService.server.scope}>
|
||||
<GlobalDialogs />
|
||||
|
||||
@@ -222,8 +222,8 @@ export const AFFINE_FLAGS = {
|
||||
'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.description',
|
||||
configurable: isDesktopEnvironment,
|
||||
defaultState: false,
|
||||
configurable: false,
|
||||
defaultState: isDesktopEnvironment,
|
||||
},
|
||||
enable_mobile_edgeless_editing: {
|
||||
category: 'affine',
|
||||
|
||||
@@ -7,7 +7,7 @@ export class GlobalContext extends Entity {
|
||||
workspaceId = this.define<string>('workspaceId');
|
||||
workspaceFlavour = this.define<string>('workspaceFlavour');
|
||||
|
||||
serverId = this.define<string>('serverId');
|
||||
serverId = this.define<string>('serverId', 'affine-cloud');
|
||||
|
||||
/**
|
||||
* is in doc page
|
||||
@@ -39,12 +39,14 @@ export class GlobalContext extends Entity {
|
||||
*/
|
||||
isAllDocs = this.define<boolean>('isAllDocs');
|
||||
|
||||
define<T>(key: string) {
|
||||
this.memento.set(key, null);
|
||||
const livedata$ = LiveData.from(this.memento.watch<T>(key), null);
|
||||
define<T>(key: string, defaultValue: T | null = null) {
|
||||
this.memento.set(key, defaultValue);
|
||||
const livedata$ = LiveData.from(this.memento.watch<T>(key), defaultValue);
|
||||
return {
|
||||
get: () => this.memento.get(key) as T | null,
|
||||
set: (value: T | null) => this.memento.set(key, value),
|
||||
set: (value: T | null) => {
|
||||
this.memento.set(key, value);
|
||||
},
|
||||
$: livedata$,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -848,7 +848,6 @@
|
||||
"com.affine.payment.billing-type-form.title": "Tell us your use case",
|
||||
"com.affine.payment.blob-limit.description.local": "The maximum file upload size for local workspaces is {{quota}}.",
|
||||
"com.affine.payment.blob-limit.description.member": "The maximum file upload size for this joined workspace is {{quota}}. You can contact the owner of this workspace.",
|
||||
|
||||
"com.affine.payment.blob-limit.description.owner": "The maximum file upload size for this workspace is {{quota}}. To proceed, you can:",
|
||||
"com.affine.payment.blob-limit.description.owner.tips-1": "Upgrade your account for larger file upload limits",
|
||||
"com.affine.payment.blob-limit.description.owner.tips-2": "Upgrade the workspace plan to increase storage for all member",
|
||||
@@ -1617,5 +1616,6 @@
|
||||
"com.affine.payment.sync-paused.member.both.description": "This workspace has exceeded both storage and member limits, causing synchronization to pause. Please contact your workspace owner to address these limits and resume syncing.",
|
||||
"com.affine.payment.sync-paused.member.storage.description": "This workspace has exceeded its storage limit and synchronization has been paused. Please contact your workspace owner to either reduce storage usage or upgrade the plan to resume syncing.",
|
||||
"com.affine.payment.sync-paused.member.member.description": "This workspace has reached its maximum member capacity and synchronization has been paused. Please contact your workspace owner to either adjust team membership or upgrade the plan to resume syncing.",
|
||||
"com.affine.payment.sync-paused.member.member.confirm": "Got It"
|
||||
"com.affine.payment.sync-paused.member.member.confirm": "Got It",
|
||||
"com.affine.server.delete": "Delete Server"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user