您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

136 行
4.4KB

  1. marked.setOptions({
  2. breaks: true,
  3. })
  4. const scrollIntoView = (vnode) => {
  5. vnode.dom.scrollIntoView()
  6. }
  7. const TextBox = {
  8. autoSize: () => {
  9. textbox.style.height = `0px`
  10. textbox.style.height = `${textbox.scrollHeight}px`
  11. },
  12. sendPost: () => {
  13. if(textbox.value) {
  14. wire({kind: 'post', value: textbox.value})
  15. textbox.value = ''
  16. textbox.focus()
  17. TextBox.autoSize()
  18. }
  19. },
  20. blockIndent: (text, iStart, iEnd, nLevels) => {
  21. const startLine = text.slice(0, iStart).split('\n').length - 1
  22. const endLine = text.slice(0, iEnd).split('\n').length - 1
  23. const newText = text
  24. .split('\n')
  25. .map((line, i) => {
  26. if(i < startLine || i > endLine || nLevels === 0) {
  27. newLine = line
  28. }
  29. else if(nLevels > 0) {
  30. newLine = line.replace(/^/, ' ')
  31. }
  32. else if(nLevels < 0) {
  33. newLine = line.replace(/^ /, '')
  34. }
  35. if(i === startLine) {
  36. iStart = iStart + newLine.length - line.length
  37. }
  38. iEnd = iEnd + newLine.length - line.length
  39. return newLine
  40. })
  41. .join('\n')
  42. return [newText, Math.max(0, iStart), Math.max(0, iEnd)]
  43. },
  44. hotKey: (e) => {
  45. // if isDesktop, Enter posts, unless Shift+Enter
  46. // use isLandscape as proxy for isDesktop
  47. if(e.key === 'Enter' && isLandscape && !e.shiftKey) {
  48. e.preventDefault()
  49. TextBox.sendPost()
  50. }
  51. // indent and dedent
  52. const modKey = e.ctrlKey || e.metaKey
  53. const {value: text, selectionStart: A, selectionEnd: B} = textbox
  54. if(e.key === 'Tab') {
  55. e.preventDefault()
  56. const regex = new RegExp(`([\\s\\S]{${A}})([\\s\\S]{${B - A}})`)
  57. textbox.value = text.replace(regex, (m, a, b) => a + ' '.repeat(4))
  58. textbox.setSelectionRange(A + 4, A + 4)
  59. }
  60. if(']['.includes(e.key) && modKey) {
  61. e.preventDefault()
  62. const nLevels = {']': 1, '[': -1}[e.key]
  63. const [newText, newA, newB] = TextBox.blockIndent(text, A, B, nLevels)
  64. textbox.value = newText
  65. textbox.setSelectionRange(newA, newB)
  66. }
  67. },
  68. view() {
  69. return m('.actions',
  70. m('textarea#textbox', {
  71. oncreate: (vnode) => {
  72. TextBox.autoSize()
  73. autoFocus(vnode)
  74. },
  75. onkeydown: TextBox.hotKey,
  76. oninput: TextBox.autoSize,
  77. }),
  78. m('button', {
  79. onclick: ({target}) => {
  80. TextBox.sendPost()
  81. TextBox.autoSize()
  82. },
  83. },
  84. 'Send'),
  85. )
  86. },
  87. }
  88. const Chat = {
  89. log: (message) => {
  90. signal({kind: 'post', ts: +new Date(), value: '' + message})
  91. },
  92. prettifyTime: (ts) => {
  93. const dt = new Date(ts)
  94. const H = `0${dt.getHours()}`.slice(-2)
  95. const M = `0${dt.getMinutes()}`.slice(-2)
  96. const S = `0${dt.getSeconds()}`.slice(-2)
  97. return `${H}:${M}:${S}`
  98. },
  99. outboundLinks: (vnode) => {
  100. vnode.dom.querySelectorAll('a').forEach(anchor => {
  101. anchor.target = '_blank'
  102. anchor.rel = 'noopener'
  103. })
  104. },
  105. view() {
  106. return m('.chat',
  107. m('.posts',
  108. State.posts.map(post => m('.post', {oncreate: scrollIntoView},
  109. m('.ts', Chat.prettifyTime(post.ts)),
  110. m('.source', post.source || '~'),
  111. m('.text', {oncreate: Chat.outboundLinks},
  112. m.trust(DOMPurify.sanitize(marked(post.value)))
  113. ),
  114. )),
  115. ),
  116. m(TextBox),
  117. m('.online',
  118. m('button', {onclick: Login.sendLogout}, 'Logout'),
  119. m('.user-list', State.online.map(username =>
  120. m('details',
  121. m('summary',
  122. m('span', username),
  123. ),
  124. m(VideoOptions, {username}),
  125. ),
  126. )),
  127. ),
  128. m('.tabbed-area',
  129. m(Media),
  130. // m(Hanabi),
  131. ),
  132. )
  133. },
  134. }