mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
🐛 fix(desktop): return OFFICIAL_URL in cloud mode for remoteServerUrl selector (#11502)
The remoteServerUrl selector was returning an empty string in cloud mode, causing avatar URLs with relative paths to not be properly prefixed with the remote server URL in desktop environment. Changes: - Update remoteServerUrl selector to return OFFICIAL_URL in cloud mode - Add rawRemoteServerUrl selector for forms that need the original config value - Fix avatar URL handling in UserAvatar and useCategory - Update tests to reflect new selector behavior Fixes LOBE-3197
This commit is contained in:
@@ -26,6 +26,8 @@ import {
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useElectronStore } from '@/store/electron';
|
||||
import { electronSyncSelectors } from '@/store/electron/selectors';
|
||||
import { SettingsTabs } from '@/store/global/initialState';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useUserStore } from '@/store/user';
|
||||
@@ -63,13 +65,24 @@ export const useCategory = () => {
|
||||
userProfileSelectors.userAvatar(s),
|
||||
userProfileSelectors.nickName(s),
|
||||
]);
|
||||
const remoteServerUrl = useElectronStore(electronSyncSelectors.remoteServerUrl);
|
||||
|
||||
// Process avatar URL for desktop environment
|
||||
const avatarUrl = useMemo(() => {
|
||||
if (!avatar) return undefined;
|
||||
if (isDesktop && avatar.startsWith('/') && remoteServerUrl) {
|
||||
return remoteServerUrl + avatar;
|
||||
}
|
||||
return avatar;
|
||||
}, [avatar, remoteServerUrl]);
|
||||
|
||||
const categoryGroups: CategoryGroup[] = useMemo(() => {
|
||||
const groups: CategoryGroup[] = [];
|
||||
|
||||
// 个人资料组 - Profile 相关设置
|
||||
const profileItems: CategoryItem[] = [
|
||||
{
|
||||
icon: avatar ? <Avatar avatar={avatar} shape={'square'} size={26} /> : UserCircle,
|
||||
icon: avatarUrl ? <Avatar avatar={avatarUrl} shape={'square'} size={26} /> : UserCircle,
|
||||
key: SettingsTabs.Profile,
|
||||
label: username ? username : tAuth('tab.profile'),
|
||||
},
|
||||
@@ -227,7 +240,7 @@ export const useCategory = () => {
|
||||
showAiImage,
|
||||
showApiKeyManage,
|
||||
isLoginWithClerk,
|
||||
avatar,
|
||||
avatarUrl,
|
||||
username,
|
||||
]);
|
||||
|
||||
|
||||
@@ -82,12 +82,12 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setWaiting }) => {
|
||||
|
||||
const connect = useElectronStore((s) => s.connectRemoteServer);
|
||||
const storageMode = useElectronStore(electronSyncSelectors.storageMode);
|
||||
const remoteServerUrl = useElectronStore(electronSyncSelectors.remoteServerUrl);
|
||||
const rawRemoteServerUrl = useElectronStore(electronSyncSelectors.rawRemoteServerUrl);
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<RemoteStorageMode>(
|
||||
storageMode === StorageModeEnum.SelfHost ? StorageModeEnum.SelfHost : StorageModeEnum.Cloud,
|
||||
);
|
||||
const [selfHostedUrl, setSelfHostedUrl] = useState(remoteServerUrl);
|
||||
const [selfHostedUrl, setSelfHostedUrl] = useState(rawRemoteServerUrl);
|
||||
|
||||
const validateUrl = useCallback((url: string) => {
|
||||
if (!url) {
|
||||
|
||||
@@ -19,6 +19,7 @@ vi.mock('@lobechat/const', async (importOriginal) => {
|
||||
return mockIsDesktop;
|
||||
},
|
||||
DEFAULT_USER_AVATAR: 'default-avatar.png',
|
||||
OFFICIAL_URL: 'https://app.lobehub.com',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -77,7 +78,7 @@ describe('useUserAvatar', () => {
|
||||
expect(result.current).toBe(mockAvatar);
|
||||
});
|
||||
|
||||
it('should prepend remote server URL when avatar starts with / in desktop environment', () => {
|
||||
it('should prepend remote server URL when avatar starts with / in desktop environment (selfHost mode)', () => {
|
||||
mockIsDesktop = true;
|
||||
const mockAvatar = '/api/avatar.png';
|
||||
const mockServerUrl = 'https://server.com';
|
||||
@@ -85,7 +86,7 @@ describe('useUserAvatar', () => {
|
||||
act(() => {
|
||||
useUserStore.setState({ user: { avatar: mockAvatar } as any });
|
||||
useElectronStore.setState({
|
||||
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'cloud' },
|
||||
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'selfHost' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,7 +103,7 @@ describe('useUserAvatar', () => {
|
||||
act(() => {
|
||||
useUserStore.setState({ user: { avatar: mockAvatar } as any });
|
||||
useElectronStore.setState({
|
||||
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'cloud' },
|
||||
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'selfHost' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,7 +112,7 @@ describe('useUserAvatar', () => {
|
||||
expect(result.current).toBe(mockAvatar);
|
||||
});
|
||||
|
||||
it('should handle empty remote server URL in desktop environment', () => {
|
||||
it('should use OFFICIAL_URL when storageMode is cloud in desktop environment', () => {
|
||||
mockIsDesktop = true;
|
||||
const mockAvatar = '/api/avatar.png';
|
||||
|
||||
@@ -124,6 +125,24 @@ describe('useUserAvatar', () => {
|
||||
|
||||
const { result } = renderHook(() => useUserAvatar());
|
||||
|
||||
// In cloud mode, selector returns OFFICIAL_URL regardless of remoteServerUrl config
|
||||
expect(result.current).toBe('https://app.lobehub.com/api/avatar.png');
|
||||
});
|
||||
|
||||
it('should return original avatar when storageMode is selfHost but no URL configured', () => {
|
||||
mockIsDesktop = true;
|
||||
const mockAvatar = '/api/avatar.png';
|
||||
|
||||
act(() => {
|
||||
useUserStore.setState({ user: { avatar: mockAvatar } as any });
|
||||
useElectronStore.setState({
|
||||
dataSyncConfig: { remoteServerUrl: '', storageMode: 'selfHost' },
|
||||
});
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUserAvatar());
|
||||
|
||||
// In selfHost mode with empty URL, avatar is not prepended
|
||||
expect(result.current).toBe(mockAvatar);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import { OFFICIAL_URL } from '@lobechat/const';
|
||||
|
||||
import { type ElectronState } from '../initialState';
|
||||
|
||||
const isSyncActive = (s: ElectronState) => s.dataSyncConfig.active;
|
||||
|
||||
const storageMode = (s: ElectronState) => s.dataSyncConfig.storageMode;
|
||||
const remoteServerUrl = (s: ElectronState) => s.dataSyncConfig.remoteServerUrl || '';
|
||||
|
||||
/**
|
||||
* Returns the effective remote server URL based on storage mode:
|
||||
* - Cloud mode: returns OFFICIAL_URL
|
||||
* - SelfHost mode: returns the configured remoteServerUrl
|
||||
*/
|
||||
const remoteServerUrl = (s: ElectronState) =>
|
||||
s.dataSyncConfig.storageMode === 'cloud' ? OFFICIAL_URL : s.dataSyncConfig.remoteServerUrl || '';
|
||||
|
||||
/**
|
||||
* Returns the raw remoteServerUrl from config without transformation.
|
||||
* Use this when you need the original configured value (e.g., for editing forms).
|
||||
*/
|
||||
const rawRemoteServerUrl = (s: ElectronState) => s.dataSyncConfig.remoteServerUrl || '';
|
||||
|
||||
export const electronSyncSelectors = {
|
||||
isSyncActive,
|
||||
rawRemoteServerUrl,
|
||||
remoteServerUrl,
|
||||
storageMode,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user