Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

220 lines
5.9KB

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