├── src ├── pages │ ├── SignIn.css │ ├── ImportFeed.css │ ├── NewChannel.css │ ├── InviteRequest.css │ ├── DeleteChannel.css │ ├── Channel.css │ ├── DeleteChannel.js │ ├── NewChannel.js │ ├── ImportFeed.js │ ├── InviteRequest.js │ ├── SignIn.js │ └── Channel.js ├── App.css ├── components │ ├── MessageList.css │ ├── SelectIdentity.css │ ├── ui │ │ ├── Icon.js │ │ ├── Button.js │ │ ├── Button.css │ │ ├── File.js │ │ └── File.css │ ├── UserList.css │ ├── Notifications.css │ ├── UserList.js │ ├── SelectIdentity.js │ ├── Message.css │ ├── ChannelList.css │ ├── MessageList.js │ ├── Notifications.js │ ├── Message.js │ ├── Compose.css │ ├── ChannelList.js │ └── Compose.js ├── App.test.js ├── layouts │ ├── FullScreen.css │ ├── FullScreen.js │ ├── Channel.css │ └── Channel.js ├── images │ ├── icon.js │ ├── cabal-emoji-picker.svg │ ├── new-channel.svg │ ├── new-channel-active.svg │ ├── down-arrow.svg │ └── download.svg ├── electron │ ├── preload.js │ ├── menu.js │ ├── main.js │ └── network.js ├── index.js ├── redux │ ├── store.js │ ├── commands.js │ ├── utils.test.js │ ├── reducers.test.js │ ├── utils.js │ ├── network.js │ └── reducers.js ├── utils.js ├── index.css └── App.js ├── .env ├── assets └── icon.icns ├── public ├── favicon.png ├── electron.js └── index.html ├── Artwork ├── desktop-demo.gif ├── logo-512x512.png ├── logo-1024x1024.png ├── desktop-screenshot.png ├── banner-slim-1280x360.png ├── avatar-twitter-640x640.png ├── banner-github-1280x640.png ├── banner-twitter-1500x500.png ├── cabal-emoji-picker.svg ├── new-channel.svg ├── new-channel-active.svg ├── logo.svg └── avatar-twitter.svg ├── CODE_OF_CONDUCT.md ├── .gitignore ├── .eslintrc.js ├── README.md └── package.json /src/pages/SignIn.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/ImportFeed.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | INLINE_RUNTIME_CHUNK=false 2 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: red; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/assets/icon.icns -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/pages/NewChannel.css: -------------------------------------------------------------------------------- 1 | .new-channel-form .new-channel-note { 2 | margin-top: 0.75rem; 3 | } 4 | -------------------------------------------------------------------------------- /Artwork/desktop-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/desktop-demo.gif -------------------------------------------------------------------------------- /Artwork/logo-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/logo-512x512.png -------------------------------------------------------------------------------- /Artwork/logo-1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/logo-1024x1024.png -------------------------------------------------------------------------------- /Artwork/desktop-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/desktop-screenshot.png -------------------------------------------------------------------------------- /Artwork/banner-slim-1280x360.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/banner-slim-1280x360.png -------------------------------------------------------------------------------- /Artwork/avatar-twitter-640x640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/avatar-twitter-640x640.png -------------------------------------------------------------------------------- /Artwork/banner-github-1280x640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/banner-github-1280x640.png -------------------------------------------------------------------------------- /Artwork/banner-twitter-1500x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/banner-twitter-1500x500.png -------------------------------------------------------------------------------- /src/components/MessageList.css: -------------------------------------------------------------------------------- 1 | .message-list { 2 | flex-grow: 1; 3 | padding: 0.75rem; 4 | 5 | overflow-y: auto; 6 | } 7 | -------------------------------------------------------------------------------- /public/electron.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | // NOTE: we are in ./build 4 | const esmRequire = require('esm')(module); 5 | esmRequire('../src/electron/main.js'); 6 | -------------------------------------------------------------------------------- /src/pages/InviteRequest.css: -------------------------------------------------------------------------------- 1 | .invite-request-form .invite-request-tips { 2 | margin-top: 1rem; 3 | } 4 | 5 | .invite-request-form .invite-request-tips ul { 6 | margin: 0; 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | * [Node.js Code of Conduct](https://github.com/nodejs/admin/blob/master/CODE_OF_CONDUCT.md) 4 | * [Node.js Moderation Policy](https://github.com/nodejs/admin/blob/master/Moderation-Policy.md) 5 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import App from './App'; 5 | 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render(, div); 9 | ReactDOM.unmountComponentAtNode(div); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/SelectIdentity.css: -------------------------------------------------------------------------------- 1 | .form-select { 2 | position: relative; 3 | } 4 | 5 | .form-select .form-select-hidden { 6 | position: absolute; 7 | width: 100%; 8 | height: 100%; 9 | top: 0; 10 | left: 0; 11 | opacity: 0; 12 | } 13 | 14 | .form-select .form-select-hidden select { 15 | width: 100%; 16 | height: 100%; 17 | } 18 | -------------------------------------------------------------------------------- /src/layouts/FullScreen.css: -------------------------------------------------------------------------------- 1 | .full-screen-layout-container { 2 | display: flex; 3 | height: 100%; 4 | width: 100%; 5 | 6 | flex-direction: column; 7 | justify-content: center; 8 | text-align: center; 9 | } 10 | 11 | .full-screen-layout-container .full-screen-layout { 12 | display: block; 13 | margin: 0 auto; 14 | max-width: 32rem; 15 | padding: 0.5rem; 16 | } 17 | -------------------------------------------------------------------------------- /src/images/icon.js: -------------------------------------------------------------------------------- 1 | import emoji from './cabal-emoji-picker.svg'; 2 | import downArrow from './down-arrow.svg'; 3 | import download from './download.svg'; 4 | import newChannel from './new-channel.svg'; 5 | import newChannelActive from './new-channel-active.svg'; 6 | 7 | export default { 8 | emoji, 9 | downArrow, 10 | download, 11 | newChannel, 12 | newChannelActive, 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/ui/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import icons from '../../images/icon'; 5 | 6 | // todo (tony-go) : add color prop; 7 | 8 | const Icon = ({ iconName, ...rest }) => {`${iconName}; 9 | 10 | Icon.propTypes = { 11 | iconName: PropTypes.oneOf(Object.keys(icons)).isRequired, 12 | }; 13 | 14 | export default Icon; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # editors 27 | /.vscode 28 | -------------------------------------------------------------------------------- /Artwork/cabal-emoji-picker.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/cabal-emoji-picker.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/electron/preload.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const { ipcRenderer: ipc } = require('electron'); 3 | 4 | ipc.on('response', (_, { seq, payload, error, stack }) => { 5 | window.postMessage({ sender: 'preload', seq, payload, error, stack }); 6 | }); 7 | 8 | window.addEventListener('message', ({ data: message }) => { 9 | if (message.sender === 'preload') { 10 | return; 11 | } 12 | 13 | const { type, seq, payload } = message; 14 | ipc.send(type, { seq, payload }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { ConnectedRouter } from 'connected-react-router'; 5 | 6 | import './index.css'; 7 | import App from './App'; 8 | import createStore from './redux/store'; 9 | 10 | const { store, history } = createStore(); 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | , document.getElementById('root')); 17 | -------------------------------------------------------------------------------- /src/layouts/FullScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './FullScreen.css'; 5 | 6 | export default function FullScreen({ children }) { 7 | return
8 |
9 | {children} 10 |
11 |
; 12 | } 13 | 14 | FullScreen.propTypes = { 15 | children: PropTypes.oneOfType([ 16 | PropTypes.element, 17 | PropTypes.arrayOf(PropTypes.element.isRequired), 18 | ]), 19 | }; 20 | -------------------------------------------------------------------------------- /src/pages/DeleteChannel.css: -------------------------------------------------------------------------------- 1 | .delete-channel .delete-channel-row { 2 | margin-bottom: 0.5rem; 3 | } 4 | 5 | .delete-channel .delete-channel-name { 6 | border-radius: 0.25rem; 7 | padding: 1rem 0.25rem; 8 | background: #F9F9F9; 9 | 10 | font-size: 1.75rem; 11 | font-weight: bold; 12 | text-overflow: ellipsis; 13 | white-space: nowrap; 14 | overflow: hidden; 15 | } 16 | 17 | .delete-channel .button { 18 | font-size: 1.5rem; 19 | line-height: 2rem; 20 | } 21 | 22 | .delete-channel .delete-button { 23 | margin-right: 0.5rem; 24 | } 25 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createHashHistory as createHistory } from 'history'; 2 | import { applyMiddleware, compose, createStore } from 'redux'; 3 | import { routerMiddleware } from 'connected-react-router'; 4 | import logger from 'redux-logger'; 5 | import thunk from 'redux-thunk'; 6 | 7 | import createRootReducer from './reducers'; 8 | 9 | export default () => { 10 | const history = createHistory(); 11 | 12 | const store = createStore( 13 | createRootReducer(history), 14 | compose(applyMiddleware(routerMiddleware(history), thunk, logger))) 15 | 16 | return { store, history }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/UserList.css: -------------------------------------------------------------------------------- 1 | .user-list { 2 | height: 100%; 3 | background: #f9f9f9; 4 | 5 | overflow-y: auto; 6 | } 7 | 8 | .user-list .user-list-title { 9 | width: 100%; 10 | padding: 0.25rem 0.5rem; 11 | margin-bottom: 0.5rem; 12 | border-bottom: 1px solid #E3E3E3; 13 | 14 | font-weight: bold; 15 | font-size: 1.25rem; 16 | } 17 | 18 | .user-list .user-list-elem { 19 | display: block; 20 | 21 | margin-bottom: 0.25rem; 22 | margin-left: 0.5rem; 23 | font-size: 1rem; 24 | font-weight: bold; 25 | 26 | text-overflow: ellipsis; 27 | white-space: nowrap; 28 | overflow: hidden; 29 | } 30 | -------------------------------------------------------------------------------- /src/layouts/Channel.css: -------------------------------------------------------------------------------- 1 | .channel-layout { 2 | display: flex; 3 | flex-direction: row; 4 | 5 | height: 100%; 6 | } 7 | 8 | .channel-layout .channel-layout-sidebar { 9 | flex: 0 1 16rem; 10 | border-right: 1px #090E20 solid; 11 | 12 | background: #0B132B; 13 | color: #797e8b; 14 | 15 | height: 100%; 16 | overflow-y: auto; 17 | } 18 | 19 | .channel-layout .channel-layout-sidebar .title { 20 | margin: 0.5rem 0 1rem 0.5rem; 21 | 22 | font-size: 1.5rem; 23 | font-weight: bold; 24 | line-height: 2rem; 25 | } 26 | 27 | .channel-layout .channel-layout-main { 28 | flex: 1 1 32rem; 29 | overflow: hidden; 30 | } 31 | -------------------------------------------------------------------------------- /src/images/new-channel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/new-channel-active.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/redux/commands.js: -------------------------------------------------------------------------------- 1 | import { 2 | invite, acceptInvite, displayHelp, displayFeedURL, renameIdentityPair, 3 | displayPeerID, 4 | } from './actions'; 5 | 6 | export default new Map([ 7 | [ 8 | 'help', 9 | { args: [ ], action: displayHelp }, 10 | ], 11 | [ 12 | 'invite', 13 | { args: [ 'inviteeName', 'request' ], action: invite }, 14 | ], 15 | [ 16 | 'accept-invite', 17 | { args: [ 'requestId', 'box' ], action: acceptInvite }, 18 | ], 19 | [ 20 | 'get-feed-url', 21 | { args: [ ], action: displayFeedURL }, 22 | ], 23 | [ 24 | 'rename', 25 | { args: [ 'newName' ], action: renameIdentityPair }, 26 | ], 27 | [ 28 | 'get-peer-id', 29 | { args: [ ], action: displayPeerID }, 30 | ], 31 | ]); 32 | 33 | -------------------------------------------------------------------------------- /src/layouts/Channel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import ChannelList from '../components/ChannelList'; 5 | import Notifications from '../components/Notifications'; 6 | 7 | import './Channel.css'; 8 | 9 | export default function ChannelLayout({ children }) { 10 | return
11 | 12 | 13 | 16 | 17 |
18 | {children} 19 |
20 |
; 21 | } 22 | 23 | ChannelLayout.propTypes = { 24 | children: PropTypes.oneOfType([ 25 | PropTypes.element, 26 | PropTypes.arrayOf(PropTypes.element.isRequired), 27 | ]), 28 | }; 29 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | PeerLinks 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function keyToColor(publicKey) { 2 | let r = parseInt(publicKey.slice(0, 2), 16); 3 | let g = parseInt(publicKey.slice(2, 4), 16); 4 | let b = parseInt(publicKey.slice(4, 6), 16); 5 | 6 | const scale = 255 / (Math.sqrt(r ** 2 + g ** 2 + b ** 2) + 1e-23); 7 | r *= scale; 8 | g *= scale; 9 | b *= scale; 10 | 11 | return `rgb(${r},${g},${b})`; 12 | } 13 | 14 | export function prerenderUserName({ name, publicKey, isInternal = false }) { 15 | if (!isInternal) { 16 | name = name.trim().replace(/^[#@]+/, ''); 17 | } 18 | 19 | return { 20 | name, 21 | color: keyToColor(publicKey), 22 | }; 23 | } 24 | 25 | export function getFeedURL(feed) { 26 | return `peerlinks://feed/${feed.publicKeyB58}?` + 27 | `name=${encodeURIComponent(feed.name)}`; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ui/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | 5 | // todo (tony-go) : add iconName Props and render Icon component 6 | // todo (tony-go) : replace all buttons in the app 7 | 8 | import './Button.css'; 9 | 10 | const Button = ({ onClick, color, label, ...rest }) => 11 | ; 21 | Button.propTypes = { 22 | onClick: PropTypes.func.isRequired, 23 | color: PropTypes.oneOf([ 24 | 'success', 25 | 'danger', 26 | ]), 27 | label: PropTypes.string.isRequired, 28 | }; 29 | 30 | Button.defaultProps = { 31 | color: 'black', 32 | }; 33 | 34 | export default Button; 35 | -------------------------------------------------------------------------------- /src/components/ui/Button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | border: 1px solid #4c4c9d; 3 | border-radius: 0.25rem; 4 | padding: 0.5rem; 5 | background: none; 6 | text-decoration:none; 7 | color: #4c4c9d; 8 | font-size: 1rem; 9 | 10 | cursor: pointer; 11 | transition: background 0.25s, color 0.25s; 12 | } 13 | 14 | .button:focus { 15 | outline: 0; 16 | } 17 | 18 | .button:disabled { 19 | color: #afafaf; 20 | } 21 | 22 | .button:hover:not(:disabled), .button:focus:not(:disabled) { 23 | background: #4c4c9d; 24 | color: #f9f9f9; 25 | } 26 | 27 | /* Types : danger, success */ 28 | 29 | .button.danger { 30 | background: none; 31 | color: #ff3c38; 32 | border-color: #ff3c38; 33 | } 34 | 35 | .button.danger:hover:not(:disabled) { 36 | background: #ff3c38; 37 | color: #fbf5f3; 38 | } 39 | 40 | .button.success { 41 | background: none; 42 | color: forestgreen; 43 | border-color: forestgreen; 44 | } 45 | 46 | .button.success:hover:not(:disabled) { 47 | background: forestgreen; 48 | color: #fbf5f3; 49 | } 50 | -------------------------------------------------------------------------------- /src/redux/utils.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | import * as assert from 'assert'; 3 | 4 | import { appendMessage } from './utils'; 5 | 6 | it('appends message to empty list', () => { 7 | const original = []; 8 | const next = original.slice(); 9 | appendMessage(next, { hash: 'a', height: 1 }); 10 | 11 | // No modifications 12 | assert.strictEqual(original.length, 0); 13 | 14 | // Appends 15 | assert.strictEqual(next.length, 1); 16 | assert.strictEqual(next[0].hash, 'a'); 17 | }); 18 | 19 | it('sorts by height and then by hash', () => { 20 | const list = []; 21 | appendMessage(list, { hash: 'a', height: 1 }); 22 | appendMessage(list, { hash: 'b', height: 1 }); 23 | appendMessage(list, { hash: 'a', height: 2 }); 24 | appendMessage(list, { hash: 'c', height: 3 }); 25 | appendMessage(list, { hash: 'a', height: 3 }); 26 | appendMessage(list, { hash: 'x', height: 0 }); 27 | 28 | assert.deepStrictEqual(list.map((msg) => `${msg.height}:${msg.hash}`), [ 29 | '0:x', 30 | '1:a', 31 | '1:b', 32 | '2:a', 33 | '3:a', 34 | '3:c', 35 | ]); 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/Notifications.css: -------------------------------------------------------------------------------- 1 | .notification-container { 2 | position: fixed; 3 | z-index: 100; 4 | top: 0; 5 | width: 100%; 6 | } 7 | 8 | .notification-container .notification-list { 9 | flex: 0 0 0; 10 | } 11 | 12 | .notification-container .notification-rest { 13 | flex: 1 0 0; 14 | } 15 | 16 | .notification-list .notification { 17 | padding: 0.25rem 0.5rem; 18 | height: 3rem; 19 | line-height: 2.5rem; 20 | overflow-y: hidden; 21 | 22 | display: flex; 23 | flex-direction: row; 24 | transition: 0.25 height; 25 | } 26 | 27 | .notification-list .notification { 28 | background: #fbf5f3; 29 | border-bottom: 1px solid #222; 30 | color: #222; 31 | } 32 | 33 | .notification-list .notification-error { 34 | background: #b14643; 35 | border-bottom: 1px solid #582322; 36 | color: #fbf5f3; 37 | } 38 | 39 | .notification-list .notification .notification-content { 40 | flex-grow: 1; 41 | } 42 | 43 | .notification-list .notification .notification-dismiss-container { 44 | flex-grow: 0; 45 | } 46 | 47 | .notification-list .notification .notification-dismiss { 48 | height: 2.5rem; 49 | } 50 | 51 | .notification-list .notification-error .notification-dismiss { 52 | background: #fbf5f3; 53 | } 54 | -------------------------------------------------------------------------------- /src/images/down-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/UserList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { prerenderUserName } from '../utils'; 5 | 6 | import './UserList.css'; 7 | 8 | export default function UserList({ channelName, users }) { 9 | const renderUser = (user, index) => { 10 | if (user.displayPath.length === 0) { 11 | return
#{channelName}
; 12 | } 13 | 14 | const { name, color } = prerenderUserName({ 15 | name: user.displayPath[user.displayPath.length - 1], 16 | publicKey: user.publicKeys[user.publicKeys.length - 1], 17 | }); 18 | 19 | const style = { color }; 20 | return
21 | {name} 22 |
; 23 | }; 24 | 25 | return
26 |
27 | Active peers: 28 |
29 | {users.map(renderUser)} 30 |
; 31 | } 32 | 33 | UserList.propTypes = { 34 | channelName: PropTypes.string.isRequired, 35 | users: PropTypes.arrayOf(PropTypes.shape({ 36 | displayPath: PropTypes.arrayOf(PropTypes.string.isRequired), 37 | publicKeys: PropTypes.arrayOf(PropTypes.string.isRequired), 38 | })), 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/SelectIdentity.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './SelectIdentity.css'; 5 | 6 | export default function SelectIdentity(props) { 7 | const { children } = props; 8 | 9 | const onChange = (e) => { 10 | e.preventDefault(); 11 | props.onChange(e.target.value); 12 | }; 13 | 14 | const active = children.find((child) => child.props.value === props.value); 15 | 16 | return
17 |
18 | {active && active.props.label} 19 |
20 |
21 | 24 |
25 |
; 26 | } 27 | 28 | SelectIdentity.propTypes = { 29 | className: PropTypes.string, 30 | onChange: PropTypes.func.isRequired, 31 | value: PropTypes.string, 32 | 33 | children: PropTypes.arrayOf(PropTypes.shape({ 34 | props: PropTypes.object.isRequired, 35 | })), 36 | }; 37 | 38 | export const Option = ({ value, label }) => { 39 | return