├── room ├── git_bridge_js.yaml ├── src │ ├── common │ │ ├── index.ts │ │ └── UserPayload.ts │ ├── less │ │ ├── replayer │ │ │ └── index.less │ │ ├── components │ │ │ ├── index.less │ │ │ ├── LoadingPage.less │ │ │ ├── WhiteboardChat.less │ │ │ └── WhiteboardPerspectiveSet.less │ │ ├── index.less │ │ ├── realtime │ │ │ ├── index.less │ │ │ ├── RealtimeRoomBottomLeft.less │ │ │ ├── UploadBtn.less │ │ │ ├── MenuPPTDoc.less │ │ │ ├── RealtimeRoomTopLeft.less │ │ │ ├── RealtimeRoomTopRight.less │ │ │ ├── MenuHotKey.less │ │ │ ├── RealtimeRoomBottomRight.less │ │ │ ├── MenuAnnexBox.less │ │ │ └── RealtimeRoom.less │ │ ├── antd.less │ │ └── theme.less │ ├── replayer │ │ ├── index.ts │ │ └── ReplayerPage.tsx │ ├── realtime │ │ ├── index.ts │ │ ├── RealtimeRoomBottomLeft.tsx │ │ ├── RealtimeRoomTopLeft.tsx │ │ └── MenuBox.tsx │ ├── tools │ │ ├── index.ts │ │ └── OSSCreator.ts │ ├── declare │ │ └── svg.d.ts │ ├── assets │ │ ├── ppt │ │ │ ├── bigcat.jpeg │ │ │ └── qipao.jpeg │ │ └── image │ │ │ ├── hotkey │ │ │ ├── ellipse.svg │ │ │ ├── rectangle.svg │ │ │ ├── selector.svg │ │ │ ├── pencil.svg │ │ │ ├── text.svg │ │ │ └── eraser.svg │ │ │ ├── up_cursor.svg │ │ │ ├── down_cursor.svg │ │ │ ├── player.svg │ │ │ ├── add_icon.svg │ │ │ ├── home.svg │ │ │ ├── ellipse.svg │ │ │ ├── rectangle.svg │ │ │ ├── close.svg │ │ │ ├── selector.svg │ │ │ ├── board.svg │ │ │ ├── board_black.svg │ │ │ ├── annex_box.svg │ │ │ ├── chat.svg │ │ │ ├── player_stop.svg │ │ │ ├── player_begin.svg │ │ │ ├── pencil.svg │ │ │ ├── eraser.svg │ │ │ ├── left_arrow.svg │ │ │ ├── whiteboard_keyboard.svg │ │ │ ├── text.svg │ │ │ ├── right_arrow.svg │ │ │ ├── like_icon.svg │ │ │ ├── arrow.svg │ │ │ ├── add.svg │ │ │ └── image.svg │ ├── components │ │ ├── index.ts │ │ ├── LoadingPage.tsx │ │ ├── ToolBoxUpload.tsx │ │ └── WhiteboardPerspectiveSet.tsx │ ├── custom.d.ts │ └── index.ts ├── .gitignore ├── gib_lib.yaml ├── tsconfig.json ├── package.json └── scripts │ └── svg2base64.js ├── website ├── git_bridge_js.yaml ├── .gitignore ├── tsconfig.prod.json ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── tsconfig.test.json ├── src │ ├── assets │ │ └── image │ │ │ ├── logo.png │ │ │ ├── name_bg.jpg │ │ │ ├── web_app.png │ │ │ ├── hotkey │ │ │ ├── ellipse.svg │ │ │ ├── rectangle.svg │ │ │ ├── selector.svg │ │ │ ├── pencil.svg │ │ │ ├── text.svg │ │ │ ├── arrow.svg │ │ │ ├── eraser.svg │ │ │ └── magic_pen.svg │ │ │ ├── up_cursor.svg │ │ │ ├── down_cursor.svg │ │ │ ├── player.svg │ │ │ ├── add_icon.svg │ │ │ ├── video_gray.svg │ │ │ ├── video_white.svg │ │ │ ├── home.svg │ │ │ ├── more_horizontal.svg │ │ │ ├── video_active.svg │ │ │ ├── ellipse.svg │ │ │ ├── rectangle.svg │ │ │ ├── voice.svg │ │ │ ├── close.svg │ │ │ ├── selector.svg │ │ │ ├── video.svg │ │ │ ├── board.svg │ │ │ ├── board_black.svg │ │ │ ├── user_head.svg │ │ │ ├── rtc_media.svg │ │ │ ├── annex_box.svg │ │ │ ├── chat.svg │ │ │ ├── player_stop.svg │ │ │ ├── player_begin.svg │ │ │ ├── pencil.svg │ │ │ ├── eraser.svg │ │ │ ├── left_arrow.svg │ │ │ ├── whiteboard_keyboard.svg │ │ │ ├── text.svg │ │ │ ├── right_arrow.svg │ │ │ ├── like_icon.svg │ │ │ ├── drop_icon.svg │ │ │ ├── arrow.svg │ │ │ ├── add.svg │ │ │ ├── image.svg │ │ │ ├── mute_white.svg │ │ │ ├── mute_gray.svg │ │ │ ├── set.svg │ │ │ ├── menu.svg │ │ │ └── player_full_screen.svg │ ├── apiMiddleware │ │ ├── index.ts │ │ ├── netlessWhiteboardApi.ts │ │ ├── UserOperator.ts │ │ └── RoomOperator.ts │ ├── declare │ │ ├── react-draggable.d.ts │ │ ├── react-identicons.d.ts │ │ ├── react-clipboard.js.d.ts │ │ └── svg.d.ts │ ├── SDK.ts │ ├── index.tsx │ ├── locale │ │ └── index.ts │ ├── AppOptions.ts │ ├── tokenConfig.json │ ├── pages │ │ ├── PageError.less │ │ ├── AppRoutes.tsx │ │ ├── PageError.tsx │ │ ├── Homepage.less │ │ ├── WhiteboardCreatorPage.tsx │ │ ├── PlayerPage.tsx │ │ ├── WhiteboardPage.tsx │ │ ├── LandingFooter.less │ │ └── Homepage.tsx │ ├── whiteUIKit │ │ ├── WhiteUIInput.tsx │ │ ├── WhiteUIInput.less │ │ ├── WhiteUIButton.less │ │ └── WhiteUIButton.tsx │ └── custom.d.ts ├── theme.js ├── Dockerfile ├── gib_lib.yaml ├── token.js ├── tsconfig.json ├── nginx.conf ├── tslint.json ├── config-overrides.js ├── package.json ├── theme.less └── webpack.config.js ├── .gitignore ├── gib_repo.yaml ├── .vscode └── settings.json ├── package.json ├── LICENSE ├── tslint.json └── README.md /room/git_bridge_js.yaml: -------------------------------------------------------------------------------- 1 | rootDirectory: ../ -------------------------------------------------------------------------------- /website/git_bridge_js.yaml: -------------------------------------------------------------------------------- 1 | rootDirectory: ../ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gib 2 | .gib_stage 3 | node_modules 4 | -------------------------------------------------------------------------------- /room/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./UserPayload"; 2 | -------------------------------------------------------------------------------- /room/src/less/replayer/index.less: -------------------------------------------------------------------------------- 1 | @import "./Replayer"; 2 | -------------------------------------------------------------------------------- /room/src/replayer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ReplayerPage"; 2 | -------------------------------------------------------------------------------- /room/.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | /build 4 | /dist 5 | -------------------------------------------------------------------------------- /room/src/realtime/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RealtimeRoomPage"; 2 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | /build 4 | /dist 5 | -------------------------------------------------------------------------------- /website/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } -------------------------------------------------------------------------------- /room/src/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./OSSCreator"; 2 | export * from "./UploadManager"; 3 | -------------------------------------------------------------------------------- /room/src/declare/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default context; 4 | } 5 | -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-react-whiteboard/HEAD/website/public/favicon.ico -------------------------------------------------------------------------------- /room/src/assets/ppt/bigcat.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-react-whiteboard/HEAD/room/src/assets/ppt/bigcat.jpeg -------------------------------------------------------------------------------- /room/src/assets/ppt/qipao.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-react-whiteboard/HEAD/room/src/assets/ppt/qipao.jpeg -------------------------------------------------------------------------------- /website/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /room/src/less/components/index.less: -------------------------------------------------------------------------------- 1 | @import "./LoadingPage"; 2 | @import "./WhiteboardChat"; 3 | @import "./WhiteboardPerspectiveSet"; 4 | -------------------------------------------------------------------------------- /website/src/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-react-whiteboard/HEAD/website/src/assets/image/logo.png -------------------------------------------------------------------------------- /website/src/assets/image/name_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-react-whiteboard/HEAD/website/src/assets/image/name_bg.jpg -------------------------------------------------------------------------------- /website/src/assets/image/web_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-react-whiteboard/HEAD/website/src/assets/image/web_app.png -------------------------------------------------------------------------------- /website/src/apiMiddleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./netlessWhiteboardApi"; 2 | export * from "./UserOperator"; 3 | export * from "./RoomOperator"; 4 | -------------------------------------------------------------------------------- /website/src/declare/react-draggable.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-draggable" { 2 | const Draggable: any; 3 | export default Draggable; 4 | } 5 | -------------------------------------------------------------------------------- /website/src/declare/react-identicons.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-identicons" { 2 | const Identicon: any; 3 | export default Identicon; 4 | } 5 | -------------------------------------------------------------------------------- /website/src/declare/react-clipboard.js.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-clipboard.js" { 2 | const Clipboard: any; 3 | export default Clipboard; 4 | } 5 | -------------------------------------------------------------------------------- /room/src/less/index.less: -------------------------------------------------------------------------------- 1 | @import "./antd"; 2 | @import "./theme"; 3 | @import "./components/index"; 4 | @import "./realtime/index"; 5 | @import "./replayer/index"; 6 | -------------------------------------------------------------------------------- /website/src/SDK.ts: -------------------------------------------------------------------------------- 1 | import {WhiteWebSdk, DeviceType} from "white-react-sdk"; 2 | 3 | export default new WhiteWebSdk({ 4 | appIdentifier: "283/VGiScM9Wiw2HJg", 5 | }); -------------------------------------------------------------------------------- /website/theme.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "antd": { 3 | '@icon-url': '"~antd-iconfont/iconfont"', 4 | "@primary-color": "#5B908E", 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /room/src/common/UserPayload.ts: -------------------------------------------------------------------------------- 1 | export type UserPayload = { 2 | readonly userId: string; 3 | readonly userUUID: string; 4 | readonly cursorName: string; 5 | }; 6 | -------------------------------------------------------------------------------- /room/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./LoadingPage"; 2 | export * from "./ToolBoxUpload"; 3 | export * from "./WhiteboardChat"; 4 | export * from "./WhiteboardPerspectiveSet"; 5 | -------------------------------------------------------------------------------- /room/src/less/components/LoadingPage.less: -------------------------------------------------------------------------------- 1 | .white-board-loading { 2 | width: 100%; 3 | height: 100vh; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | -------------------------------------------------------------------------------- /gib_repo.yaml: -------------------------------------------------------------------------------- 1 | defaultGitRepoPath: git@github.com:netless-io 2 | scripts: 3 | setup: yarn install --frozen-lockfile 4 | didSetup: if test -d ./node_modules; then echo "true"; else echo "false"; fi -------------------------------------------------------------------------------- /website/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openresty/openresty:centos 2 | COPY ./build /usr/local/openresty/nginx/build 3 | COPY ./nginx.conf /usr/local/openresty/nginx/conf/nginx.conf 4 | 5 | CMD ["openresty"] 6 | 7 | -------------------------------------------------------------------------------- /website/src/declare/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | import * as React from "react"; 3 | const content: (props: React.SVGProps) => React.ReactNode; 4 | export default content; 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "tslint.nodePath": "./node_modules/tslint/lib", 4 | "tslint.configFile": "./tslint.json", 5 | "tslint.enable": true 6 | } -------------------------------------------------------------------------------- /room/src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.jpeg" { 7 | const content: string; 8 | export default content; 9 | } 10 | -------------------------------------------------------------------------------- /website/src/apiMiddleware/netlessWhiteboardApi.ts: -------------------------------------------------------------------------------- 1 | import {UserOperator} from "./UserOperator"; 2 | import {RoomOperator} from "./RoomOperator"; 3 | 4 | export const netlessWhiteboardApi = Object.freeze({ 5 | user: new UserOperator(), 6 | room: new RoomOperator(), 7 | }); 8 | -------------------------------------------------------------------------------- /room/src/assets/image/hotkey/ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /website/src/assets/image/hotkey/ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /room/src/assets/image/hotkey/rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /room/src/assets/image/up_cursor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /website/src/assets/image/hotkey/rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /website/src/assets/image/up_cursor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /website/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | import "netless-react-whiteboard-room/style/index.css"; 5 | import {AppRoutes} from "./pages/AppRoutes"; 6 | 7 | ReactDOM.render( 8 | , 9 | document.getElementById("app-root"), 10 | ); 11 | -------------------------------------------------------------------------------- /room/src/less/realtime/index.less: -------------------------------------------------------------------------------- 1 | @import "./MenuAnnexBox"; 2 | @import "./MenuHotKey"; 3 | @import "./MenuPPTDoc"; 4 | @import "./RealtimeRoom"; 5 | @import "./RealtimeRoomBottomLeft"; 6 | @import "./RealtimeRoomBottomRight"; 7 | @import "./RealtimeRoomTopLeft"; 8 | @import "./RealtimeRoomTopRight"; 9 | @import "./UploadBtn"; 10 | -------------------------------------------------------------------------------- /room/src/assets/image/down_cursor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /website/src/assets/image/down_cursor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netless-react-whiteboard", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "build": "gib build" 8 | }, 9 | "workspaces": [ 10 | "room", 11 | "website" 12 | ], 13 | "devDependencies": { 14 | "typescript": "3.7.5", 15 | "tslint": "^6.1.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /room/src/assets/image/player.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /website/src/assets/image/player.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /room/src/less/antd.less: -------------------------------------------------------------------------------- 1 | @import "~antd/lib/tooltip/style/index.css"; 2 | @import "~antd/lib/popover/style/index.css"; 3 | @import "~antd/lib/message/style/index.css"; 4 | @import "~antd/lib/input/style/index.css"; 5 | @import "~antd/lib/button/style/index.css"; 6 | @import "~antd/lib/modal/style/index.css"; 7 | @import "~antd/lib/badge/style/index.css"; 8 | @import "~antd/lib/upload/style/index.css"; 9 | -------------------------------------------------------------------------------- /website/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /room/src/components/LoadingPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import LoadingIcon from "../assets/image/loading.svg"; 4 | 5 | export class LoadingPage extends React.Component { 6 | 7 | public render(): React.ReactNode { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/gib_lib.yaml: -------------------------------------------------------------------------------- 1 | name: netless-react-whiteboard 2 | path: 3 | dist: ./dist 4 | saveTo: ./node_modules 5 | 6 | scripts: 7 | didSetup: if test -d ./node_modules; then echo "true"; else echo "false"; fi 8 | setup: yarn install --frozen-lockfile 9 | buildDev: yarn run build 10 | buildProd: 11 | - yarn run build 12 | - rm -rf ./dist/prod 13 | - cp -a ./dist/dev ./dist/prod 14 | 15 | dependencies: 16 | - ./netless-react-whiteboard-room -------------------------------------------------------------------------------- /website/src/locale/index.ts: -------------------------------------------------------------------------------- 1 | import {Language, addLocaleData} from "@netless/i18n-react-router"; 2 | 3 | addLocaleData([ 4 | ...require("react-intl/locale-data/zh"), 5 | ...require("react-intl/locale-data/en"), 6 | ]); 7 | 8 | export type Lan = "en" | "zh-CN"; 9 | 10 | export const language = new Language({ 11 | defaultLan: "zh-CN", 12 | localeDescriptions: { 13 | "en": require("./en.yml"), 14 | "zh-CN": require("./zh-CN.yml"), 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /room/src/index.ts: -------------------------------------------------------------------------------- 1 | export {createOSS, OSSOptions, OSSBucketInformation} from "./tools"; 2 | export {UserPayload} from "./common"; 3 | export {LoadingPage} from "./components"; 4 | export {RealtimeRoomPageProps, RealtimeRoomPageCallbacks, RealtimeRoomPageState} from "./realtime"; 5 | export {ReplayerPageProps, ReplayerPageCallbacks, ReplayerPageState} from "./replayer"; 6 | export {default as RealtimeRoomPage} from "./realtime/RealtimeRoomPage"; 7 | export {default as ReplayerPage} from "./replayer/ReplayerPage"; 8 | -------------------------------------------------------------------------------- /room/src/less/components/WhiteboardChat.less: -------------------------------------------------------------------------------- 1 | .chat-box { 2 | height: 450px; 3 | width: 360px; 4 | overflow: auto; 5 | } 6 | 7 | .chat-box-message { 8 | margin-top: 4px; 9 | height: 396px; 10 | } 11 | 12 | .chat-box-input { 13 | position: fixed; 14 | z-index: 2; 15 | width: 360px; 16 | border-bottom-right-radius: 4px; 17 | border-bottom-left-radius: 4px; 18 | textarea { 19 | margin-top: 4.5px; 20 | } 21 | } 22 | 23 | .under-cell { 24 | width: 100%; 25 | height: 42px; 26 | } 27 | -------------------------------------------------------------------------------- /room/src/assets/image/hotkey/selector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /website/src/assets/image/hotkey/selector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /room/src/assets/image/add_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/assets/image/add_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/AppOptions.ts: -------------------------------------------------------------------------------- 1 | import {OSSOptions} from "netless-react-whiteboard-room"; 2 | 3 | const config = require("./tokenConfig"); 4 | 5 | export const netlessToken = { 6 | sdkToken: config.netlessToken, 7 | }; 8 | 9 | export const ossOptions: OSSOptions = Object.freeze({ 10 | accessKeyId: config.ossConfigObj.accessKeyId, 11 | accessKeySecret: config.ossConfigObj.accessKeySecret, 12 | region: config.ossConfigObj.region, 13 | bucket: config.ossConfigObj.bucket, 14 | folder: config.ossConfigObj.folder, 15 | prefix: config.ossConfigObj.prefix, 16 | }); 17 | -------------------------------------------------------------------------------- /website/src/tokenConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "netlessToken": "NETLESSSDK_YWs9M2R5WmdQcFlLcFlTdlQ1ZjRkOFBiNjNnY1RoZ3BDSDlwQXk3Jm5vbmNlPTE2MDI3NTEzNDgwNjgwMCZyb2xlPTAmc2lnPWQzNGIyY2NmOTA5ZjdhYjZlM2NhYmU1OTdjYWJjNGIzOTUzZmViZjMwNmQ2NjRjNGI3NWM2MzYzN2E3NjNkZjM", 3 | "ossConfigObj": { 4 | "accessKeyId": "LTAI4Fv7DfKEtR347iL3uDmu", 5 | "accessKeySecret": "ycdfrOnz0a3MbDzi9Px4fqhXfKWqsy", 6 | "bucket": "netless-agora-whiteboard", 7 | "region": "oss-cn-hangzhou", 8 | "folder": "ppt", 9 | "prefix": "https://netless-agora-whiteboard.oss-cn-hangzhou.aliyuncs.com/" 10 | } 11 | } -------------------------------------------------------------------------------- /website/src/assets/image/video_gray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /website/src/assets/image/video_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /room/src/assets/image/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /website/src/assets/image/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /room/src/tools/OSSCreator.ts: -------------------------------------------------------------------------------- 1 | import * as OSS from "ali-oss"; 2 | 3 | export type OSSBucketInformation = { 4 | readonly bucket: string; 5 | readonly folder: string; 6 | readonly prefix: string; 7 | }; 8 | 9 | export type OSSOptions = OSSBucketInformation & { 10 | readonly accessKeyId: string; 11 | readonly accessKeySecret: string; 12 | readonly region: string; 13 | }; 14 | 15 | export function createOSS(options: OSSOptions): OSS { 16 | return new OSS({ 17 | accessKeyId: options.accessKeyId, 18 | accessKeySecret: options.accessKeySecret, 19 | region: options.region, 20 | bucket: options.bucket, 21 | }); 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /room/gib_lib.yaml: -------------------------------------------------------------------------------- 1 | name: netless-react-whiteboard-room 2 | path: 3 | dist: ./dist 4 | saveTo: ./node_modules 5 | 6 | scripts: 7 | didSetup: if test -d ./node_modules; then echo "true"; else echo "false"; fi 8 | setup: yarn install --frozen-lockfile 9 | buildDev: 10 | - node scripts/svg2base64.js 11 | - yarn run build 12 | - node_modules/.bin/lessc src/less/index.less dist/dev/style/index.css 13 | - cp -a ./dist/assets ./dist/dev/src 14 | buildProd: 15 | - node scripts/svg2base64.js 16 | - yarn run build 17 | - node_modules/.bin/lessc src/less/index.less dist/dev/style/index.css 18 | - rm -rf ./dist/prod 19 | - cp -a ./dist/assets ./dist/dev/src 20 | - cp -a ./dist/dev ./dist/prod -------------------------------------------------------------------------------- /room/src/assets/image/hotkey/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/assets/image/hotkey/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/assets/image/more_horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /website/src/assets/image/video_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /room/src/assets/image/ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ellipse 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/assets/image/ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ellipse 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /room/src/assets/image/hotkey/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /website/src/assets/image/hotkey/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /room/src/assets/image/rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | rectangle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/assets/image/rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | rectangle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/assets/image/voice.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /website/token.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | let config = require("./src/tokenConfig"); 3 | const netlessToken = process.env.netlessToken; 4 | const accessKeyId = process.env.accessKeyId; 5 | const accessKeySecret = process.env.accessKeySecret; 6 | const region = process.env.region; 7 | const bucket = process.env.bucket; 8 | const folder = process.env.folder; 9 | const prefix = process.env.prefix; 10 | 11 | config.netlessToken = netlessToken; 12 | config.ossConfigObj = {}; 13 | config.ossConfigObj.accessKeyId = accessKeyId; 14 | config.ossConfigObj.accessKeySecret = accessKeySecret; 15 | config.ossConfigObj.bucket = bucket; 16 | config.ossConfigObj.region = region; 17 | config.ossConfigObj.folder = folder; 18 | config.ossConfigObj.prefix = prefix; 19 | 20 | fs.writeFileSync("./src/tokenConfig.json", JSON.stringify(config, null, 2)); 21 | -------------------------------------------------------------------------------- /room/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "rootDir": "src", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "skipLibCheck": true, 8 | "lib": [ 9 | "es6", 10 | "dom" 11 | ], 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "declaration": true, 15 | "experimentalDecorators": true, 16 | "jsx": "react", 17 | "moduleResolution": "node", 18 | "forceConsistentCasingInFileNames": false, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "strictNullChecks": true, 22 | "suppressImplicitAnyIndexErrors": true, 23 | "noUnusedLocals": false 24 | }, 25 | "exclude": [ 26 | "node_modules", 27 | "dist", 28 | "build", 29 | "scripts" 30 | ], 31 | "include": [ 32 | "./src/**/*", 33 | "../node_modules/@types/*" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /website/src/pages/PageError.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .page404-box { 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | flex-direction: column; 8 | } 9 | 10 | .page404-image-box { 11 | height: 100vh; 12 | width: 100%; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | } 18 | 19 | .page404-image-inner { 20 | width: 420px; 21 | } 22 | 23 | .page404-image-inner-disconnected { 24 | width: 320px; 25 | } 26 | 27 | .page404-inner { 28 | margin-top: 24px; 29 | text-align: center; 30 | font-size: 16px; 31 | color: @gray; 32 | } 33 | 34 | .page404-btn { 35 | margin-top: 40px; 36 | width: 148px; 37 | } 38 | 39 | 40 | @media screen and (max-width: 660px) { 41 | .page404-image-inner { 42 | width: 280px; 43 | } 44 | .page404-image-inner-disconnected { 45 | width: 100%; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "module": "ESNext", 5 | "target": "es5", 6 | "skipLibCheck": true, 7 | "lib": [ 8 | "es6", 9 | "dom" 10 | ], 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "declaration": true, 14 | "experimentalDecorators": true, 15 | "jsx": "react", 16 | "moduleResolution": "node", 17 | "forceConsistentCasingInFileNames": false, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "strictNullChecks": true, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "noUnusedLocals": false, 23 | "paths": { 24 | "*" : ["../node_modules/white-web-sdk/types/*"] 25 | } 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "dist", 30 | "build", 31 | "scripts" 32 | ], 33 | "include": [ 34 | "./src/**/*" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /room/src/assets/image/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/assets/image/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /room/src/assets/image/selector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | selector 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | user root; 3 | worker_processes 1; 4 | daemon off; 5 | 6 | #error_log logs/error.log notice; 7 | #error_log logs/error.log info; 8 | 9 | #pid logs/nginx.pid; 10 | 11 | events { 12 | worker_connections 1024; 13 | } 14 | 15 | 16 | http { 17 | 18 | 19 | include mime.types; 20 | default_type application/octet-stream; 21 | 22 | #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 23 | # '$status $body_bytes_sent "$http_referer" ' 24 | # '"$http_user_agent" "$http_x_forwarded_for"'; 25 | 26 | #access_log logs/access.log main; 27 | 28 | sendfile on; 29 | #tcp_nopush on; 30 | 31 | #keepalive_timeout 0; 32 | keepalive_timeout 65; 33 | 34 | #gzip on; 35 | 36 | 37 | server { 38 | listen 80; 39 | location / { 40 | root /usr/local/openresty/nginx/build; 41 | try_files $uri $uri/ /index.html; 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /website/src/assets/image/hotkey/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/assets/image/selector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | selector 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/assets/image/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | video 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /room/src/assets/image/board.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /website/src/assets/image/board.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /room/src/assets/image/board_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /website/src/assets/image/board_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /room/src/less/realtime/RealtimeRoomBottomLeft.less: -------------------------------------------------------------------------------- 1 | @import "../theme"; 2 | 3 | .whiteboard-box-bottom-left { 4 | position: absolute; 5 | z-index: 10; 6 | display: flex; 7 | left: 16px; 8 | bottom: 48px; 9 | height: 0; 10 | } 11 | 12 | .whiteboard-box-bottom-left-player { 13 | width: 32px; 14 | height: 32px; 15 | margin-left: 4px; 16 | border-radius: 16px; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | cursor: pointer; 21 | background-color: white; 22 | .hover-transition; 23 | img { 24 | margin-left: 4px; 25 | } 26 | &:hover { 27 | .hover-transition; 28 | background-color: @white_nearly; 29 | } 30 | } 31 | 32 | .whiteboard-box-bottom-left-cell { 33 | width: 32px; 34 | height: 32px; 35 | margin-left: 4px; 36 | border-radius: 16px; 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | cursor: pointer; 41 | background-color: white; 42 | .hover-transition; 43 | &:hover { 44 | .hover-transition; 45 | background-color: @white_nearly; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /website/src/whiteUIKit/WhiteUIInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Input} from "antd"; 3 | import "./WhiteUIInput.less"; 4 | import {InputProps} from "antd/lib/input/Input"; 5 | 6 | export class WhiteUIInput extends React.Component { 7 | 8 | public constructor(props: InputProps) { 9 | super(props); 10 | } 11 | 12 | public render(): React.ReactNode { 13 | const {size, ...restProps} = this.props; 14 | return ( 15 |
16 | 18 |
19 | ); 20 | } 21 | } 22 | 23 | export class WhiteUIInputGray extends React.Component { 24 | 25 | public constructor(props: InputProps) { 26 | super(props); 27 | } 28 | 29 | public render(): React.ReactNode { 30 | return ( 31 |
32 | 33 |
34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /website/src/assets/image/user_head.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 netless 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /website/src/assets/image/rtc_media.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /website/src/whiteUIKit/WhiteUIInput.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .white-input { 4 | .ant-input { 5 | border-radius: 24px; 6 | outline: none; 7 | border: 1px solid #E7EAEE; 8 | box-sizing: border-box; 9 | padding-left: 24px; 10 | padding-right: 24px; 11 | color: @black_darker; 12 | font-size: 14px; 13 | background-color: white; 14 | width: 100%; 15 | &:focus { 16 | outline: 0; 17 | box-shadow: 0 0 0 2px rgba(0,0,0,0); 18 | border-color: @gray; 19 | } 20 | &:hover { 21 | border-color: @gray; 22 | } 23 | } 24 | .ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled) { 25 | border-color: @gray; 26 | } 27 | } 28 | 29 | .white-input-gray { 30 | .ant-input { 31 | border-radius: 0; 32 | outline: none; 33 | border: none; 34 | color: @black_dark; 35 | font-weight: 400; 36 | padding: 0; 37 | height: 24px; 38 | width: 100%; 39 | &:focus { 40 | outline: 0; 41 | box-shadow: 0 0 0 2px rgba(0,0,0,0); 42 | border-color: @gray; 43 | } 44 | &:hover { 45 | border-color: @gray; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /room/src/assets/image/annex_box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /website/src/assets/image/annex_box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /room/src/assets/image/chat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /room/src/less/components/WhiteboardPerspectiveSet.less: -------------------------------------------------------------------------------- 1 | .whiteboard-perspective-box { 2 | padding-bottom: 5px; 3 | } 4 | 5 | .whiteboard-perspective-title { 6 | font-size: 12px; 7 | color: #A3A3A3; 8 | line-height: 16px; 9 | margin-bottom: 8px; 10 | margin-top: 8px; 11 | margin-left: 8px; 12 | } 13 | 14 | .whiteboard-perspective-set-title { 15 | font-size: 12px; 16 | color: #A3A3A3; 17 | } 18 | 19 | .whiteboard-perspective-user-box { 20 | width: 100%; 21 | height: 24px; 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: center; 25 | padding-left: 8px; 26 | padding-right: 8px; 27 | } 28 | 29 | .whiteboard-perspective-set-box { 30 | width: 100%; 31 | padding-right: 8px; 32 | padding-left: 8px; 33 | display: flex; 34 | justify-content: space-between; 35 | align-items: center; 36 | font-size: 12px; 37 | margin-top: 12px; 38 | } 39 | 40 | .whiteboard-perspective-user-head { 41 | width: 24px; 42 | height: 24px; 43 | border-radius: 50%; 44 | border: 1px solid #E7EAEE; 45 | box-sizing: border-box; 46 | overflow: hidden; 47 | } 48 | 49 | .whiteboard-perspective-user-name { 50 | font-size: 12px; 51 | color: #A3A3A3; 52 | } 53 | -------------------------------------------------------------------------------- /website/src/assets/image/chat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /room/src/assets/image/player_stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 播放器播放 (1) 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/assets/image/player_stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 播放器播放 (1) 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /room/src/assets/image/player_begin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 暂停 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/assets/image/player_begin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 暂停 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Netless 17 | 18 | 19 | 26 | 27 | 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /room/src/assets/image/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pencil 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/assets/image/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pencil 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/whiteUIKit/WhiteUIButton.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | .white-button-primary { 3 | .ant-btn-primary { 4 | color: @white; 5 | background-color: @main_color; 6 | border-color: @main_color; 7 | box-shadow: 2px 4px 8px rgba(91, 144, 142, 0.32); 8 | &:hover { 9 | background-color: @main_color; 10 | opacity: 0.8; 11 | box-shadow: 2px 6px 24px rgba(91, 144, 142, 0.32); 12 | } 13 | } 14 | .ant-btn { 15 | border-radius: 100px; 16 | } 17 | .ant-btn-clicked:after { 18 | opacity: 0; 19 | } 20 | } 21 | 22 | 23 | .white-button { 24 | .ant-btn { 25 | border-radius: 100px; 26 | background-color: @white; 27 | box-sizing: border-box; 28 | box-shadow: 2px 4px 8px #E7EAEE; 29 | &:hover { 30 | box-sizing: border-box; 31 | border: 1px solid #E7EAEE; 32 | box-shadow: 2px 6px 24px #E7EAEE; 33 | } 34 | } 35 | .ant-btn-clicked:after { 36 | opacity: 0; 37 | } 38 | } 39 | 40 | .white-button-danger { 41 | .ant-btn { 42 | border-radius: 100px; 43 | box-sizing: border-box; 44 | box-shadow: 2px 4px 8px #E7EAEE; 45 | &:hover { 46 | box-sizing: border-box; 47 | border: 1px solid #E7EAEE; 48 | box-shadow: 2px 6px 24px #E7EAEE; 49 | } 50 | } 51 | .ant-btn-clicked:after { 52 | opacity: 0; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /room/src/assets/image/eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | eraser 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /website/src/assets/image/eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | eraser 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /website/src/pages/AppRoutes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import WhiteboardCreatorPage from "./WhiteboardCreatorPage"; 4 | import WhiteboardPage from "./WhiteboardPage"; 5 | import PlayerPage from "./PlayerPage"; 6 | import Homepage from "./Homepage"; 7 | import PageError from "./PageError"; 8 | 9 | import {AppRouter, HistoryType} from "@netless/i18n-react-router"; 10 | import {language} from "../locale"; 11 | import {message} from "antd"; 12 | 13 | export class AppRoutes extends React.Component<{}, {}> { 14 | 15 | public constructor(props: {}) { 16 | super(props); 17 | } 18 | 19 | public componentDidCatch(error: any, inf: any): void { 20 | message.error(`网页加载发生错误:${error}`); 21 | } 22 | 23 | public render(): React.ReactNode { 24 | return ( 25 | 33 | ); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /room/src/assets/image/hotkey/eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 10 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /website/src/assets/image/hotkey/eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 10 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /room/src/assets/image/left_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 指向-left 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/assets/image/left_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 指向-left 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /room/src/assets/image/whiteboard_keyboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /website/src/assets/image/whiteboard_keyboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /website/src/pages/PageError.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import "./PageError.less"; 4 | 5 | import * as RoomNotFound from "../assets/image/room_not_find.svg"; 6 | 7 | import {withRouter} from "react-router-dom"; 8 | import {RouteComponentProps} from "react-router"; 9 | import {FormattedMessage} from "react-intl"; 10 | import {WhiteUIButton} from "../whiteUIKit/WhiteUIButton"; 11 | 12 | type PageErrorProps = RouteComponentProps<{}>; 13 | 14 | class PageError extends React.Component { 15 | 16 | public render(): React.ReactNode { 17 | return ( 18 |
19 |
20 | 21 |
22 | 23 |
24 | this.props.history.goBack()}> 28 | 29 | 30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | export default withRouter(PageError); 37 | -------------------------------------------------------------------------------- /website/src/apiMiddleware/UserOperator.ts: -------------------------------------------------------------------------------- 1 | import {v1} from "uuid"; 2 | 3 | export type UserInf = { 4 | readonly userId: string; 5 | readonly name: string; 6 | readonly uuid: string; 7 | }; 8 | 9 | export class UserOperator { 10 | 11 | public getUserAndCreateIfNotExit(userId: string): UserInf { 12 | let user = this.getUser(userId); 13 | if (!user) { 14 | user = this.createUser(userId); 15 | } 16 | return user; 17 | } 18 | 19 | public createUser(name: string = "Netless user"): UserInf { 20 | const user: UserInf = Object.freeze({ 21 | userId: "" + Math.floor(Math.random() * 10000), 22 | uuid: v1(), 23 | name: name, 24 | }); 25 | localStorage.setItem(user.userId, JSON.stringify(user)); 26 | 27 | return user; 28 | } 29 | 30 | public getUser(userId: string): UserInf | undefined { 31 | let user: UserInf | undefined = undefined; 32 | const json = localStorage.getItem(userId); 33 | 34 | if (json) { 35 | try { 36 | user = Object.freeze(JSON.parse(json)); 37 | 38 | } catch (error) { 39 | console.error(error); 40 | localStorage.removeItem(userId); 41 | } 42 | } 43 | return user; 44 | } 45 | 46 | public clearUsers(): void { 47 | localStorage.clear(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /room/src/assets/image/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | text 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/assets/image/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | text 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /room/src/assets/image/right_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 指向-left 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/assets/image/right_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 指向-left 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /room/src/less/realtime/UploadBtn.less: -------------------------------------------------------------------------------- 1 | .tool-box-cell-box { 2 | width: 32px; 3 | height: 32px; 4 | margin-left: 4px; 5 | margin-right: 4px; 6 | -webkit-user-select: none; 7 | -moz-user-select: none; 8 | user-select: none; 9 | cursor: pointer; 10 | background-color: white; 11 | } 12 | 13 | .tool-box-cell { 14 | width: 32px; 15 | height: 32px; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | z-index: 1; 20 | } 21 | 22 | .popover-box { 23 | width: 420px; 24 | height: 140px; 25 | } 26 | 27 | .popover-box-cell { 28 | width: 140px; 29 | height: 140px; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | flex-direction: column; 34 | cursor: pointer; 35 | text-align: center; 36 | &:hover { 37 | background-color: #EBEBEB; 38 | transition-timing-function: ease-in-out; 39 | transition-duration: 200ms; 40 | } 41 | } 42 | 43 | .popover-box-cell-img-box { 44 | width: 80px; 45 | height: 40px; 46 | margin-bottom: 6px; 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | } 51 | 52 | .popover-box-cell-img { 53 | height: 40px; 54 | } 55 | 56 | .popover-box-cell-title { 57 | font-size: 12px; 58 | font-weight: bold; 59 | } 60 | 61 | .popover-box-cell-script { 62 | font-size: 11px; 63 | margin-left: 12px; 64 | margin-right: 12px; 65 | height: 36px; 66 | margin-top: 4px; 67 | } 68 | -------------------------------------------------------------------------------- /room/src/assets/image/like_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/assets/image/like_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/assets/image/drop_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/pages/Homepage.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .page-input-box { 4 | width: 100%; 5 | height: 100vh; 6 | display: flex; 7 | flex-direction: row; 8 | background-color: white; 9 | img { 10 | cursor: pointer; 11 | position: absolute; 12 | margin-left: 48px; 13 | margin-top: 48px; 14 | } 15 | } 16 | 17 | .page-input-mid-box { 18 | width: 300px; 19 | } 20 | 21 | .name-button { 22 | width: 300px; 23 | margin-top: 24px; 24 | } 25 | 26 | .name-title { 27 | margin-bottom: 24px; 28 | font-weight: bold; 29 | font-size: 16px; 30 | } 31 | 32 | .page-input-left-mid-box { 33 | width: 360px; 34 | height: 320px; 35 | display: flex; 36 | justify-content: center; 37 | box-sizing: border-box; 38 | padding-right: 20px; 39 | padding-left: 20px; 40 | border-radius: 8px; 41 | box-shadow: 0 0 16px #E7EAEE; 42 | } 43 | 44 | .page-input-left-mid-box-tab { 45 | width: 300px; 46 | margin-top: 12px; 47 | } 48 | 49 | .page-input-left-inner-box { 50 | height: 220px; 51 | width: 100%; 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | flex-direction: column; 56 | margin-top: 8px; 57 | } 58 | 59 | .page-input { 60 | width: 240px; 61 | font-size: 14px; 62 | } 63 | 64 | .page-input-left-box { 65 | width: 50%; 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | flex-direction: column; 70 | } 71 | .page-input-right-box { 72 | width: 50%; 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | flex-direction: column; 77 | background: url("../assets/image/name_bg.jpg") no-repeat scroll center center; 78 | } 79 | -------------------------------------------------------------------------------- /website/src/apiMiddleware/RoomOperator.ts: -------------------------------------------------------------------------------- 1 | import Fetcher from "@netless/fetch-middleware"; 2 | import {netlessToken} from "../AppOptions"; 3 | 4 | const fetcher = new Fetcher(5000, "https://console-api.netless.link"); 5 | 6 | export class RoomOperator { 7 | 8 | public async createRoomApi(name: string, limit: number, isRecord: boolean): Promise { 9 | const json = await fetcher.post({ 10 | path: `v5/rooms`, 11 | headers: {token: netlessToken.sdkToken}, 12 | body: { 13 | name: name, 14 | limit: limit, 15 | isRecord: isRecord, 16 | }, 17 | }); 18 | return json as any; 19 | } 20 | 21 | public async joinRoomApi(uuid: string): Promise { 22 | const json = await fetcher.post({ 23 | path: `v5/tokens/rooms/${uuid}`, 24 | headers: {token: netlessToken.sdkToken}, 25 | body: { 26 | role: "writer", 27 | lifespan: 0, 28 | }, 29 | }); 30 | return json as any; 31 | } 32 | 33 | public async staticConversionApi(sourceUrl: string, targetBucket: string, targetFolder: string): Promise { 34 | const json = await fetcher.post({ 35 | path: `services/static-conversion/tasks`, 36 | query: { 37 | token: netlessToken.sdkToken, 38 | }, 39 | body: { 40 | sourceUrl: sourceUrl, 41 | targetBucket: targetBucket, 42 | targetFolder: targetFolder, 43 | }, 44 | }); 45 | return json as any; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /room/src/components/ToolBoxUpload.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export type IconProps = { 4 | color: string; 5 | }; 6 | 7 | export class ToolBoxUpload extends React.Component { 8 | 9 | public constructor(props: IconProps) { 10 | super(props); 11 | } 12 | 13 | public render(): React.ReactNode { 14 | return ( 15 | 16 | 20 | 23 | 30 | 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /room/src/less/realtime/MenuPPTDoc.less: -------------------------------------------------------------------------------- 1 | @import "../theme"; 2 | .menu-ppt-box { 3 | width: 100%; 4 | height: 100%; 5 | background-color: @black_light; 6 | overflow: scroll; 7 | } 8 | 9 | .menu-ppt-line { 10 | width: 100%; 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | background-color: @black_light; 15 | height: 42px; 16 | position: absolute; 17 | border-bottom: 1px solid #F5F5F5; 18 | z-index: 2; 19 | } 20 | 21 | .menu-ppt-text-box { 22 | margin-left: 24px; 23 | color: white; 24 | } 25 | 26 | .menu-ppt-inner-box { 27 | width: 320px; 28 | margin-left: auto; 29 | margin-right: auto; 30 | display: flex; 31 | flex-wrap: wrap; 32 | } 33 | 34 | .menu-ppt-inner-cell-tag { 35 | margin-top: -8px; 36 | } 37 | 38 | .menu-ppt-inner-cell { 39 | width: 160px; 40 | height: 120px; 41 | } 42 | 43 | .menu-ppt-image-box { 44 | width: 100%; 45 | height: 100%; 46 | padding: 8px; 47 | background-color: @black_light; 48 | cursor: pointer; 49 | &:hover { 50 | background-color: @gray; 51 | } 52 | } 53 | 54 | .menu-ppt-image-box-inner { 55 | width: 144px; 56 | height: 104px; 57 | display: flex; 58 | align-items: center; 59 | justify-content: center; 60 | position: relative; 61 | img { 62 | width: 100%; 63 | } 64 | div { 65 | width: 100%; 66 | position: absolute; 67 | height: 32px; 68 | background-color: @main_color; 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | color: white; 73 | z-index: 2; 74 | bottom: 11px; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /room/src/assets/image/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 箭头 (1) 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/assets/image/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 箭头 (1) 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "member-access": [true, "check-accessor", "check-constructor"], 5 | "semicolon": [true, "always"], 6 | "comment-format": [ 7 | true, 8 | "check-space" 9 | ], 10 | "no-eval": true, 11 | "no-internal-module": true, 12 | "no-unsafe-finally": true, 13 | "one-line": [ 14 | true, 15 | "check-open-brace", 16 | "check-whitespace" 17 | ], 18 | "quotemark": [ 19 | true, 20 | "double" 21 | ], 22 | "triple-equals": [ 23 | true, 24 | "allow-null-check" 25 | ], 26 | "typedef": [true, "call-signature", "parameter", "member-variable-declaration"], 27 | "typedef-whitespace": [ 28 | true, 29 | { 30 | "call-signature": "nospace", 31 | "index-signature": "nospace", 32 | "parameter": "nospace", 33 | "property-declaration": "nospace", 34 | "variable-declaration": "nospace" 35 | } 36 | ], 37 | "curly": true, 38 | "no-invalid-this": [true, "check-function-in-method"], 39 | "no-string-throw": true, 40 | "no-unused-expression": true, 41 | "no-var-keyword": true, 42 | "use-isnan": true, 43 | "indent": [true, "spaces"], 44 | "no-trailing-whitespace": true, 45 | "prefer-const": true, 46 | "trailing-comma": [true, {"multiline": "always", "singleline": "never"}], 47 | "array-type": [true, "array"], 48 | "arrow-parens": [true, "ban-single-arg-parens"], 49 | "no-angle-bracket-type-assertion": true, 50 | "whitespace": [ 51 | true, 52 | "check-branch", 53 | "check-decl", 54 | "check-operator", 55 | "check-separator", 56 | "check-type", 57 | "check-typecast" 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /website/config-overrides.js: -------------------------------------------------------------------------------- 1 | const tsImportPluginFactory = require("ts-import-plugin"); 2 | const {getLoader} = require("react-app-rewired"); 3 | const rewireLess = require('react-app-rewire-less'); 4 | const rewireYAML = require('react-app-rewire-yaml'); 5 | const rewireDefinePlugin = require("react-app-rewire-define-plugin"); 6 | 7 | module.exports = function override(config, env) { 8 | config = rewireYAML(config, env); 9 | 10 | const tsLoader = getLoader( 11 | config.module.rules, 12 | rule => 13 | rule.loader && 14 | typeof rule.loader === "string" && 15 | rule.loader.includes("ts-loader") 16 | ); 17 | 18 | tsLoader.options = { 19 | getCustomTransformers: () => ({ 20 | before: [ 21 | tsImportPluginFactory({ 22 | libraryDirectory: "es", 23 | libraryName: "antd", 24 | style: true, 25 | }), 26 | ], 27 | }) 28 | }; 29 | config = rewireLess.withLoaderOptions({ 30 | modifyVars: require("./theme").antd, 31 | javascriptEnabled: true, 32 | })(config, env); 33 | 34 | config = setupProcessEnv(config, env); 35 | 36 | return config; 37 | }; 38 | 39 | function setupProcessEnv(config, env) { 40 | let scope = process.env.SCOPE; 41 | switch (process.env.SCOPE) { 42 | case "testing": 43 | default: { 44 | scope = "development"; 45 | consoleLambdaOrigin = "https://cloudcapiv4.herewhite.com"; 46 | break; 47 | } 48 | } 49 | config = rewireDefinePlugin(config, env, { 50 | "process.env.SCOPE": JSON.stringify(scope), 51 | "process.env.CONSOLE_LAMBDA_ORIGIN": JSON.stringify(consoleLambdaOrigin), 52 | }); 53 | return config; 54 | } 55 | -------------------------------------------------------------------------------- /room/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netless-react-whiteboard-room", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "style": "dist/index.css", 7 | "private": true, 8 | "license": "MIT", 9 | "scripts": { 10 | "clean": "rimraf dist", 11 | "build": "gjs build" 12 | }, 13 | "peerDependencies": { 14 | "@livechat/ui-kit": "^0.4.0", 15 | "ali-oss": "^6.0.0", 16 | "antd": "^3.0.0", 17 | "rc-tween-one": "^2.4.0", 18 | "react": "^16.8.6", 19 | "react-burger-menu": "^2.6.0", 20 | "react-clipboard.js": "^2.0.0", 21 | "react-dom": "^16.8.6", 22 | "react-dropzone": "4.0.0", 23 | "react-identicons": "^1.0.0", 24 | "uuid": "^3.0.0", 25 | "white-react-sdk": "^2.11.1" 26 | }, 27 | "dependencies": { 28 | "@netless/oss-upload-manager": "^1.0.6", 29 | "@netless/react-loading-bar": "^1.0.0", 30 | "@netless/react-scale-controller": "^1.0.2", 31 | "@netless/react-seek-slider": "^1.0.4", 32 | "@netless/react-tool-box": "^1.0.3", 33 | "react-device-detect": "^1.11.14", 34 | "react-draggable": "^4.2.0" 35 | }, 36 | "devDependencies": { 37 | "@livechat/ui-kit": "^0.4.0-1", 38 | "@types/ali-oss": "^6.0.3", 39 | "@types/react": "^16.4.12", 40 | "@types/react-burger-menu": "^2.2.5", 41 | "@types/react-dom": "^16.0.5", 42 | "@types/react-dropzone": "4.2.0", 43 | "ali-oss": "^6.1.1", 44 | "antd": "^3.10.4", 45 | "less": "^3.9.0", 46 | "mkdirp": "^0.5.1", 47 | "rc-tween-one": "^2.4.1", 48 | "react": "16.8.6", 49 | "react-burger-menu": "^2.6.10", 50 | "react-clipboard.js": "^2.0.12", 51 | "react-dom": "16.8.6", 52 | "react-dropzone": "4.2.10", 53 | "react-identicons": "^1.1.7", 54 | "rimraf": "^2.6.3", 55 | "uuid": "^3.3.2", 56 | "white-react-sdk": "^2.11.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /website/src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export = content; 4 | } 5 | 6 | declare module "*.jpg" { 7 | const content: string; 8 | export = content; 9 | } 10 | declare module "*.png" { 11 | const content: string; 12 | export = content; 13 | } 14 | 15 | declare module "@livechat/ui-kit" { 16 | export const ThemeProvider: any; 17 | export const MessageGroup: any; 18 | export const Message: any; 19 | export const MessageText: any; 20 | export const defaultTheme: any; 21 | export const purpleTheme: any; 22 | export const elegantTheme: any; 23 | export const darkTheme: any; 24 | export const FixedWrapper: any; 25 | export const Avatar: any; 26 | export const TitleBar: any; 27 | export const TextInput: any; 28 | export const MessageList: any; 29 | export const AgentBar: any; 30 | export const Title: any; 31 | export const Subtitle: any; 32 | export const MessageButtons: any; 33 | export const MessageButton: any; 34 | export const MessageTitle: any; 35 | export const MessageMedia: any; 36 | export const TextComposer: any; 37 | export const Row: any; 38 | export const Fill: any; 39 | export const Fit: any; 40 | export const IconButton: any; 41 | export const SendButton: any; 42 | export const EmojiIcon: any; 43 | export const CloseIcon: any; 44 | export const Column: any; 45 | export const RateGoodIcon: any; 46 | export const RateBadIcon: any; 47 | export const Bubble: any; 48 | export const ChatIcon: any; 49 | export const SampleMaximized: any; 50 | export const SampleMinimized: any; 51 | export const AddIcon: any; 52 | export const SendIcon: any; 53 | // purpleTheme, elegantTheme, darkTheme 54 | // MessageText 55 | // Message 56 | // MessageGroup 57 | } 58 | -------------------------------------------------------------------------------- /room/src/assets/image/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 邀请 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/assets/image/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 邀请 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /room/src/assets/image/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 图片 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/assets/image/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 图片 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/src/whiteUIKit/WhiteUIButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Button} from "antd"; 3 | import "./WhiteUIButton.less"; 4 | import {ButtonProps, ButtonSize, ButtonType} from "antd/lib/button/button"; 5 | 6 | export class WhiteUIButton extends React.Component { 7 | 8 | public constructor(props: ButtonProps) { 9 | super(props); 10 | this.getBoxClassName = this.getBoxClassName.bind(this); 11 | this.getButtonHeight = this.getButtonHeight.bind(this); 12 | } 13 | 14 | private getBoxClassName(whiteUIButtonType: ButtonType | undefined): string { 15 | switch (whiteUIButtonType) { 16 | case "primary": 17 | return "white-button-primary"; 18 | case "danger": 19 | return "white-button-danger"; 20 | default: 21 | return "white-button"; 22 | } 23 | } 24 | 25 | private getButtonHeight(buttonSize: ButtonSize | undefined): number { 26 | switch (buttonSize) { 27 | case "small": 28 | return 24; 29 | case "large": 30 | return 48; 31 | default: 32 | return 32; 33 | } 34 | } 35 | 36 | public render(): React.ReactNode { 37 | const {size, ...restProps} = this.props; 38 | if (this.props.type !== "danger") { 39 | return ( 40 |
41 |
45 | ); 46 | } else { 47 | return ( 48 |
49 |
53 | ); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /room/scripts/svg2base64.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const mkdirp = require('mkdirp'); 4 | 5 | const workspace = path.resolve(__dirname, ".."); 6 | const sourcePath = path.resolve(workspace, "src"); 7 | const distPath = path.resolve(workspace, "dist"); 8 | 9 | searchFiles(sourcePath, "assets", function(filePath) { 10 | const absoluteFilePath = path.resolve(sourcePath, filePath); 11 | const targetContent = convertToBase64(absoluteFilePath); 12 | const targetPath = path.resolve(distPath, filePath + ".js"); 13 | const targetDirectory = targetPath.replace(/\/(\w|\.)+$/i, ""); 14 | 15 | mkdirp.sync(targetDirectory); 16 | 17 | fs.writeFileSync(targetPath, targetContent, "utf8"); 18 | }); 19 | 20 | function convertToBase64(filePath) { 21 | switch (filePath.match(/\.[a-zA-Z0-9]+$/i)[0]) { 22 | case ".svg": { 23 | const fileContent = fs.readFileSync(filePath, "utf8"); 24 | const base64 = Buffer.from(fileContent, "utf-8").toString("base64"); 25 | 26 | return "exports.default = \"data:image/svg+xml;base64," + base64 + "\";"; 27 | } 28 | case ".jpeg": { 29 | const fileBuffer = fs.readFileSync(filePath); 30 | const base64 = fileBuffer.toString("base64"); 31 | 32 | return "exports.default = \"data:image/jpeg;base64," + base64 + "\";"; 33 | } 34 | default: { 35 | throw new Error("unexpect file format " + filePath); 36 | } 37 | } 38 | } 39 | 40 | function searchFiles(basicPath, directory, fileHandler) { 41 | const files = fs.readdirSync(path.resolve(basicPath, directory)); 42 | 43 | for (const file of files) { 44 | const absolutePath = path.resolve(basicPath, directory, file); 45 | const relativePath = directory + "/" + file; 46 | 47 | if (fs.lstatSync(absolutePath).isDirectory()) { 48 | searchFiles(basicPath, relativePath, fileHandler); 49 | } else { 50 | fileHandler(relativePath); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netless-react-whiteboard", 3 | "version": "2.0.0", 4 | "main": "src/index.ts", 5 | "author": "Wushuang", 6 | "license": "MIT", 7 | "theme": "./theme.js", 8 | "proxy": "http://localhost:3009/", 9 | "private": true, 10 | "scripts": { 11 | "start": "react-app-rewired start --scripts-version react-scripts-ts", 12 | "build": "gjs build" 13 | }, 14 | "dependencies": { 15 | "ali-oss": "^6.1.1", 16 | "antd": "^3.10.4", 17 | "is-touch-device": "^1.0.1", 18 | "query-string": "5", 19 | "rc-queue-anim": "^1.6.12", 20 | "rc-tween-one": "^2.4.1", 21 | "react": "16.8.6", 22 | "react-burger-menu": "^2.6.10", 23 | "react-clipboard.js": "^2.0.7", 24 | "react-dom": "16.8.6", 25 | "react-dropzone": "4.2.10", 26 | "react-identicons": "^1.1.7", 27 | "uuid": "^3.3.2" 28 | }, 29 | "devDependencies": { 30 | "@livechat/ui-kit": "^0.4.0-1", 31 | "@netless/fetch-middleware": "^1.0.0", 32 | "@netless/i18n-react-router": "^1.0.5", 33 | "@netless/oss-upload-manager": "^1.0.6", 34 | "@netless/react-loading-bar": "^1.0.0", 35 | "@netless/react-scale-controller": "^1.0.0", 36 | "@netless/react-seek-slider": "^1.0.4", 37 | "@netless/react-tool-box": "^1.0.3", 38 | "@types/ali-oss": "^6.0.3", 39 | "@types/is-touch-device": "^1.0.0", 40 | "@types/js-yaml": "^3.11.2", 41 | "@types/node": "^11.12.2", 42 | "@types/query-string": "5", 43 | "@types/react": "^16.4.12", 44 | "@types/react-burger-menu": "^2.2.5", 45 | "@types/react-dom": "^16.0.5", 46 | "@types/react-dropzone": "4.2.0", 47 | "@types/uuid": "^3.4.4", 48 | "@types/webaudioapi": "^0.0.27", 49 | "fork-ts-checker-webpack-plugin": "^1.3.4", 50 | "js-yaml": "^3.13.1", 51 | "react-app-rewire-define-plugin": "^1.0.0", 52 | "react-app-rewire-less": "^2.1.1", 53 | "react-app-rewire-yaml": "^1.1.0", 54 | "react-app-rewired": "^1.5.2", 55 | "react-scripts-ts": "^3.1.0", 56 | "rimraf": "^2.6.3", 57 | "ts-import-plugin": "^1.5.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /website/src/assets/image/mute_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /website/src/assets/image/mute_gray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /website/src/pages/WhiteboardCreatorPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import PageError from "./PageError"; 4 | 5 | import {parse} from "query-string"; 6 | import {Redirect} from "@netless/i18n-react-router"; 7 | import {RouteComponentProps} from "react-router"; 8 | import {netlessWhiteboardApi} from "../apiMiddleware"; 9 | 10 | export type WhiteboardCreatorPageProps = RouteComponentProps<{ 11 | readonly uuid?: string; 12 | }>; 13 | 14 | export type WhiteboardCreatorPageState = { 15 | readonly foundError: boolean; 16 | readonly roomUUID?: string; 17 | }; 18 | 19 | class WhiteboardCreatorPage extends React.Component { 20 | 21 | private readonly userId: string; 22 | 23 | public constructor(props: WhiteboardCreatorPageProps) { 24 | super(props); 25 | this.state = { 26 | foundError: false, 27 | roomUUID: props.match.params.uuid, 28 | }; 29 | const {userId} = parse(props.location.search); 30 | 31 | if (typeof userId !== "string") { 32 | this.userId = netlessWhiteboardApi.user.createUser().userId; 33 | } else { 34 | this.userId = userId; 35 | } 36 | } 37 | 38 | public async componentWillMount(): Promise { 39 | if (this.state.roomUUID === undefined) { 40 | const limit = 0; 41 | const isRecord = true; 42 | try { 43 | const response = await netlessWhiteboardApi.room.createRoomApi("test1", limit, isRecord); 44 | this.setState({roomUUID: response.uuid}); 45 | 46 | } catch (error) { 47 | console.error(error); 48 | this.setState({foundError: true}); 49 | } 50 | } 51 | } 52 | 53 | public render(): React.ReactNode { 54 | if (this.state.foundError) { 55 | return ; 56 | } else if (this.state.roomUUID) { 57 | return ; 58 | } 59 | return null; 60 | } 61 | } 62 | 63 | export default WhiteboardCreatorPage; 64 | -------------------------------------------------------------------------------- /website/theme.less: -------------------------------------------------------------------------------- 1 | @base_black: #141414; 2 | @base_gray: #7A7A7A; 3 | @toolboxShadow: 0 1px 2px 0 rgba(0, 0, 0, 0.20); 4 | @toolboxShadowHover: 0 4px 8px 0 rgba(0, 0, 0, 0.20); 5 | 6 | @main_color: #5B908E; 7 | @main_color_green: #16BD5D; 8 | @black_darker: #141414; 9 | @black_dark: #292929; 10 | @black: #3D3D3D; 11 | @black_light: #525252; 12 | @black_lighter: #666666; 13 | @gray_darker: #7A7A7A; 14 | @gray_dark: #8F8F8F; 15 | @gray: #A2A7AD; 16 | @gray_light: #B8B8B8; 17 | @gray_lighter: #CCCCCC; 18 | @white_not_nearly: #E0E0E0; 19 | @white_nearly: #F5F5F5; 20 | @white_almost: #FAFAFA; 21 | @white: #FFFFFF; 22 | // Roboto,-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif 23 | body { 24 | margin: 0; 25 | padding: 0; 26 | overflow-x: hidden; 27 | font-family: MarkPro, -apple-system, BlinkMacSystemFont, Helvetica Neue, Helvetica, Roboto, Arial, HYQiHei, "PingFang SC", "PingFang TC", sans-serif; 28 | } 29 | 30 | a { 31 | text-decoration: none; 32 | outline: none; 33 | color: @black_dark; 34 | } 35 | a:link { 36 | text-decoration: none; 37 | outline: none; 38 | } 39 | a:visited { 40 | text-decoration: none; 41 | outline: none; 42 | } 43 | a:hover { 44 | text-decoration: none; 45 | outline: none; 46 | } 47 | a:active { 48 | text-decoration: none; 49 | outline: none; 50 | } 51 | 52 | 53 | select { 54 | outline: none; 55 | } 56 | 57 | textarea { 58 | outline: none; 59 | -webkit-appearance: none; 60 | resize: none; 61 | } 62 | 63 | .ant-menu-vertical { 64 | border-right: white; 65 | } 66 | 67 | .hover-transition { 68 | transition-duration: 0.2s; 69 | } 70 | 71 | .ant-menu-inline { 72 | border-right: 1px solid white; 73 | } 74 | 75 | .main-duration { 76 | transition-duration: 0.3s; 77 | -moz-transition-duration: 0.3s; /* Firefox 4 */ 78 | -webkit-transition-duration: 0.3s; /* Safari 和 Chrome */ 79 | -o-transition-duration: 0.3s; /* Opera */ 80 | 81 | transition-timing-function: ease-in-out; 82 | -moz-transition-timing-function: ease-in-out; 83 | -webkit-transition-timing-function: ease-in-out; 84 | -o-transition-timing-function: ease-in-out; 85 | } 86 | 87 | .ant-popover-inner-content { 88 | padding: 0; 89 | } 90 | -------------------------------------------------------------------------------- /room/src/less/theme.less: -------------------------------------------------------------------------------- 1 | @base_black: #141414; 2 | @base_gray: #7A7A7A; 3 | @toolboxShadow: 0 1px 2px 0 rgba(0, 0, 0, 0.20); 4 | @toolboxShadowHover: 0 4px 8px 0 rgba(0, 0, 0, 0.20); 5 | 6 | @main_color: #5B908E; 7 | @main_color_green: #16BD5D; 8 | @black_darker: #141414; 9 | @black_dark: #292929; 10 | @black: #3D3D3D; 11 | @black_light: #525252; 12 | @black_lighter: #666666; 13 | @gray_darker: #7A7A7A; 14 | @gray_dark: #8F8F8F; 15 | @gray: #A2A7AD; 16 | @gray_light: #B8B8B8; 17 | @gray_lighter: #CCCCCC; 18 | @white_not_nearly: #E0E0E0; 19 | @white_nearly: #F5F5F5; 20 | @white_almost: #FAFAFA; 21 | @white: #FFFFFF; 22 | // Roboto,-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif 23 | body { 24 | margin: 0; 25 | padding: 0; 26 | overflow-x: hidden; 27 | font-family: MarkPro, -apple-system, BlinkMacSystemFont, Helvetica Neue, Helvetica, Roboto, Arial, HYQiHei, "PingFang SC", "PingFang TC", sans-serif; 28 | } 29 | 30 | a { 31 | text-decoration: none; 32 | outline: none; 33 | color: @black_dark; 34 | } 35 | a:link { 36 | text-decoration: none; 37 | outline: none; 38 | } 39 | a:visited { 40 | text-decoration: none; 41 | outline: none; 42 | } 43 | a:hover { 44 | text-decoration: none; 45 | outline: none; 46 | } 47 | a:active { 48 | text-decoration: none; 49 | outline: none; 50 | } 51 | 52 | 53 | select { 54 | outline: none; 55 | } 56 | 57 | textarea { 58 | outline: none; 59 | -webkit-appearance: none; 60 | resize: none; 61 | } 62 | 63 | .ant-menu-vertical { 64 | border-right: white; 65 | } 66 | 67 | .hover-transition { 68 | transition-duration: 0.2s; 69 | } 70 | 71 | .ant-menu-inline { 72 | border-right: 1px solid white; 73 | } 74 | 75 | .main-duration { 76 | transition-duration: 0.3s; 77 | -moz-transition-duration: 0.3s; /* Firefox 4 */ 78 | -webkit-transition-duration: 0.3s; /* Safari 和 Chrome */ 79 | -o-transition-duration: 0.3s; /* Opera */ 80 | 81 | transition-timing-function: ease-in-out; 82 | -moz-transition-timing-function: ease-in-out; 83 | -webkit-transition-timing-function: ease-in-out; 84 | -o-transition-timing-function: ease-in-out; 85 | } 86 | 87 | .ant-popover-inner-content { 88 | padding: 0; 89 | } 90 | -------------------------------------------------------------------------------- /room/src/realtime/RealtimeRoomBottomLeft.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import PlayerIcon from "../assets/image/player.svg"; 4 | import LikeIcon from "../assets/image/like_icon.svg"; 5 | 6 | import Tooltip from "antd/lib/tooltip"; 7 | import ScaleController from "@netless/react-scale-controller"; 8 | 9 | import {Room, RoomState} from "white-react-sdk"; 10 | import {UserPayload} from "../common"; 11 | 12 | export type RealtimeRoomBottomLeftProps = { 13 | readonly room: Room; 14 | readonly roomState: RoomState; 15 | readonly userPayload: UserPayload; 16 | readonly disableCustomEvents: boolean; 17 | readonly onGoReplay?: (uuid: string, slice?: string) => void; 18 | }; 19 | 20 | export default class RealtimeRoomBottomLeft extends React.Component { 21 | 22 | private zoomChange = (scale: number): void => { 23 | this.props.room.moveCamera({scale}); 24 | } 25 | 26 | private replay = async (): Promise => { 27 | const room = this.props.room; 28 | await room.disconnect(); 29 | 30 | if (this.props.onGoReplay) { 31 | this.props.onGoReplay(room.uuid, room.slice); 32 | } 33 | } 34 | 35 | public render(): React.ReactNode { 36 | const {roomState} = this.props; 37 | return ( 38 |
39 | 40 | 41 |
43 | 44 |
45 |
46 | {!this.props.disableCustomEvents && ( 47 |
{ 49 | this.props.room.dispatchMagixEvent("handclap", "handclap"); 50 | }}> 51 | 52 |
53 | )} 54 |
55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /website/src/assets/image/set.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/assets/image/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | menu (1) 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /room/src/less/realtime/RealtimeRoomTopLeft.less: -------------------------------------------------------------------------------- 1 | @import "../theme"; 2 | .whiteboard-box-top-left { 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | top: 16px; 7 | left: 8px; 8 | border-radius: 16px; 9 | position: absolute; 10 | height: 32px; 11 | width: 48px; 12 | background-color: white; 13 | border: 1px solid #ffffff; 14 | box-sizing: border-box; 15 | cursor: pointer; 16 | transition-timing-function: ease-in-out; 17 | transition-delay: 100ms; 18 | transition-duration: 100ms; 19 | z-index: 1001; 20 | &:hover { 21 | border: 1px solid @gray_light; 22 | transition-timing-function: ease-in-out; 23 | transition-delay: 100ms; 24 | transition-duration: 100ms; 25 | } 26 | } 27 | 28 | .go-back-title { 29 | width: 100%; 30 | text-align: center; 31 | font-size: 20px; 32 | margin-bottom: 12px; 33 | margin-top: 20px; 34 | } 35 | 36 | .go-back-script { 37 | margin-bottom: 32px; 38 | width: 100%; 39 | text-align: center; 40 | } 41 | 42 | .go-back-btn-box { 43 | display: flex; 44 | align-items: center; 45 | flex-direction: row; 46 | justify-content: space-between; 47 | width: 240px; 48 | margin-left: auto; 49 | margin-right: auto; 50 | margin-bottom: 12px; 51 | } 52 | 53 | .whiteboard-box-top-left-home { 54 | width: 24px; 55 | height: 24px; 56 | border-radius: 2px; 57 | display: flex; 58 | justify-content: center; 59 | align-items: center; 60 | } 61 | 62 | .whiteboard-box-top-left-edit { 63 | display: flex; 64 | justify-content: center; 65 | align-items: center; 66 | color: @gray_light; 67 | font-weight: 400; 68 | margin-right: 6px; 69 | } 70 | 71 | .whiteboard-box-icon { 72 | width: 30px; 73 | height: 30px; 74 | display: flex; 75 | justify-content: center; 76 | align-items: center; 77 | cursor: pointer; 78 | &:hover { 79 | border-radius: 4px; 80 | } 81 | } 82 | 83 | .whiteboard-box-name { 84 | margin-left: 4px; 85 | padding-right: 8px; 86 | -webkit-user-select: none; 87 | -moz-user-select: none; 88 | user-select: none; 89 | -webkit-user-select: none; 90 | cursor: pointer; 91 | display: flex; 92 | flex-direction: row; 93 | } 94 | 95 | .whiteboard-box-date { 96 | padding-left: 4px; 97 | } 98 | 99 | .whiteboard-box-name-input { 100 | padding: 0; 101 | outline: none; 102 | border: none; 103 | } 104 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsRules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-trailing-whitespace": true, 15 | "no-unsafe-finally": true, 16 | "one-line": [ 17 | true, 18 | "check-open-brace", 19 | "check-whitespace" 20 | ], 21 | "quotemark": [ 22 | true, 23 | "double" 24 | ], 25 | "triple-equals": [ 26 | true, 27 | "allow-null-check" 28 | ], 29 | "variable-name": [ 30 | true 31 | ], 32 | "whitespace": [ 33 | true, 34 | "check-branch", 35 | "check-decl", 36 | "check-operator", 37 | "check-separator", 38 | "check-type" 39 | ] 40 | }, 41 | "rules": { 42 | "class-name": true, 43 | "member-access": [true, "check-accessor", "check-constructor"], 44 | "semicolon": [true, "always"], 45 | "comment-format": [ 46 | true, 47 | "check-space" 48 | ], 49 | "no-eval": true, 50 | "no-internal-module": true, 51 | "no-unsafe-finally": true, 52 | "one-line": [ 53 | true, 54 | "check-open-brace", 55 | "check-whitespace" 56 | ], 57 | "quotemark": [ 58 | true, 59 | "double" 60 | ], 61 | "triple-equals": [ 62 | true, 63 | "allow-null-check" 64 | ], 65 | "typedef": [true, "call-signature", "parameter", "member-variable-declaration"], 66 | "typedef-whitespace": [ 67 | true, 68 | { 69 | "call-signature": "nospace", 70 | "index-signature": "nospace", 71 | "parameter": "nospace", 72 | "property-declaration": "nospace", 73 | "variable-declaration": "nospace" 74 | } 75 | ], 76 | "curly": true, 77 | "no-invalid-this": [true, "check-function-in-method"], 78 | "no-string-throw": true, 79 | "no-unused-expression": true, 80 | "no-var-keyword": true, 81 | "use-isnan": true, 82 | "indent": [true, "spaces"], 83 | "no-trailing-whitespace": true, 84 | "prefer-const": true, 85 | "trailing-comma": [true, {"multiline": "always", "singleline": "never"}], 86 | "array-type": [true, "array"], 87 | "arrow-parens": [true, "ban-single-arg-parens"], 88 | "no-angle-bracket-type-assertion": true, 89 | "whitespace": [ 90 | true, 91 | "check-branch", 92 | "check-decl", 93 | "check-operator", 94 | "check-separator", 95 | "check-type", 96 | "check-typecast" 97 | ] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /website/src/pages/PlayerPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import {message} from "antd"; 4 | import {LoadingPage, ReplayerPage} from "netless-react-whiteboard-room"; 5 | import {RouteComponentProps} from "react-router"; 6 | import {Redirect} from "@netless/i18n-react-router"; 7 | import {netlessWhiteboardApi} from "../apiMiddleware"; 8 | import PageError from "./PageError"; 9 | import whiteWebSdk from "../SDK"; 10 | 11 | export type PlayerPageProps = RouteComponentProps<{ 12 | readonly uuid: string; 13 | readonly userId: string; 14 | }>; 15 | 16 | export type PlayerPageState = { 17 | readonly roomToken?: string; 18 | readonly redirectPath?: string; 19 | readonly fetchTokenFailed: boolean; 20 | }; 21 | 22 | export default class PlayerPage extends React.Component { 23 | 24 | private readonly userId: string; 25 | 26 | public constructor(props: PlayerPageProps) { 27 | super(props); 28 | this.userId = this.props.match.params.userId; 29 | this.state = {fetchTokenFailed: false}; 30 | } 31 | 32 | public componentWillMount(): void { 33 | this.replayRoom().catch(error => { 34 | message.error("回放房间失败:" + error.message); 35 | console.error(error); 36 | this.setState({fetchTokenFailed: true}); 37 | }); 38 | } 39 | 40 | private async replayRoom(): Promise { 41 | const uuid = this.props.match.params.uuid; 42 | const roomToken = await netlessWhiteboardApi.room.joinRoomApi(uuid); 43 | this.setState({roomToken}); 44 | } 45 | 46 | private onGoBack = (): void => { 47 | this.setState({redirectPath: "/"}); 48 | } 49 | 50 | private onGoToRealtimeRoom = (uuid: string): void => { 51 | this.setState({redirectPath: `/whiteboard/${uuid}/${this.userId}/`}); 52 | } 53 | 54 | public render(): React.ReactNode { 55 | if (this.state.redirectPath) { 56 | return ; 57 | 58 | } else if (this.state.fetchTokenFailed) { 59 | return ; 60 | 61 | } else if (this.state.roomToken === undefined) { 62 | return ; 63 | 64 | } else { 65 | return ; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /room/src/realtime/RealtimeRoomTopLeft.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import HomeIcon from "../assets/image/home.svg"; 4 | 5 | import message from "antd/lib/message"; 6 | import Button from "antd/lib/button"; 7 | import Modal from "antd/lib/modal"; 8 | import Tooltip from "antd/lib/tooltip"; 9 | import {Room} from "white-react-sdk"; 10 | 11 | export type RealtimeRoomLeftProps = { 12 | readonly room: Room; 13 | readonly onGoBack?: () => void; 14 | }; 15 | 16 | export type RealtimeRoomLeftState = { 17 | readonly isMouseOn: boolean; 18 | readonly isVisible: boolean; 19 | }; 20 | 21 | export default class RealtimeRoomTopLeft extends React.Component { 22 | 23 | public constructor(props: RealtimeRoomLeftProps) { 24 | super(props); 25 | this.state = { 26 | isMouseOn: false, 27 | isVisible: false, 28 | }; 29 | } 30 | private handleGoBackHome = (): void => { 31 | this.setState({isVisible: !this.state.isVisible}); 32 | } 33 | 34 | private disconnect = async (): Promise => { 35 | try { 36 | await this.props.room.disconnect(); 37 | 38 | if (this.props.onGoBack) { 39 | this.props.onGoBack(); 40 | } 41 | } catch (err) { 42 | message.error("disconnect fail"); 43 | this.handleGoBackHome(); 44 | } 45 | } 46 | 47 | public render(): React.ReactNode { 48 | 49 | return ( 50 | 51 |
52 | 53 |
54 | this.setState({isVisible: false})}> 58 |
59 | Are you leaving the room? 60 |
61 |
62 | If you leave, we will delete all temporary user information. 63 |
64 |
65 | 69 | 73 |
74 |
75 |
76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /room/src/less/realtime/RealtimeRoomTopRight.less: -------------------------------------------------------------------------------- 1 | .whiteboard-box-top-right { 2 | position: fixed; 3 | right: 8px; 4 | height: 0; 5 | margin-top: 16px; 6 | z-index: 2; 7 | } 8 | 9 | .whiteboard-box-top-right-mid { 10 | border-radius: 16px; 11 | height: 32px; 12 | display: flex; 13 | flex-direction: row-reverse; 14 | background-color: white; 15 | width: 154px; 16 | -webkit-user-select: none; 17 | -moz-user-select: none; 18 | user-select: none; 19 | padding-right: 16px; 20 | } 21 | 22 | .whiteboard-top-bar-box { 23 | margin-left: 16px; 24 | height: 32px; 25 | width: 32px; 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | cursor: pointer; 30 | background-color: white; 31 | border-radius: 50%; 32 | overflow: hidden; 33 | border: 2px solid #005BF6; 34 | margin-bottom: 8px; 35 | } 36 | 37 | .whiteboard-top-bar-btn { 38 | width: 32px; 39 | height: 32px; 40 | display: flex; 41 | justify-content: center; 42 | align-items: center; 43 | cursor: pointer; 44 | pointer-events: auto; 45 | -webkit-user-select: none; 46 | -moz-user-select: none; 47 | user-select: none; 48 | background-color: white; 49 | } 50 | 51 | .whiteboard-perspective { 52 | width: 128px; 53 | } 54 | 55 | 56 | .whiteboard-chat { 57 | width: 360px; 58 | } 59 | 60 | .white-btn-size { 61 | width: 82px; 62 | } 63 | 64 | .whiteboard-share-box { 65 | display: flex; 66 | flex-direction: row; 67 | justify-content: center; 68 | height: 48px; 69 | align-items: center; 70 | margin-top: 8px; 71 | } 72 | 73 | .whiteboard-set-box { 74 | display: flex; 75 | flex-direction: column; 76 | justify-content: center; 77 | //height: 48px; 78 | align-items: center; 79 | margin-top: 8px; 80 | } 81 | 82 | .whiteboard-set-box-img { 83 | height: 36px; 84 | width: 36px; 85 | display: flex; 86 | justify-content: center; 87 | align-items: center; 88 | cursor: pointer; 89 | background-color: white; 90 | border-radius: 50%; 91 | overflow: hidden; 92 | border: 2px solid #005BF6; 93 | margin-bottom: 8px; 94 | } 95 | 96 | .whiteboard-set-box-inner { 97 | margin-top: 8px; 98 | span { 99 | font-weight: bold; 100 | color: black; 101 | } 102 | } 103 | 104 | .whiteboard-share-footer { 105 | display: flex; 106 | flex-direction: row; 107 | justify-content: flex-end; 108 | margin-top: 24px; 109 | } 110 | 111 | .whiteboard-set-footer { 112 | display: flex; 113 | flex-direction: row; 114 | justify-content: center; 115 | align-items: center; 116 | margin-top: 24px; 117 | } 118 | 119 | .whiteboard-share-text { 120 | width: 100%; 121 | font-size: 12px; 122 | margin-left: 12px; 123 | background-color: #E9EDF4; 124 | color: #ADBCD9; 125 | input { 126 | font-size: 12px; 127 | background-color: #E9EDF4; 128 | color: #ADBCD9; 129 | } 130 | } 131 | 132 | .whiteboard-copy-box { 133 | width: 28px; 134 | height: 28px; 135 | display: flex; 136 | align-items: center; 137 | justify-content: center; 138 | cursor: pointer; 139 | } 140 | -------------------------------------------------------------------------------- /room/src/components/WhiteboardPerspectiveSet.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Identicon from "react-identicons"; 4 | import Switch from "antd/lib/switch"; 5 | import message from "antd/lib/message"; 6 | 7 | import {RoomState, ViewMode, Room} from "white-react-sdk"; 8 | 9 | export type WhiteboardPerspectiveSetProps = { 10 | readonly room: Room; 11 | readonly roomState: RoomState; 12 | }; 13 | 14 | export default class WhiteboardPerspectiveSet extends React.Component { 15 | 16 | public render(): React.ReactNode { 17 | const {roomState, room} = this.props; 18 | const perspectiveState = roomState.broadcastState; 19 | return ( 20 |
21 |
22 |
23 | 当前视角 24 |
25 |
26 |
27 | 30 |
31 |
32 | {perspectiveState.broadcasterInformation && perspectiveState.broadcasterInformation.nickName.substring(0, 6)} 33 |
34 |
35 |
36 |
37 |
38 | 跟随视角 39 |
40 | { 44 | if (checked) { 45 | room.setViewMode(ViewMode.Follower); 46 | } else { 47 | room.setViewMode(ViewMode.Freedom); 48 | } 49 | }}/> 50 |
51 |
52 |
53 | 成为演讲者 54 |
55 | { 58 | if (checked) { 59 | room.setViewMode(ViewMode.Broadcaster); 60 | message.info("进入演讲模式,他人会跟随您的视角"); 61 | } else { 62 | room.setViewMode(ViewMode.Freedom); 63 | } 64 | }}/> 65 |
66 |
67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /website/src/assets/image/player_full_screen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 全屏 (1) 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /room/src/less/realtime/MenuHotKey.less: -------------------------------------------------------------------------------- 1 | .menu-hot-key-box { 2 | width: 100%; 3 | height: 100%; 4 | background-color: white; 5 | overflow-y: scroll; 6 | } 7 | 8 | .menu-tool-box { 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | height: 42px; 13 | width: 100%; 14 | background-color: white; 15 | } 16 | 17 | .menu-title-line { 18 | width: 100%; 19 | display: flex; 20 | justify-content: space-between; 21 | align-items: center; 22 | height: 42px; 23 | background-color: white; 24 | position: absolute; 25 | border-bottom: 1px solid #F5F5F5; 26 | z-index: 10; 27 | } 28 | 29 | .menu-title-text-box { 30 | margin-left: 24px; 31 | } 32 | 33 | .menu-title-close-icon { 34 | width: 16px; 35 | } 36 | 37 | .menu-hot-key-title { 38 | font-size: 12px; 39 | color: #A3A3A3; 40 | margin-left: 24px; 41 | margin-top: 20px; 42 | margin-bottom: 8px; 43 | } 44 | 45 | .menu-close-btn { 46 | width: 32px; 47 | height: 32px; 48 | border-radius: 50%; 49 | display: flex; 50 | justify-content: center; 51 | align-items: center; 52 | cursor: pointer; 53 | margin-right: 16px; 54 | } 55 | 56 | .menu-tool-box-left { 57 | display: flex; 58 | flex-direction: row; 59 | margin-left: 24px; 60 | height: 24px; 61 | line-height: 24px; 62 | } 63 | 64 | .menu-tool-box-right { 65 | margin-right: 24px; 66 | width: 24px; 67 | height: 24px; 68 | border-radius: 4px; 69 | background-color: #F5F5F5; 70 | display: flex; 71 | justify-content: center; 72 | align-items: center; 73 | font-size: 12px; 74 | } 75 | 76 | .menu-other-hot-box { 77 | width: 24px; 78 | height: 24px; 79 | border-radius: 4px; 80 | background-color: #F5F5F5; 81 | display: flex; 82 | justify-content: center; 83 | align-items: center; 84 | font-size: 12px; 85 | font-weight: 600; 86 | } 87 | 88 | .menu-hot-key-title { 89 | font-size: 12px; 90 | color: #A3A3A3; 91 | margin-left: 24px; 92 | margin-top: 20px; 93 | margin-bottom: 8px; 94 | } 95 | 96 | .menu-other-hot-box-word { 97 | height: 24px; 98 | border-radius: 4px; 99 | background-color: #F5F5F5; 100 | display: flex; 101 | justify-content: center; 102 | align-items: center; 103 | font-size: 12px; 104 | padding-left: 8px; 105 | padding-right: 8px; 106 | font-weight: 600; 107 | } 108 | 109 | .menu-other-hot-box-plus { 110 | margin-right: 5px; 111 | } 112 | 113 | .menu-other-hot-out-box { 114 | display: flex; 115 | flex-direction: row; 116 | background-color: white; 117 | } 118 | 119 | .menu-other-hot-out-box-mix { 120 | margin-right: 8px; 121 | } 122 | 123 | .menu-other-box { 124 | display: flex; 125 | justify-content: space-between; 126 | align-items: center; 127 | height: 42px; 128 | background-color: white; 129 | } 130 | 131 | .menu-other-box-left { 132 | margin-left: 24px; 133 | } 134 | 135 | .menu-other-array-box { 136 | display: flex; 137 | flex-direction: row; 138 | margin-right: 24px; 139 | } 140 | 141 | .menu-tool-box-icon { 142 | height: 17px; 143 | opacity: 0.7; 144 | } 145 | 146 | .menu-tool-box-icon-box { 147 | width: 24px; 148 | height: 24px; 149 | display: flex; 150 | justify-content: center; 151 | align-items: center; 152 | margin-right: 10px; 153 | } 154 | -------------------------------------------------------------------------------- /website/src/pages/WhiteboardPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import PageError from "./PageError"; 4 | 5 | import {message} from "antd"; 6 | import {LoadingPage, RealtimeRoomPage, UserPayload} from "netless-react-whiteboard-room"; 7 | import {RouteComponentProps} from "react-router"; 8 | import {Redirect} from "@netless/i18n-react-router"; 9 | import {netlessWhiteboardApi} from "../apiMiddleware"; 10 | import {ossOptions} from "../AppOptions"; 11 | import whiteWebSdk from "../SDK"; 12 | 13 | export type WhiteboardPageProps = RouteComponentProps<{ 14 | readonly uuid: string; 15 | readonly userId: string; 16 | }>; 17 | 18 | export type WhiteboardPageState = { 19 | readonly connectFailed: boolean; 20 | readonly redirectPath?: string; 21 | readonly roomInfo?: { 22 | readonly roomToken: string; 23 | readonly userPayload: UserPayload; 24 | }; 25 | }; 26 | 27 | export default class WhiteboardPage extends React.Component { 28 | 29 | public constructor(props: WhiteboardPageProps) { 30 | super(props); 31 | this.state = {connectFailed: false}; 32 | } 33 | 34 | public componentWillMount(): void { 35 | this.joinRoom().catch(error => { 36 | message.error("加入房间失败:" + error.message); 37 | console.error(error); 38 | this.setState({connectFailed: true}); 39 | }); 40 | } 41 | 42 | private async joinRoom(): Promise { 43 | const uuid = this.props.match.params.uuid; 44 | const userId = this.props.match.params.userId; 45 | const roomToken = await netlessWhiteboardApi.room.joinRoomApi(uuid); 46 | const user = netlessWhiteboardApi.user.getUserAndCreateIfNotExit(userId); 47 | 48 | this.setState({roomInfo: { 49 | roomToken: roomToken, 50 | userPayload: Object.freeze({ 51 | userId: userId, 52 | userUUID: user.uuid, 53 | cursorName: user.name, 54 | }), 55 | }}); 56 | } 57 | 58 | private onGoBack = (): void => { 59 | this.setState({redirectPath: "/"}); 60 | } 61 | 62 | private onGoReplay = (uuid: string, slice?: string): void => { 63 | this.setState({redirectPath: `/replay/${uuid}/${this.props.match.params.userId}/`}); 64 | } 65 | 66 | public render(): React.ReactNode { 67 | if (this.state.redirectPath) { 68 | return ; 69 | 70 | } else if (this.state.connectFailed) { 71 | return ; 72 | 73 | } else if (!this.state.roomInfo) { 74 | return ; 75 | 76 | } else { 77 | return ( 78 | 87 | ); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /room/src/less/realtime/RealtimeRoomBottomRight.less: -------------------------------------------------------------------------------- 1 | @import "../theme"; 2 | .whiteboard-box-bottom-right { 3 | position: absolute; 4 | z-index: 10; 5 | display: flex; 6 | right: 8px; 7 | bottom: 42px; 8 | height: 0; 9 | } 10 | 11 | .whiteboard-box-bottom-right-mid { 12 | height: 32px; 13 | background-color: white; 14 | border-radius: 16px; 15 | display: flex; 16 | justify-content: space-between; 17 | padding-right: 16px; 18 | padding-left: 16px; 19 | } 20 | 21 | .whiteboard-bottom-right-cell { 22 | width: 32px; 23 | height: 32px; 24 | background-color: white; 25 | -webkit-user-select: none; 26 | -moz-user-select: none; 27 | user-select: none; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | cursor: pointer; 32 | } 33 | 34 | .whiteboard-chat { 35 | width: 360px; 36 | } 37 | 38 | .whiteboard-annex-box { 39 | height: 32px; 40 | background-color: @white; 41 | border-radius: 2px; 42 | display: flex; 43 | flex-direction: row; 44 | cursor: pointer; 45 | -webkit-user-select: none; 46 | -moz-user-select: none; 47 | user-select: none; 48 | } 49 | 50 | .whiteboard-annex-arrow-left { 51 | width: 32px; 52 | height: 32px; 53 | border-top-left-radius: 2px; 54 | border-bottom-left-radius: 2px; 55 | display: flex; 56 | justify-content: center; 57 | align-items: center; 58 | &:hover { 59 | background-color: #EBEBEB; 60 | transition-timing-function: ease-in-out; 61 | transition-delay: 100ms; 62 | transition-duration: 200ms; 63 | cursor: pointer; 64 | } 65 | img { 66 | height: 14px; 67 | opacity: 0.6; 68 | } 69 | } 70 | 71 | .whiteboard-annex-arrow-right { 72 | width: 32px; 73 | height: 32px; 74 | border-bottom-right-radius: 2px; 75 | border-top-right-radius: 2px; 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | &:hover { 80 | background-color: #EBEBEB; 81 | transition-timing-function: ease-in-out; 82 | transition-delay: 100ms; 83 | transition-duration: 200ms; 84 | cursor: pointer; 85 | } 86 | img { 87 | height: 14px; 88 | opacity: 0.6; 89 | } 90 | } 91 | 92 | .whiteboard-annex-arrow-mid { 93 | display: flex; 94 | justify-content: center; 95 | align-items: center; 96 | flex-direction: row; 97 | padding-right: 6px; 98 | &:hover { 99 | background-color: #EBEBEB; 100 | transition-timing-function: ease-in-out; 101 | transition-delay: 100ms; 102 | transition-duration: 200ms; 103 | cursor: pointer; 104 | } 105 | } 106 | 107 | .whiteboard-annex-arrow-page { 108 | margin-left: 8px; 109 | color: #7A7A7A; 110 | } 111 | 112 | .whiteboard-annex-img-box { 113 | width: 30px; 114 | height: 32px; 115 | display: flex; 116 | justify-content: center; 117 | align-items: center; 118 | cursor: pointer; 119 | } 120 | 121 | .whiteboard-update-box { 122 | width: 152px; 123 | height: 152px; 124 | display: flex; 125 | flex-wrap: wrap; 126 | justify-content: space-between; 127 | } 128 | 129 | .whiteboard-update-box-cell { 130 | width: 76px; 131 | height: 76px; 132 | display: flex; 133 | justify-content: center; 134 | align-items: center; 135 | cursor: pointer; 136 | .hover-transition; 137 | &:hover { 138 | .hover-transition; 139 | background-color: #F5F5F5; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netless-react-whiteboard 2 | 3 | ## 一、前言 4 | 5 | 1. `netless-react-whiteboard` 是 netless 提供的 web 实践项目,目的是为了让用户更加具象化的了解 netless 白板的功能和场景。 6 | 2. 我们采用 react 前端框架,Typescript 作为编程语言的技术选型编写这个项目,目的是让项目更加容易维护和迭代。 7 | 3. 我们将项目很多可复用的组件都抽象成了 react 的控件托管在 [netless-io](https://github.com/netless-io) 这个仓库,用户可以参考相关代码或者直接使用组件。我们也非常欢迎指正错误提交 PR. 8 | 4. 如有疑问可以发邮件到: rick@herewhite.com 9 | 10 | ## 二、开发准备 11 | 12 | ### 1. 概述 13 | 14 | 1. 云服务 token 获取,要启动这个项目完整的功能需要接入三个类型的云服务。 15 | 16 | - 互动白板 17 | - 云存储 18 | 19 | 该 demo 使用的是 netless 自研的互动白板,阿里云的云存储作为基础选型。 20 | 21 | 2. 填写 `AppOptions.ts` 文件 22 | 23 | ``` typescript 24 | export const netlessToken = "xxx"; 25 | 26 | export const ossConfigObj = { 27 | accessKeyId: "xxx", 28 | accessKeySecret: "xxx", 29 | region: "oss-cn-xxx", 30 | bucket: "xxx", 31 | folder: "xxx", 32 | prefix: "https://xxx.oss-cn-xxx.aliyuncs.com/", 33 | }; 34 | 35 | ``` 36 | 37 | ### 2. 白板 Token 38 | 39 | 1. 用途:用于白板的权限管理。 40 | 2. 获取方式: 41 | - 地址: 42 | ![1558157918260](https://ohuuyffq2.qnssl.com/1558157918260.jpg) 43 | 44 | 3. 填写参数 45 | 46 | ``` js 47 | export const netlessToken = "xxx"; 48 | ``` 49 | 50 | 4. 如果要体验 `ppt、pptx、word、pdf 转图片` 或者 `pptx 转网页` 服务请去管理控制台先开启对应的服务。 51 | 52 | ### 3. 云存储 Token 53 | 54 | 1. 用途:存储互动白板的图片 ppt 等静态资源。 55 | 2. 获取方式: 56 | - 地址: 57 | ![1558158253900](https://ohuuyffq2.qnssl.com/1558158253900.jpg) 58 | 59 | 3. 填写参数 60 | 61 | ``` js 62 | export const ossConfigObj = { 63 | accessKeyId: "xxx", 64 | accessKeySecret: "xxx", 65 | region: "oss-cn-xxx", 66 | bucket: "xxx", 67 | folder: "xxx", 68 | prefix: "https://xxx.oss-cn-xxx.aliyuncs.com/", 69 | }; 70 | ``` 71 | 72 | ### 4. 注意事项 73 | 74 | **以上 token 都是用户的核心资产,本项目只是为了方便演示才直接放在项目当中,客户正式商用的时候请妥善保管。** 75 | 76 | ## 三、安装启动 77 | 78 | ### 1. 基础工具 79 | 80 | 1. node >= 8 81 | 2. 使用 `npm` 或者 `yarn` 管理依赖库。以下都用 `yarn` 命令说明。 82 | 83 | ### 2. 获取 84 | 85 | ```shell 86 | git clone git@github.com:netless-io/netless-react-whiteboard.git 87 | ``` 88 | 89 | ### 3. 安装 90 | 91 | ```shell 92 | # 访问目标文件 93 | cd netless-react-whiteboard 94 | 95 | # 安装依赖 96 | yarn 97 | ``` 98 | 99 | ### 4. 填写配置文件 100 | 101 | > 如果前面已经填写,这里不用重复 102 | 103 | ```typescript 104 | export const netlessToken = ""; 105 | 106 | export const ossConfigObj = { 107 | accessKeyId: "", 108 | accessKeySecret: "", 109 | region: "", 110 | bucket: "", 111 | folder: "", 112 | prefix: "", 113 | }; 114 | 115 | ``` 116 | 117 | ### 5. 启动 118 | 119 | ```shell 120 | # 启动项目 121 | yarn start 122 | ``` 123 | 124 | ### 6. 构建 125 | 126 | ```shell 127 | # 构建项目 128 | yarn build 129 | ``` 130 | 131 | ### 7. 效果 132 | 133 | 1. 首页 134 | ![1558160175316](https://ohuuyffq2.qnssl.com/1558160175316.jpg) 135 | 136 | 2. 白板 137 | ![1558160181194](https://ohuuyffq2.qnssl.com/1558160181194.jpg) 138 | 139 | ## 四、深度使用 140 | 141 | 1. 文档站 142 | 143 | 地址: 144 | 145 | ![develop-netless-io](https://ohuuyffq2.qnssl.com/develop-netless-io.png) 146 | 147 | 2. 管理控制台 148 | 149 | 地址: 150 | 151 | ![console-netless-io](https://ohuuyffq2.qnssl.com/console-netless-io.png) 152 | 153 | 3. 官网 154 | 155 | 地址: 156 | 157 | ![home-netless-io](https://ohuuyffq2.qnssl.com/home-netless-io.png) 158 | 159 | 4. 开源控件托管 160 | 161 | 地址: 162 | 163 | ![netless-io-github](https://ohuuyffq2.qnssl.com/netless-io-github.png) 164 | -------------------------------------------------------------------------------- /room/src/realtime/MenuBox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import {slide as Menu, reveal as MenuLeft} from "react-burger-menu"; 4 | 5 | function sleep(duration: number): Promise { 6 | return new Promise(resolve => setTimeout(resolve, duration)); 7 | } 8 | 9 | const styles: any = { 10 | bmMenu: { 11 | boxShadow: "0 8px 24px 0 rgba(0,0,0,0.15)", 12 | }, 13 | bmBurgerButton: { 14 | display: "none", 15 | }, 16 | }; 17 | 18 | const styles2: any = { 19 | bmBurgerButton: { 20 | display: "none", 21 | }, 22 | }; 23 | 24 | const styles3: any = { 25 | bmOverlay: { 26 | background: "rgba(0, 0, 0, 0.0)", 27 | }, 28 | }; 29 | 30 | export enum MenuInnerType { 31 | HotKey = "HotKey", 32 | AnnexBox = "AnnexBox", 33 | PPTBox = "PPTBox", 34 | } 35 | 36 | export type MenuBoxProps = { 37 | readonly isVisible: boolean; 38 | readonly menuInnerState: MenuInnerType; 39 | readonly pageWrapId: string; 40 | readonly outerContainerId: string; 41 | readonly isLeft?: boolean; 42 | readonly resetMenu: () => void; 43 | readonly setMenuState: (state: boolean) => void; 44 | }; 45 | 46 | export type MenuBoxStyleState = { 47 | readonly menuStyles: any, 48 | }; 49 | 50 | export default class MenuBox extends React.Component { 51 | 52 | public constructor(props: MenuBoxProps) { 53 | super(props); 54 | this.state = { 55 | menuStyles: this.props.isVisible ? styles : styles2, 56 | }; 57 | } 58 | 59 | private async getMenuStyle(): Promise { 60 | if (this.props.isVisible) { 61 | this.setState({ 62 | menuStyles: styles, 63 | }); 64 | } else { 65 | await sleep(500); 66 | this.setState({ 67 | menuStyles: styles2, 68 | }); 69 | } 70 | } 71 | public render(): React.ReactNode { 72 | if (this.props.isLeft) { 73 | return ( 74 | { 80 | if (!menuState.isOpen) { 81 | await sleep(500); 82 | this.props.resetMenu(); 83 | } 84 | }}> 85 | {this.props.children} 86 | 87 | ); 88 | } else { 89 | return ( 90 | { 98 | if (!menuState.isOpen) { 99 | await sleep(500); 100 | this.props.setMenuState(false); 101 | } 102 | else { 103 | this.props.setMenuState(true); 104 | } 105 | await this.getMenuStyle(); 106 | }}> 107 | {this.props.children} 108 | 109 | ); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /room/src/replayer/ReplayerPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Replayer from "./Replayer"; 4 | import {message} from "antd"; 5 | import {Player, PlayerPhase, ReplayRoomParams, WhiteWebSdk} from "white-react-sdk"; 6 | import {LoadingPage} from "../components"; 7 | 8 | export type ReplayerPageProps = { 9 | readonly uuid: string; 10 | readonly region?: string; 11 | readonly roomToken: string; 12 | readonly beginTimestamp?: number; 13 | readonly duration?: number; 14 | readonly slice?: string; 15 | readonly disableAppFeatures?: boolean; 16 | readonly sdk: WhiteWebSdk; 17 | readonly callbacks?: ReplayerPageCallbacks; 18 | readonly mediaURL?: string; 19 | }; 20 | 21 | export type ReplayerPageCallbacks = { 22 | readonly onGoBack?: () => void; 23 | readonly onGoToRealtimeRoom?: (uuid: string) => void; 24 | }; 25 | 26 | export type ReplayerPageState = { 27 | readonly player?: Player; 28 | readonly phase: PlayerPhase; 29 | readonly currentTime: number; 30 | }; 31 | 32 | const EmptyObject = Object.freeze({}); 33 | 34 | export default class ReplayerPage extends React.Component { 35 | 36 | private readonly uuid: string; 37 | private readonly roomToken: string; 38 | 39 | private didLeavePage: boolean = false; 40 | 41 | public constructor(props: ReplayerPageProps) { 42 | super(props); 43 | this.uuid = props.uuid; 44 | this.roomToken = props.roomToken; 45 | this.state = { 46 | phase: PlayerPhase.WaitingFirstFrame, 47 | currentTime: 0, 48 | }; 49 | } 50 | 51 | public componentWillMount(): void { 52 | this.startReplay().catch(this.findError); 53 | } 54 | 55 | public componentWillUnmount(): void { 56 | this.didLeavePage = true; 57 | } 58 | 59 | private async startReplay(): Promise { 60 | const playerParams: ReplayRoomParams = { 61 | room: this.uuid, 62 | region: this.props.region, 63 | roomToken: this.roomToken, 64 | slice: this.props.slice, 65 | beginTimestamp: this.props.beginTimestamp, 66 | duration: this.props.duration, 67 | mediaURL: this.props.mediaURL, 68 | }; 69 | const player = await this.props.sdk.replayRoom(playerParams, { 70 | onPhaseChanged: phase => { 71 | if (!this.didLeavePage) { 72 | this.setState({phase: phase}); 73 | } 74 | }, 75 | onProgressTimeChanged: progressTime => { 76 | this.setState({currentTime: progressTime}); 77 | }, 78 | onStoppedWithError: this.findError, 79 | }); 80 | (window as any).player = player; 81 | this.setState({player, phase: player.phase}); 82 | } 83 | 84 | private findError = (error: Error): void => { 85 | message.error("回放录像出错:" + error.message); 86 | console.error(message); 87 | this.setState({player: undefined, phase: PlayerPhase.Stopped}); 88 | } 89 | 90 | public render(): React.ReactNode { 91 | if (this.state.player) { 92 | return this.setState({currentTime})}/>; 99 | } else { 100 | return ; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /website/src/pages/LandingFooter.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .footer-box { 4 | width: 1080px; 5 | margin-right: auto; 6 | margin-left: auto; 7 | height: 360px; 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | 12 | .footer-mid-box-up { 13 | width: 100%; 14 | display: flex; 15 | justify-content: space-between; 16 | margin-top: 64px; 17 | margin-bottom: 32px; 18 | } 19 | 20 | .footer-mid-box-down { 21 | width: 75%; 22 | margin-left: 25%; 23 | } 24 | 25 | .footer-cut-line { 26 | width: 100%; 27 | height: 1px; 28 | background-color: #E7EAEE; 29 | } 30 | 31 | .footer-mid-box-inner { 32 | width: 100%; 33 | height: 80px; 34 | display: flex; 35 | justify-content: space-between; 36 | align-items: center; 37 | } 38 | 39 | .footer-mid-box-inner-left { 40 | width: 300px; 41 | color: #A2A7AD; 42 | span { 43 | font-size: 14px; 44 | } 45 | } 46 | 47 | .footer-mid-box-inner-right { 48 | width: 240px; 49 | display: flex; 50 | flex-direction: row; 51 | height: 40px; 52 | align-items: center; 53 | } 54 | 55 | .footer-mid-box-inner-right-Language { 56 | color: #A2A7AD; 57 | margin-right: 12px; 58 | } 59 | 60 | .footer-cell { 61 | width: 240px; 62 | } 63 | 64 | .footer-copyright { 65 | margin-top: 14px; 66 | font-size: 14px; 67 | line-height: 32px; 68 | color: @black_darker; 69 | } 70 | 71 | .footer-title { 72 | text-transform: Uppercase; 73 | /* PAGES: */ 74 | font-size: 14px; 75 | color: #A2A7AD; 76 | line-height: 24px; 77 | margin-bottom: 16px; 78 | } 79 | 80 | .footer-inner { 81 | font-size: 14px; 82 | line-height: 24px; 83 | color: @black_darker; 84 | } 85 | 86 | .footer-inner-link { 87 | margin-top: 18px; 88 | cursor: pointer; 89 | &:hover { 90 | opacity: 0.5; 91 | .main-duration 92 | } 93 | a { 94 | color: @black_dark; 95 | &:hover { 96 | color: @black_dark; 97 | } 98 | } 99 | } 100 | 101 | 102 | @media screen and (max-width: 1140px) { 103 | .footer-box { 104 | width: 960px; 105 | margin-right: auto; 106 | margin-left: auto; 107 | height: 360px; 108 | display: flex; 109 | flex-direction: column; 110 | } 111 | } 112 | 113 | @media screen and (max-width: 980px) { 114 | .footer-box { 115 | width: 800px; 116 | margin-right: auto; 117 | margin-left: auto; 118 | height: 360px; 119 | display: flex; 120 | flex-direction: column; 121 | } 122 | } 123 | 124 | @media screen and (max-width: 820px) { 125 | .footer-box { 126 | width: 640px; 127 | margin-right: auto; 128 | margin-left: auto; 129 | height: 360px; 130 | display: flex; 131 | flex-direction: column; 132 | } 133 | } 134 | 135 | @media screen and (max-width: 660px) { 136 | .footer-box { 137 | width: 100%; 138 | display: flex; 139 | flex-direction: column; 140 | height: auto; 141 | } 142 | 143 | .footer-mid-box-up { 144 | width: 100%; 145 | display: flex; 146 | flex-direction: column; 147 | margin-top: 64px; 148 | margin-bottom: 32px; 149 | text-align: center; 150 | } 151 | 152 | .footer-cell { 153 | width: 100%; 154 | margin-top: 36px; 155 | } 156 | 157 | .footer-mid-box-down { 158 | width: 100%; 159 | margin-left: 0; 160 | } 161 | 162 | .footer-mid-box-inner-left { 163 | width: 100%; 164 | text-align: center; 165 | margin-top: 16px; 166 | } 167 | 168 | .footer-mid-box-inner-right { 169 | display: flex; 170 | justify-content: center; 171 | } 172 | 173 | .footer-mid-box-inner { 174 | width: 100%; 175 | height: auto; 176 | display: flex; 177 | justify-content: space-between; 178 | align-items: center; 179 | flex-direction: column-reverse; 180 | margin-top: 32px; 181 | margin-bottom: 48px; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /room/src/less/realtime/MenuAnnexBox.less: -------------------------------------------------------------------------------- 1 | @import "../theme"; 2 | .menu-annex-box { 3 | width: 100%; 4 | height: 100%; 5 | background-color: #F6F6F6; 6 | overflow: scroll; 7 | } 8 | 9 | .menu-add-page { 10 | margin-right: 16px; 11 | } 12 | 13 | .menu-under-btn-inner { 14 | width: 100%; 15 | height: 42px; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | &:hover { 20 | background-color: #F5F5F5; 21 | transition-timing-function: ease-in-out; 22 | transition-duration: 200ms; 23 | } 24 | img { 25 | width: 14px; 26 | } 27 | } 28 | 29 | .ppt-image { 30 | width: 192px; 31 | height: 112.5px; 32 | overflow: hidden; 33 | } 34 | 35 | .menu-under-btn-right { 36 | width: 42px; 37 | height: 42px; 38 | } 39 | 40 | .menu-under-btn { 41 | width: 100%; 42 | height: 42px; 43 | position: absolute; 44 | bottom: 0; 45 | display: flex; 46 | justify-content: center; 47 | align-items: center; 48 | z-index: 15; 49 | border-top: 1px solid #F5F5F5; 50 | cursor: pointer; 51 | background-color: white; 52 | img { 53 | width: 14px; 54 | opacity: 0.6; 55 | margin-right: 12px; 56 | } 57 | } 58 | 59 | .menu-under-btn-right-inner { 60 | width: 42px; 61 | height: 42px; 62 | display: flex; 63 | justify-content: center; 64 | align-items: center; 65 | background-color: @main_color; 66 | } 67 | 68 | .menu-page-box { 69 | width: 280px; 70 | height: 157.5px; 71 | background-color: white; 72 | display: flex; 73 | justify-content: center; 74 | align-items: center; 75 | cursor: pointer; 76 | } 77 | 78 | .menu-cell-box { 79 | width: 200px; 80 | height: 112.5px; 81 | /* Rectangle 11: */ 82 | background: #FFFFFF; 83 | box-shadow: 0 1px 3px 0 rgba(0,0,0,0.15); 84 | border-radius: 4px; 85 | display: flex; 86 | justify-content: center; 87 | align-items: center; 88 | img { 89 | width: 40px; 90 | } 91 | } 92 | 93 | .page-out-box { 94 | width: 280px; 95 | height: 157.5px; 96 | display: flex; 97 | justify-content: center; 98 | align-items: center; 99 | background-color: #FAFAFA; 100 | outline:none; 101 | } 102 | 103 | .page-out-box-active { 104 | width: 280px; 105 | height: 157.5px; 106 | display: flex; 107 | justify-content: center; 108 | align-items: center; 109 | background-color: #DDDDDD; 110 | outline:none; 111 | } 112 | 113 | .page-box-inner-index-left { 114 | width: 32px; 115 | height: 112.5px; 116 | display: flex; 117 | align-items: center; 118 | justify-content: center; 119 | } 120 | 121 | .page-mid-box { 122 | width: 192px; 123 | height: 112.5px; 124 | display: flex; 125 | align-items: center; 126 | justify-content: center; 127 | cursor: pointer; 128 | margin-right: 6px; 129 | margin-left: 6px; 130 | } 131 | 132 | .page-box { 133 | width: 192px; 134 | height: 112.5px; 135 | background-color: white; 136 | 137 | > img { 138 | position: absolute; 139 | left: 49px; 140 | right: 0px; 141 | width: 192px; 142 | height: 112.5px; 143 | } 144 | } 145 | 146 | .page-box-inner-index-right { 147 | width: 20px; 148 | height: 112.5px; 149 | } 150 | 151 | .page-box-inner-index-delete { 152 | width: 24px; 153 | height: 24px; 154 | background-color: @gray_lighter; 155 | display: flex; 156 | align-items: center; 157 | justify-content: center; 158 | cursor: pointer; 159 | border-radius: 50%; 160 | .hover-transition; 161 | &:hover { 162 | .hover-transition; 163 | background-color: @gray_light; 164 | } 165 | } 166 | 167 | .page-box-inner-index-delete-box { 168 | width: 24px; 169 | height: 24px; 170 | } 171 | 172 | .page-box-close { 173 | width: 24px; 174 | height: 24px; 175 | background-color: white; 176 | border-radius: 50%; 177 | position: absolute; 178 | box-shadow: 0 1px 3px 0 rgba(0,0,0,0.15); 179 | display: flex; 180 | justify-content: center; 181 | align-items: center; 182 | margin-left: 182px; 183 | margin-top: -10px; 184 | } 185 | -------------------------------------------------------------------------------- /website/src/assets/image/hotkey/magic_pen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 11 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /room/src/less/realtime/RealtimeRoom.less: -------------------------------------------------------------------------------- 1 | @import "../theme"; 2 | .whiteboard-out-box { 3 | width: 100%; 4 | height: 100vh; 5 | overflow: hidden; 6 | } 7 | 8 | .ppt-box { 9 | width: 100%; 10 | height: 100%; 11 | background-color: @black_light; 12 | } 13 | 14 | .white-board-loading { 15 | width: 100%; 16 | height: 100vh; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | } 21 | 22 | .whiteboard-tool-layer-down { 23 | position: absolute; 24 | z-index: 1; 25 | width: 100%; 26 | height: 100%; 27 | overflow: hidden; 28 | } 29 | 30 | .tool-out-box { 31 | width: 304px; 32 | } 33 | 34 | .head-box { 35 | width: 32px; 36 | height: 32px; 37 | background-color: #ebf7fd; 38 | border-radius: 50%; 39 | margin-left: 120px; 40 | overflow:hidden; 41 | } 42 | 43 | .slide-box-active { 44 | width: 18px; 45 | height: 80px; 46 | background-color: @black_light; 47 | position: absolute; 48 | z-index: 4; 49 | margin-top: calc(~'50vh - 40px'); 50 | cursor: pointer; 51 | border-bottom-right-radius: 4px; 52 | border-top-right-radius: 4px; 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | left: -2px; 57 | img { 58 | width: 9px; 59 | transform: scaleX(-1); 60 | } 61 | } 62 | 63 | .slide-box { 64 | width: 18px; 65 | height: 80px; 66 | background-color: @black_light; 67 | opacity: 0.3; 68 | position: absolute; 69 | z-index: 4; 70 | margin-top: calc(~'50vh - 40px'); 71 | cursor: pointer; 72 | border-bottom-right-radius: 4px; 73 | border-top-right-radius: 4px; 74 | .hover-transition; 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | left: -2px; 79 | img { 80 | width: 9px; 81 | } 82 | span { 83 | color: white; 84 | } 85 | &:hover { 86 | opacity: 1; 87 | .hover-transition; 88 | } 89 | } 90 | 91 | 92 | .whiteboard-tool-box { 93 | width: 100%; 94 | height: 0; 95 | display: flex; 96 | justify-content: center; 97 | position: absolute; 98 | z-index: 2; 99 | margin-top: 16px; 100 | } 101 | 102 | .whiteboard-tool-box-disable { 103 | width: 100%; 104 | height: 0; 105 | display: flex; 106 | justify-content: center; 107 | position: absolute; 108 | z-index: 2; 109 | margin-top: 16px; 110 | opacity: 0.6; 111 | pointer-events: none; 112 | cursor: not-allowed; 113 | } 114 | 115 | .video-box { 116 | width: 300px; 117 | height: 300px; 118 | background-color: yellow; 119 | position: absolute; 120 | z-index: 3; 121 | } 122 | 123 | .whiteboard-top-bar { 124 | width: 100%; 125 | height: 0; 126 | display: flex; 127 | justify-content: space-between; 128 | position: absolute; 129 | z-index: 2; 130 | margin-top: 16px; 131 | } 132 | 133 | .whiteboard-top-left { 134 | width: 48px; 135 | height: 32px; 136 | border-radius: 16px; 137 | display: flex; 138 | align-items: center; 139 | justify-content: center; 140 | margin-left: 8px; 141 | } 142 | 143 | .whiteboard-box-gift-box { 144 | position: absolute; 145 | z-index: 2000; 146 | height: 0; 147 | width: 0; 148 | margin-left: calc(~'50% - 32px'); 149 | margin-top: calc(~'50vh - 35px'); 150 | } 151 | 152 | .whiteboard-box-gift-inner-box { 153 | width: 64px; 154 | height: 70px; 155 | display: flex; 156 | align-items: center; 157 | justify-content: center; 158 | img { 159 | width: 64px; 160 | } 161 | } 162 | 163 | //覆盖在白板之上的 div 164 | .user-cursor-layout { 165 | 166 | pointer-events: none; 167 | 168 | z-index: 4; 169 | position: absolute; 170 | top: 0; 171 | bottom: 0; 172 | left: 0; 173 | right: 0; 174 | 175 | > * { 176 | position: absolute; 177 | } 178 | } 179 | 180 | //用户头像 181 | .user-cursor-inner { 182 | width: 32px; 183 | height: 32px; 184 | border-radius: 50%; 185 | } 186 | 187 | //用户图片 188 | .user-cursor-img { 189 | width: 28px; 190 | height: 28px; 191 | border-radius: 14px; 192 | margin: 2px; 193 | } 194 | 195 | .user-cursor-tool { 196 | width: 16px; 197 | height: 16px; 198 | position: absolute; 199 | border-radius: 8px; 200 | border: 1px solid #FFFFFF; 201 | box-sizing: border-box; 202 | margin-top: -14px; 203 | margin-left: 16px; 204 | z-index: 10; 205 | display: flex; 206 | justify-content: center; 207 | align-items: center; 208 | } 209 | -------------------------------------------------------------------------------- /website/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by taozeyu on 2017/6/4. 3 | */ 4 | 5 | const path = require("path"); 6 | const webpack = require("webpack"); 7 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 8 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 9 | 10 | const basic = { 11 | 12 | entry: [ 13 | "./src/index.ts", 14 | ], 15 | 16 | output: { 17 | filename: "javascript/index-[hash].js", 18 | path: __dirname + "/build", 19 | publicPath: "/", 20 | }, 21 | 22 | resolve: { 23 | extensions: [".ts", ".tsx", ".js", ".svg"], 24 | }, 25 | 26 | devtool: 'source-map', 27 | 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.js$/, 32 | enforce: "pre", 33 | loader: "source-map-loader", 34 | exclude: [ 35 | path.resolve(__dirname, "../submodules/white-framework/node_modules") 36 | ] 37 | }, { 38 | test: /\.tsx?$/, 39 | use: [ 40 | 'ts-loader', 41 | { 42 | loader: 'ui-component-loader', 43 | options: { 44 | onlyCompileBundledFiles: true, 45 | transpileOnly: true, 46 | 'lib': 'antd', 47 | 'libDir': 'es', 48 | 'style': false, 49 | } 50 | }, 51 | ], 52 | exclude: /node_modules/, 53 | }, { 54 | test: /\.less$/, 55 | use: [ 56 | {loader: "style-loader"}, {loader: "css-loader"}, {loader: "less-loader"} 57 | ] 58 | }, { 59 | test: /\.css$/, 60 | use: [ 61 | {loader: "style-loader"}, {loader: "css-loader"} 62 | ] 63 | }, { 64 | test: /\.svg$/, 65 | exclude: /node_modules/, 66 | loader: 'svg-react-loader', 67 | query: { 68 | classIdPrefix: '[name]-[hash:8]__', 69 | propsMap: { 70 | fillRule: 'fill-rule', 71 | foo: 'bar' 72 | }, 73 | xmlnsTest: /^xmlns.*$/ 74 | } 75 | }, { 76 | test: /\.(png|jpg)$/, 77 | loader: 'url-loader?limit=8192&name=images/[hash:8].[name].[ext]' 78 | }] 79 | }, 80 | plugins: [ 81 | new HtmlWebpackPlugin({ 82 | template: "./src/index-template.html", 83 | filename: "index.html", 84 | path: __dirname + "/build", 85 | inject: "body", 86 | }), 87 | new ForkTsCheckerWebpackPlugin({ memoryLimit : 10000, workers: 2 }) 88 | ] 89 | }; 90 | 91 | const development = { 92 | devServer: { 93 | port: 3000, 94 | historyApiFallback: true, 95 | }, 96 | }; 97 | 98 | const production = { 99 | plugins: [ 100 | new webpack.optimize.UglifyJsPlugin({ 101 | compress: { 102 | warnings: false 103 | } 104 | }), 105 | ], 106 | }; 107 | 108 | function merge(conf1, conf2) { 109 | if (conf1 instanceof Array && conf2 instanceof Array) { 110 | const array = []; 111 | conf1.forEach(function (e) { 112 | array.push(e); 113 | }); 114 | conf2.forEach(function (e) { 115 | array.push(e); 116 | }); 117 | return array; 118 | } 119 | const result = {}; 120 | 121 | function mergeValue(v1, v2) { 122 | if (typeof v1 === "object" && typeof v2 === "object") { 123 | return merge(v1, v2); 124 | 125 | } else if (v1 === undefined) { 126 | return v2; 127 | 128 | } else { 129 | return v1; 130 | } 131 | } 132 | 133 | for (const key in conf1) { 134 | result[key] = mergeValue(conf1[key], conf2[key]); 135 | } 136 | for (const key in conf2) { 137 | if (!(key in conf1)) { 138 | result[key] = mergeValue(conf1[key], conf2[key]); 139 | } 140 | } 141 | return result; 142 | } 143 | 144 | module.exports = merge( 145 | basic, 146 | process.env.NODE_ENV === 'production' ? production : development 147 | ); 148 | -------------------------------------------------------------------------------- /website/src/pages/Homepage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./Homepage.less"; 3 | import NetlessBlack from "../assets/image/netless_black.svg"; 4 | import {stringify} from "query-string"; 5 | import {Input, Button, Tabs} from "antd"; 6 | import {RouteComponentProps} from "react-router"; 7 | import {Link} from "@netless/i18n-react-router"; 8 | import {netlessWhiteboardApi} from "../apiMiddleware"; 9 | const { TabPane } = Tabs; 10 | export type HomepageProps = RouteComponentProps<{}>; 11 | export type HomepageState = { 12 | readonly name: string; 13 | url: string; 14 | }; 15 | 16 | class Homepage extends React.Component { 17 | 18 | public constructor(props: HomepageProps) { 19 | super(props); 20 | this.state = { 21 | name: "", 22 | url: "", 23 | }; 24 | } 25 | 26 | private onInputNameChanged(name: string): void { 27 | this.setState({name: name.trim()}); 28 | } 29 | private getActiveSelectedKey = (url: string): string => { 30 | let str = url; 31 | const regex = /([^\/]+)/gm; 32 | regex.exec(str); 33 | regex.exec(str); 34 | regex.exec(str); 35 | regex.exec(str); 36 | regex.exec(str); 37 | const arr2 = regex.exec(str); 38 | if (arr2 === null) { 39 | str = ""; 40 | } else { 41 | str = arr2[0]; 42 | } 43 | return str; 44 | } 45 | private handleClickBtnUrl = (): void => { 46 | const isUrl = this.state.url.substring(0, 4) === "http"; 47 | if (this.state.url) { 48 | if (isUrl) { 49 | const uuid = this.getActiveSelectedKey(this.state.url); 50 | this.props.history.push(`/whiteboard/${uuid}/`); 51 | } else { 52 | if (this.state.url.length === 32) { 53 | this.props.history.push(`/whiteboard/${this.state.url}/`); 54 | } 55 | } 56 | } 57 | } 58 | 59 | private onClickButton = (): void => { 60 | let name: string | undefined = this.state.name; 61 | 62 | if (name === "") { 63 | name = undefined; 64 | } 65 | const user = netlessWhiteboardApi.user.createUser(name); 66 | 67 | this.props.history.push("/whiteboard?" + stringify({userId: user.userId})); 68 | } 69 | 70 | public render(): React.ReactNode { 71 | return ( 72 |
73 | 74 | 75 | 76 |
77 |
78 | 79 | 80 |
81 | this.onInputNameChanged(e.target.value)} size={"large"} placeholder={"输入用户名"}/> 82 | 89 |
90 |
91 | 92 |
93 | this.setState({url: e.target.value})} 95 | size={"large"} placeholder={"输入房间地址或者 UUID"}/> 96 | 104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | ); 112 | } 113 | } 114 | 115 | export default Homepage; 116 | --------------------------------------------------------------------------------