|
|
@@ -120,66 +120,85 @@ const autoFocus = (vnode) => { |
|
|
|
const scrollIntoView = (vnode) => { |
|
|
|
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 = { |
|
|
|
available: ['mirror', 'square', 'full-screen'], |
|
|
@@ -215,7 +234,6 @@ const Video = { |
|
|
|
dom.autoplay = true |
|
|
|
dom.muted = (username === State.username) |
|
|
|
dom.srcObject = stream |
|
|
|
dom.ondblclick = toggleFullscreen(dom) |
|
|
|
}, |
|
|
|
view({attrs}) { |
|
|
|
const classList = VideoOptions.getClassListFor(attrs.username) |
|
|
@@ -330,25 +348,23 @@ const Login = { |
|
|
|
}, |
|
|
|
} |
|
|
|
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() { |
|
|
|
return m('.chat', |
|
|
|
m('.posts', |
|
|
|
State.posts.map(post => m('.post', {oncreate: scrollIntoView}, |
|
|
|
m('.ts', prettyTime(post.ts)), |
|
|
|
m('.ts', Chat.prettifyTime(post.ts)), |
|
|
|
m('.source', post.source || '~'), |
|
|
|
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('button', {onclick: Login.sendLogout}, 'Logout'), |
|
|
|
m('.user-list', State.online.map(username => |