You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

147 line
4.5KB

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