ConnectionBanner.vue 2.6 KB

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