├── .editorconfig
├── .github
└── FUNDING.yml
├── .gitignore
├── .npmrc
├── @types
├── arraybuffer-mime
│ └── index.d.ts
├── prettysize
│ └── index.d.ts
└── string-to-arraybuffer
│ └── index.d.ts
├── CODE_OF_CONDUCT.md
├── LICENSE
├── Makefile
├── README.md
├── examples
└── embed
│ └── index.html
├── package.json
├── public
├── favicon.ico
├── index.html
├── manifest.json
└── robots.txt
├── src
├── App.test.js
├── App.tsx
├── components
│ ├── channel
│ │ ├── Channel.tsx
│ │ ├── ChatForm.tsx
│ │ ├── SidePanel.tsx
│ │ └── index.ts
│ ├── embed
│ │ ├── Embed.tsx
│ │ └── index.ts
│ ├── footer
│ │ ├── Footer.tsx
│ │ └── index.ts
│ ├── functional
│ │ ├── Clipboard.tsx
│ │ ├── DragAndDrop.tsx
│ │ ├── HelpTooltip.tsx
│ │ ├── MaxWidthContainer.tsx
│ │ ├── Tag.tsx
│ │ └── Terminal.tsx
│ ├── header
│ │ ├── Header.tsx
│ │ └── index.ts
│ ├── home
│ │ ├── Demo.tsx
│ │ ├── GettingStarted.tsx
│ │ ├── Hero.tsx
│ │ ├── Home.tsx
│ │ ├── OpenSource.tsx
│ │ ├── SelfHost.tsx
│ │ ├── Subscribe.tsx
│ │ ├── SubscribeForm.tsx
│ │ ├── UseCases.tsx
│ │ └── index.ts
│ └── notFound
│ │ ├── NotFound.tsx
│ │ └── index.ts
├── config
│ ├── config.ts
│ └── index.ts
├── global.ts
├── index.tsx
├── react-app-env.d.ts
├── serviceWorker.ts
├── theme.ts
├── themeContext.tsx
├── utils
│ ├── changeFavicon.ts
│ ├── generateRandomString.ts
│ ├── getUrlParams.ts
│ └── updateWindowTitle.ts
└── ws
│ └── index.ts
├── tsconfig.json
├── tsconfig.paths.json
└── tslint.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | # use line feed
8 | end_of_line = lf
9 |
10 | # ensure file ends with a newline when saving
11 | insert_final_newline = true
12 |
13 | # soft tabs
14 | indent_style = space
15 |
16 | # number of columns used for each indentation level
17 | indent_size = 2
18 |
19 | # remove any whitespace characters preceding newline characters
20 | trim_trailing_whitespace = true
21 |
22 | # character set
23 | charset = utf-8
24 |
25 | max_line_length = 80
26 |
27 | [*.md]
28 | max_line_length = 0
29 | trim_trailing_whitespace = false
30 |
31 | [COMMIT_EDITMSG]
32 | max_line_length = 0
33 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [miguelmota]
2 | patreon: miguelmota
3 |
--------------------------------------------------------------------------------
/.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 |
14 | # release
15 | /release
16 |
17 | # misc
18 | .DS_Store
19 | .env.local
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 |
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | .DS_Store
29 |
30 | *.tar.gz
31 |
32 | package-lock.json
33 | yarn.lock
34 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 | npm_config_save_exact=true
3 |
--------------------------------------------------------------------------------
/@types/arraybuffer-mime/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'arraybuffer-mime'
2 |
--------------------------------------------------------------------------------
/@types/prettysize/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'prettysize'
2 |
--------------------------------------------------------------------------------
/@types/string-to-arraybuffer/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'string-to-arraybuffer'
2 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at team@github.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT license
2 |
3 | Copyright (C) 2014 Miguel Mota
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9 | of the Software, and to permit persons to whom the Software is furnished to do
10 | 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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build
2 | build:
3 | npm run build
4 |
5 | .PHONY: gzip
6 | gzip:
7 | mkdir -p release
8 | tar -cv build/ | gzip > release/streamhut-web.tar.gz
9 |
10 | .PHONY: unzip
11 | unzip:
12 | tar -zxvf release/streamhut-web.tar.gz
13 |
14 | .PHONY: release
15 | release: build gzip
16 | git tag $(version)
17 | hub release create -a release/streamhut-web.tar.gz -m '$(version)' $(version)
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # streamhut-web
2 |
3 | > Web app for [streamhut](https://github.com/streamhut/streamhut)
4 |
5 | [](https://raw.githubusercontent.com/streamhut/web/master/LICENSE)
6 |
7 | ## Development
8 |
9 | Install dependencies:
10 |
11 | ```bash
12 | npm install
13 | ```
14 |
15 | Start application:
16 |
17 | ```bash
18 | npm run dev
19 | ```
20 |
21 | Build:
22 |
23 | ```bash
24 | npm run build
25 | ```
26 |
27 | ## License
28 |
29 | [MIT](LICENSE)
30 |
--------------------------------------------------------------------------------
/examples/embed/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@streamhut/web",
3 | "version": "0.0.1",
4 | "dependencies": {
5 | "@material-ui/core": "3.9.2",
6 | "@types/ansi-styles": "^3.2.1",
7 | "@types/jest": "26.0.0",
8 | "@types/lodash": "^4.14.157",
9 | "@types/node": "14.0.13",
10 | "@types/randomstring": "^1.1.6",
11 | "@types/react": "16.9.38",
12 | "@types/react-dom": "16.9.8",
13 | "@types/react-router-dom": "5.1.5",
14 | "@types/styled-components": "^5.1.1",
15 | "ansi-styles": "3.2.1",
16 | "arraybuffer-mime": "1.0.0",
17 | "chalk": "2.4.2",
18 | "clipboard": "2.0.4",
19 | "hyperlinkify": "0.0.3",
20 | "lodash": "4.17.11",
21 | "mdi-material-ui": "5.10.0",
22 | "moment": "2.24.0",
23 | "prettysize": "2.0.0",
24 | "randomstring": "1.1.5",
25 | "react": "16.8.4",
26 | "react-clipboard.js": "2.0.6",
27 | "react-dom": "16.8.4",
28 | "react-dropzone": "10.0.4",
29 | "react-router-dom": "5.0.0",
30 | "react-scripts": "2.1.8",
31 | "screenfull": "5.0.2",
32 | "string-to-arraybuffer": "1.0.2",
33 | "styled-components": "4.1.3",
34 | "typescript": "3.9.5",
35 | "xterm": "3.12.2"
36 | },
37 | "scripts": {
38 | "start": "HOST=0.0.0.0 PORT=3000 NODE_PATH=./ SKIP_PREFLIGHT_CHECK=true BROWSER=none react-scripts --max-old-space-size=4096 start",
39 | "dev": "npm run start",
40 | "build": "NODE_ENV=production NODE_PATH=./ SKIP_PREFLIGHT_CHECK=true GENERATE_SOURCEMAP=false react-scripts build",
41 | "test": "react-scripts test",
42 | "eject": "react-scripts eject",
43 | "lint": "tslint --fix -c tslint.json src/*.{ts,tsx} src/**/*.{ts,tsx} src/**/**/*.{ts,tsx}"
44 | },
45 | "eslintConfig": {
46 | "extends": "react-app"
47 | },
48 | "browserslist": [
49 | ">0.2%",
50 | "not dead",
51 | "not ie <= 11",
52 | "not op_mini all"
53 | ],
54 | "devDependencies": {
55 | "eslint-config-standard": "10.2.1",
56 | "eslint-config-standard-jsx": "6.0.2",
57 | "eslint-config-standard-react": "5.0.0",
58 | "standard": "12.0.1",
59 | "standardx": "5.0.0",
60 | "tslint": "5.20.0",
61 | "tslint-config-standard": "8.0.1",
62 | "tslint-etc": "1.5.6",
63 | "tslint-origin-ordered-imports-rule": "1.2.2",
64 | "tslint-react": "4.0.0"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/streamhut/web/4df4bb8834e942e36b4b48c69f4533b3ebc82403/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
29 | Streamhut
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
54 |
55 |
56 | You need to enable JavaScript to run this app.
57 |
58 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/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": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div')
7 | ReactDOM.render( , div)
8 | ReactDOM.unmountComponentAtNode(div)
9 | })
10 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
3 | import Home from 'src/components/home'
4 | import Channel from 'src/components/channel'
5 | import Embed from 'src/components/embed'
6 | import NotFound from 'src/components/notFound'
7 | import { ThemeProvider } from 'styled-components'
8 | import { lightTheme, darkTheme } from './theme'
9 | import { GlobalStyles } from './global'
10 | import ThemeContext, { ThemeContextProvider } from './themeContext'
11 |
12 | function App () {
13 | return (
14 |
15 |
16 | {theme =>
17 |
18 | <>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | >
29 |
30 | }
31 |
32 |
33 | )
34 | }
35 |
36 | export default App
37 |
--------------------------------------------------------------------------------
/src/components/channel/Channel.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { arrayBufferWithMime, arrayBufferMimeDecouple } from 'arraybuffer-mime'
3 | import styled from 'styled-components'
4 | import Header from 'src/components/header'
5 | import Footer from 'src/components/footer'
6 | import Terminal from 'src/components/functional/Terminal'
7 | import ChatForm from 'src/components/channel/ChatForm'
8 | import { getShareUrl } from 'src/config'
9 | import getUrlParams from 'src/utils/getUrlParams'
10 | import { updateWindowTitle, resetWindowTitle } from 'src/utils/updateWindowTitle'
11 | import { createWs } from 'src/ws'
12 |
13 | const UI = {
14 | SiteContainer: styled.main`
15 | display: flex;
16 | flex-direction: column;
17 | `,
18 | Header: styled.header`
19 | display: flex;
20 | justify-content: space-between;
21 | background: #e2e2e2;
22 | padding: 5px;
23 | position: relative;
24 | padding-right: 35px;
25 | `,
26 | Connections: styled.div`
27 | display: inline-block;
28 | width: auto;
29 | max-height: 120px;
30 | overflow: auto;
31 | font-size: 0.8rem;
32 | white-space: pre-wrap;
33 | margin-bottom: 2rem;
34 | background: rgba(239, 239, 239, 0.35);
35 | padding: 1rem;
36 | `
37 | }
38 |
39 | interface Props {
40 |
41 | }
42 |
43 | interface State {
44 | messages: any[]
45 | shareUrl: string
46 | fullscreen: boolean
47 | fullscreenUrl: string
48 | channel: string
49 | terminalText: any
50 | writable: boolean
51 | }
52 |
53 | class Channel extends Component {
54 | lineNumber: number
55 | ws: any
56 |
57 | constructor (props: Props) {
58 | super(props)
59 |
60 | const channel = window.location.pathname.substr(3)
61 |
62 | const state = {
63 | messages: [],
64 | shareUrl: getShareUrl(channel),
65 | fullscreen: false,
66 | fullscreenUrl: '',
67 | channel,
68 | terminalText: null,
69 | writable: false
70 | }
71 |
72 | const urlParams = getUrlParams()
73 | if ('f' in urlParams) {
74 | this.setFullScreen()
75 | }
76 |
77 | let p = window.location.pathname
78 | let q = window.location.search
79 |
80 | this.lineNumber = 0
81 |
82 | state.fullscreenUrl = `${p}${q}${q.length ? '&' : '?'}f=1`
83 | this.state = state
84 | }
85 |
86 | componentDidMount () {
87 | this.ws = createWs(this.state.channel)
88 |
89 | /*
90 | const connectionsLog = document.querySelector(`#connections`)
91 | */
92 |
93 | // function logMessage(data) {
94 | // connectionsLog.innerHTML = JSON.stringify(data, null, 2)
95 | // }
96 |
97 | this.ws.addEventListener('message', (event: any) => {
98 | this.handleIncomingMessage(event)
99 | })
100 |
101 | this.ws.addEventListener(`open`, () => {
102 | console.log(`connected`)
103 | // this.readCachedMessages()
104 | })
105 |
106 | this.ws.addEventListener(`close`, () => {
107 | console.log(`connection closed`)
108 | })
109 |
110 | document.addEventListener('visibilitychange', () => {
111 | if (!document.hidden) {
112 | resetWindowTitle()
113 | }
114 | }, false)
115 | }
116 |
117 | render () {
118 | const { writable } = this.state
119 |
120 | return (
121 | <>
122 |
123 |
126 |
127 | {/*
128 |
129 |
130 |
131 | */}
132 |
133 | this.resizeOutputContainer()}
135 | channel={this.state.channel}
136 | onData={this.handleFormSubmit}
137 | writable={writable}
138 | onWritable={this.handleWritableChange}
139 | text={this.state.terminalText} />
140 |
143 |
144 |
145 | >
146 | )
147 | }
148 |
149 | handleWritableChange = (writable: boolean) => {
150 | this.setState({
151 | writable
152 | })
153 | }
154 |
155 | setFullScreen = () => {
156 | document.body.classList.add('fullscreen')
157 | }
158 |
159 | resizeOutputContainer = () => {
160 | /*
161 | const outputContainer = document.getElementById('output-container') as HTMLElement
162 | const output = document.getElementById('output') as HTMLElement
163 | const form = document.getElementById('form') as HTMLElement
164 |
165 | const terminalContainer = this.terminalContainerRef.current
166 | const maxHeightAllowed = window.outerHeight - terminalContainer.offsetHeight - terminalContainer.offsetTop - form.offsetHeight - 25
167 | const maxHeight = outputContainer.offsetHeight - form.offsetHeight
168 |
169 | output.style.maxHeight = `${Math.min(maxHeight, maxHeightAllowed)}px`
170 | */
171 | }
172 |
173 | sendArrayBuffer = (arrayBuffer: any, mime: string) => {
174 | const abWithMime = arrayBufferWithMime(arrayBuffer, mime)
175 | try {
176 | this.ws.send(abWithMime)
177 | } catch (err) {
178 | console.error(err)
179 | }
180 | }
181 |
182 | handleIncomingMessage = async (event: any) => {
183 | let data: any = null
184 | if (typeof event === 'string') {
185 | let value: any = Buffer.from(event, 'hex')
186 | value = value.buffer
187 | data = value
188 | } else {
189 | data = event.data
190 | }
191 |
192 | console.log('incoming...')
193 |
194 | try {
195 | const json = JSON.parse(data)
196 | if (json.__server_message__) {
197 | // this.logMessage(json.__server_message__.data)
198 | return false
199 | }
200 | } catch (error) {
201 |
202 | }
203 |
204 | updateWindowTitle()
205 |
206 | // console.log('data:', data)
207 |
208 | // function buf2hex(buffer) { // buffer is an ArrayBuffer
209 | // return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
210 | // }
211 |
212 | const { mime, arrayBuffer } = arrayBufferMimeDecouple(data)
213 |
214 | console.log('received', mime)
215 |
216 | // TODO: fix
217 | if (mime.length > 20) {
218 | return
219 | }
220 |
221 | if (mime === 'shell' || mime === 'shell-stdin') {
222 | let text = new window.TextDecoder('utf-8').decode(new Uint8Array(arrayBuffer))
223 | /*
224 | text = text.replace(/(\r\n|\n\r|\n|\r)/g, (match, p1, offset, string) => {
225 | return green(`${p1}${`${(this.lineNumber++)}`.padEnd(4)} `)
226 | })
227 | */
228 |
229 | if (mime !== 'shell-stdin') {
230 | this.setState({
231 | terminalText: text
232 | })
233 | }
234 |
235 | return
236 | }
237 |
238 | const blob = new Blob([arrayBuffer], { type: mime })
239 |
240 | let ext = mime.split(`/`).join(`_`).replace(/[^\w\d_]/gi, ``)
241 | let url = window.URL.createObjectURL(blob)
242 |
243 | const t = await new Promise(resolve => {
244 | const reader = new FileReader()
245 | reader.onload = (evt: any) => {
246 | resolve(reader.result)
247 | }
248 |
249 | reader.readAsText(blob)
250 | })
251 |
252 | const message = {
253 | blob: {
254 | size: blob.size,
255 | type: blob.type
256 | },
257 | url,
258 | ext,
259 | mime,
260 | t
261 | }
262 |
263 | if (blob.size !== 0) {
264 | const messages = this.state.messages
265 | messages.push(message)
266 |
267 | this.setState({
268 | messages
269 | })
270 | }
271 | }
272 |
273 | handleFormSubmit = (item: any) => {
274 | const [arrayBuffer, mime] = item
275 | this.sendArrayBuffer(arrayBuffer, mime)
276 | }
277 | }
278 |
279 | export default Channel
280 |
--------------------------------------------------------------------------------
/src/components/channel/ChatForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import moment from 'moment'
4 | import prettysize from 'prettysize'
5 | import Tag from 'src/components/functional/Tag'
6 | import Clipboard from 'src/components/functional/Clipboard'
7 | import DragAndDrop from 'src/components/functional/DragAndDrop'
8 |
9 | const UI = {
10 | Clipboard: styled(Clipboard)`
11 | font-size: 1em;
12 | margin-left: 1em;
13 | `,
14 | OutputContainer: styled.div`
15 | display: flex;
16 | flex-direction: column;
17 | flex: 2;
18 | background: ${({ theme }) => (theme as any).outputBackground};
19 | `,
20 | Output: styled.output`
21 | width: 100%;
22 | max-height: 300px;
23 | overflow-x: hidden;
24 | overflow-y: auto;
25 | padding: 1rem;
26 | width: 100%;
27 | position: relative;
28 | display: flex;
29 | flex: 2;
30 | flex-direction: column;
31 | box-shadow: 0 1px 10px rgba(151,164,175,0.1);
32 | @media (max-width: 500px) {
33 | padding: 0;
34 | }
35 | textarea {
36 | font-size: 12px;
37 | }
38 | video {
39 | width: 100%;
40 | height: auto;
41 | max-width: 500px;
42 | }
43 | `,
44 | Footer: styled.footer`
45 | .copy {
46 | margin-left: 0.5rem;
47 | }
48 | `,
49 | FooterLeft: styled.div`
50 | display: flex;
51 | align-items: center;
52 | `,
53 | FooterRight: styled.div`
54 | display: inline-flex;
55 | align-items: flex-end;
56 | time {
57 | font-size: 12px;
58 | text-align: right;
59 | color: #999;
60 | }
61 | `,
62 | Form: styled.form`
63 | display: flex;
64 | justify-content: space-between;
65 | width: 100%;
66 | margin: 0;
67 | background: ${({ theme }) => (theme as any).chatFormBackground};
68 | color: ${({ theme }) => (theme as any).text};
69 | padding: 10px;
70 | position: relative;
71 | @media (max-width: 500px) {
72 | flex-direction: column;
73 | }
74 | `,
75 | FormGroup: styled.div`
76 | display: flex;
77 | justify-content: center;
78 | align-items: start;
79 | flex-direction: column;
80 | margin: 0;
81 | padding: 0.5rem;
82 | &.file-form-group {
83 | min-width: 250px;
84 | }
85 | &.input-form-group {
86 | width: 100%;
87 | }
88 | &.submit-form-group {
89 | }
90 | `,
91 | FileInputContainer: styled.div`
92 | margin-bottom: 0.5em;
93 | `,
94 | Header: styled.header`
95 | display: flex;
96 | justify-content: space-between;
97 | background: #e2e2e2;
98 | padding: 5px;
99 | position: relative;
100 | padding-right: 35px;
101 | `,
102 | NoMessages: styled.div`
103 | display: flex;
104 | align-items: flex-end;
105 | flex: 1;
106 | span {
107 | display: inline-block;
108 | font-style: italic;
109 | color: #7b7b7b;
110 | font-size: 1rem;
111 | }
112 | @media (max-width: 500px) {
113 | padding: 2em;
114 | }
115 | `,
116 | Message: styled.div`
117 | color: ${({ theme }) => (theme as any).text};
118 | background: ${({ theme }) => (theme as any).chatMessageBackground};
119 | width: 100%;
120 | font-size: 12px;
121 | margin: 0 0 0.2rem 0;
122 | article {
123 | display: flex;
124 | margin: 10px 0 15px 0;
125 | padding: 5px;
126 | }
127 | pre,
128 | code {
129 | width: 100%;
130 | overflow: auto;
131 | white-space: pre-wrap;
132 | word-wrap: break-word;
133 | }
134 | img {
135 | object-fit: contain;
136 | width: 100%;
137 | }
138 | header {
139 | display: flex;
140 | justify-content: space-between;
141 | align-items: center;
142 | color: ${({ theme }) => (theme as any).text};
143 | background: ${({ theme }) => (theme as any).chatMessageHeaderBackground};
144 | font-size: 0.8rem;
145 | position: relative;
146 | padding: 0.4rem 2rem 0.4rem 0.4rem;
147 | overflow: hidden;
148 | }
149 | header:after {
150 | content: "";
151 | display: block;
152 | position: absolute;
153 | width: 2rem;
154 | height: 4rem;
155 | background: ${({ theme }) => (theme as any).outputBackground};
156 | right: 0;
157 | top: 0;
158 | transform: rotate(-45deg) translate(1.7rem,-1em);
159 | }
160 | footer {
161 | display: flex;
162 | font-size: 0.8rem;
163 | justify-content: space-between;
164 | align-items: center;
165 | color: ${({ theme }) => (theme as any).text};
166 | background: ${({ theme }) => (theme as any).chatMessageHeaderBackground};
167 | padding: 0.4rem;
168 | }
169 | footer .download {
170 | margin-right: 10px;
171 | }
172 | footer .left {
173 | display: inline-flex;
174 | align-items: flex-start;
175 | }
176 | `,
177 | ImageLink: styled.a`
178 | max-width: 500px;
179 | `
180 | }
181 |
182 | interface Props {
183 | messages: any[]
184 | onSubmit: (item: any) => void
185 | }
186 |
187 | interface State {
188 | text: string
189 | file: any
190 | queuedFiles: any[]
191 | }
192 |
193 | class ChatForm extends Component {
194 | output: any
195 | fileInput: any
196 |
197 | constructor (props: Props) {
198 | super(props)
199 |
200 | this.state = {
201 | text: '',
202 | file: null,
203 | queuedFiles: []
204 | }
205 |
206 | this.output = React.createRef()
207 | this.fileInput = React.createRef()
208 | }
209 |
210 | componentWillReceiveProps (props: Props) {
211 | setTimeout(() => {
212 | this.scrollToLatestMessages()
213 | }, 0)
214 | }
215 |
216 | render () {
217 | let messages: any =
218 | no messages
219 |
220 | if (this.props.messages.length) {
221 | messages = this.props.messages.map(x => this.renderMessage(x))
222 | }
223 |
224 | return (
225 | <>
226 |
227 |
230 | {messages}
231 |
232 |
235 |
237 | Files Drag files into screen
238 |
239 |
245 |
246 |
247 | {this.state.queuedFiles.map(file =>
248 | this.handleFileRemove(event, file.name)} />
253 | )}
254 |
255 |
256 |
258 | Text enter to submit and shift-enter for newline
259 |
267 |
268 |
269 |
272 | Send
273 |
274 |
275 |
276 |
278 | >
279 | )
280 | }
281 |
282 | renderMessage = (data: any) => {
283 | if (!data) {
284 | return null
285 | }
286 | let { mime, blob, url, ext, t } = data
287 | let element: any = null
288 | let clipboardText = url
289 |
290 | if (/image/gi.test(mime)) {
291 | element =
296 |
297 |
298 | } else if (/video/gi.test(mime)) {
299 | element =
300 |
301 |
302 | } else if (/audio/gi.test(mime)) {
303 | element =
304 | } else if (/zip/gi.test(mime)) {
305 | element = .zip
306 | } else if (/pdf/gi.test(mime)) {
307 | element = .pdf
308 | // } else if (/(json|javascript|text)/gi.test(mime)) {
309 | } else {
310 | // if the text is just an image url
311 | if (/^https?:\/\/[^\s\r\n]+(png|jpe?g|svg)$/i.test(t)) {
312 | url = t
313 | clipboardText = url
314 | element =
315 |
316 |
317 | } else {
318 | clipboardText = t
319 |
320 | element = {t}
321 | }
322 | }
323 |
324 | const filename = `${Date.now()}_${ext}`
325 |
326 | const timestamp = moment().format('LLLL')
327 |
328 | return
329 |
330 | {blob.type} size: {prettysize(blob.size)}
331 | {url}↗
336 |
337 |
338 | {element}
339 |
340 |
341 |
342 | download
347 |
350 |
351 |
352 | {timestamp}
353 |
354 |
355 |
356 | }
357 |
358 | handleFileUpdate = () => {
359 | const files = this.state.queuedFiles
360 | for (let i = 0; i < files.length; i++) {
361 | const file = files[i]
362 | console.log(`file:`, file, file.type)
363 | if (!file) return
364 |
365 | const reader = new FileReader()
366 |
367 | const readFile = (event: any) => {
368 | const arrayBuffer = reader.result
369 | const mime = file.type
370 | this.props.onSubmit([arrayBuffer, mime])
371 | }
372 |
373 | reader.addEventListener('load', readFile)
374 | reader.readAsArrayBuffer(file)
375 | }
376 |
377 | this.clearFilesQueue()
378 | }
379 |
380 | handleTextUpdate = () => {
381 | const { text } = this.state
382 |
383 | if (text) {
384 | const mime = 'text/plain'
385 | const blob = new Blob([text], { type: mime })
386 | const reader = new FileReader()
387 |
388 | reader.addEventListener('load', (event: any) => {
389 | const arrayBuffer = reader.result
390 | this.props.onSubmit([arrayBuffer, mime])
391 | })
392 |
393 | reader.readAsArrayBuffer(blob)
394 | this.setState({ text: '' })
395 | }
396 | }
397 |
398 | handleSubmit = (event: any) => {
399 | event.preventDefault()
400 |
401 | this.handleTextUpdate()
402 | this.handleFileUpdate()
403 |
404 | this.setState({
405 | text: ''
406 | })
407 | }
408 |
409 | handleFileInputChange = (event: any) => {
410 | event.preventDefault()
411 |
412 | this.addFilesToQueue(event.target.files)
413 | }
414 |
415 | handleDrop = (files: any[]) => {
416 | this.addFilesToQueue(files)
417 | }
418 |
419 | handleFileRemove = (event: any, filename: string) => {
420 | this.removeFileFromQueue(filename)
421 | }
422 |
423 | addFilesToQueue = (files: any[]) => {
424 | const list = this.state.queuedFiles
425 |
426 | for (let file of files) {
427 | list.push(file)
428 | }
429 |
430 | this.setState({
431 | queuedFiles: list
432 | })
433 | }
434 |
435 | clearFilesQueue = () => {
436 | this.setState({
437 | queuedFiles: []
438 | })
439 |
440 | this.fileInput.current.value = ''
441 | }
442 |
443 | removeFileFromQueue = (filename: string) => {
444 | const list = this.state.queuedFiles
445 | // eslint-disable-next-line
446 | for (let [i, file] of list.entries()) {
447 | if (file.name === filename) {
448 | list.splice(i, 1)
449 | }
450 | }
451 | this.setState({
452 | queuedFiles: list
453 | })
454 | }
455 |
456 | handleKeyPress = (event: any) => {
457 | if (event.key === 'Enter' && (event.shiftKey || event.ctrlKey || event.altKey)) {
458 | if (!event.shiftKey) {
459 | this.setState({
460 | text: this.state.text + '\n'
461 | })
462 | }
463 | } else if (event.key === 'Enter') {
464 | this.handleSubmit(event)
465 | }
466 | }
467 |
468 | scrollToLatestMessages = () => {
469 | const container = this.output.current
470 | if ((container.scrollTop + 200) >= (container.scrollHeight - container.clientHeight)) {
471 | container.scrollTo(0, container.scrollHeight)
472 | }
473 | }
474 | }
475 |
476 | export default ChatForm
477 |
--------------------------------------------------------------------------------
/src/components/channel/SidePanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | class SidePanel extends Component {
4 | render () {
5 | return (
6 |
7 | )
8 | }
9 | }
10 |
11 | export default SidePanel
12 |
--------------------------------------------------------------------------------
/src/components/channel/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Channel'
2 |
--------------------------------------------------------------------------------
/src/components/embed/Embed.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import Terminal from 'src/components/functional/Terminal'
4 |
5 | const UI = {
6 | Container: styled.div`
7 |
8 | `
9 | }
10 |
11 | interface Props {
12 | }
13 |
14 | interface State {
15 | channel: string
16 | terminalText: string
17 | }
18 |
19 | class Embed extends Component {
20 | constructor (props: Props) {
21 | super(props)
22 |
23 | this.state = {
24 | channel: 'test',
25 | terminalText: 'testing'
26 | }
27 | }
28 |
29 | render () {
30 | const { channel, terminalText } = this.state
31 | return (
32 |
33 |
38 |
39 | )
40 | }
41 | }
42 |
43 | export default Embed
44 |
--------------------------------------------------------------------------------
/src/components/embed/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Embed'
2 |
--------------------------------------------------------------------------------
/src/components/footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Twitter from 'mdi-material-ui/Twitter'
3 | import GithubCircle from 'mdi-material-ui/GithubCircle'
4 | import moment from 'moment'
5 | import styled from 'styled-components'
6 |
7 | import MaxWidthContainer from 'src/components/functional/MaxWidthContainer'
8 |
9 | const UI = {
10 | Footer: styled.footer`
11 | align-items: start;
12 | padding: 4em 2rem;
13 | width: 100%;
14 | font-size: 1rem;
15 | text-align: right;
16 | background: ${({ theme }) => (theme as any).footerBackground};
17 | `,
18 | Container: styled.footer`
19 | display: flex;
20 | justify-content: space-between;
21 | opacity: 0.5;
22 | @media (max-width: 500px) {
23 | flex-direction: column;
24 | }
25 | `,
26 | LogoImage: styled.img`
27 | width: 100px;
28 | margin-left: 0.4rem;
29 | margin-bottom: 0.5rem;
30 | `,
31 | Copyright: styled.div`
32 | font-weight: bold;
33 | display: flex;
34 | align-items: center;
35 | color: #fff;
36 | @media (max-width: 500px) {
37 | margin-bottom: 1rem;
38 | }
39 | `,
40 | Social: styled.div`
41 | display: flex;
42 | align-items: center;
43 | vertical-align: middle;
44 | a {
45 | display: inline-flex;
46 | align-items: center;
47 | margin: 0 0 0 1rem;
48 | font-weight: 700;
49 | color: #fff;
50 | }
51 | @media (max-width: 500px) {
52 | flex-direction: column;
53 | align-items: flex-start;
54 | a {
55 | margin: 0 0 0.5rem 0;
56 | }
57 | }
58 | `
59 | }
60 |
61 | class Footer extends Component {
62 | render () {
63 | const year = moment().year()
64 |
65 | return (
66 |
97 | )
98 | }
99 | }
100 |
101 | export default Footer
102 |
--------------------------------------------------------------------------------
/src/components/footer/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Footer'
2 |
--------------------------------------------------------------------------------
/src/components/functional/Clipboard.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import ClipboardJS from 'react-clipboard.js'
4 | import ContentCopyIcon from 'mdi-material-ui/ContentCopy'
5 |
6 | const UI = {
7 | CopyIcon: styled(ContentCopyIcon)`
8 | font-size: 0.9rem;
9 | `
10 | }
11 |
12 | interface Props {
13 | className?: string
14 | style?: any
15 | clipboardText: string
16 | }
17 |
18 | interface State {
19 | copied: boolean
20 | }
21 |
22 | class Clipboard extends Component {
23 | constructor (props: Props) {
24 | super(props)
25 |
26 | this.state = {
27 | copied: false
28 | }
29 | }
30 |
31 | onClipboardCopy (event: any) {
32 | this.setState({
33 | copied: true
34 | })
35 |
36 | setTimeout(() => {
37 | this.setState({
38 | copied: false
39 | })
40 | }, 3e3)
41 | }
42 |
43 | render () {
44 | return (
45 | this.onClipboardCopy(event)}>
51 | {this.state.copied ?
52 | 'copied!'
53 | :
54 |
55 | }
56 |
57 | )
58 | }
59 | }
60 |
61 | export default Clipboard
62 |
--------------------------------------------------------------------------------
/src/components/functional/DragAndDrop.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 |
4 | const UI = {
5 | Overlay: styled.div`
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | width: 100%;
10 | height: 100%;
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | color: #000;
15 | border: 4px dashed #0075ff;
16 | background: rgba(255, 255, 255, 0.7);
17 | `,
18 | CenterBox: styled.div`
19 | border: 4px dashed #0075ff;
20 | font-size: 2rem;
21 | padding: 1rem;
22 | `
23 | }
24 |
25 | interface Props {
26 | handleDrop: any
27 | }
28 |
29 | interface State {
30 | drag: boolean
31 | }
32 |
33 | class DragAndDrop extends Component {
34 | dragCounter: number
35 |
36 | constructor (props: Props) {
37 | super(props)
38 | this.state = {
39 | drag: false
40 | }
41 |
42 | this.dragCounter = 0
43 | }
44 |
45 | handleDrag (event: any) {
46 | event.preventDefault()
47 | event.stopPropagation()
48 | }
49 |
50 | handleDragIn (event: any) {
51 | event.preventDefault()
52 | event.stopPropagation()
53 |
54 | this.dragCounter++
55 |
56 | if (event.dataTransfer.items &&
57 | event.dataTransfer.items.length > 0) {
58 | this.setState({ drag: true })
59 | }
60 | }
61 |
62 | handleDragOut (event: any) {
63 | event.preventDefault()
64 | event.stopPropagation()
65 |
66 | this.dragCounter--
67 |
68 | if (this.dragCounter === 0) {
69 | this.setState({ drag: false })
70 | }
71 | }
72 |
73 | handleDrop (event: any) {
74 | event.preventDefault()
75 | event.stopPropagation()
76 |
77 | this.setState({ drag: false })
78 |
79 | if (event.dataTransfer.files &&
80 | event.dataTransfer.files.length > 0) {
81 | this.props.handleDrop(event.dataTransfer.files)
82 | event.dataTransfer.clearData()
83 | this.dragCounter = 0
84 | }
85 | }
86 |
87 | componentDidMount () {
88 | let el = document.body
89 | el.addEventListener('dragenter', event => this.handleDragIn(event))
90 | el.addEventListener('dragleave', event => this.handleDragOut(event))
91 | el.addEventListener('dragover', event => this.handleDrag(event))
92 | el.addEventListener('drop', event => this.handleDrop(event))
93 | }
94 |
95 | componentWillUnmount () {
96 | let el = document.body
97 | el.removeEventListener('dragenter', event => this.handleDragIn(event))
98 | el.removeEventListener('dragleave', event => this.handleDragOut(event))
99 | el.removeEventListener('dragover', event => this.handleDrag(event))
100 | el.removeEventListener('drop', event => this.handleDrop(event))
101 | }
102 |
103 | render () {
104 | return (
105 |
106 | {this.state.drag &&
107 |
108 |
109 | Drop file here
110 |
111 |
112 | }
113 | {this.props.children}
114 |
115 | )
116 | }
117 | }
118 | export default DragAndDrop
119 |
--------------------------------------------------------------------------------
/src/components/functional/HelpTooltip.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Tooltip from '@material-ui/core/Tooltip'
3 |
4 | interface Props {
5 | text: string
6 | iconStyle: any
7 | }
8 |
9 | interface State {
10 | }
11 |
12 | class HelpTooltip extends Component {
13 | render () {
14 | return (
15 |
19 | ?
20 |
21 | )
22 | }
23 | }
24 |
25 | export default HelpTooltip
26 |
--------------------------------------------------------------------------------
/src/components/functional/MaxWidthContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 |
4 | const UI = {
5 | Container: styled.div`
6 | width: 100%;
7 | max-width: 900px;
8 | margin: 0 auto;
9 | `
10 | }
11 |
12 | class MaxWidthContainer extends Component {
13 | render () {
14 | return (
15 |
16 | {this.props.children}
17 |
18 | )
19 | }
20 | }
21 |
22 | export default MaxWidthContainer
23 |
--------------------------------------------------------------------------------
/src/components/functional/Tag.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Chip from '@material-ui/core/Chip'
3 |
4 | interface Props {
5 | className: string
6 | text: string
7 | onDelete: any
8 | }
9 |
10 | interface State {
11 | }
12 |
13 | class Tag extends Component {
14 | render () {
15 | return (
16 |
21 | )
22 | }
23 | }
24 |
25 | export default Tag
26 |
--------------------------------------------------------------------------------
/src/components/functional/Terminal.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import throttle from 'lodash/throttle'
4 | import Switch from '@material-ui/core/Switch'
5 | import ArrowExpandAll from 'mdi-material-ui/ArrowExpandAll'
6 | import ArrowExpand from 'mdi-material-ui/ArrowExpand'
7 | import ArrowCollapse from 'mdi-material-ui/ArrowCollapse'
8 | import FullscreenExit from 'mdi-material-ui/FullscreenExit'
9 | import HelpTooltip from 'src/components/functional/HelpTooltip'
10 | import str2ab from 'string-to-arraybuffer'
11 | import screenfull from 'screenfull'
12 | import { Terminal } from 'xterm'
13 | import * as fit from 'xterm/lib/addons/fit/fit'
14 | import * as termFullscreen from 'xterm/lib/addons/fullscreen/fullscreen'
15 | import ansi from 'ansi-styles'
16 | import { streamHostname, streamPort } from 'src/config'
17 |
18 | Terminal.applyAddon(fit)
19 | Terminal.applyAddon(termFullscreen)
20 |
21 | const green = (t: any) => `${ansi.greenBright.open}${t}${ansi.greenBright.close}`
22 |
23 | const ESC_KEY = 27
24 | const ENTER_KEY = 13
25 | const LETTER_I_KEY = 73
26 |
27 | const UI = {
28 | Container: styled.div`
29 | position: relative;
30 | padding-bottom: 1.2rem; /* same as resizer height */
31 | `,
32 | /* background: #293238; */
33 | TerminalContainer: styled.div`
34 | background-color: #000;
35 | position: relative;
36 | width: 100%;
37 | height: auto;
38 | overflow: hidden;
39 | `,
40 | Terminal: styled.div`
41 | &.blur {
42 | pointer-events: none;
43 | opacity: 0.8;
44 | }
45 | `,
46 | TerminalFooter: styled.footer`
47 | background: #000;
48 | width: 100%;
49 | display: flex;
50 | align-items: center;
51 | justify-content: flex-end;
52 | padding: 1rem;
53 | color: #fff;
54 | z-index: 1000;
55 | &.fixed {
56 | position: fixed;
57 | left: 0;
58 | bottom: 0;
59 | }
60 | `,
61 | TerminalFooterInner: styled.div`
62 | display: inline-block;
63 | margin-right: auto;
64 | `,
65 | TerminalResizer: styled.div`
66 | width: 100%;
67 | height: 1.2rem;
68 | position: absolute;
69 | bottom: 0;
70 | cursor: row-resize;
71 | border-width: 1px;
72 | border-style: solid;
73 | background: ${({ theme }) => (theme as any).resizerBackground};
74 | border-color: ${({ theme }) => (theme as any).resizerBorderColor};
75 | text-align: center;
76 | font-size: 1rem;
77 | line-height: 1;
78 | color: #797979;
79 | &:hover {
80 | background: ${({ theme }) => (theme as any).resizerBackgroundHover};
81 | border-color: ${({ theme }) => (theme as any).resizerBorderColorHover};
82 | }
83 | `,
84 | FullscreenButton: styled.button`
85 | font-size: 1rem;
86 | margin-left: 0.4em;
87 | color: #fff;
88 | opacity: 0.5;
89 | &:active,
90 | &:focus,
91 | &:hover {
92 | text-decoration: none;
93 | color: #fff;
94 | opacity: 1;
95 | }
96 | `,
97 | ReadWriteLabel: styled.label`
98 | display: inline-flex;
99 | justify-items: center;
100 | align-items: center;
101 | font-size: 0.8rem;
102 | opacity: 0.5;
103 | margin-right: 1rem;
104 | font-weight: 700
105 | cursor: pointer;
106 | `,
107 | TerminalPressedKey: styled.div`
108 | display: inline-block;
109 | fontSize: 0.8rem;
110 | opacity: 0.5;
111 | `,
112 | TerminalSmallHelperText: styled.div`
113 | display: inline-block;
114 | margin-right: 1rem;
115 | font-size: 0.8rem;
116 | opacity: 0.5;
117 | `
118 | }
119 |
120 | interface Props {
121 | channel?: string
122 | text?: any
123 | onResize?: () => void
124 | onData?: (item: any) => void
125 | hideFooter?: boolean
126 | resizable?: boolean
127 | writable?: boolean
128 | onWritable?: (writable: boolean) => void
129 | }
130 |
131 | interface State {
132 | hostname: string
133 | port: number
134 | expandedView: boolean
135 | fullscreen: boolean
136 | terminalPressedKey: any
137 | terminalBlurred: boolean
138 | terminalScrollable: boolean
139 | lastKeyPress: any
140 | lastKeyTimeout: any
141 | borderSize: any
142 | pos: any
143 | lastHeight: any
144 | }
145 |
146 | class TerminalComponent extends Component {
147 | terminalRef: any
148 | terminalContainerRef: any
149 | terminalResizerRef: any
150 | term: any
151 | lastInputChar: any
152 |
153 | constructor (props: Props) {
154 | super(props)
155 |
156 | this.state = {
157 | expandedView: false,
158 | fullscreen: false,
159 | terminalPressedKey: null,
160 | terminalBlurred: true,
161 | terminalScrollable: false,
162 | hostname: streamHostname,
163 | port: streamPort,
164 | lastKeyPress: null,
165 | lastKeyTimeout: null,
166 | borderSize: null,
167 | pos: null,
168 | lastHeight: null
169 | }
170 |
171 | this.terminalRef = React.createRef()
172 | this.terminalContainerRef = React.createRef()
173 | this.terminalResizerRef = React.createRef()
174 | }
175 |
176 | componentWillReceiveProps (props: Props) {
177 | let { text, writable } = props
178 | if (text) {
179 | this.term.write(text)
180 | this.lastInputChar = text
181 | }
182 |
183 | this.term.setOption('cursorStyle', writable ? 'block' : 'underline')
184 | this.term.setOption('cursorBlink', writable)
185 | this.term.setOption('disableStdin', !writable)
186 | }
187 |
188 | componentDidMount () {
189 | const { channel, writable } = this.props
190 | const { hostname } = this.state
191 |
192 | this.term = new Terminal({
193 | allowTransparency: false,
194 | bellStyle: 'none',
195 | bellSound: '',
196 | convertEol: true,
197 | scrollback: 10000,
198 | cursorStyle: writable ? 'block' : 'underline',
199 | cursorBlink: writable,
200 | disableStdin: !writable,
201 | drawBoldTextInBrightColors: true,
202 | fontFamily: '"Source Code Pro", Menlo, Monaco, Consolas, "Courier New", monospace'
203 | })
204 |
205 | let termNode = this.terminalRef.current
206 | termNode.style.display = 'block'
207 | this.term.open(termNode, false)
208 | this.blurTerminal()
209 | this.term.fit()
210 | const termScrollArea = document.querySelector('.xterm-viewport') as HTMLElement
211 |
212 | let echoChannel = channel ? `echo \\#${channel}` : ''
213 | let cmd = green(`exec &> >(nc ${hostname} 1337);${echoChannel}`)
214 | this.term.writeln(`To get started, run in your terminal:\n\n${cmd}\n`)
215 |
216 | this.setupTerminalResizer()
217 |
218 | this.term.on('blur', () => {
219 | this.blurTerminal()
220 | })
221 |
222 | const handleStdin = (data: string) => {
223 | if (this.props.onData) {
224 | this.lastInputChar = data
225 | const ab = str2ab(data)
226 | this.props.onData([ab, 'shell-stdin'])
227 | }
228 | }
229 |
230 | this.term.on('data', (data: any) => {
231 | handleStdin(data)
232 | })
233 |
234 | this.term.on('key', (_, event) => {
235 | if (event.keyCode === ENTER_KEY) {
236 | // handleStdin('\n')
237 | } else if (event.keyCode === ESC_KEY) {
238 | this.blurTerminal()
239 |
240 | if (this.state.expandedView) {
241 | this.exitExpandedView()
242 | }
243 |
244 | if (this.state.fullscreen) {
245 | this.exitFullscreen()
246 | }
247 | }
248 | })
249 |
250 | termScrollArea.addEventListener('scroll', throttle((event: any) => {
251 | if (this.state.terminalScrollable) {
252 | return
253 | }
254 | if (event.target.scrollHeight > event.target.clientHeight) {
255 | this.setState({
256 | terminalScrollable: true
257 | })
258 | }
259 | }, 100))
260 |
261 | window.addEventListener('resize', this.onWindowResize, false)
262 |
263 | window.addEventListener('keydown', throttle((event: any) => {
264 | if (event.keyCode === ESC_KEY) {
265 | this.blurTerminal()
266 | }
267 |
268 | this.handleNavigationKeys(event)
269 | this.handleKeyPressLog(event)
270 |
271 | const lastKeyPress = event.key
272 | clearTimeout(this.state.lastKeyTimeout)
273 | const lastKeyTimeout = setTimeout(() => {
274 | this.setState({
275 | lastKeyPress,
276 | terminalPressedKey: null
277 | })
278 | }, 1800)
279 | this.setState({
280 | lastKeyPress,
281 | lastKeyTimeout
282 | })
283 | }, 10), true)
284 | }
285 |
286 | componentWillUnmount () {
287 | if (this.terminalResizerRef.current) {
288 | let resizer = this.terminalResizerRef.current
289 | resizer.removeEventListener('mousedown', this.onTerminalResizer, false)
290 | // resizer.removeEventListener('touchstart', this.onTerminalResizer, false)
291 | }
292 |
293 | window.removeEventListener('resize', this.onWindowResize, false)
294 | }
295 |
296 | render () {
297 | const { resizable } = this.props
298 |
299 | return (
300 |
301 | this.focusTerminal()}>
304 |
310 |
311 | {this.renderFooter()}
312 | {this.renderResizer()}
313 |
314 | )
315 | }
316 |
317 | renderFooter () {
318 | if (this.props.hideFooter) return null
319 |
320 | const {
321 | terminalBlurred,
322 | terminalScrollable,
323 | expandedView,
324 | fullscreen
325 | } = this.state
326 |
327 | const { writable } = this.props
328 |
329 | return (
330 |
331 |
332 |
335 |
343 | {writable ? 'READ/WRITE' : 'READ-ONLY'}
344 |
350 |
351 | {this.state.terminalPressedKey}
352 |
353 | {(terminalBlurred && terminalScrollable) ?
354 | click to scroll terminal
355 | : null}
356 | {!terminalBlurred && !writable ? vim-shortcut keys enabled : null}
357 | {!terminalBlurred ?
358 | ESC to focus out
359 |
360 | : null}
361 | {this.renderExpandViewButton()}
362 | {this.renderFullscreenButton()}
363 |
364 | )
365 | }
366 |
367 | renderExpandViewButton () {
368 | const { expandedView } = this.state
369 |
370 | if (expandedView) {
371 | return (
372 | {
375 | event.preventDefault()
376 | this.exitExpandedView()
377 | }}
378 | className="link">
379 |
380 |
381 | )
382 | }
383 |
384 | return (
385 | {
388 | event.preventDefault()
389 | this.showExpandedView()
390 | }}
391 | className="link">
392 |
393 |
394 | )
395 | }
396 |
397 | renderFullscreenButton () {
398 | const { fullscreen } = this.state
399 |
400 | if (fullscreen) {
401 | return (
402 | {
405 | event.preventDefault()
406 | this.exitFullscreen()
407 | }}
408 | className="link">
409 |
410 |
411 | )
412 | }
413 |
414 | return (
415 | {
418 | event.preventDefault()
419 | this.showFullscreen()
420 | }}
421 | className="link">
422 |
423 |
424 | )
425 | }
426 |
427 | renderResizer () {
428 | // explicit check is required
429 | if (this.props.resizable === false) return null
430 |
431 | return (
432 | ☰
435 | )
436 | }
437 |
438 | handleWritableChange = (event: any, enabled: boolean) => {
439 | if (this.props.onWritable) {
440 | this.props.onWritable(enabled)
441 | }
442 | }
443 |
444 | focusTerminal () {
445 | this.terminalRef.current.classList.remove('blur')
446 | this.term.focus()
447 | this.setState({
448 | terminalBlurred: false
449 | })
450 | }
451 |
452 | blurTerminal () {
453 | const { expandedView } = this.state
454 | if (expandedView) {
455 | return
456 | }
457 |
458 | this.terminalRef.current.classList.add('blur')
459 | this.setState({
460 | terminalBlurred: true,
461 | terminalPressedKey: null
462 | })
463 | }
464 |
465 | isTerminalBlurred () {
466 | return this.terminalRef.current.classList.contains('blur')
467 | }
468 |
469 | showFullscreen = async () => {
470 | try {
471 | let container = this.terminalContainerRef.current
472 | const lastHeight = container.clientHeight
473 |
474 | let terminal = this.terminalRef.current
475 | if (!screenfull.isEnabled) {
476 | throw new Error('Not supported')
477 | }
478 |
479 | screenfull.request(terminal)
480 | this.focusTerminal()
481 |
482 | this.setState({
483 | fullscreen: true,
484 | lastHeight
485 | })
486 |
487 | const cb = (event: any) => {
488 | if (!document.fullscreenElement) {
489 | this.exitExpandedView()
490 | this.setState({
491 | fullscreen: false
492 | })
493 |
494 | terminal.removeEventListener('fullscreenchange', cb)
495 | }
496 | }
497 |
498 | terminal.addEventListener('fullscreenchange', cb)
499 | } catch (err) {
500 | // noop
501 | }
502 | }
503 |
504 | exitFullscreen () {
505 | if (!screenfull.isEnabled) {
506 | throw new Error('Not supported')
507 | }
508 |
509 | screenfull.exit()
510 | }
511 |
512 | showExpandedView () {
513 | const { borderSize } = this.state
514 | // window.location.href = window.location.href + '?f=1'
515 |
516 | let container = this.terminalContainerRef.current
517 | let terminal = this.terminalRef.current
518 | const lastHeight = container.clientHeight
519 | const offset = 125
520 | container.style.height = window.outerHeight - offset + 'px'
521 | terminal.style.height = window.outerHeight - borderSize - offset + 'px'
522 | this.term.toggleFullScreen(true)
523 | this.term.fit()
524 | this.focusTerminal()
525 | this.setState({
526 | expandedView: true,
527 | lastHeight
528 | })
529 | }
530 |
531 | exitExpandedView () {
532 | const { lastHeight, borderSize } = this.state
533 | this.term.toggleFullScreen(false)
534 | let container = this.terminalContainerRef.current
535 | let terminal = this.terminalRef.current
536 | container.style.height = lastHeight + borderSize + 'px'
537 | terminal.style.height = lastHeight - borderSize + 'px'
538 | this.term.fit()
539 | this.setState({
540 | expandedView: false
541 | })
542 | }
543 |
544 | handleNavigationKeys (event: any) {
545 | if (!this.isTerminalBlurred()) {
546 | if (event.key === 'j' || event.key === 'ArrowDown') {
547 | this.terminalScrollDown()
548 | }
549 | if (event.key === 'k' || event.key === 'ArrowUp') {
550 | this.terminalScrollUp()
551 | }
552 | if (event.key === 'u' && event.ctrlKey) {
553 | this.terminalScrollPageUp()
554 | }
555 | if (event.key === 'd' && event.ctrlKey) {
556 | this.terminalScrollPageDown()
557 | }
558 | if (event.key === 'g' && this.state.lastKeyPress === 'g') {
559 | this.terminalScrollHome()
560 | }
561 | if (event.key === 'G' || (event.key === 'g' && event.shiftKey)) {
562 | this.terminalScrollEnd()
563 | }
564 | if (event.key === 'H') {
565 | this.terminalScrollPageHome()
566 | }
567 | if (event.key === 'L') {
568 | this.terminalScrollPageEnd()
569 | }
570 | if (event.key === 'M') {
571 | this.terminalScrollPageMiddle()
572 | }
573 | }
574 | }
575 |
576 | handleKeyPressLog (event: any) {
577 | if (event.keyCode !== LETTER_I_KEY) {
578 | if (!this.isTerminalBlurred()) {
579 | this.setState({
580 | terminalPressedKey: `${event.ctrlKey ? 'ctrl-' : ''}${event.key}`
581 | })
582 | }
583 | }
584 | }
585 |
586 | terminalScrollUp () {
587 | this.term.scrollLines(-1)
588 | }
589 |
590 | terminalScrollDown () {
591 | this.term.scrollLines(1)
592 | }
593 |
594 | terminalScrollPageUp () {
595 | this.term.scrollPages(-1)
596 | }
597 |
598 | terminalScrollPageDown () {
599 | this.term.scrollPages(1)
600 | }
601 |
602 | terminalScrollPageHome () {
603 | this.term.scrollToLine(0)
604 | }
605 |
606 | terminalScrollPageEnd () {
607 | this.term.scrollToLine(this.term.rows)
608 | }
609 |
610 | terminalScrollHome () {
611 | this.term.scrollToTop()
612 | }
613 |
614 | terminalScrollEnd () {
615 | this.term.scrollToBottom()
616 | }
617 |
618 | terminalScrollPageMiddle () {
619 | this.term.scrollToLine(parseInt(this.term.rows, 10) / 2)
620 | }
621 |
622 | onWindowResize = throttle(() => {
623 | this.term.fit()
624 | }, 20)
625 |
626 | onTerminalResizer = (event: any) => {
627 | if (event.offsetY < this.state.borderSize) {
628 | const pos = event.y
629 | document.addEventListener('mousemove', this.resizeTerminal, false)
630 | // document.addEventListener('touchmove', this.resizeTerminal, false)
631 |
632 | this.setState({
633 | pos
634 | })
635 | }
636 | }
637 |
638 | resizeTerminal = throttle((event: any) => {
639 | let { borderSize, pos } = this.state
640 | let container = this.terminalContainerRef.current
641 | let terminal = this.terminalRef.current
642 | const dy = pos - event.y
643 | pos = event.y
644 | const newHeight = (parseInt(getComputedStyle(container, '').height, 10) - dy)
645 | container.style.height = newHeight + 'px'
646 | terminal.style.height = (newHeight - borderSize) + 'px'
647 | this.term.fit()
648 |
649 | this.setState({ pos })
650 |
651 | if (typeof this.props.onResize === 'function') {
652 | this.props.onResize()
653 | }
654 | }, 20)
655 |
656 | setupTerminalResizer () {
657 | if (!this.terminalResizerRef.current) {
658 | return
659 | }
660 |
661 | let resizer = this.terminalResizerRef.current
662 | const borderSize = parseInt(getComputedStyle(resizer, '').height, 10)
663 | const pos = 0
664 |
665 | this.setState({
666 | borderSize,
667 | pos
668 | })
669 |
670 | resizer.addEventListener('mousedown', this.onTerminalResizer, false)
671 | // resizer.addEventListener('touchend', this.onTerminalResizer, false)
672 |
673 | document.addEventListener('mouseup', (event: any) => {
674 | document.removeEventListener('mousemove', this.resizeTerminal, false)
675 | }, false)
676 |
677 | // TODO: fix resizing on mobile
678 | // document.addEventListener('touchstart', event => {
679 | // document.removeEventListener('touchmove', this.resizeTerminal, false)
680 | // }, false)
681 | }
682 | }
683 |
684 | export default TerminalComponent
685 |
--------------------------------------------------------------------------------
/src/components/header/Header.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Tooltip from '@material-ui/core/Tooltip'
3 | import IconButton from '@material-ui/core/IconButton'
4 | import styled from 'styled-components'
5 | import ClipboardJS from 'clipboard'
6 | import LightModeIcon from 'mdi-material-ui/Brightness7'
7 | import DarkModeIcon from 'mdi-material-ui/Brightness4'
8 |
9 | import ThemeContext from 'src/themeContext'
10 | import Clipboard from 'src/components/functional/Clipboard'
11 | import HelpTooltip from 'src/components/functional/HelpTooltip'
12 | import MaxWidthContainer from 'src/components/functional/MaxWidthContainer'
13 | import { streamHostname, streamPort } from 'src/config'
14 |
15 | const UI = {
16 | Header: styled.header`
17 | margin: 0;
18 | display: flex;
19 | justify-content: space-between;
20 | flex-direction: column;
21 | background: ${({ theme }) => (theme as any).headerBackground};
22 | box-shadow: 0 1px 10px rgba(151,164,175,.1);
23 | @media (max-width: 500px) {
24 | display: block;
25 | }
26 | `,
27 | HeaderGroup: styled.hgroup`
28 | display: flex;
29 | align-items: center;
30 | justify-content: space-between;
31 | padding: 1rem;
32 | @media (max-width: 500px) {
33 | display: block;
34 | }
35 | `,
36 | Title: styled.h1`
37 | font-size: 2rem;
38 | font-weight: normal;
39 | align-items: center;
40 | display: flex;
41 | flex-direction: column;
42 | justify-content: center;
43 | @media (max-width: 500px) {
44 | display: block;
45 | margin-bottom: 0.5rem;
46 | align-items: flex-start;
47 | .tooltip {
48 | display: none;
49 | }
50 | }
51 | `,
52 | LogoImage: styled.img`
53 | width: 150px;
54 | height: auto;
55 | filter: ${({ theme }) => (theme as any).headerLogoFilter};
56 | `,
57 | LightModeIcon: styled(LightModeIcon)`
58 | color: rgba(255, 255, 255, 0.8)
59 | `,
60 | DarkModeIcon: styled(DarkModeIcon)`
61 | color: rgba(0, 0, 0, 0.8)
62 | `,
63 | UL: styled.ul`
64 | list-style: none;
65 | padding-bottom: 0.8rem;
66 | `,
67 | LI: styled.li`
68 | margin-bottom: 0.3rem;
69 | `,
70 | Channel: styled.div`
71 | width: 100%;
72 | margin-left: 2rem;
73 | max-width: 17rem;
74 | a {
75 | font-size: 0.8rem;
76 | }
77 | label {
78 | font-size: 0.8rem;
79 | margin-right: 0.4rem;
80 | display: block;
81 | font-size: 0.7rem;
82 | color: ${({ theme }) => (theme as any).headerColor};
83 | display: flex;
84 | align-items: center;
85 | }
86 | .tooltip {
87 | margin-left: 0.2rem;
88 | }
89 | small {
90 | margin-left: 2rem;
91 | color: rgba(0,0,0,0.5);
92 | }
93 | @media (max-width: 720px) {
94 | max-width: 100%;
95 | }
96 | @media (max-width: 500px) {
97 | margin-left: 0;
98 | display: inline-block;
99 | width: auto;
100 | float: left;
101 | min-width: 220px;
102 | }
103 | `,
104 | ShareUrlInput: styled.input`
105 | &[type="text"] {
106 | width: 180px;
107 | margin-right: 0.5rem;
108 | font-size: 0.7rem;
109 | border-radius: 2px;
110 | padding: 0.5rem;
111 | border-width: 1px;
112 | border-style: solid;
113 | background: ${({ theme }) => (theme as any).inputBackground};
114 | color: ${({ theme }) => (theme as any).inputColor};
115 | border-color: ${({ theme }) => (theme as any).inputBorderColor};
116 | }
117 | &:hover {
118 | border-color: ${({ theme }) => (theme as any).inputBorderColorHover};
119 | cursor: pointer;
120 | }
121 | @media (max-width: 500px) {
122 | max-width: 300px;
123 | float: left;
124 | }
125 | `,
126 | Examples: styled.div`
127 | display: flex;
128 | width: 100%;
129 | padding: 0.5rem 0.5rem 0.5rem 2rem;
130 | font-size: 0.8rem;
131 | color: ${({ theme }) => (theme as any).headerColor};
132 | line-height: 1.4;
133 | label {
134 | display: inline-block;
135 | width: 60px;
136 | font-weight: 600;
137 | text-align: right;
138 | padding-right: 0.5rem;
139 | font-size: 0.8rem;
140 | color: ${({ theme }) => (theme as any).headerColor};
141 | @media (max-width: 780px) {
142 | width: auto;
143 | }
144 | }
145 | code {
146 | background: #405a6b;
147 | color: white;
148 | padding: 0.4rem;
149 | border-radius: 2px;
150 | display: inline-block;
151 | width: 280px;
152 | @media (max-width: 780px) {
153 | width: auto;
154 | }
155 | }
156 | details {
157 | display: block;
158 | button {
159 | font-size: 1rem;
160 | }
161 | &[open] {
162 | summary div {
163 | display: inline-block;
164 | }
165 | summary:hover span {
166 | text-decoration: none;
167 | }
168 | }
169 | }
170 | summary {
171 | font-size: 1.2rem;
172 | cursor: pointer;
173 | margin-bottom: 0.5rem;
174 | color: ${({ theme }) => (theme as any).headerColor};
175 | span {
176 | display: inline-block;
177 | }
178 | div {
179 | display: none;
180 | float: right;
181 | font-size: 1rem;
182 | }
183 | button {
184 | font-size: 0.8rem;
185 | }
186 | &:hover span {
187 | text-decoration: underline;
188 | }
189 | }
190 | @media (max-width: 720px) {
191 | display: none;
192 | }
193 | `,
194 | Settings: styled.div`
195 | @media (max-width: 500px) {
196 | float: right;
197 | }
198 | `,
199 | Notice: styled.div`
200 | font-size: 0.7rem;
201 | background: ${({ theme }) => (theme as any).noticeBackground};
202 | color: ${({ theme }) => (theme as any).noticeColor};
203 | padding: 0.1rem;
204 | `,
205 | HelperText: styled.div`
206 | font-size: 0.8rem;
207 | color: ${({ theme }) => (theme as any).headerColor};
208 | `,
209 | TooltipListContainer: styled.div`
210 | padding: 0.5rem;
211 | `,
212 | Clear: styled.div`
213 | clear: both;
214 | `
215 | }
216 |
217 | interface Props {
218 | shareUrl: string
219 | }
220 |
221 | interface State {
222 | hostname: string
223 | port: number
224 | showExampleWithChannel: boolean
225 | channel: string
226 | }
227 |
228 | class Header extends Component {
229 | shareUrl: any
230 | copyHelpText: any
231 |
232 | constructor (props: Props) {
233 | super(props)
234 |
235 | this.state = {
236 | hostname: streamHostname,
237 | port: streamPort,
238 | showExampleWithChannel: true,
239 | channel: window.location.pathname.substr(3)
240 | }
241 |
242 | this.shareUrl = React.createRef()
243 | this.copyHelpText = React.createRef()
244 | }
245 |
246 | componentDidMount () {
247 | new ClipboardJS(this.shareUrl.current, {
248 | text: (trigger: any) => {
249 | return this.shareUrl.current.value
250 | }
251 | })
252 | .on('success', () => {
253 | const target = this.copyHelpText.current
254 | const text = target.textContent
255 | target.textContent = 'copied!'
256 |
257 | setTimeout(() => {
258 | target.textContent = text
259 | }, 3e3)
260 | })
261 | }
262 |
263 | shareUrlHandler (event: any) {
264 | event.currentTarget.select()
265 | }
266 |
267 | selectCode (event: any) {
268 | (window as any).getSelection().selectAllChildren(event.currentTarget)
269 | }
270 |
271 | toggleExampleWithChannel (event: any) {
272 | event.preventDefault(event)
273 | this.setState({
274 | showExampleWithChannel: !this.state.showExampleWithChannel
275 | })
276 | }
277 |
278 | render () {
279 | return (
280 |
281 | {theme =>
282 |
412 | }
413 |
414 | )
415 | }
416 | }
417 |
418 | export default Header
419 |
--------------------------------------------------------------------------------
/src/components/header/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Header'
2 |
--------------------------------------------------------------------------------
/src/components/home/Demo.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 |
4 | const UI = {
5 | Cast: styled.div`
6 | background: #0e172a;
7 | justify-content: center;
8 | flex-direction: column;
9 | div div {
10 | max-width: 900px;
11 | margin: 0 auto;
12 | }
13 | p {
14 | max-width: 900px;
15 | font-size: 1.4rem;
16 | color: #fff;
17 | margin: 0 auto;
18 | padding: 0.5rem;
19 | background: rgb(29,82,107);
20 | background: linear-gradient(90deg, rgba(29,82,107,1) 0%, rgba(14,23,42,1) 65%);
21 | }
22 | img {
23 | width: 100%;
24 | transform: scale(1.1) translate(2.4rem, 1.3rem);
25 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
26 | @media (max-width: 900px) {
27 | transform: scale(1) translate(0, 0);
28 | }
29 | }
30 | `
31 | }
32 |
33 | class Demo extends Component {
34 | render () {
35 | return (
36 |
37 |
38 |
streamhut in action 🎬
39 |
40 |
41 |
42 |
50 |
51 |
52 |
53 | )
54 | }
55 | }
56 |
57 | export default Demo
58 |
--------------------------------------------------------------------------------
/src/components/home/GettingStarted.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 |
4 | const UI = {
5 | Example: styled.div`
6 | display: flex;
7 | background: #293238;
8 | justify-content: center;
9 | flex-direction: column;
10 | padding: 5rem 2rem;
11 | text-align: center;
12 | small {
13 | display: block;
14 | color: rgba(255,255,255,0.5);
15 | font-size: 1rem;
16 | margin: 0 auto;
17 | max-width: 345px;
18 | }
19 | @media (max-width: 500px) {
20 | small {
21 | max-width: 100%;
22 | }
23 | }
24 | h3 {
25 | color: #fff;
26 | font-size: 1.4rem;
27 | font-weight: 700;
28 | margin-bottom: 1rem;
29 | }
30 | div {
31 | text-align: center;
32 | background: #151d21;
33 | padding: 2rem;
34 | border-radius: 4px;
35 | border: 1px solid #34434a;
36 | margin: 0 auto 1rem auto;
37 | @media (max-width: 500px) {
38 | padding: 1rem;
39 | border-radius: 0.4rem;
40 | }
41 | }
42 | pre {
43 | color: #fff;
44 | font-size: 1.3rem;
45 | white-space: pre-wrap;
46 | text-align: left;
47 | @media (max-width: 500px) {
48 | font-size: 0.8rem;
49 | }
50 | }
51 | `,
52 | Example2: styled.div`
53 | display: flex;
54 | justify-content: center;
55 | flex-direction: column;
56 | padding: 2rem;
57 | text-align: center;
58 | @media (max-width: 500px) {
59 | padding: 3rem 1rem;
60 | }
61 | p {
62 | font-size: 1rem;
63 | margin-bottom: 0.2rem;
64 | }
65 | small {
66 | display: block;
67 | margin-bottom: 1rem;
68 | color: rgba(0,0,0,0.5);
69 | font-size: 0.9rem;
70 | }
71 | div {
72 | text-align: center;
73 | background: #e1e1e1;
74 | padding: 1rem;
75 | border-radius: 4px;
76 | border: 1px solid #d7d7d7;
77 | margin: 0 auto;
78 | color: #151c20;
79 | @media (max-width: 500px) {
80 | padding: 1rem;
81 | border-radius: 0.4rem;
82 | }
83 | }
84 | pre {
85 | color: #151c20;
86 | font-size: 0.8rem;
87 | white-space: pre-wrap;
88 | text-align: left;
89 | @media (max-width: 500px) {
90 | font-size: 0.7rem;
91 | }
92 | }
93 | `
94 | }
95 |
96 | interface Props {
97 | hostname: string
98 | port: number
99 | }
100 |
101 | class GettingStarted extends Component {
102 | render () {
103 | const { hostname, port } = this.props
104 |
105 | return (
106 | <>
107 |
108 |
109 | To get started, run in your terminal:
110 |
111 |
112 |
113 | {`exec &> >(nc ${hostname} ${port})`}
114 |
115 |
116 |
117 | The command pipes the output of the shell to streamhut and provides a url to share
118 |
119 |
120 |
121 |
122 | Don't have netcat installed? No problem
123 |
124 |
125 | Pipe to a file descriptor with an open TCP connection
126 |
127 |
128 |
129 | {`exec 3<>/dev/tcp/${hostname}/${port} && head -1 <&3 && exec &> >(tee >(cat >&3))`}
130 |
131 |
132 |
133 |
134 | >
135 | )
136 | }
137 | }
138 |
139 | export default GettingStarted
140 |
--------------------------------------------------------------------------------
/src/components/home/Hero.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 |
4 | const UI = {
5 | Hero: styled.div`
6 | background: #fff;
7 | text-align: center;
8 | display: flex;
9 | justify-content: center;
10 | flex-direction: column;
11 | padding: 6rem 3rem;
12 | `,
13 | HeroImage: styled.div`
14 | display: block;
15 | margin-bottom: 2rem;
16 | h1 {
17 | display: inline-block;
18 | }
19 | a {
20 | display: inline-block;
21 | }
22 | img {
23 | width: 100%;
24 | max-width: 500px;
25 | height: auto;
26 | @media (max-width: 500px) {
27 | max-width: 320px;
28 | }
29 | }
30 | `,
31 | Tagline: styled.h2`
32 | font-size: 1.8rem;
33 | margin-bottom: 1rem;
34 | font-weight: 700;
35 | @media (max-width: 500px) {
36 | font-size: 1.6rem;
37 | }
38 | `,
39 | SubTagline: styled.div`
40 | font-size: 1.2rem;
41 | @media (max-width: 500px) {
42 | font-size: 1rem;
43 | }
44 | `
45 | }
46 |
47 | class Hero extends Component {
48 | render () {
49 | return (
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 💻 stream your terminal
58 |
59 |
60 | Share your terminal in real-time with anyone — without installing anything 🚀
61 |
62 |
63 | )
64 | }
65 | }
66 |
67 | export default Hero
68 |
--------------------------------------------------------------------------------
/src/components/home/Home.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import Hero from './Hero'
4 | import GettingStarted from './GettingStarted'
5 | import Demo from './Demo'
6 | import UseCases from './UseCases'
7 | import SelfHost from './SelfHost'
8 | import OpenSource from './OpenSource'
9 | import Subscribe from './Subscribe'
10 | import Footer from 'src/components/footer'
11 | import { streamHostname, streamPort } from 'src/config'
12 |
13 | const UI = {
14 | Container: styled.div`
15 | background: #efefef;
16 | `
17 | }
18 |
19 | interface Props {
20 | }
21 |
22 | interface State {
23 | hostname: string
24 | port: number
25 | }
26 |
27 | class Home extends Component {
28 | constructor (props: Props) {
29 | super(props)
30 | this.state = {
31 | hostname: streamHostname,
32 | port: streamPort
33 | }
34 | }
35 |
36 | render () {
37 | const { hostname, port } = this.state
38 |
39 | return (
40 | <>
41 |
42 |
43 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | >
55 | )
56 | }
57 | }
58 |
59 | export default Home
60 |
--------------------------------------------------------------------------------
/src/components/home/OpenSource.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import GithubIcon from 'mdi-material-ui/GithubCircle'
4 |
5 | const UI = {
6 | OpenSource: styled.div`
7 | display: flex;
8 | background: #fff;
9 | justify-content: center;
10 | flex-direction: column;
11 | padding: 5rem 2rem;
12 | text-align: center;
13 | font-size: 1.2rem;
14 | h3 {
15 | font-size: 1.8rem;
16 | font-weight: 700;
17 | margin-bottom: 1rem;
18 | }
19 | p {
20 | display: block;
21 | margin-bottom: 2rem;
22 | font-size: 1.2rem;
23 | color: rgba(0,0,0,0.8);
24 | }
25 | a {
26 | font-size: 1.6em;
27 | font-weight: 700;
28 | }
29 | `
30 | }
31 |
32 | class OpenSource extends Component {
33 | render () {
34 | return (
35 |
36 | 💖 Open Source
37 | streamhut source code is available on github
38 |
47 |
48 | )
49 | }
50 | }
51 |
52 | export default OpenSource
53 |
--------------------------------------------------------------------------------
/src/components/home/SelfHost.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 |
4 | const UI = {
5 | SelfHost: styled.div`
6 | display: flex;
7 | background: #293238;
8 | justify-content: center;
9 | flex-direction: column;
10 | padding: 5rem 2rem;
11 | text-align: center;
12 | color: #fff;
13 | font-size: 1.5rem;
14 | h3 {
15 | font-size: 1.8rem;
16 | font-weight: 700;
17 | margin-bottom: 0.4rem;
18 | }
19 | small {
20 | display: block;
21 | margin-bottom: 1.4rem;
22 | color: rgba(255,255,255,0.5);
23 | }
24 | div {
25 | text-align: center;
26 | background: #151d21;
27 | padding: 1rem;
28 | border-radius: 4px;
29 | border: 1px solid #34434a;
30 | margin: 0 auto 1rem auto;
31 | color: #151c20;
32 | @media (max-width: 500px) {
33 | padding: 1rem;
34 | border-radius: 0.4rem;
35 | }
36 | }
37 | pre {
38 | color: #fff;
39 | font-size: 0.9rem;
40 | white-space: pre-wrap;
41 | text-align: left;
42 | @media (max-width: 500px) {
43 | font-size: 0.7rem;
44 | }
45 | }
46 | p {
47 | font-size: 1rem;
48 | color: rgba(255,255,255,0.5);
49 | }
50 | `
51 | }
52 |
53 | class SelfHost extends Component {
54 | render () {
55 | return (
56 |
57 | Self-hosted option? Absolutely ✅
58 |
59 | Run streamhut as a Docker container
60 |
61 |
62 |
63 | docker run -p 8080:8080 -p 1337:1337 streamhut/streamhut
64 |
65 |
66 | Check out the
71 | documentation
72 | for more examples
73 |
74 | )
75 | }
76 | }
77 |
78 | export default SelfHost
79 |
--------------------------------------------------------------------------------
/src/components/home/Subscribe.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import SubscribeForm from './SubscribeForm'
4 |
5 | const UI = {
6 | Subscribe: styled.div`
7 | display: flex;
8 | background: #fff;
9 | justify-content: center;
10 | flex-direction: column;
11 | padding: 5rem 2rem;
12 | text-align: center;
13 | font-size: 1.2rem;
14 | background: #fff url('https://s3.amazonaws.com/assets.streamhut.io/background-pattern-round.png') repeat 0 0;
15 | border-top: 1px solid #f4f4f5;
16 | h3 {
17 | font-size: 1.8rem;
18 | font-weight: 700;
19 | margin-bottom: 1rem;
20 | }
21 | p {
22 | display: block;
23 | margin-bottom: 2.5rem;
24 | color: rgba(0,0,0,0.8);
25 | }
26 | `
27 | }
28 |
29 | class OpenSource extends Component {
30 | render () {
31 | return (
32 |
33 | Join the mailing list
34 | Subscribe to get notified of latest updates on news and features
35 |
36 |
37 | )
38 | }
39 | }
40 |
41 | export default OpenSource
42 |
--------------------------------------------------------------------------------
/src/components/home/SubscribeForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 |
4 | const UI = {
5 | Container: styled.div`
6 | font-size: 1rem;
7 | label {
8 | display: block;
9 | font-size: 1rem;
10 | margin: 0 auto 0.5rem auto;
11 | width: 100%;
12 | max-width: 360px;
13 | text-align: left;
14 | }
15 | input[type="email"] {
16 | font-size: 1rem;
17 | width: 100%;
18 | max-width: 360px;
19 | padding: 1rem;
20 | @media (max-width: 500px) {
21 | font-size: 16px;
22 | }
23 | }
24 | .mc-field-group {
25 | margin-bottom: 0.5rem;
26 | }
27 | `
28 | }
29 |
30 | class SubscribeForm extends Component {
31 | render () {
32 | return (
33 |
34 |
49 |
50 | )
51 | }
52 | }
53 |
54 | export default SubscribeForm
55 |
--------------------------------------------------------------------------------
/src/components/home/UseCases.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 |
4 | const UI = {
5 | UseCases: styled.div`
6 | display: flex;
7 | background: #fff;
8 | justify-content: center;
9 | flex-direction: column;
10 | padding: 8rem 2rem 5rem 2rem;
11 | font-size: 1.2rem;
12 | background: #fff;
13 | @media (max-width: 900px) {
14 | padding-top: 5rem;
15 | }
16 | h3 {
17 | font-size: 1.8rem;
18 | font-weight: 700;
19 | width: 500px;
20 | margin-bottom: 1rem;
21 | }
22 | p {
23 | font-size: 1.2rem;
24 | font-weight: 500;
25 | width: 300px;
26 | margin-bottom: 1rem;
27 | }
28 | ul {
29 | min-width: 380px;
30 | display: inline-block;
31 | list-style-position: inside;
32 | margin-bottom: 1rem;
33 | }
34 | li {
35 | margin-bottom: 0.8rem;
36 | }
37 | div {
38 | max-width: 380px;
39 | margin: 0 auto;
40 | }
41 | small {
42 | max-width: 380px;
43 | margin: 0 auto;
44 | font-size: 0.9rem;
45 | p {
46 | margin-bottom: 1rem;
47 | font-weight: normal;
48 | }
49 | ul {
50 | margin-bottom: 1rem;
51 | }
52 | }
53 | @media (max-width: 500px) {
54 | font-size: 1rem;
55 | h3 {
56 | width: auto;
57 | }
58 | p {
59 | width: auto;
60 | }
61 | ul {
62 | min-width: 0;
63 | }
64 | }
65 | `
66 | }
67 |
68 | class UseCases extends Component {
69 | render () {
70 | return (
71 |
72 |
73 |
Use cases for streamhut:
74 |
75 | 🐛 Debug logs withs colleagues
76 | 👥 Help a friend with programming
77 | 🤝 Live terminal sessions for interviews
78 |
79 |
80 |
81 |
82 | As well as:
83 |
84 | 💬 Pseudo-anonymous communication
85 | 📱 Transfer content and files between devices
86 |
87 |
88 |
89 | )
90 | }
91 | }
92 |
93 | export default UseCases
94 |
--------------------------------------------------------------------------------
/src/components/home/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Home'
2 |
--------------------------------------------------------------------------------
/src/components/notFound/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 |
4 | const UI = {
5 | Container: styled.div`
6 | display; flex;
7 | justify-content: center;
8 | align-items: center;
9 | padding: 3rem;
10 | font-size: 1.4rem;
11 | h3 {
12 | font-size: 2rem;
13 | margin-bottom: 1rem;
14 | @media (max-width: 500px) {
15 | font-size: 1.6rem;
16 | }
17 | }
18 | a {
19 | font-weight: 500;
20 | }
21 | `
22 | }
23 |
24 | class NotFound extends Component {
25 | render () {
26 | return (
27 |
28 | Not found
29 |
32 |
33 | )
34 | }
35 | }
36 |
37 | export default NotFound
38 |
--------------------------------------------------------------------------------
/src/components/notFound/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './NotFound'
2 |
--------------------------------------------------------------------------------
/src/config/config.ts:
--------------------------------------------------------------------------------
1 | const windowHostname = window.location.hostname
2 |
3 | export const streamHostname = /^streamhut.(io|net|org|co|me|sh)$/gi.test(windowHostname) ? 'stream.ht'
4 | : windowHostname
5 | export const streamPort = 1337
6 |
7 | export const getShareUrl = (channel: string) => {
8 | let protocol = window.location.protocol
9 | let host = window.location.host
10 | let pathname = `s/${channel}`
11 | if (host === 'streamhut.io') {
12 | host = 'stream.ht'
13 | pathname = channel
14 | }
15 |
16 | return `${protocol}//${host}/${pathname}`
17 | }
18 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config'
2 |
--------------------------------------------------------------------------------
/src/global.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components'
2 |
3 | export const GlobalStyles = createGlobalStyle`
4 | * {
5 | margin: 0;
6 | padding: 0;
7 | box-sizing: border-box;
8 | }
9 |
10 | html, body, #root {
11 | height: 100%;
12 | }
13 |
14 | body {
15 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
16 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", Verdana,Geneva,sans-serif;
17 | -webkit-font-smoothing: antialiased;
18 | -moz-osx-font-smoothing: grayscale;
19 | color: #000;
20 | font-size: 10px;
21 | line-height: 1.4;
22 | }
23 |
24 | body::-webkit-scrollbar {
25 | width: 12px;
26 | height: 12px;
27 | }
28 |
29 | body::-webkit-scrollbar-track {
30 | background: #ddd;
31 | }
32 |
33 | body::-webkit-scrollbar-thumb {
34 | background-color: #bbb;
35 | border: 1px solid #aaa;
36 | }
37 |
38 | body::-webkit-scrollbar-thumb:hover {
39 | background-color: #999;
40 | border: 1px solid #777;
41 | }
42 |
43 | #root {
44 | min-width: 280px;
45 | }
46 |
47 | code {
48 | font-family: "Source Code Pro", Menlo, Monaco, Consolas, "Courier New", monospace;
49 | }
50 |
51 | #site-container {
52 | margin: 0 auto;
53 | }
54 |
55 | input[type="text"],
56 | input[type="password"],
57 | input[type="file"],
58 | textarea {
59 | border-radius: 5px;
60 | padding: 1rem;
61 | font-family: Verdana,Geneva,sans-serif;
62 | border-width: 1px;
63 | border-style: solid;
64 | background: ${({ theme }) => (theme as any).inputBackground};
65 | color: ${({ theme }) => (theme as any).inputColor};
66 | border-color: ${({ theme }) => (theme as any).inputBorderColor};
67 | }
68 |
69 | @media (max-width: 500px) {
70 | input[type="color"],
71 | input[type="date"],
72 | input[type="datetime"],
73 | input[type="datetime-local"],
74 | input[type="email"],
75 | input[type="month"],
76 | input[type="number"],
77 | input[type="password"],
78 | input[type="search"],
79 | input[type="tel"],
80 | input[type="text"],
81 | input[type="time"],
82 | input[type="url"],
83 | input[type="week"],
84 | select:focus,
85 | textarea {
86 | font-size: 16px; /* minimum font-size so iOS doesn't zoom in */
87 | }
88 | }
89 |
90 | details:focus,
91 | summary:focus {
92 | outline: 0;
93 | }
94 |
95 | button {
96 | font-family: Verdana,Geneva,sans-serif;
97 | }
98 |
99 | input[type="file"] {
100 | width: 100%;
101 | padding: 1rem;
102 | cursor: pointer;
103 | }
104 |
105 | input[type="file"]:focus {
106 | outline: none;
107 | }
108 |
109 | a,
110 | a:hover,
111 | a:active,
112 | a:focus,
113 | .link,
114 | .link:hover,
115 | .link:active,
116 | .link:focus {
117 | display: inline-flex;
118 | justify-content: center;
119 | align-items: center;
120 | color: #067df7;
121 | text-decoration: none;
122 | -webkit-appearance: none;
123 | background: none;
124 | border: none;
125 | outline: 0;
126 | cursor: pointer;
127 | }
128 |
129 | a:hover,
130 | .link:hover {
131 | text-decoration: underline;
132 | }
133 |
134 | a:focus,
135 | .link:focus {
136 | outline: 0;
137 | }
138 |
139 | a svg {
140 | margin-right: 0.2rem;
141 | }
142 |
143 | #form textarea,
144 | #form input[type="text"] {
145 | width: 100%;
146 | margin: 0 0 10px 0;
147 | }
148 |
149 | textarea:focus,
150 | input[type="text"]:focus {
151 | outline: none;
152 | }
153 |
154 | #form input[type="text"] {
155 | margin: 0;
156 | }
157 |
158 | #form label {
159 | display: block;
160 | margin: 0 0 10px 0;
161 | font-size: 16px;
162 | }
163 |
164 | #form label small {
165 | font-size: 0.8rem;
166 | color: ${({ theme }) => (theme as any).lightText};
167 | }
168 |
169 | .button,
170 | .button:hover
171 | .button:focus {
172 | font-family: Verdana,Geneva,sans-serif;
173 | display: inline-flex;
174 | justify-content: center;
175 | align-items: center;
176 | cursor: pointer;
177 | -webkit-appearance: none;
178 | padding: 1.2rem 2rem;
179 | border-radius: 4px;
180 | outline: none;
181 | font-weight: 600;
182 | white-space: normal;
183 | color: #fff;
184 | background-color: #007aff;
185 | border-width: 1px;
186 | border-style: solid;
187 | border-color: #0062cc;
188 | line-height: 1;
189 | letter-spacing: 0.1rem;
190 | text-transform: uppercase;
191 | text-decoration: none;
192 | }
193 |
194 | :root .button:hover {
195 | background-color: #007fff;
196 | border-width: 1px;
197 | border-style: solid;
198 | border-color: #0062cc;
199 | color: #fff;
200 | text-decoration: none;
201 | }
202 |
203 | .button svg {
204 | margin-right: 0.2rem;
205 | }
206 |
207 | @media (max-width: 500px) {
208 | .button {
209 | font-size: 0.8rem;
210 | }
211 | }
212 |
213 | .copy {
214 | text-decoration: none;
215 | -webkit-appearance: none;
216 | border: none;
217 | background: none;
218 | outline: 0;
219 | color: #067df7;
220 | cursor: pointer;
221 | }
222 |
223 | .copy:hover {
224 | text-decoration: underline;
225 | }
226 |
227 | .copied {
228 | cursor: default;
229 | }
230 |
231 | .copied:hover {
232 | text-decoration: none;
233 | }
234 |
235 | /* sticky footer */
236 |
237 | #site-container {
238 | min-height: 100%;
239 | margin-bottom: -15rem;
240 | }
241 |
242 | #site-container:after {
243 | content:"";
244 | display: block;
245 | }
246 |
247 | #footer, #site-container:after {
248 | height: 15rem;
249 | }
250 |
251 | /* end sticky footer */
252 |
253 | #terminal {
254 | resize: vertical;
255 | background: #000;
256 | }
257 |
258 | .terminal {
259 | font-family: "Menlo", "DejaVu Sans Mono", "Lucida Console", monospace;
260 | }
261 |
262 | .terminal .xterm-viewport {
263 | overflow-y: auto;
264 | }
265 |
266 | .xterm-viewport::-webkit-scrollbar {
267 | width: 12px;
268 | height: 12px;
269 | }
270 |
271 | .xterm-viewport::-webkit-scrollbar-track {
272 | background: #293238;
273 | }
274 |
275 | .xterm-viewport::-webkit-scrollbar-thumb {
276 | background-color: #496171;
277 | border: 1px solid #496171;
278 | }
279 |
280 | .xterm-viewport::-webkit-scrollbar-thumb:hover {
281 | background-color: #54748a;
282 | border: 1px solid #6995b3;
283 | }
284 |
285 | .xterm.fullscreen {
286 | position: fixed;
287 | top: 0;
288 | bottom: 0;
289 | left: 0;
290 | right: 0;
291 | width: auto;
292 | height: auto;
293 | z-index: 255;
294 | }
295 |
296 | body.fullscreen {
297 | overflow: hidden;
298 | }
299 |
300 | body.fullscreen #terminal {
301 | display: block;
302 | position: fixed;
303 | left: 0;
304 | top: 0;
305 | width: 100%;
306 | height: 100%;
307 | min-height: 100%;
308 | max-height: 100%;
309 | z-index: 1;
310 | margin: 0;
311 | }
312 |
313 | .help-tooltip {
314 | border-radius: 10rem;
315 | width: 0.9rem;
316 | height: 0.9rem;
317 | border: 1px solid #6e6e6e;
318 | text-align: center;
319 | display: flex;
320 | justify-content: center;
321 | align-items: center;
322 | font-size: 0.9rem;
323 | }
324 |
325 | @media (max-width: 760px) {
326 | #header {
327 | flex-direction: column;
328 | }
329 |
330 | #output .item header,
331 | #output .item footer {
332 | flex-direction: column;
333 | }
334 | }
335 |
336 | button:focus,
337 | a:focus {
338 | outline: 0;
339 | }
340 | `
341 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 | import * as serviceWorker from './serviceWorker'
5 |
6 | ReactDOM.render( , document.getElementById('root'))
7 |
8 | // If you want your app to work offline and load faster, you can change
9 | // unregister() to register() below. Note this comes with some pitfalls.
10 | // Learn more about service workers: https://bit.ly/CRA-PWA
11 | serviceWorker.unregister()
12 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | )
22 |
23 | export function register (config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config)
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | )
48 | })
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config)
52 | }
53 | })
54 | }
55 | }
56 |
57 | function registerValidSW (swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing
63 | if (installingWorker == null) {
64 | return
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | )
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration)
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.')
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration)
90 | }
91 | }
92 | }
93 | }
94 | }
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error)
98 | })
99 | }
100 |
101 | function checkValidServiceWorker (swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type')
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload()
115 | })
116 | })
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config)
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | )
126 | })
127 | }
128 |
129 | export function unregister () {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister()
133 | })
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/theme.ts:
--------------------------------------------------------------------------------
1 | import { DefaultTheme } from 'styled-components'
2 |
3 | export const lightTheme: DefaultTheme = {
4 | text: '#000',
5 | lightText: 'rgba(0, 0, 0, 0.5)',
6 | headerBackground: '#efefef',
7 | headerColor: '#000',
8 | headerLogoFilter: 'grayscale(0)',
9 | noticeBackground: '#dededa',
10 | noticeColor: '#000',
11 | footerBackground: 'linear-gradient(90deg, rgba(0,0,0,1) 36%, rgba(32,48,56,1) 100%)',
12 | inputBackground: '#fff',
13 | inputBorderColor: '#e0e0e0',
14 | inputBorderColorHover: '#ccc',
15 | inputColor: '#000',
16 | outputBackground: '#fff',
17 | chatFormBackground: '#efefef',
18 | chatMessageBackground: '#efefef',
19 | chatMessageHeaderBackground: '#e2e2e2',
20 | chatMessageFooterBackground: '#e2e2e2',
21 | resizerBackground: '#efefef',
22 | resizerBackgroundHover: '#e6e6e6',
23 | resizerBorderColor: '#cacaca',
24 | resizerBorderColorHover: '#adadad'
25 | }
26 |
27 | export const darkTheme: DefaultTheme = {
28 | text: 'rgba(255, 255, 255, 0.8)',
29 | lightText: 'rgba(255, 255, 255, 0.5)',
30 | headerBackground: 'linear-gradient(90deg, rgba(0,0,0,1) 36%, rgba(32,48,56,1) 100%)',
31 | headerColor: 'rgba(255, 255, 255, 0.8)',
32 | headerLogoFilter: 'grayscale(1) invert(1)',
33 | noticeBackground: '#141c20',
34 | noticeColor: 'rgba(255, 255, 255, 0.5)',
35 | footerBackground: '#000',
36 | inputBackground: '#292f46cf',
37 | inputBorderColor: '#000',
38 | inputBorderColorHover: '#333',
39 | inputColor: '#fff',
40 | outputBackground: '#2f3d44',
41 | chatFormBackground: '#16171e',
42 | chatMessageBackground: '#121317',
43 | chatMessageHeaderBackground: '#16171e',
44 | chatMessageFooterBackground: '#16171e',
45 | resizerBackground: '#16171e',
46 | resizerBackgroundHover: '#191a22',
47 | resizerBorderColor: '#272933',
48 | resizerBorderColorHover: '#313340'
49 | }
50 |
--------------------------------------------------------------------------------
/src/themeContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useLayoutEffect } from 'react'
2 |
3 | const ThemeContext = React.createContext({
4 | dark: false,
5 | toggle: () => {}
6 | })
7 |
8 | export default ThemeContext
9 |
10 | export function ThemeContextProvider (props: any) {
11 | const [dark, setDark] = useState(false)
12 |
13 | // paints the app before it renders elements
14 | useLayoutEffect(() => {
15 | const lastTheme = localStorage.getItem('darkTheme')
16 |
17 | if (lastTheme === 'true') {
18 | setDark(true)
19 | } else {
20 | setDark(false)
21 | }
22 | // if state changes, repaints the app
23 | }, [dark])
24 |
25 | const toggle = () => {
26 | setDark(!dark)
27 | localStorage.setItem('darkTheme', `${!dark}`)
28 | }
29 |
30 | return (
31 |
35 | {props.children}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/utils/changeFavicon.ts:
--------------------------------------------------------------------------------
1 | const changeFavicon = (uri: string) => {
2 | const link = document.createElement('link')
3 | const oldLink = document.getElementById('favicon')
4 |
5 | link.id = 'favicon'
6 | link.rel = 'shortcut icon'
7 | link.href = uri
8 |
9 | if (oldLink) {
10 | document.head.removeChild(oldLink)
11 | }
12 |
13 | document.head.appendChild(link)
14 | }
15 |
16 | export default changeFavicon
17 |
--------------------------------------------------------------------------------
/src/utils/generateRandomString.ts:
--------------------------------------------------------------------------------
1 | import randomstring from 'randomstring'
2 |
3 | const generateRandomString = () => {
4 | return randomstring.generate({
5 | length: 3,
6 | charset: 'alphabetic',
7 | capitalization: 'lowercase',
8 | readable: true
9 | })
10 | }
11 |
12 | export default generateRandomString
13 |
--------------------------------------------------------------------------------
/src/utils/getUrlParams.ts:
--------------------------------------------------------------------------------
1 | const getUrlParams = () => {
2 | return window.location.search.substr(1).split('&')
3 | .map(x => x.split('='))
4 | .reduce((obj, x) => {
5 | obj[x[0]] = x[1]
6 | return obj
7 | }, {})
8 | }
9 |
10 | export default getUrlParams
11 |
--------------------------------------------------------------------------------
/src/utils/updateWindowTitle.ts:
--------------------------------------------------------------------------------
1 | import changeFavicon from 'src/utils/changeFavicon'
2 |
3 | export const resetWindowTitle = () => {
4 | changeFavicon('https://s3.amazonaws.com/assets.streamhut.io/favicon.ico')
5 | document.title = 'Streamhut'
6 | }
7 |
8 | export const newMessageWindowTitle = () => {
9 | changeFavicon('https://s3.amazonaws.com/assets.streamhut.io/favicon_alert.ico')
10 | document.title = '(new message) Streamhut'
11 | }
12 |
13 | export const updateWindowTitle = () => {
14 | if (document.hidden) {
15 | newMessageWindowTitle()
16 | } else {
17 | resetWindowTitle()
18 | }
19 | }
20 |
21 | export default updateWindowTitle
22 |
--------------------------------------------------------------------------------
/src/ws/index.ts:
--------------------------------------------------------------------------------
1 | export const createWs = (channel: string) => {
2 | const { host, protocol } = window.location
3 | let wsurl = `${protocol === 'https:' ? `wss` : `ws`}://${host}/ws/s/${channel}`
4 | // let wsurl = `ws://localhost:3001/ws/s/${channel}`
5 | const ws = new WebSocket(wsurl)
6 | ws.binaryType = 'arraybuffer'
7 |
8 | return ws
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "preserve",
21 | "noUnusedLocals": true,
22 | "noImplicitAny": false,
23 | "typeRoots": [
24 | "./node_modules/@types",
25 | "./@types"
26 | ],
27 | "downlevelIteration": true
28 | },
29 | "include": [
30 | "src"
31 | ],
32 | "extends": "./tsconfig.paths.json"
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.paths.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "paths": {
5 | "src/*": ["*"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint-config-standard",
4 | "tslint-react",
5 | "tslint-etc"
6 | ],
7 | "rules": {
8 | "jsx-wrap-multiline": false,
9 | "quotemark": [true, "single", "jsx-double"],
10 | "object-literal-sort-keys": false,
11 | "no-console": false,
12 | "no-bitwise": false,
13 | "jsx-closing-tag-location": false,
14 | "max-line-length": false,
15 | "interface-name": false,
16 | "member-access": [true, "no-public"],
17 | "ordered-imports": false,
18 | "jsx-alignment": false,
19 | "jsx-no-multiline-js": false,
20 | "jsx-no-lambda": false,
21 | "no-empty": false,
22 | "no-floating-promises": false,
23 | "no-shadowed-variable": true,
24 | "variable-name": [
25 | false,
26 | "allow-pascal-case",
27 | "allow-leading-underscore"
28 | ]
29 | },
30 | "allowJs": true,
31 | "jsRules": {
32 | "no-empty": true
33 | }
34 | }
35 |
--------------------------------------------------------------------------------