ConnectionBanner.vue 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. <template>
  2. <span id="connection-banner" v-if="!connectionState.connected" class="inline-notification critical user-info-connection">
  3. <span class="connection-banner-sr-only" role="status">{{ staticAnnouncement }}</span>
  4. <span aria-hidden="true">
  5. <a :href="websocketDocsUrl" target="_blank" rel="noopener noreferrer" class="connection-banner-link">{{ linkText }}</a>{{ bannerSuffix }}
  6. </span>
  7. </span>
  8. </template>
  9. <script setup>
  10. import { ref, computed, watch, onUnmounted } from 'vue'
  11. import { useI18n } from 'vue-i18n'
  12. import { connectionState } from '../stores/connectionState.js'
  13. const { t } = useI18n()
  14. const websocketDocsUrl = 'https://docs.olivetin.app/troubleshooting/err-websocket-connection.html'
  15. const linkText = computed(() => t('disconnected-banner-link-text'))
  16. function formatShortRelative(ms) {
  17. if (ms < 0) return '0s'
  18. const secs = Math.floor(ms / 1000)
  19. const mins = Math.floor(secs / 60)
  20. const hours = Math.floor(mins / 60)
  21. if (hours > 0) return `${hours}h`
  22. if (mins > 0) return `${mins}m`
  23. return `${secs}s`
  24. }
  25. function formatShortTime(ts) {
  26. if (ts == null) return '--:--'
  27. return new Date(ts).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
  28. }
  29. const now = ref(Date.now())
  30. let ticker = null
  31. watch(() => connectionState.connected, (connected) => {
  32. if (ticker) {
  33. clearInterval(ticker)
  34. ticker = null
  35. }
  36. if (!connected) {
  37. now.value = Date.now()
  38. ticker = setInterval(() => { now.value = Date.now() }, 1000)
  39. }
  40. }, { immediate: true })
  41. onUnmounted(() => {
  42. if (ticker) {
  43. clearInterval(ticker)
  44. ticker = null
  45. }
  46. })
  47. const staticAnnouncement = computed(() => t('disconnected-banner-announcement'))
  48. const bannerSuffix = computed(() => {
  49. const at = connectionState.disconnectedAt
  50. const next = connectionState.nextReconnectAt
  51. const n = now.value
  52. const disconnectedSince = formatShortTime(at)
  53. if (next != null && next > n) {
  54. const reconnectIn = formatShortRelative(next - n)
  55. return t('disconnected-banner-suffix', { disconnectedSince, reconnectIn })
  56. }
  57. return t('disconnected-banner-suffix-reconnecting', { disconnectedSince })
  58. })
  59. </script>
  60. <style scoped>
  61. #connection-banner.user-info-connection {
  62. font-weight: 500;
  63. }
  64. .inline-notification {
  65. border: 0;
  66. margin: 0;
  67. }
  68. .connection-banner-link {
  69. color: inherit;
  70. text-decoration: underline;
  71. }
  72. .connection-banner-sr-only {
  73. position: absolute;
  74. width: 1px;
  75. height: 1px;
  76. padding: 0;
  77. margin: -1px;
  78. overflow: hidden;
  79. clip: rect(0, 0, 0, 0);
  80. white-space: nowrap;
  81. border: 0;
  82. }
  83. </style>