|  |  |  |  |  |  | 
														
													
														
															|  |  | const scrollIntoView = (vnode) => { |  |  | const scrollIntoView = (vnode) => { | 
														
													
														
															|  |  | vnode.dom.scrollIntoView() |  |  | vnode.dom.scrollIntoView() | 
														
													
														
															|  |  | } |  |  | } | 
														
													
														
															|  |  | const toggleFullscreen = (el) => (event) => { |  |  |  | 
														
													
														
															|  |  | const requestFullscreen = (el.requestFullscreen || el.webkitRequestFullscreen).bind(el) |  |  |  | 
														
													
														
															|  |  | document.fullscreenElement ? document.exitFullscreen() : requestFullscreen() |  |  |  | 
														
													
														
															|  |  | } |  |  |  | 
														
													
														
															|  |  | const prettyTime = (ts) => { |  |  |  | 
														
													
														
															|  |  | const dt = new Date(ts) |  |  |  | 
														
													
														
															|  |  | const H = `0${dt.getHours()}`.slice(-2) |  |  |  | 
														
													
														
															|  |  | const M = `0${dt.getMinutes()}`.slice(-2) |  |  |  | 
														
													
														
															|  |  | const S = `0${dt.getSeconds()}`.slice(-2) |  |  |  | 
														
													
														
															|  |  | return `${H}:${M}:${S}` |  |  |  | 
														
													
														
															|  |  | } |  |  |  | 
														
													
														
															|  |  | const blockIndent = (text, iStart, iEnd, nLevels) => { |  |  |  | 
														
													
														
															|  |  | // prop |  |  |  | 
														
													
														
															|  |  | const startLine = text.slice(0, iStart).split('\n').length - 1 |  |  |  | 
														
													
														
															|  |  | const endLine = text.slice(0, iEnd).split('\n').length - 1 |  |  |  | 
														
													
														
															|  |  | const newText = text |  |  |  | 
														
													
														
															|  |  | .split('\n') |  |  |  | 
														
													
														
															|  |  | .map((line, i) => { |  |  |  | 
														
													
														
															|  |  | if(i < startLine || i > endLine || nLevels === 0) { |  |  |  | 
														
													
														
															|  |  | newLine = line |  |  |  | 
														
													
														
															|  |  | } |  |  |  | 
														
													
														
															|  |  | else if(nLevels > 0) { |  |  |  | 
														
													
														
															|  |  | newLine = line.replace(/^/, '    ') |  |  |  | 
														
													
														
															|  |  | } |  |  |  | 
														
													
														
															|  |  | else if(nLevels < 0) { |  |  |  | 
														
													
														
															|  |  | newLine = line.replace(/^    /, '') |  |  |  | 
														
													
														
															|  |  | } |  |  |  | 
														
													
														
															|  |  |  |  |  | const TextBox = { | 
														
													
														
															|  |  |  |  |  | autoSize: () => { | 
														
													
														
															|  |  |  |  |  | textbox.rows = textbox.value.split('\n').length | 
														
													
														
															|  |  |  |  |  | }, | 
														
													
														
															|  |  |  |  |  | sendPost: () => { | 
														
													
														
															|  |  |  |  |  | if(textbox.value) { | 
														
													
														
															|  |  |  |  |  | wire({kind: 'post', value: textbox.value}) | 
														
													
														
															|  |  |  |  |  | textbox.value = '' | 
														
													
														
															|  |  |  |  |  | textbox.focus() | 
														
													
														
															|  |  |  |  |  | } | 
														
													
														
															|  |  |  |  |  | }, | 
														
													
														
															|  |  |  |  |  | blockIndent: (text, iStart, iEnd, nLevels) => { | 
														
													
														
															|  |  |  |  |  | const startLine = text.slice(0, iStart).split('\n').length - 1 | 
														
													
														
															|  |  |  |  |  | const endLine = text.slice(0, iEnd).split('\n').length - 1 | 
														
													
														
															|  |  |  |  |  | const newText = text | 
														
													
														
															|  |  |  |  |  | .split('\n') | 
														
													
														
															|  |  |  |  |  | .map((line, i) => { | 
														
													
														
															|  |  |  |  |  | if(i < startLine || i > endLine || nLevels === 0) { | 
														
													
														
															|  |  |  |  |  | newLine = line | 
														
													
														
															|  |  |  |  |  | } | 
														
													
														
															|  |  |  |  |  | else if(nLevels > 0) { | 
														
													
														
															|  |  |  |  |  | newLine = line.replace(/^/, '    ') | 
														
													
														
															|  |  |  |  |  | } | 
														
													
														
															|  |  |  |  |  | else if(nLevels < 0) { | 
														
													
														
															|  |  |  |  |  | newLine = line.replace(/^    /, '') | 
														
													
														
															|  |  |  |  |  | } | 
														
													
														
															|  |  | 
 |  |  | 
 | 
														
													
														
															|  |  | if(i === startLine) { |  |  |  | 
														
													
														
															|  |  | iStart = iStart + newLine.length - line.length |  |  |  | 
														
													
														
															|  |  | } |  |  |  | 
														
													
														
															|  |  | iEnd = iEnd + newLine.length - line.length |  |  |  | 
														
													
														
															|  |  | return newLine |  |  |  | 
														
													
														
															|  |  | }) |  |  |  | 
														
													
														
															|  |  | .join('\n') |  |  |  | 
														
													
														
															|  |  | return [newText, Math.max(0, iStart), Math.max(0, iEnd)] |  |  |  | 
														
													
														
															|  |  | } |  |  |  | 
														
													
														
															|  |  | const hotKey = (e) => { |  |  |  | 
														
													
														
															|  |  | // if isDesktop, Enter posts, unless Shift+Enter |  |  |  | 
														
													
														
															|  |  | // use isLandscape as proxy for isDesktop |  |  |  | 
														
													
														
															|  |  | if(e.key === 'Enter' && isLandscape && !e.shiftKey) { |  |  |  | 
														
													
														
															|  |  | e.preventDefault() |  |  |  | 
														
													
														
															|  |  | Chat.sendPost() |  |  |  | 
														
													
														
															|  |  | } |  |  |  | 
														
													
														
															|  |  | // indent and dedent |  |  |  | 
														
													
														
															|  |  | const modKey = e.ctrlKey || e.metaKey |  |  |  | 
														
													
														
															|  |  | const {value: text, selectionStart: A, selectionEnd: B} = textbox |  |  |  | 
														
													
														
															|  |  | if(e.key === 'Tab') { |  |  |  | 
														
													
														
															|  |  | e.preventDefault() |  |  |  | 
														
													
														
															|  |  | const regex = new RegExp(`([\\s\\S]{${A}})([\\s\\S]{${B - A}})`) |  |  |  | 
														
													
														
															|  |  | textbox.value = text.replace(regex, (m, a, b) => a + ' '.repeat(4)) |  |  |  | 
														
													
														
															|  |  | textbox.setSelectionRange(A + 4, A + 4) |  |  |  | 
														
													
														
															|  |  | } |  |  |  | 
														
													
														
															|  |  | if(']['.includes(e.key) && modKey) { |  |  |  | 
														
													
														
															|  |  | e.preventDefault() |  |  |  | 
														
													
														
															|  |  | const nLevels = {']': 1, '[': -1}[e.key] |  |  |  | 
														
													
														
															|  |  | const [newText, newA, newB] = blockIndent(text, A, B, nLevels) |  |  |  | 
														
													
														
															|  |  | textbox.value = newText |  |  |  | 
														
													
														
															|  |  | textbox.setSelectionRange(newA, newB) |  |  |  | 
														
													
														
															|  |  | } |  |  |  | 
														
													
														
															|  |  |  |  |  | if(i === startLine) { | 
														
													
														
															|  |  |  |  |  | iStart = iStart + newLine.length - line.length | 
														
													
														
															|  |  |  |  |  | } | 
														
													
														
															|  |  |  |  |  | iEnd = iEnd + newLine.length - line.length | 
														
													
														
															|  |  |  |  |  | return newLine | 
														
													
														
															|  |  |  |  |  | }) | 
														
													
														
															|  |  |  |  |  | .join('\n') | 
														
													
														
															|  |  |  |  |  | return [newText, Math.max(0, iStart), Math.max(0, iEnd)] | 
														
													
														
															|  |  |  |  |  | }, | 
														
													
														
															|  |  |  |  |  | hotKey: (e) => { | 
														
													
														
															|  |  |  |  |  | // if isDesktop, Enter posts, unless Shift+Enter | 
														
													
														
															|  |  |  |  |  | // use isLandscape as proxy for isDesktop | 
														
													
														
															|  |  |  |  |  | if(e.key === 'Enter' && isLandscape && !e.shiftKey) { | 
														
													
														
															|  |  |  |  |  | e.preventDefault() | 
														
													
														
															|  |  |  |  |  | TextBox.sendPost() | 
														
													
														
															|  |  |  |  |  | } | 
														
													
														
															|  |  |  |  |  | // indent and dedent | 
														
													
														
															|  |  |  |  |  | const modKey = e.ctrlKey || e.metaKey | 
														
													
														
															|  |  |  |  |  | const {value: text, selectionStart: A, selectionEnd: B} = textbox | 
														
													
														
															|  |  |  |  |  | if(e.key === 'Tab') { | 
														
													
														
															|  |  |  |  |  | e.preventDefault() | 
														
													
														
															|  |  |  |  |  | const regex = new RegExp(`([\\s\\S]{${A}})([\\s\\S]{${B - A}})`) | 
														
													
														
															|  |  |  |  |  | textbox.value = text.replace(regex, (m, a, b) => a + ' '.repeat(4)) | 
														
													
														
															|  |  |  |  |  | textbox.setSelectionRange(A + 4, A + 4) | 
														
													
														
															|  |  |  |  |  | } | 
														
													
														
															|  |  |  |  |  | if(']['.includes(e.key) && modKey) { | 
														
													
														
															|  |  |  |  |  | e.preventDefault() | 
														
													
														
															|  |  |  |  |  | const nLevels = {']': 1, '[': -1}[e.key] | 
														
													
														
															|  |  |  |  |  | const [newText, newA, newB] = TextBox.blockIndent(text, A, B, nLevels) | 
														
													
														
															|  |  |  |  |  | textbox.value = newText | 
														
													
														
															|  |  |  |  |  | textbox.setSelectionRange(newA, newB) | 
														
													
														
															|  |  |  |  |  | } | 
														
													
														
															|  |  |  |  |  | }, | 
														
													
														
															|  |  |  |  |  | view() { | 
														
													
														
															|  |  |  |  |  | return m('.actions', | 
														
													
														
															|  |  |  |  |  | m('textarea#textbox', { | 
														
													
														
															|  |  |  |  |  | oncreate: (vnode) => { | 
														
													
														
															|  |  |  |  |  | TextBox.autoSize() | 
														
													
														
															|  |  |  |  |  | autoFocus(vnode) | 
														
													
														
															|  |  |  |  |  | }, | 
														
													
														
															|  |  |  |  |  | onkeydown: TextBox.hotKey, | 
														
													
														
															|  |  |  |  |  | onkeyup: TextBox.autoSize, | 
														
													
														
															|  |  |  |  |  | }), | 
														
													
														
															|  |  |  |  |  | m('button', { | 
														
													
														
															|  |  |  |  |  | onclick: ({target}) => { | 
														
													
														
															|  |  |  |  |  | TextBox.sendPost() | 
														
													
														
															|  |  |  |  |  | TextBox.autoSize() | 
														
													
														
															|  |  |  |  |  | }, | 
														
													
														
															|  |  |  |  |  | }, | 
														
													
														
															|  |  |  |  |  | 'Send'), | 
														
													
														
															|  |  |  |  |  | ) | 
														
													
														
															|  |  |  |  |  | }, | 
														
													
														
															|  |  | } |  |  | } | 
														
													
														
															|  |  | const VideoOptions = { |  |  | const VideoOptions = { | 
														
													
														
															|  |  | available: ['mirror', 'square', 'full-screen'], |  |  | available: ['mirror', 'square', 'full-screen'], | 
														
													
												
													
														
															|  |  |  |  |  |  | 
														
													
														
															|  |  | dom.autoplay = true |  |  | dom.autoplay = true | 
														
													
														
															|  |  | dom.muted = (username === State.username) |  |  | dom.muted = (username === State.username) | 
														
													
														
															|  |  | dom.srcObject = stream |  |  | dom.srcObject = stream | 
														
													
														
															|  |  | dom.ondblclick = toggleFullscreen(dom) |  |  |  | 
														
													
														
															|  |  | }, |  |  | }, | 
														
													
														
															|  |  | view({attrs}) { |  |  | view({attrs}) { | 
														
													
														
															|  |  | const classList = VideoOptions.getClassListFor(attrs.username) |  |  | const classList = VideoOptions.getClassListFor(attrs.username) | 
														
													
												
													
														
															|  |  |  |  |  |  | 
														
													
														
															|  |  | }, |  |  | }, | 
														
													
														
															|  |  | } |  |  | } | 
														
													
														
															|  |  | const Chat = { |  |  | const Chat = { | 
														
													
														
															|  |  | sendPost: () => { |  |  |  | 
														
													
														
															|  |  | if(textbox.value) { |  |  |  | 
														
													
														
															|  |  | wire({kind: 'post', value: textbox.value}) |  |  |  | 
														
													
														
															|  |  | textbox.value = '' |  |  |  | 
														
													
														
															|  |  | } |  |  |  | 
														
													
														
															|  |  |  |  |  | prettifyTime: (ts) => { | 
														
													
														
															|  |  |  |  |  | const dt = new Date(ts) | 
														
													
														
															|  |  |  |  |  | const H = `0${dt.getHours()}`.slice(-2) | 
														
													
														
															|  |  |  |  |  | const M = `0${dt.getMinutes()}`.slice(-2) | 
														
													
														
															|  |  |  |  |  | const S = `0${dt.getSeconds()}`.slice(-2) | 
														
													
														
															|  |  |  |  |  | return `${H}:${M}:${S}` | 
														
													
														
															|  |  | }, |  |  | }, | 
														
													
														
															|  |  | view() { |  |  | view() { | 
														
													
														
															|  |  | return m('.chat', |  |  | return m('.chat', | 
														
													
														
															|  |  | m('.posts', |  |  | m('.posts', | 
														
													
														
															|  |  | State.posts.map(post => m('.post', {oncreate: scrollIntoView}, |  |  | State.posts.map(post => m('.post', {oncreate: scrollIntoView}, | 
														
													
														
															|  |  | m('.ts', prettyTime(post.ts)), |  |  |  | 
														
													
														
															|  |  |  |  |  | m('.ts', Chat.prettifyTime(post.ts)), | 
														
													
														
															|  |  | m('.source', post.source || '~'), |  |  | m('.source', post.source || '~'), | 
														
													
														
															|  |  | m('.text', m.trust(DOMPurify.sanitize(marked(post.value)))), |  |  | m('.text', m.trust(DOMPurify.sanitize(marked(post.value)))), | 
														
													
														
															|  |  | )), |  |  | )), | 
														
													
														
															|  |  | ), |  |  | ), | 
														
													
														
															|  |  | m('.actions', |  |  |  | 
														
													
														
															|  |  | m('textarea#textbox', {oncreate: autoFocus, onkeydown: hotKey}), |  |  |  | 
														
													
														
															|  |  | m('button', {onclick: Chat.sendPost}, 'Send'), |  |  |  | 
														
													
														
															|  |  | ), |  |  |  | 
														
													
														
															|  |  |  |  |  | m(TextBox), | 
														
													
														
															|  |  | m('.online', |  |  | m('.online', | 
														
													
														
															|  |  | m('button', {onclick: Login.sendLogout}, 'Logout'), |  |  | m('button', {onclick: Login.sendLogout}, 'Logout'), | 
														
													
														
															|  |  | m('.user-list', State.online.map(username => |  |  | m('.user-list', State.online.map(username => |