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.

217 lines
5.8KB

  1. const State = Object.seal({
  2. username: null,
  3. websocket: null,
  4. online: [],
  5. get isConnected() {
  6. return State.websocket && State.websocket.readyState === 1
  7. },
  8. })
  9. const params = (new URL(document.location)).searchParams
  10. /*
  11. *
  12. * SIGNALING
  13. *
  14. */
  15. addEventListener('resize', () => m.redraw())
  16. const wire = (message) => State.websocket.send(JSON.stringify(message))
  17. const signal = (message) => dispatchEvent(new CustomEvent(message.kind, {detail: message}))
  18. const listen = (kind, handler) => {
  19. addEventListener(kind, handler)
  20. }
  21. listen('login', ({detail}) => {
  22. State.username = detail.value
  23. })
  24. listen('logout', ({detail}) => {
  25. State.online = []
  26. })
  27. listen('state', ({detail}) => {
  28. delete detail.ts
  29. delete detail.kind
  30. Object.assign(State, detail)
  31. })
  32. const doNotLog = new Set(['login', 'state', 'post', 'peerInfo', 'volumeMapMove'])
  33. /*
  34. *
  35. * UTILS
  36. *
  37. */
  38. const autoFocus = (vnode) => {
  39. vnode.dom.focus()
  40. }
  41. /*
  42. *
  43. * WEBSOCKET
  44. *
  45. */
  46. const connect = (username) => {
  47. const wsUrl = location.href.replace('http', 'ws')
  48. State.websocket = new WebSocket(wsUrl)
  49. State.websocket.onopen = (e) => {
  50. wire({kind: 'login', value: username})
  51. }
  52. State.websocket.onmessage = (e) => {
  53. const message = JSON.parse(e.data)
  54. if(message.online) {
  55. const difference = (l1, l2) => l1.filter(u => !l2.includes(u))
  56. difference(message.online, State.online).forEach(username => {
  57. if(username === State.username) return
  58. signal({kind: 'post', ts: message.ts, value: `${username} joined`})
  59. })
  60. difference(State.online, message.online).forEach(username => {
  61. if(username === State.username) return
  62. signal({kind: 'post', ts: message.ts, value: `${username} left`})
  63. })
  64. }
  65. if(!doNotLog.has(message.kind)) {
  66. console.log(message)
  67. }
  68. signal(message)
  69. m.redraw()
  70. }
  71. State.websocket.onclose = (e) => {
  72. State.online.forEach(signalPeerStop)
  73. if(!e.wasClean) {
  74. setTimeout(connect, 1000, username)
  75. }
  76. m.redraw()
  77. }
  78. }
  79. /*
  80. *
  81. * BASE
  82. *
  83. */
  84. const Shadow = {
  85. oncreate({dom, attrs}) {
  86. dom.listener = () => attrs.app.isOn = !attrs.app.isOn
  87. addEventListener(attrs.key, dom.listener)
  88. },
  89. onremove({dom, attrs}) {
  90. removeEventListener(attrs.key, dom.listener)
  91. },
  92. view({attrs}) {
  93. const style = {
  94. zIndex: 999,
  95. backgroundColor: 'rgba(0, 0, 0, 0.2)',
  96. visibility: attrs.app.isOn ? 'unset' : 'hidden',
  97. position: 'fixed',
  98. right: 0,
  99. top: 0,
  100. height: '100vh',
  101. width: '100vw',
  102. display: 'flex',
  103. alignItems: 'center',
  104. justifyContent: 'center',
  105. }
  106. const onclick = ({target: {classList}}) => {
  107. classList.contains('shadow') && signal({kind: attrs.key})
  108. }
  109. return m('.shadow', {style, onclick}, m(attrs.app))
  110. },
  111. }
  112. const Settings = {
  113. get(key) {
  114. try {
  115. return JSON.parse(localStorage.getItem(key))
  116. }
  117. catch(error) {
  118. return null
  119. }
  120. },
  121. set(key, value) {
  122. localStorage.setItem(key, JSON.stringify(value))
  123. },
  124. multiField: ([key, options]) => {
  125. let current = Settings.get(key)
  126. if(!options.includes(current)) {
  127. Settings.set(key, options[0])
  128. }
  129. return m('.field',
  130. m('label', key),
  131. options.map(value => {
  132. const style = {
  133. fontWeight: Settings.get(key) == value ? 'bold' : 'unset'
  134. }
  135. const onclick = () => Settings.set(key, value)
  136. return m('button', {style, onclick}, `${value}`)
  137. })
  138. )
  139. },
  140. view() {
  141. return m('.settings',
  142. Object.entries({
  143. blackBars: [true, false],
  144. }).map(Settings.multiField)
  145. )
  146. },
  147. }
  148. const Base = {
  149. oncreate: () => {
  150. const randomName = ('' + Math.random()).substring(2)
  151. connect(localStorage.username || randomName)
  152. },
  153. sendLogin: (e) => {
  154. e.preventDefault()
  155. const username = e.target.username.value
  156. localStorage.username = username
  157. connect(username)
  158. },
  159. sendLogout: (e) => {
  160. e.preventDefault()
  161. wire({kind: 'logout'})
  162. signal({kind: 'logout'})
  163. },
  164. view() {
  165. const attrs = {
  166. oncreate: autoFocus,
  167. name: 'username',
  168. autocomplete: 'off',
  169. value: localStorage.username,
  170. }
  171. const headerStyle = {
  172. display: 'grid',
  173. gridAutoFlow: 'column',
  174. justifyItems: 'start',
  175. marginRight: 'auto',
  176. }
  177. return [
  178. m('header', {style: headerStyle},
  179. State.isConnected
  180. ? Headers.map(h => m(...h))
  181. : [
  182. m('form.login',
  183. {onsubmit: Base.sendLogin},
  184. m('input', attrs),
  185. m('button', 're-join'),
  186. ),
  187. ],
  188. ),
  189. m('main', {style: {overflow: 'hidden'}},
  190. State.isConnected
  191. ? Apps.map(a => m(...a))
  192. : [
  193. m('span.error', State.info),
  194. m(Settings),
  195. ],
  196. ),
  197. ]
  198. },
  199. }
  200. const Headers = [
  201. ['button', {onclick: Base.sendLogout}, 'log-out'],
  202. // m('button', {onclick: VolumeMap.toggle}, 'volume'),
  203. // m('button', {onclick: ScreenShare.toggle}, 'screen'),
  204. ]
  205. const Apps = [
  206. // m(Shadow, {key: 'map-shadow', app: VolumeMap}),
  207. // m(Shadow, {key: 'screen-shadow', app: ScreenShare}),
  208. ]
  209. addEventListener('load', () => m.mount(document.body, Base))