Cached fetches no longer issue a /cache_keys preflight per call. The client trusts whatever it has in IDB and keeps it fresh through ActionCable broadcasts and the existing reconnect-time batch revalidate. On warm boots a new hydrateStoresFromCache helper seeds Vuex from IDB before ActionCable connects, so inboxes, labels, teams, canned responses, and agents render immediately from cache. The helper snapshots local cache keys before fetching server keys — without that ordering the comparison would always see "fresh" against the just-written key and stale data would be served forever. Stale entries are revalidated in the background; cold devices stay on the network-fetch path with no regression. Also awaits replace inside refetchAndCommit so the data write completes before the cache key is persisted, closing a window where a concurrent reader could see a fresh key paired with stale data.
179 lines
5.4 KiB
Vue
179 lines
5.4 KiB
Vue
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import LoadingState from './components/widgets/LoadingState.vue';
|
|
import NetworkNotification from './components/NetworkNotification.vue';
|
|
import UpdateBanner from './components/app/UpdateBanner.vue';
|
|
import StatusBanner from './components/app/StatusBanner.vue';
|
|
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
|
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
|
|
import vueActionCable from './helper/actionCable';
|
|
import { useRouter } from 'vue-router';
|
|
import { useStore } from 'dashboard/composables/store';
|
|
import WootSnackbarBox from './components/SnackbarContainer.vue';
|
|
import { setColorTheme } from './helper/themeHelper';
|
|
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
|
|
import { useAccount } from 'dashboard/composables/useAccount';
|
|
import { useFontSize } from 'dashboard/composables/useFontSize';
|
|
import {
|
|
registerSubscription,
|
|
verifyServiceWorkerExistence,
|
|
} from './helper/pushHelper';
|
|
import ReconnectService from 'dashboard/helper/ReconnectService';
|
|
import hydrateStoresFromCache from 'dashboard/helper/CacheHelper/hydrateStoresFromCache';
|
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
|
|
|
export default {
|
|
name: 'App',
|
|
|
|
components: {
|
|
LoadingState,
|
|
NetworkNotification,
|
|
UpdateBanner,
|
|
StatusBanner,
|
|
PaymentPendingBanner,
|
|
WootSnackbarBox,
|
|
PendingEmailVerificationBanner,
|
|
},
|
|
setup() {
|
|
const router = useRouter();
|
|
const store = useStore();
|
|
const { accountId } = useAccount();
|
|
// Use the font size composable (it automatically sets up the watcher)
|
|
const { currentFontSize } = useFontSize();
|
|
const { uiSettings } = useUISettings();
|
|
|
|
return {
|
|
router,
|
|
store,
|
|
currentAccountId: accountId,
|
|
currentFontSize,
|
|
uiSettings,
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
latestChatwootVersion: null,
|
|
reconnectService: null,
|
|
};
|
|
},
|
|
computed: {
|
|
...mapGetters({
|
|
getAccount: 'accounts/getAccount',
|
|
isRTL: 'accounts/isRTL',
|
|
currentUser: 'getCurrentUser',
|
|
authUIFlags: 'getAuthUIFlags',
|
|
}),
|
|
hideOnOnboardingView() {
|
|
return !isOnOnboardingView(this.$route);
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
currentAccountId: {
|
|
immediate: true,
|
|
handler() {
|
|
if (this.currentAccountId) {
|
|
this.initializeAccount();
|
|
}
|
|
},
|
|
},
|
|
},
|
|
mounted() {
|
|
this.initializeColorTheme();
|
|
this.listenToThemeChanges();
|
|
// If user locale is set, use it; otherwise use account locale
|
|
this.setLocale(
|
|
this.uiSettings?.locale || window.chatwootConfig.selectedLocale
|
|
);
|
|
},
|
|
unmounted() {
|
|
if (this.reconnectService) {
|
|
this.reconnectService.disconnect();
|
|
}
|
|
},
|
|
methods: {
|
|
initializeColorTheme() {
|
|
setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
},
|
|
listenToThemeChanges() {
|
|
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
|
mql.onchange = e => setColorTheme(e.matches);
|
|
},
|
|
setLocale(locale) {
|
|
if (locale) {
|
|
this.$root.$i18n.locale = locale;
|
|
}
|
|
},
|
|
async initializeAccount() {
|
|
await this.$store.dispatch('accounts/get');
|
|
this.$store.dispatch('setActiveAccount', {
|
|
accountId: this.currentAccountId,
|
|
});
|
|
// Seed Vuex from IndexedDB before ActionCable connects so warm boots
|
|
// paint cached config (inboxes, labels, teams, canned responses, agents)
|
|
// instantly. Stale entries are revalidated in the background against the
|
|
// server's authoritative cache keys.
|
|
await hydrateStoresFromCache(this.$store, this.currentAccountId);
|
|
const account = this.getAccount(this.currentAccountId);
|
|
const { locale, latest_chatwoot_version: latestChatwootVersion } =
|
|
account;
|
|
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
|
// If user locale is set, use it; otherwise use account locale
|
|
this.setLocale(this.uiSettings?.locale || locale);
|
|
this.latestChatwootVersion = latestChatwootVersion;
|
|
vueActionCable.init(this.store, pubsubToken);
|
|
this.reconnectService = new ReconnectService(this.store, this.router);
|
|
window.reconnectService = this.reconnectService;
|
|
|
|
verifyServiceWorkerExistence(registration =>
|
|
registration.pushManager.getSubscription().then(subscription => {
|
|
if (subscription) {
|
|
registerSubscription();
|
|
}
|
|
})
|
|
);
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-if="!authUIFlags.isFetching"
|
|
id="app"
|
|
class="flex flex-col w-full h-screen min-h-0 bg-n-background"
|
|
:dir="isRTL ? 'rtl' : 'ltr'"
|
|
>
|
|
<UpdateBanner :latest-chatwoot-version="latestChatwootVersion" />
|
|
<StatusBanner />
|
|
<template v-if="currentAccountId">
|
|
<PendingEmailVerificationBanner v-if="hideOnOnboardingView" />
|
|
<PaymentPendingBanner v-if="hideOnOnboardingView" />
|
|
</template>
|
|
<router-view v-slot="{ Component }">
|
|
<transition name="fade" mode="out-in">
|
|
<component :is="Component" />
|
|
</transition>
|
|
</router-view>
|
|
<WootSnackbarBox />
|
|
<NetworkNotification />
|
|
</div>
|
|
<LoadingState v-else />
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
@import './assets/scss/app';
|
|
|
|
.v-popper--theme-tooltip .v-popper__inner {
|
|
background: black !important;
|
|
font-size: 0.75rem;
|
|
padding: 4px 8px !important;
|
|
border-radius: 6px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.v-popper--theme-tooltip .v-popper__arrow-container {
|
|
display: none;
|
|
}
|
|
</style>
|