diff --git a/demos/rtcube-vite-react/package.json b/demos/rtcube-vite-react/package.json index 72f0eb0..1de0506 100644 --- a/demos/rtcube-vite-react/package.json +++ b/demos/rtcube-vite-react/package.json @@ -9,17 +9,17 @@ "preview": "vite preview" }, "dependencies": { - "@tencentcloud/chat-uikit-react": "^4.5.3", - "@tencentcloud/uikit-base-component-react": "1.1.4", - "@trtc/calls-uikit-react": "4.4.0", - "tuikit-atomicx-react": "4.5.3", + "@tencentcloud/chat-uikit-react": "~6.0.0", + "@tencentcloud/uikit-base-component-react": "~1.3.0", + "@trtc/calls-uikit-react": "~4.5.1", + "tuikit-atomicx-react": "~6.0.0", "classnames": "^2.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.8.0" }, "devDependencies": { - "@tencentcloud/eslint-config-hybrid": "0.0.1", + "@tencentcloud/eslint-config-hybrid": "~0.0.1", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@vitejs/plugin-react": "^4.2.1", diff --git a/demos/rtcube-vite-react/src/App.tsx b/demos/rtcube-vite-react/src/App.tsx index bb3055b..9b8fc4d 100644 --- a/demos/rtcube-vite-react/src/App.tsx +++ b/demos/rtcube-vite-react/src/App.tsx @@ -5,15 +5,9 @@ import { router } from './router'; i18nInit(); -const browserLang = navigator.language; -const language = browserLang.startsWith('zh') ? 'zh-CN' : 'en-US'; - function App() { return ( - + ); diff --git a/demos/rtcube-vite-react/src/locales/en-US/index.ts b/demos/rtcube-vite-react/src/locales/en-US/index.ts index 4241a94..ff18028 100644 --- a/demos/rtcube-vite-react/src/locales/en-US/index.ts +++ b/demos/rtcube-vite-react/src/locales/en-US/index.ts @@ -52,6 +52,17 @@ export const resource = { 'scene.chat.tab.contact': 'Contacts', 'scene.chat.drawer.settings': 'Settings', 'scene.chat.drawer.search': 'Search', + 'scene.chat.profile.edit': 'Edit', + 'scene.chat.profile.dialogTitle': 'Edit Profile', + 'scene.chat.profile.nicknameLabel': 'Nickname', + 'scene.chat.profile.nicknamePlaceholder': 'Enter your nickname', + 'scene.chat.profile.avatarUrlLabel': 'Avatar URL', + 'scene.chat.profile.avatarUrlPlaceholder': 'Enter image URL', + 'scene.chat.profile.avatarUrlHint': 'Avatar supports network URLs only. Please use an image URL starting with http or https.', + 'scene.chat.profile.saveSuccess': 'Profile updated successfully', + 'scene.chat.profile.saveFailed': 'Failed to update profile', + 'scene.chat.profile.validation.nicknameRequired': 'Nickname cannot be empty', + 'scene.chat.profile.validation.avatarUrlInvalid': 'Avatar URL must be a valid http/https URL', 'scene.chat.placeholder.chat.title': 'No Messages', 'scene.chat.placeholder.chat.description': 'Select a conversation to start chatting, or create a new conversation', 'scene.chat.placeholder.contact.title': 'No Contacts', diff --git a/demos/rtcube-vite-react/src/locales/zh-CN/index.ts b/demos/rtcube-vite-react/src/locales/zh-CN/index.ts index 636e28e..55cb622 100644 --- a/demos/rtcube-vite-react/src/locales/zh-CN/index.ts +++ b/demos/rtcube-vite-react/src/locales/zh-CN/index.ts @@ -52,6 +52,17 @@ export const resource = { 'scene.chat.tab.contact': '通讯录', 'scene.chat.drawer.settings': '设置', 'scene.chat.drawer.search': '搜索', + 'scene.chat.profile.edit': '编辑', + 'scene.chat.profile.dialogTitle': '修改个人资料', + 'scene.chat.profile.nicknameLabel': '昵称', + 'scene.chat.profile.nicknamePlaceholder': '请输入昵称', + 'scene.chat.profile.avatarUrlLabel': '头像 URL', + 'scene.chat.profile.avatarUrlPlaceholder': '请输入头像图片链接', + 'scene.chat.profile.avatarUrlHint': '头像仅支持网络 URL,请使用 http 或 https 开头的图片地址。', + 'scene.chat.profile.saveSuccess': '个人资料更新成功', + 'scene.chat.profile.saveFailed': '个人资料更新失败', + 'scene.chat.profile.validation.nicknameRequired': '昵称不能为空', + 'scene.chat.profile.validation.avatarUrlInvalid': '头像 URL 仅支持 http/https 网络地址', 'scene.chat.placeholder.chat.title': '暂无消息', 'scene.chat.placeholder.chat.description': '选择一个对话开始聊天,或创建新的对话', 'scene.chat.placeholder.contact.title': '暂无联系人', diff --git a/demos/rtcube-vite-react/src/pages/StagesPage/StagesPage.tsx b/demos/rtcube-vite-react/src/pages/StagesPage/StagesPage.tsx index 8cd3cec..33d2462 100644 --- a/demos/rtcube-vite-react/src/pages/StagesPage/StagesPage.tsx +++ b/demos/rtcube-vite-react/src/pages/StagesPage/StagesPage.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useUIKit } from '@tencentcloud/chat-uikit-react'; import { Button } from '@tencentcloud/uikit-base-component-react'; import { useNavigate, useParams } from 'react-router-dom'; @@ -13,15 +13,48 @@ import styles from './StagesPage.module.scss'; function StagesPage() { const navigate = useNavigate(); - const { logout, status } = useLoginState(); + const { login, logout, status } = useLoginState(); const { sceneId } = useParams(); const { t } = useUIKit(); + const [isRestoring, setIsRestoring] = useState(status !== 'success'); + const restoreAttempted = useRef(false); useEffect(() => { - if (status !== 'success') { - navigate('/'); + if (status === 'success') { + setIsRestoring(false); + return; + } + + if (restoreAttempted.current) { + return; } - }, []); + restoreAttempted.current = true; + + const stored = localStorage.getItem('userInfo'); + if (!stored) { + navigate('/', { replace: true }); + return; + } + + try { + const userInfo = JSON.parse(stored); + if (userInfo.SDKAppID && userInfo.userID && userInfo.userSig) { + login({ + SDKAppID: userInfo.SDKAppID, + userID: userInfo.userID, + userSig: userInfo.userSig, + }).catch(() => { + localStorage.removeItem('userInfo'); + navigate('/', { replace: true }); + }); + } else { + navigate('/', { replace: true }); + } + } catch { + localStorage.removeItem('userInfo'); + navigate('/', { replace: true }); + } + }, [status]); const scenes = getEnabledScenes(); @@ -49,10 +82,14 @@ function StagesPage() { function handleLogout() { logout(); - localStorage.removeItem('userinfo'); + localStorage.removeItem('userInfo'); navigate('/'); } + if (isRestoring) { + return null; + } + return (
diff --git a/demos/rtcube-vite-react/src/scenes/ChatPage/ChatPage.module.scss b/demos/rtcube-vite-react/src/scenes/ChatPage/ChatPage.module.scss index 647cd86..5609069 100644 --- a/demos/rtcube-vite-react/src/scenes/ChatPage/ChatPage.module.scss +++ b/demos/rtcube-vite-react/src/scenes/ChatPage/ChatPage.module.scss @@ -32,11 +32,20 @@ } } -.chat-content-panel { +.chat-with-search { flex: 1; + display: flex; + flex-direction: row; + overflow: hidden; + min-width: 0; position: relative; } +.chat-content-panel { + flex: 1; + min-width: 0; +} + .contact-detail-panel { flex: 1; } @@ -94,8 +103,7 @@ right: 0; top: 0; bottom: 0; - min-width: 300px; - max-width: 400px; + width: 358px; display: flex; flex-direction: column; background-color: var(--uikit-theme-light-bg-color-operate); @@ -114,7 +122,22 @@ } } -.chat-sidebar__header { +.search-panel { + width: 358px; + flex-shrink: 0; + display: flex; + flex-direction: column; + background-color: var(--uikit-theme-light-bg-color-operate); + border-left: 1px solid var(--uikit-theme-light-stroke-color-primary); + overflow: auto; + + &.dark { + background-color: var(--uikit-theme-dark-bg-color-operate); + border-left-color: var(--uikit-theme-dark-stroke-color-primary); + } +} + +.search-panel__header { position: sticky; top: 0; display: flex; @@ -125,18 +148,18 @@ border-bottom: 1px solid var(--uikit-theme-light-stroke-color-primary); z-index: 10; - .chat-sidebar.dark & { + .search-panel.dark & { background-color: var(--uikit-theme-dark-bg-color-operate); border-bottom: 1px solid var(--uikit-theme-dark-stroke-color-primary); } } -.chat-sidebar__title { +.search-panel__title { font-size: 16px; font-weight: 500; color: var(--uikit-theme-light-text-color-primary); - .chat-sidebar.dark & { + .search-panel.dark & { color: var(--uikit-theme-dark-text-color-primary); } } diff --git a/demos/rtcube-vite-react/src/scenes/ChatPage/ChatPage.tsx b/demos/rtcube-vite-react/src/scenes/ChatPage/ChatPage.tsx index 96ab1a3..eb3daf5 100644 --- a/demos/rtcube-vite-react/src/scenes/ChatPage/ChatPage.tsx +++ b/demos/rtcube-vite-react/src/scenes/ChatPage/ChatPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { ConversationList, Chat, @@ -10,7 +10,7 @@ import { ContactList, ContactInfo, useUIKit, - useConversationListState, + useChatContext, } from '@tencentcloud/chat-uikit-react'; import { TUICallKit } from '@trtc/calls-uikit-react'; import cs from 'classnames'; @@ -20,17 +20,11 @@ import type { TabKey } from './components'; function ChatPage() { const { t, theme } = useUIKit(); - const { activeConversation, setActiveConversation } = useConversationListState(); + const { activeConversation } = useChatContext(); const [activeTab, setActiveTab] = useState('conversation'); const [isChatSettingShow, setChatSettingShow] = useState(false); const [isSearchInChatShow, setSearchInChatShow] = useState(false); - useLayoutEffect(() => { - // default open a c2c conversation, you can change it. - const peerUserID = 'administrator'; - setActiveConversation(`C2C${peerUserID}`); - }, []); - useEffect(() => { setChatSettingShow(false); setSearchInChatShow(false); @@ -41,7 +35,7 @@ function ChatPage() { }; return ( -
+
{/* SideTab Navigation */} @@ -55,51 +49,44 @@ function ChatPage() { {activeTab === 'contact' && }
- {/* Chat Content Panel */} + {/* Chat Content Panel + Search Panel Container */} {activeTab === 'conversation' && ( - } - className={styles['chat-content-panel']} - > - setChatSettingShow(!isChatSettingShow)} - onSearchClick={() => setSearchInChatShow(!isSearchInChatShow)} - /> - - +
+ } + className={styles['chat-content-panel']} + > + setChatSettingShow(!isChatSettingShow)} + onSearchClick={() => setSearchInChatShow(!isSearchInChatShow)} + /> + + + - {/* Chat Setting Sidebar */} - {isChatSettingShow && ( -
-
- {t('scene.chat.drawer.settings')} + {/* Search in Chat Panel (side-by-side with chat, not overlapping) */} + {isSearchInChatShow && ( +
+
+ {t('scene.chat.drawer.search')}
- +
)} - {/* Search in Chat Sidebar */} - {isSearchInChatShow && ( + {/* Chat Setting Sidebar (overlay on the entire chat-with-search area) */} + {isChatSettingShow && (
-
- {t('scene.chat.drawer.search')} - -
- + setChatSettingShow(false)} />
)} - +
)} {/* Contact Detail Panel */} diff --git a/demos/rtcube-vite-react/src/scenes/ChatPage/components/SideTab/SideTab.module.scss b/demos/rtcube-vite-react/src/scenes/ChatPage/components/SideTab/SideTab.module.scss index 86a6fd0..2d368be 100644 --- a/demos/rtcube-vite-react/src/scenes/ChatPage/components/SideTab/SideTab.module.scss +++ b/demos/rtcube-vite-react/src/scenes/ChatPage/components/SideTab/SideTab.module.scss @@ -5,7 +5,8 @@ display: flex; flex-direction: column; align-items: center; - padding: 20px 0; + justify-content: space-between; + padding: 20px 0 24px; transition: background 0.3s; &.dark { @@ -13,9 +14,15 @@ } } +.side-tab__top { + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; +} + .avatar-wrapper { position: relative; - margin-bottom: 24px; cursor: pointer; &:hover { @@ -84,6 +91,12 @@ gap: 16px; } +.profile-action { + .side-tab.dark & { + color: var(--uikit-theme-dark-text-color-primary); + } +} + .tab-item { width: 48px; height: 48px; diff --git a/demos/rtcube-vite-react/src/scenes/ChatPage/components/SideTab/SideTab.tsx b/demos/rtcube-vite-react/src/scenes/ChatPage/components/SideTab/SideTab.tsx index 155a246..ed3eec4 100644 --- a/demos/rtcube-vite-react/src/scenes/ChatPage/components/SideTab/SideTab.tsx +++ b/demos/rtcube-vite-react/src/scenes/ChatPage/components/SideTab/SideTab.tsx @@ -1,6 +1,7 @@ -import { useLoginState, Avatar, useUIKit } from '@tencentcloud/chat-uikit-react'; +import { Avatar, ProfileEditButton, useUIKit } from '@tencentcloud/chat-uikit-react'; import { IconChat, IconUsergroup } from '@tencentcloud/uikit-base-component-react'; import cs from 'classnames'; +import { useLoginState } from 'tuikit-atomicx-react'; import styles from './SideTab.module.scss'; export type TabKey = 'conversation' | 'contact'; @@ -22,30 +23,35 @@ function SideTab({ activeTab, onChange }: SideTabProps) { return (
- {/* User Avatar */} -
- -
-
{loginUserInfo?.userName}
-
ID: {loginUserInfo?.userId}
+
+
+ +
+
{loginUserInfo?.userName}
+
+ ID: + {loginUserInfo?.userId} +
+
-
- {/* Tab Buttons */} -
-
handleTabChange('conversation')} - > - -
-
handleTabChange('contact')} - > - +
+
handleTabChange('conversation')} + > + +
+
handleTabChange('contact')} + > + +
+ +
); } diff --git a/demos/rtcube-vite-react/src/scenes/LivePage/LivePage.module.scss b/demos/rtcube-vite-react/src/scenes/LivePage/LivePage.module.scss index 69051fb..b2bd27e 100644 --- a/demos/rtcube-vite-react/src/scenes/LivePage/LivePage.module.scss +++ b/demos/rtcube-vite-react/src/scenes/LivePage/LivePage.module.scss @@ -1,9 +1,8 @@ -@use "@tencentcloud/uikit-base-component-react/dist/styles/theme/util" as *; - .LivePage { + display: flex; + flex: 1; width: 100%; min-height: 0; - display: flex; flex-direction: column; background: #131417; color: #f6f8ff; @@ -34,12 +33,6 @@ color: rgba(243, 246, 255, 0.7); } -.LivePage__controls { - display: flex; - gap: 14px; - align-items: center; -} - .LivePage__buttonPrimary { min-width: 120px; border-radius: 999px; @@ -60,6 +53,10 @@ color: #cbd5f5; } +.LivePage_leaveLive { + cursor: pointer; +} + .LivePage__roomInput { width: 240px; padding: 10px 16px; @@ -93,6 +90,19 @@ overflow: hidden; } +.LivePage__listContainer { + flex: 1; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + overflow: hidden; + background: rgba(8, 10, 22, 0.96); + border-radius: 20px; + margin: 0 16px; + padding-top: 16px; +} + .LivePage__content { flex: 1; display: flex; @@ -109,9 +119,7 @@ gap: 12px; padding: 0 24px; height: 64px; - @include theme() { - background-color: get(bg-color-operate); - } + background-color: #1f2024; } .LivePage__stageAvatar { diff --git a/demos/rtcube-vite-react/src/scenes/LivePage/LivePage.tsx b/demos/rtcube-vite-react/src/scenes/LivePage/LivePage.tsx index 9ef20ac..8206083 100644 --- a/demos/rtcube-vite-react/src/scenes/LivePage/LivePage.tsx +++ b/demos/rtcube-vite-react/src/scenes/LivePage/LivePage.tsx @@ -1,14 +1,16 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useUIKit } from '@tencentcloud/chat-uikit-react'; -import { Toast, Button, IconLoading } from '@tencentcloud/uikit-base-component-react'; +import { Toast, IconLoading, IconChevronLeft } from '@tencentcloud/uikit-base-component-react'; import { useLiveListState, useRoomEngine, useLoginState, LoginStatus, LiveView, - LiveGift + LiveGift, + LiveList, + type LiveInfo, } from 'tuikit-atomicx-react'; import { AudiencePanel } from './components/AudiencePanel/AudiencePanel'; import { ChatPanel } from './components/ChatPanel/ChatPanel'; @@ -29,31 +31,21 @@ function LivePage() { const { t } = useUIKit(); const { currentLive, joinLive, leaveLive } = useLiveListState(); const roomEngine = useRoomEngine(); - const liveIdFromQuery = searchParams.get('liveId')?.trim() ?? ''; - const [roomId, setRoomId] = useState(liveIdFromQuery); const [isJoinLoading, setIsJoinLoading] = useState(false); - useEffect(() => { - setRoomId(liveIdFromQuery); - }, [liveIdFromQuery]); - const handleJoinLive = async () => { + // Show live list when not in a live room + const showLiveList = !currentLive?.liveId; + + // Handle clicking on a live room from LiveList + const handleLiveRoomClick = async (liveInfo: LiveInfo) => { if (status !== LoginStatus.SUCCESS) { - console.warn('[LivePage] please login before joining live'); - return; - } - if (!roomId) { - console.warn('[LivePage] room id is required'); - return; - } - if (!roomEngine.instance) { - console.warn('[LivePage] room engine not ready'); + Toast.warning({ message: t('scene.live.pleaseLoginFirst') }); return; } try { setIsJoinLoading(true); - await joinLive({ liveId: roomId.trim() }); - console.log('[LivePage] joinLive success', roomId.trim()); + await joinLive({ liveId: liveInfo.liveId }); Toast.success({ message: t('scene.live.joinLiveSuccess') }); } catch (error) { Toast.error({ message: `${t('scene.live.joinLiveFailed')}. error: ${error}` }); @@ -89,85 +81,76 @@ function LivePage() { LiveSuite
{t('scene.live.title')}
-
- setRoomId(event.target.value)} - placeholder={t('scene.live.roomPlaceholder')} - /> - - -
-
-
-
- {hostAvatar ? {hostName} : hostName?.charAt(0) ?? 'H'} -
-
- {hostName} - - {showLive ? `${t('scene.live.roomIdLabel')}: ${currentLive?.liveId ?? ''}` : t('scene.live.noLive')} - -
+ {showLiveList ? ( +
+
-
-
- {showLive ? ( -
- + ) : ( + <> +
+
+ +
+ {hostAvatar ? {hostName} : hostName?.charAt(0) ?? 'H'}
- ) : ( -
{t('scene.live.noLive')}
- )} - {isJoinLoading && ( -
- - {t('scene.live.joinLiveLoading')} +
+ {hostName} + + {showLive ? `${t('scene.live.roomIdLabel')}: ${currentLive?.liveId ?? ''}` : t('scene.live.noLive')} +
- )} -
- {currentLive?.liveId && ( -
-
- )} -
-
+
+
+ {showLive ? ( +
+ +
+ ) : ( +
{t('scene.live.noLive')}
+ )} + {isJoinLoading && ( +
+ + {t('scene.live.joinLiveLoading')} +
+ )} +
+ {currentLive?.liveId && ( +
+ +
+ )} +
+
- + + + )}
);