Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

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