From 901e0691ab8f86fef3d786f685e1141610b5fed2 Mon Sep 17 00:00:00 2001 From: hzoou Date: Fri, 20 Dec 2019 10:11:47 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20#313=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=20=EA=B8=B0=EB=8A=A5=EC=97=90=20=EC=B0=B8=EC=97=AC?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=EC=9D=98=20=EC=BB=A4=EC=84=9C=20?= =?UTF-8?q?=EC=83=89=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 라이브 기능에 참여한 유저의 커서 색이 랜덤값으로 구분되는 기능을 구현하였습니다. --- cocode/src/constants/cursorColors.js | 16 +++++++++++++++ cocode/src/containers/Live/Editor/index.js | 23 +++++++++++----------- cocode/src/utils/monacoWidget.js | 6 ++++-- 3 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 cocode/src/constants/cursorColors.js diff --git a/cocode/src/constants/cursorColors.js b/cocode/src/constants/cursorColors.js new file mode 100644 index 00000000..dd6120e3 --- /dev/null +++ b/cocode/src/constants/cursorColors.js @@ -0,0 +1,16 @@ +const colors = [ + '#CC0000', + '#FF650F', + '#FFBA02', + '#458d53', + '#006b04', + '#006b04', + '#2E588D', + '#52338D', + '#741551', + '#13133C', + '#220D38', + '#000000' +]; + +export { colors }; \ No newline at end of file diff --git a/cocode/src/containers/Live/Editor/index.js b/cocode/src/containers/Live/Editor/index.js index c75558fc..71f68ff0 100644 --- a/cocode/src/containers/Live/Editor/index.js +++ b/cocode/src/containers/Live/Editor/index.js @@ -5,6 +5,7 @@ import * as Styled from './style'; import FileTabBar from 'components/Project/FileTabBar'; import MonacoEditor from 'components/Project/MonacoEditor'; +import { colors } from 'constants/cursorColors'; import { LiveContext, UserContext, ProjectContext } from 'contexts'; import { updateCodeActionCreator, @@ -54,7 +55,7 @@ function Editor({ handleForkCoconut }) { }, DEBOUNCING_TIME); }; - const handleChnageSelectedFileMonaco = ( + const handleChangeSelectedFileMonaco = ( source, text, range = MAX_RANGE @@ -80,7 +81,7 @@ function Editor({ handleForkCoconut }) { selectedRef.current = selectedFileId; setCode(project.editingCode); - handleChnageSelectedFileMonaco( + handleChangeSelectedFileMonaco( 'changeFile', filesRef.current[selectedFileId].contents ); @@ -132,7 +133,7 @@ function Editor({ handleForkCoconut }) { if (!isEditorMounted) return; selectedRef.current = selectedFileId; isBusy.current = true; - handleChnageSelectedFileMonaco('initial', project.editingCode); + handleChangeSelectedFileMonaco('initial', project.editingCode); }, [isEditorMounted]); useEffect(() => { @@ -160,12 +161,8 @@ function Editor({ handleForkCoconut }) { const str2 = originCode.slice(op.rangeOffset + op.rangeLength); const changedCode = `${str1}${op.text}${str2}`; filesRef.current[fileId].contents = changedCode; - const updateCodeFromFileIdAction = updateCodeFromFileIdActionCreator( - { - fileId, - changedCode - } - ); + const updateCodeFromFileIdAction = + updateCodeFromFileIdActionCreator({ fileId, changedCode }); dispatchProject(updateCodeFromFileIdAction); return; } @@ -181,7 +178,7 @@ function Editor({ handleForkCoconut }) { .getModel() .getPositionAt(rangeOffset + rangeLength); - handleChnageSelectedFileMonaco(socketId, text, { + handleChangeSelectedFileMonaco(socketId, text, { startLineNumber: startPosition.lineNumber, startColumn: startPosition.column, endLineNumber: endPosition.lineNumber, @@ -190,11 +187,15 @@ function Editor({ handleForkCoconut }) { }; const handleMoveCursor = (username, fileId, position) => { + const min = 0; + const max = colors.length - 1; if (!userCursor[username]) { + const color = colors[Math.floor(Math.random() * (min, max))]; const widget = new CursorWidget( editorRef.current, username, - position + position, + color ); userCursor[username] = widget; editorRef.current.addContentWidget(widget); diff --git a/cocode/src/utils/monacoWidget.js b/cocode/src/utils/monacoWidget.js index 83bbe24a..d2774043 100644 --- a/cocode/src/utils/monacoWidget.js +++ b/cocode/src/utils/monacoWidget.js @@ -1,9 +1,10 @@ class CursorWidget { - constructor(editor, userName, position) { + constructor(editor, userName, position, color) { this.editor = editor; this.id = userName; this.domNode = null; this.position = position; + this.color = color; } getId() { @@ -14,7 +15,8 @@ class CursorWidget { if (!this.domNode) { this.domNode = document.createElement('div'); this.domNode.innerHTML = this.id; - this.domNode.style.background = 'grey'; + this.domNode.style.background = this.color; + this.domNode.style.color = 'white'; this.domNode.id = this.id; } return this.domNode; From 6ee2bc4e69634e0b7b8fb540fe40e8735990b9ad Mon Sep 17 00:00:00 2001 From: YukJiSoo Date: Sat, 21 Dec 2019 14:16:00 +0900 Subject: [PATCH 02/11] =?UTF-8?q?fix:=20fork=EB=90=98=EA=B8=B0=EC=A0=84=20?= =?UTF-8?q?project=20page=EC=97=90=EC=84=9C=20file=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 새로 생성한 프로젝트에서 파일이름 수정, 파일삭제, 파일 생성 시 API server에 요청을 보내는 로직이 구현되어 있었습니다. 이는 아직 생성이 안된 파일 id를 참조하는 문제가 있기에 이에 관한 분기처리를 해주었습니다. with @lallaheeee --- cocode/src/components/Project/File/index.js | 12 +++++- .../src/components/Project/NewFile/index.js | 43 +++++++++++++------ cocode/src/constants/notificationMessage.js | 4 +- cocode/src/pages/Project/index.js | 4 +- cocode/src/reducers/ProjectReducer.js | 8 +++- 5 files changed, 51 insertions(+), 20 deletions(-) diff --git a/cocode/src/components/Project/File/index.js b/cocode/src/components/Project/File/index.js index fa2f5f47..8aa20805 100644 --- a/cocode/src/components/Project/File/index.js +++ b/cocode/src/components/Project/File/index.js @@ -101,7 +101,12 @@ function File({ if (isNotChangeableFileName({ files, parentId, changedName })) { currentTarget.textContent = fileName; - addToast.error(NOTIFICATION.FILE_IS_DUPLICATED); + addToast.error(NOTIFICATION.FILE_NAME_IS_INCORRECT); + return; + } + + if (projectId === 'new') { + successHandler[UPDATE_FILE_NAME](); return; } @@ -123,6 +128,11 @@ function File({ const acceptDeleteThisFile = confirm(NOTIFICATION.CONFIRM_DELETE_FILE); if (!acceptDeleteThisFile) return; + if (projectId === 'new') { + successHandler[DELETE_FILE](_id); + return; + } + const deleteFileId = _id; const deleteFileAPI = deleteFileAPICreator(projectId, deleteFileId, { parentId diff --git a/cocode/src/components/Project/NewFile/index.js b/cocode/src/components/Project/NewFile/index.js index bfcb42f2..39f0599c 100644 --- a/cocode/src/components/Project/NewFile/index.js +++ b/cocode/src/components/Project/NewFile/index.js @@ -1,6 +1,7 @@ import * as React from 'react'; import { useState, useEffect, useContext, useRef } from 'react'; import { useParams } from 'react-router-dom'; +import ObjectID from 'bson-objectid'; import * as Styled from './style'; import addToast from 'components/Common/Toast'; @@ -30,17 +31,38 @@ function NewFile({ depth, type, parentId, handleEndCreateFile }) { const fileNameInputReference = useRef(null); const [{ data, error }, setRequest] = useFetch({}); - const isDuplicatedFileName = fileName => { - return files[parentId].child - .map(id => files[id].name) - .some(name => name === fileName); + const isIncorrectFileName = fileName => { + return ( + fileName.trim() === '' || + files[parentId].child + .map(id => files[id].name) + .some(name => name === fileName) + ); + }; + + const updateFileState = newFileId => { + const createFileAction = createFileActionCreator({ + newFileId, + name: fileName, + parentId, + type + }); + dispatchProject(createFileAction); + changeDivEditable(fileNameInputReference.current, false); }; const requestCreateFile = e => { const name = e.currentTarget.textContent; - if (isDuplicatedFileName(name)) { + if (isIncorrectFileName(name)) { e.preventDefault(); - addToast.error(NOTIFICATION.FILE_IS_DUPLICATED); + addToast.error(NOTIFICATION.FILE_NAME_IS_INCORRECT); + return; + } + + if (projectId === 'new') { + const newFileId = ObjectID().str; + updateFileState(newFileId); + return; } @@ -69,14 +91,7 @@ function NewFile({ depth, type, parentId, handleEndCreateFile }) { if (!data) return; const { newFileId } = data; - const createFileAction = createFileActionCreator({ - newFileId, - name: fileName, - parentId, - type - }); - dispatchProject(createFileAction); - changeDivEditable(fileNameInputReference.current, false); + updateFileState(newFileId); }; const handleErrorResponse = () => { diff --git a/cocode/src/constants/notificationMessage.js b/cocode/src/constants/notificationMessage.js index f312f88c..4485dc9b 100644 --- a/cocode/src/constants/notificationMessage.js +++ b/cocode/src/constants/notificationMessage.js @@ -8,6 +8,7 @@ const FAIL_TO_UPDATE_PROJECT_CARD = 'Fail to request. sorry, try again please'; const FILE_IS_NOT_MOVABLE = 'This file is not movable'; const FILE_IS_NOT_EDITABLE = 'This file is not editable'; const FILE_IS_DUPLICATED = 'This file name is duplicated'; +const FILE_NAME_IS_INCORRECT = 'This file name is incorrect'; const FILE_IS_NOT_REMOVABLE = 'This file is not removable'; const CONFIRM_DELETE_FILE = 'Are you delete this file?'; @@ -32,6 +33,7 @@ export { FILE_IS_NOT_MOVABLE, FILE_IS_NOT_EDITABLE, FILE_IS_DUPLICATED, + FILE_NAME_IS_INCORRECT, FILE_IS_NOT_REMOVABLE, CONFIRM_DELETE_FILE, CONFIRM_DELETE_COCONUT, @@ -40,5 +42,5 @@ export { LOADING_LIVE, SUCCESS_FORK, CONFLICT_FORK, - SHUT_DOWN_LIVE_SHARE, + SHUT_DOWN_LIVE_SHARE }; diff --git a/cocode/src/pages/Project/index.js b/cocode/src/pages/Project/index.js index ce1ea936..563eacbe 100644 --- a/cocode/src/pages/Project/index.js +++ b/cocode/src/pages/Project/index.js @@ -29,7 +29,7 @@ import { LOADING_PROJECT } from 'constants/notificationMessage'; -const DEFAULT_CLICKED_TAB_INDEX = 0; +const DEFAULT_CLICKED_TAB_INDEX = 1; function Project() { const { user } = useContext(UserContext); @@ -121,7 +121,7 @@ function Project() { forkCoconut }} > -
+
{isFetched && ( diff --git a/cocode/src/reducers/ProjectReducer.js b/cocode/src/reducers/ProjectReducer.js index dea3c481..a3b24b38 100644 --- a/cocode/src/reducers/ProjectReducer.js +++ b/cocode/src/reducers/ProjectReducer.js @@ -225,11 +225,15 @@ const deleteFile = (state, { deleteFileId }) => { id => id !== deleteFileId ); + const updatedFiles = Object.entries(files).reduce((acc, [fileId, file]) => { + if (fileId === deleteFileId) return acc; + return { ...acc, [fileId]: file }; + }, {}); + return { ...state, files: { - ...state.files, - [deleteFileId]: undefined, + ...updatedFiles, [parentId]: { ...state.files[parentId], child: updatedParentChilds From 6a1e29ffd4d8e5a5806b323af46eb4360157e8da Mon Sep 17 00:00:00 2001 From: YukJiSoo Date: Sat, 21 Dec 2019 17:48:57 +0900 Subject: [PATCH 03/11] =?UTF-8?q?chore:=20react=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 불필요한 파일을 삭제하고 styled component를 초기 dependency에 추가해주었습니다. --- cocode/src/template/react.js | 153 +++++------------------------------ 1 file changed, 21 insertions(+), 132 deletions(-) diff --git a/cocode/src/template/react.js b/cocode/src/template/react.js index 3ffca09f..94b18437 100644 --- a/cocode/src/template/react.js +++ b/cocode/src/template/react.js @@ -1,71 +1,5 @@ const template = { - css: - '.App {\n' + - ' font-family: sans-serif;\n' + - ' text-align: center;\n' + - '}\n', - html: - '\n' + - '\n' + - '\n' + - '\n' + - '\t\n' + - '\t\n' + - '\t\n' + - '\t\n' + - '\t\n' + - '\t\n' + - '\t\n' + - '\tReact App\n' + - '\n' + - '\n' + - '\n' + - '\t\n' + - '\t
\n' + - '\t\n' + - '\n' + - '\n' + - '', - js: - 'import React from "react";\n' + - 'import ReactDOM from "react-dom";\n' + - '\n' + - 'import "./styles.css";\n' + - '\n' + - 'function App() {\n' + - ' return (\n' + - '
\n' + - '

Hello CodeSandbox

\n' + - '

Start editing to see some magic happen!

\n' + - '
\n' + - ' );\n' + - '}\n' + - '\n' + - 'const rootElement = document.getElementById("root");\n' + - 'ReactDOM.render(, rootElement);\n', - package: + 'package.json': '{\n' + ' "name": "new",\n' + ' "version": "1.0.0",\n' + @@ -75,58 +9,31 @@ const template = { ' "dependencies": {\n' + ' "react": "16.8.6",\n' + ' "react-dom": "16.8.6",\n' + - ' "react-scripts": "3.0.1"\n' + - ' },\n' + - ' "devDependencies": {\n' + - ' "typescript": "3.3.3"\n' + - ' },\n' + - ' "scripts": {\n' + - ' "start": "react-scripts start",\n' + - ' "build": "react-scripts build",\n' + - ' "test": "react-scripts test --env=jsdom",\n' + - ' "eject": "react-scripts eject"\n' + - ' },\n' + - ' "browserslist": [">0.2%", "not dead", "not ie <= 11", "not op_mini all"]\n' + + ' "styled-components": "4.4.1"\n' + + ' }\n' + '}\n', - version1: - 'import React, { useState } from "react";\n' + + 'index.js': + 'import React from "react";\n' + 'import ReactDOM from "react-dom";\n' + + 'import * as Styled from "./style"\n' + '\n' + 'function App() {\n' + - ' const [state, setState] = useState("Cocode");\n' + - '\n' + - ' return(\n' + - ' <>\n' + - '

Hi! {state}

\n' + - ' \n' + + ' return (\n' + + ' \n' + + '

🥥 Hello Cocode World 🥥

\n' + + '
\n' + ' )\n' + '}\n' + '\n' + 'ReactDOM.render(, document.getElementById("coconut-root"));\n', - Apple: - 'import React from "react";\n' + - '\n' + - 'function Apple() {\n' + - ' return(\n' + - ' <>\n' + - '

Apple!

\n' + - ' \n' + - ' )\n' + - '}\n' + + 'style.js': + 'import styled from "styled-components";\n' + '\n' + - 'export default Apple;\n', - Banana: - 'import React from "react";\n' + - '\n' + - 'function Banana() {\n' + - ' return(\n' + - ' <>\n' + - '

Banana!

\n' + - ' \n' + - ' )\n' + - '}\n' + + 'const App = styled.div`\n' + + ' text-align: center;\n' + + '`\n' + '\n' + - 'export default Banana;\n' + 'export { App };\n' }; function getTemplate(file) { @@ -153,46 +60,28 @@ const reactTemplate = () => ({ projectId: '5dd61901353f4858e1b5a9d0', name: 'src', type: 'directory', - child: [ - '5dd553be4561ae2bae9cb45d', - '5dd553be4561ae2bae9cb45e', - '5dd553be4561ae2bae9cb461' - ] + child: ['5dd553be4561ae2bae9cb45d', '5dd553be4561ae2bae9cb45e'] }, { _id: '5dd553be4561ae2bae9cb45d', name: 'index.js', projectId: '5dd61901353f4858e1b5a9d0', type: 'js', - contents: getTemplate('version1') + contents: getTemplate('index.js') }, { _id: '5dd553be4561ae2bae9cb45e', - name: 'Apple.js', - projectId: '5dd61901353f4858e1b5a9d0', - type: 'js', - contents: getTemplate('Apple') - }, - { - _id: '5dd553be4561ae2bae9cb461', - projectId: '5dd61901353f4858e1b5a9d0', - name: 'Component', - type: 'directory', - child: ['5dd553be4561ae2bae9cb460'] - }, - { - _id: '5dd553be4561ae2bae9cb460', - name: 'Banana.js', + name: 'style.js', projectId: '5dd61901353f4858e1b5a9d0', type: 'js', - contents: getTemplate('Banana') + contents: getTemplate('style.js') }, { _id: '5dd553be4561ae2bae9cb462', name: 'package.json', projectId: '5dd61901353f4858e1b5a9d0', type: 'npm', - contents: getTemplate('package') + contents: getTemplate('package.json') } ] }); From a2293a49aa4c4237ec9252e73110b1c013747b45 Mon Sep 17 00:00:00 2001 From: hzoou Date: Sun, 22 Dec 2019 13:31:25 +0900 Subject: [PATCH 04/11] =?UTF-8?q?chore:=20=EB=94=94=ED=8E=9C=EB=8D=98?= =?UTF-8?q?=EC=8B=9C=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 설치된 디펜던시 목록의 아이콘의 위치를 올바르게 수정했습니다. --- cocode/src/components/Project/DependencySearchItem/style.js | 3 ++- cocode/src/components/Project/PlusImage/style.js | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cocode/src/components/Project/DependencySearchItem/style.js b/cocode/src/components/Project/DependencySearchItem/style.js index 6a9641a8..4215b123 100644 --- a/cocode/src/components/Project/DependencySearchItem/style.js +++ b/cocode/src/components/Project/DependencySearchItem/style.js @@ -16,8 +16,9 @@ const Item = styled.li` const Description = styled.div` & { display: flex; - margin-top: 0.3rem; + justify-content: flex-end; align-items: center; + margin-top: 0.5rem; } `; diff --git a/cocode/src/components/Project/PlusImage/style.js b/cocode/src/components/Project/PlusImage/style.js index 3dceb5db..f04f19cc 100644 --- a/cocode/src/components/Project/PlusImage/style.js +++ b/cocode/src/components/Project/PlusImage/style.js @@ -5,7 +5,6 @@ const Image = styled.svg` & { width: ${DEPENDENCY_TAB_THEME.dependencyTabIconSize}; height: ${DEPENDENCY_TAB_THEME.dependencyTabIconSize}; - margin-left: auto; margin-right: 0.5rem; fill: ${DEPENDENCY_TAB_THEME.dependencyTabIconColor} } From e1d5846d691adcc232b7a8a9c886a93e06b691bc Mon Sep 17 00:00:00 2001 From: hzoou Date: Sun, 22 Dec 2019 13:44:27 +0900 Subject: [PATCH 05/11] =?UTF-8?q?chore:=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=97=90=EC=84=9C=20=EC=BB=A4=EC=84=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 해당 유저를 나타내는 커서 스타일에 코드가 가려지는 이슈가 있어 해당 사항을 수정하고자 글씨크기와 배경색에 투명도를 추가했습니다. --- cocode/src/constants/cursorColors.js | 23 +++++++++++----------- cocode/src/containers/Live/Editor/index.js | 16 ++++++++------- cocode/src/utils/monacoWidget.js | 6 ++++-- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/cocode/src/constants/cursorColors.js b/cocode/src/constants/cursorColors.js index dd6120e3..238ee312 100644 --- a/cocode/src/constants/cursorColors.js +++ b/cocode/src/constants/cursorColors.js @@ -1,16 +1,15 @@ const colors = [ - '#CC0000', - '#FF650F', - '#FFBA02', - '#458d53', - '#006b04', - '#006b04', - '#2E588D', - '#52338D', - '#741551', - '#13133C', - '#220D38', - '#000000' + '#CC000080', + '#FF650F80', + '#FFBA0280', + '#458d5380', + '#006b0480', + '#2E588D80', + '#52338D80', + '#74155180', + '#13133C80', + '#220D3880', + '#00000080' ]; export { colors }; \ No newline at end of file diff --git a/cocode/src/containers/Live/Editor/index.js b/cocode/src/containers/Live/Editor/index.js index 71f68ff0..d2167cf7 100644 --- a/cocode/src/containers/Live/Editor/index.js +++ b/cocode/src/containers/Live/Editor/index.js @@ -14,7 +14,7 @@ import { import useFetch from 'hooks/useFetch'; -import { CursorWidget } from 'utils/monacoWidget'; +import { LabelWidget } from 'utils/monacoWidget'; let timer; const DEBOUNCING_TIME = 1000; @@ -191,18 +191,20 @@ function Editor({ handleForkCoconut }) { const max = colors.length - 1; if (!userCursor[username]) { const color = colors[Math.floor(Math.random() * (min, max))]; - const widget = new CursorWidget( + const label = new LabelWidget( editorRef.current, username, position, color ); - userCursor[username] = widget; - editorRef.current.addContentWidget(widget); + userCursor[username] = { label }; + editorRef.current.addContentWidget(label); + } + if (selectedRef.current === fileId) { + userCursor[username].label.showCursor(position); + } else { + userCursor[username].label.hiddenCursor(); } - if (selectedRef.current === fileId) - userCursor[username].showCursor(position); - else userCursor[username].hiddenCursor(); }; return ( diff --git a/cocode/src/utils/monacoWidget.js b/cocode/src/utils/monacoWidget.js index d2774043..14adab32 100644 --- a/cocode/src/utils/monacoWidget.js +++ b/cocode/src/utils/monacoWidget.js @@ -1,4 +1,4 @@ -class CursorWidget { +class LabelWidget { constructor(editor, userName, position, color) { this.editor = editor; this.id = userName; @@ -15,8 +15,10 @@ class CursorWidget { if (!this.domNode) { this.domNode = document.createElement('div'); this.domNode.innerHTML = this.id; + this.domNode.style.fontSize = '0.5rem'; this.domNode.style.background = this.color; this.domNode.style.color = 'white'; + this.domNode.style.transform = 'translateX(3px)'; this.domNode.id = this.id; } return this.domNode; @@ -45,4 +47,4 @@ class CursorWidget { } } -export { CursorWidget }; +export { LabelWidget }; From 40133dbd8685d9c1d8ea090386afff1d27fdd790 Mon Sep 17 00:00:00 2001 From: hzoou Date: Sun, 22 Dec 2019 14:38:51 +0900 Subject: [PATCH 06/11] =?UTF-8?q?chore:=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=97=90=EC=84=9C=20=EC=BA=90=EB=9F=BF=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 라이브 기능에서 해당 유저의 커서를 나타내기 위해 캐럿 스타일을 추가했습니다. --- .../components/Common/GlobalStyle/index.js | 8 ++++ cocode/src/containers/Live/Editor/index.js | 12 ++++- cocode/src/utils/monacoWidget.js | 48 ++++++++++++++++++- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/cocode/src/components/Common/GlobalStyle/index.js b/cocode/src/components/Common/GlobalStyle/index.js index c4975b3c..62d81b40 100644 --- a/cocode/src/components/Common/GlobalStyle/index.js +++ b/cocode/src/components/Common/GlobalStyle/index.js @@ -81,6 +81,14 @@ const GlobalStyle = createGlobalStyle` div, span{ user-select: none; } + + .blink{ + animation: blink 1s step-start 0s infinite; + } + + @keyframes blink { + 50% { opacity: 0.0; } + } `; export default GlobalStyle; diff --git a/cocode/src/containers/Live/Editor/index.js b/cocode/src/containers/Live/Editor/index.js index d2167cf7..45ec8126 100644 --- a/cocode/src/containers/Live/Editor/index.js +++ b/cocode/src/containers/Live/Editor/index.js @@ -14,7 +14,7 @@ import { import useFetch from 'hooks/useFetch'; -import { LabelWidget } from 'utils/monacoWidget'; +import { LabelWidget, CaratWidget } from 'utils/monacoWidget'; let timer; const DEBOUNCING_TIME = 1000; @@ -197,13 +197,21 @@ function Editor({ handleForkCoconut }) { position, color ); - userCursor[username] = { label }; + const carat = new CaratWidget( + editorRef.current, + position, + color + ); + userCursor[username] = { label, carat }; editorRef.current.addContentWidget(label); + editorRef.current.addContentWidget(carat); } if (selectedRef.current === fileId) { userCursor[username].label.showCursor(position); + userCursor[username].carat.showCursor(position); } else { userCursor[username].label.hiddenCursor(); + userCursor[username].carat.hiddenCursor(); } }; diff --git a/cocode/src/utils/monacoWidget.js b/cocode/src/utils/monacoWidget.js index 14adab32..8900e241 100644 --- a/cocode/src/utils/monacoWidget.js +++ b/cocode/src/utils/monacoWidget.js @@ -47,4 +47,50 @@ class LabelWidget { } } -export { LabelWidget }; +class CaratWidget { + constructor(editor, position, color) { + this.editor = editor; + this.domNode = null; + this.position = position; + this.color = color; + } + + getId() { + return this.id; + } + + getDomNode() { + if (!this.domNode) { + this.domNode = document.createElement('div'); + this.domNode.style.background = this.color; + this.domNode.classList.add('blink'); + this.domNode.style.width = '0.2rem'; + this.domNode.style.height = '1.5rem'; + } + return this.domNode; + } + + getPosition() { + return { + position: this.position, + preference: [0] + }; + } + updatePosition(position) { + this.position = position; + this.editor.layoutContentWidget(this); + } + + showCursor(position) { + this.domNode.style.visibility = 'inherit'; + this.position = position; + this.editor.layoutContentWidget(this); + } + + hiddenCursor() { + this.domNode.style.visibility = 'hidden'; + this.editor.layoutContentWidget(this); + } +} + +export { LabelWidget, CaratWidget }; \ No newline at end of file From 1ecfd5e3f7d62619bfd646637a6c92197d083d3b Mon Sep 17 00:00:00 2001 From: hzoou Date: Sun, 22 Dec 2019 16:42:17 +0900 Subject: [PATCH 07/11] =?UTF-8?q?chore:=20=ED=81=B4=EB=A6=BD=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 클립보트 아이콘에 alt, title 속성을 추가했습니다. --- cocode/src/containers/Live/LiveOnTab/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cocode/src/containers/Live/LiveOnTab/index.js b/cocode/src/containers/Live/LiveOnTab/index.js index 61529764..9f140b7b 100644 --- a/cocode/src/containers/Live/LiveOnTab/index.js +++ b/cocode/src/containers/Live/LiveOnTab/index.js @@ -40,7 +40,12 @@ function LiveOnTab() { {ON_DESCRIPTION} - + {url} {user === owner ? ( @@ -56,4 +61,4 @@ function LiveOnTab() { ); } -export default LiveOnTab; \ No newline at end of file +export default LiveOnTab; From 34db13d1e89ba5d64dad6fd9bc1f0eb683e4e6ce Mon Sep 17 00:00:00 2001 From: hzoou Date: Sun, 22 Dec 2019 16:47:22 +0900 Subject: [PATCH 08/11] =?UTF-8?q?chore:=20=EC=83=88=ED=83=AD=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 브라우저 우측에 새탭으로 이동할 수 있는 아이콘을 추가했습니다. --- cocode/src/components/Project/BrowserV2/open.svg | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 cocode/src/components/Project/BrowserV2/open.svg diff --git a/cocode/src/components/Project/BrowserV2/open.svg b/cocode/src/components/Project/BrowserV2/open.svg new file mode 100644 index 00000000..912733d5 --- /dev/null +++ b/cocode/src/components/Project/BrowserV2/open.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file From a42f305a42fe8f0a3d931c9d5120f6d27b094f87 Mon Sep 17 00:00:00 2001 From: hzoou Date: Sun, 22 Dec 2019 16:48:23 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20=EB=B8=8C=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=EC=A0=80=20=EC=83=88=20=ED=83=AD=EC=9C=BC=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 브라우저의 아이콘을 클릭하면 해당 url을 새 탭으로 이동하는 기능을 추가했습니다. 해당 프로젝트가 템플릿 프로젝트라면 토스트를 띄웠습니다. --- .../src/components/Project/BrowserV2/index.js | 15 ++++++++ .../src/components/Project/BrowserV2/style.js | 38 ++++++------------- cocode/src/constants/notificationMessage.js | 4 +- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/cocode/src/components/Project/BrowserV2/index.js b/cocode/src/components/Project/BrowserV2/index.js index e7c8a6b7..8279a39f 100644 --- a/cocode/src/components/Project/BrowserV2/index.js +++ b/cocode/src/components/Project/BrowserV2/index.js @@ -8,6 +8,7 @@ import React, { import { useParams } from 'react-router-dom'; import * as Styled from './style'; +import open from './open.svg'; import search from './search.svg'; import addToast from 'components/Common/Toast'; @@ -30,6 +31,7 @@ import { KEY_CODE_ENTER } from 'constants/keyCode'; const MIN_WAIT_TIME = 1500; const UPDATE_PROJECT = 'updateProject'; const PROTOCOLS = ['http://', 'https://']; +const NEW_COCONUT = `${COCONUT_SERVER}/new`; function BrowserV2({ ...props }) { const { projectId } = useParams(); @@ -137,6 +139,13 @@ function BrowserV2({ ...props }) { iframeReference.current.contentWindow.postMessage(data, '*'); }, [project]); + const handleClickOpenTab = () => { + const coconutUrl = addressReference.current.value; + if (NEW_COCONUT === coconutUrl) + return addToast.error(NOTIFICATION.NEED_TO_SAVE); + window.open(coconutUrl, '_blank'); + }; + useEffect(handleChangeCurrentURL, [projectId]); useEffect(handleUpdateDependency, [dependencyInstalling]); useEffect(handleUpdateFile, [files]); @@ -154,6 +163,12 @@ function BrowserV2({ ...props }) { defaultValue={addressInputURL} onKeyUp={handleAddressInputKeyDown} /> + Date: Sun, 22 Dec 2019 16:50:29 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20?= =?UTF-8?q?=EC=97=90=EB=94=94=ED=84=B0=20=EB=8F=99=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cocode/src/actions/Project.js | 6 ++++++ cocode/src/actions/types.js | 2 ++ cocode/src/containers/Live/Editor/index.js | 13 ++++++++----- cocode/src/pages/Live/index.js | 20 ++++++++++++++++---- cocode/src/reducers/ProjectReducer.js | 9 +++++++++ 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/cocode/src/actions/Project.js b/cocode/src/actions/Project.js index 0c84f278..bf32a06f 100644 --- a/cocode/src/actions/Project.js +++ b/cocode/src/actions/Project.js @@ -3,6 +3,7 @@ import { UPDATE_CODE, UPDATE_CODE_FROM_FILE_ID, FETCH_PROJECT, + UPDATE_FILES, SELECT_FILE, UPDATE_FILE_NAME, CREATE_FILE, @@ -21,6 +22,10 @@ function fetchProjectActionCreator(payload) { return { type: FETCH_PROJECT, payload }; } +function updateFilesActionCreator(payload) { + return { type: UPDATE_FILES, payload }; +} + function updateCodeActionCreator(payload) { return { type: UPDATE_CODE, payload }; } @@ -65,6 +70,7 @@ export { updateCodeActionCreator, updateCodeFromFileIdActionCreator, fetchProjectActionCreator, + updateFilesActionCreator, selectFileActionCreator, updateFileNameActionCreator, createFileActionCreator, diff --git a/cocode/src/actions/types.js b/cocode/src/actions/types.js index 6532f671..7f8aace7 100644 --- a/cocode/src/actions/types.js +++ b/cocode/src/actions/types.js @@ -9,6 +9,7 @@ const UPDATE_PROJECT_INFO = 'updateProjectInfo'; const UPDATE_CODE = 'updateCode'; const UPDATE_CODE_FROM_FILE_ID = 'updateCodeFromFileId'; const FETCH_PROJECT = 'fetchProject'; +const UPDATE_FILES = 'updateFiles'; const SELECT_FILE = 'selectFile'; const CREATE_FILE = 'createFile'; const UPDATE_FILE_NAME = 'updateFileName'; @@ -38,6 +39,7 @@ export { UPDATE_CODE, UPDATE_CODE_FROM_FILE_ID, FETCH_PROJECT, + UPDATE_FILES, SELECT_FILE, UPDATE_FILE_NAME, CREATE_FILE, diff --git a/cocode/src/containers/Live/Editor/index.js b/cocode/src/containers/Live/Editor/index.js index 71f68ff0..3ceb500b 100644 --- a/cocode/src/containers/Live/Editor/index.js +++ b/cocode/src/containers/Live/Editor/index.js @@ -29,11 +29,12 @@ const MAX_RANGE = { const userCursor = {}; -function Editor({ handleForkCoconut }) { +function Editor({ isConnected }) { const { user } = useContext(UserContext); const { projectId } = useParams(); const { project, dispatchProject } = useContext(ProjectContext); - const { socket } = useContext(LiveContext); + const liveContext = useContext(LiveContext); + const { socket } = liveContext; const [code, setCode] = useState(project.editingCode); const [isEditorMounted, setIsEditorMounted] = useState(false); const [_, setRequest] = useFetch({}); @@ -140,11 +141,12 @@ function Editor({ handleForkCoconut }) { //initialize if (!socket) return; if (!isEditorMounted) return; + if (!isConnected) return; isBusy.current = false; filesRef.current = JSON.parse(JSON.stringify(files)); socket.on('change', handleOnChangeCode); socket.on('moveCursor', handleMoveCursor); - }, [socket, isEditorMounted]); + }, [socket, isEditorMounted, isConnected]); const handleOnChangeCode = (socketId, fileId, op) => { if (socket.id === socketId) { @@ -161,8 +163,9 @@ function Editor({ handleForkCoconut }) { const str2 = originCode.slice(op.rangeOffset + op.rangeLength); const changedCode = `${str1}${op.text}${str2}`; filesRef.current[fileId].contents = changedCode; - const updateCodeFromFileIdAction = - updateCodeFromFileIdActionCreator({ fileId, changedCode }); + const updateCodeFromFileIdAction = updateCodeFromFileIdActionCreator( + { fileId, changedCode } + ); dispatchProject(updateCodeFromFileIdAction); return; } diff --git a/cocode/src/pages/Live/index.js b/cocode/src/pages/Live/index.js index f5d490da..ab055c51 100644 --- a/cocode/src/pages/Live/index.js +++ b/cocode/src/pages/Live/index.js @@ -20,7 +20,10 @@ import addToast from 'components/Common/Toast'; import { LiveContext, ProjectContext, UserContext } from 'contexts'; import { ProjectReducer } from 'reducers'; -import { fetchProjectActionCreator } from 'actions/Project'; +import { + fetchProjectActionCreator, + updateFilesActionCreator +} from 'actions/Project'; import { liveOnActionCreator, liveOffActionCreator, @@ -32,7 +35,10 @@ import { TAB_BAR_THEME } from 'constants/theme'; import { COCODE_SERVER } from 'config'; import useFetch from 'hooks/useFetch'; import { getProjectInfoAPICreator } from 'apis/Project'; -import { SHUT_DOWN_LIVE_SHARE, LOADING_LIVE } from 'constants/notificationMessage'; +import { + SHUT_DOWN_LIVE_SHARE, + LOADING_LIVE +} from 'constants/notificationMessage'; import { getCookie } from 'utils/controlCookie'; const DEFAULT_CLICKED_TAB_INDEX = 0; @@ -77,6 +83,12 @@ function Live() { }; const handleAlreadyExistRoom = ({ host, project, participants }) => { + const { files } = project; + const filesCopy = JSON.parse(JSON.stringify(files)); + const updateFilesAction = updateFilesActionCreator({ + files: filesCopy + }); + dispatchProject(updateFilesAction); dispatchLive( liveOnActionCreator({ socket, @@ -150,14 +162,14 @@ function Live() { setClickedTabIndex }} > -
+
{isFetched && ( - + diff --git a/cocode/src/reducers/ProjectReducer.js b/cocode/src/reducers/ProjectReducer.js index a3b24b38..a003b9bd 100644 --- a/cocode/src/reducers/ProjectReducer.js +++ b/cocode/src/reducers/ProjectReducer.js @@ -1,6 +1,7 @@ // 참고: https://github.com/dal-lab/frontend-tdd-examples/blob/master/6-todo-redux/src/reducers.js import { UPDATE_PROJECT_INFO, + UPDATE_FILES, UPDATE_CODE, UPDATE_CODE_FROM_FILE_ID, FETCH_PROJECT, @@ -94,6 +95,13 @@ function getDependencyList(files, root) { }, {}); } +const updateFiles = (state, { files }) => { + return { + ...state, + files + }; +}; + // Update code const updateCode = (state, { changedCode }) => { return { @@ -332,6 +340,7 @@ function ProjectReducer(state, { type, payload }) { const reducers = { [UPDATE_PROJECT_INFO]: updateProjectInfo, [FETCH_PROJECT]: fetchProject, + [UPDATE_FILES]: updateFiles, [UPDATE_CODE]: updateCode, [UPDATE_CODE_FROM_FILE_ID]: updateCodeFromFileId, [SELECT_FILE]: selectFile, From 171c69564af6fb433c97d130bfc7f0f50e9f1acf Mon Sep 17 00:00:00 2001 From: hzoou Date: Sun, 22 Dec 2019 18:18:45 +0900 Subject: [PATCH 11/11] =?UTF-8?q?chore:=20=ED=97=A4=EB=8D=94=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=9A=B0=EC=B8=A1=EC=97=90=20=ED=99=94?= =?UTF-8?q?=EC=82=B4=ED=91=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 해당 영역을 클릭하라는 인터렉션을 위해 헤더 프로필 우측에 화살표를 추가했습니다. --- .../components/Common/UserProfile/avatar.jpeg | Bin 2620 -> 0 bytes .../src/components/Common/UserProfile/down.svg | 4 ++++ .../src/components/Common/UserProfile/index.js | 5 ++++- .../src/components/Common/UserProfile/style.js | 17 +++++++++++++++-- 4 files changed, 23 insertions(+), 3 deletions(-) delete mode 100644 cocode/src/components/Common/UserProfile/avatar.jpeg create mode 100644 cocode/src/components/Common/UserProfile/down.svg diff --git a/cocode/src/components/Common/UserProfile/avatar.jpeg b/cocode/src/components/Common/UserProfile/avatar.jpeg deleted file mode 100644 index 3c082f339ee8c7de071a57f0f424ba49cdf5b770..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2620 zcmbW$cQoAT8VB&-m>DxdFpNZtQAP`4jW)U@k|>dh7Hx+p$ttTGWh_>S79>Ooi>T2> z7m_GheI*!Sh#oD7Fr&AbYj^M2bN{^ee$RQIKc46P=X2iop^wuifk*(($OvPE!eKBN z6BGO#3-UY*GcyYh2PYfy5-&>N5-&f$AX-LTP*_@opI<^*Qu+ru1qB6^xQYfAqb?(_ zfB`WvF|jbSaGyWVjS=D(!u-#nw*v?`U;$WwLBaq70t7~Y=$)W^002Qje+Bq=Kn!3A zlo1AJI>*coFo3{d1_&4mg+R`32A|CU2m;C>BzKjO)5s1c?1jXHCFQ|IuGMsK84quv zS1ik57D_|FW>Sw7l|lb&W#Z-r3#TKR7%B0pPzmXZ3H;e|QjQ9tH>m41xXW0WtWW z9WVj{6_R7*xM~Em^Wqf7gu#*5lJaUgm_+1_x47)RhtFL=E6j^h{v`b+`tLyD|Ci_= z&_5pfXMhC^I(slM0?+~$d9F2LjW$erB(yGXhppnMNJ*cBcY%tW`blDUS%W2GGJf^= z4PsjEUYj_ZV4{~}d`laO^WcL?PXIpJkEQI3fkrPJUc*|jr0a4oGBD$?#R2))OX%s- zDS`sPCs81RSVUR5T6TXJ0e{YElG$j8W-M}kTht*KL}GyNyk1Vd$;{zdFxJEtE*sMC zE)t7wA57ESpSPMp3}@bNs$)xy{@(FT!*wVxMi`VRp%Cv;t{K`=GhQswz5+pzN`q5E zJ!xiNZB#AHJ3-R#4->fM7hY~sx}`Fx;&i}1TwuvkU$m41^p(xaauXw?ZHf+QaQS|5 ziI2}>#V@d`xP!)^T%>pf=iMHt+rk4Y-9dJU%|xpdIg^im|GBm{Y4BP<7G*MI?(DF& ze7L|!rDAXGJMcYO!Jjs~z2I4KZy0U#3Uv3rX{8)Ex6moKC96;X7!g9f&HQ3AD-EL} zCw}3}m(W7z9>t#bHJOftDTW1?vOMK|=Es;HZ!pNbIvlb14ew2q~jV zr?oG|KsYz7jF<>aC$IZ@D9X82e;8I9IGIAb(37Gv4=rly44ljm+JHk`F9jFtFuXCq zj#W-nW%Xk-*|cuD$mGhpteykxDlWAOjZ|InSZsNn^wJLX zGrrYtDRwdQr%ASx9ozn_Mpv{p9}BE6u@)`FUY#0By|;8Th5(W)@lHBmS5510NO$`%^n20?V*lhQgb>8nn4iG>!A0u0dKl|u9(g}N zb62Xue6QB(L%*)vi|ga$^aEQX!mRsdb|p1zL}2u*pS=5x23} z144&mo3V<`3eytL+24|)bb9W3Wzo!*l!#d^HYMrzO0M4|hIQ;6@op&giH!MTUu|61 zWARuv;C+#)9?tg3$HaJ5jcefW=S1~80&~n{MEpiF!Ax8v%YU$W-AvY9gq_=MqDl9# zU0gc&3=)?Ul=5IEX7Bt8x4f0BHVVgajVxewC_>Cd1#H1=KQ=o&DE~qK z+qydE{EZQI+v)t%Mx*u~glz7<#`=)``zlrYnw?c^*UrAk>r@r*uGZ$|XGWvO9~Bw2 z6Rn?4`z5v&8CNt1CKfs6yWwmNr3_l$yu)3wdkDv9dYxeR73ayS4Ktv%3ZzX) z*dy$m&$yRWhztdpxk1uNc&z*46;_=5M8A_-6o59QY=_koG@oDE`@@dAd_3ey=V-+HE7#8IDrTl6KJ0d!dbqas8Q~bbN75_PMlWs$zGY>lwiwqWgDB~k}16V@plWhpSQH6jf11KaAVmID+;Ys zy5-XN?;npvPdqu3K$9Wu?w41XlFmvh&w| zeauvzC>=Ok$4RjSCCZefUC$8j$|fwP!5NlaFdM02{1{o^|?IfDn87HWZ362z<%-N zl2*#`oGNK&#T1}TeL{PNj$TnK@!~X%MLO^ciJ`Kr;Y|T^sRrm)$Kc_Hwdc&NbOogL zQYCfq*_-VO8bK6{lJ zMvZqZMB`PIcj(bt=nldVwQhZSq + + \ No newline at end of file diff --git a/cocode/src/components/Common/UserProfile/index.js b/cocode/src/components/Common/UserProfile/index.js index 43b0ac47..47ca89e5 100644 --- a/cocode/src/components/Common/UserProfile/index.js +++ b/cocode/src/components/Common/UserProfile/index.js @@ -1,13 +1,16 @@ import React from 'react'; import * as Styled from './style'; + import DropDownMenu from 'components/Common/DropDownMenu'; +import down from './down.svg'; function UserProfile({ username, avatar, menuItems }) { return ( {username} + - + ); diff --git a/cocode/src/components/Common/UserProfile/style.js b/cocode/src/components/Common/UserProfile/style.js index df4d63ad..e727023b 100644 --- a/cocode/src/components/Common/UserProfile/style.js +++ b/cocode/src/components/Common/UserProfile/style.js @@ -20,8 +20,21 @@ const UserAvatar = styled.img` width: 3rem; height: 3rem; border-radius: 0.5rem; - cursor: pointer; } `; -export { UserProfile, UserName, UserAvatar }; \ No newline at end of file +const DownArrow = styled.img` + & { + width: 1rem; + height: 1rem; + margin: auto 0 auto 1rem; + cursor: pointer; + filter: invert(0.3); + } + + &:hover { + filter: invert(0); + } +`; + +export { UserProfile, UserName, UserAvatar, DownArrow }; \ No newline at end of file