mobilizon/js/src/App.vue

263 lines
7.3 KiB
Vue

<template>
<div id="mobilizon">
<VueAnnouncer />
<VueSkipTo to="#main" :label="$t('Skip to main content')" />
<NavBar />
<div v-if="config && config.demoMode">
<o-message
class="container"
type="is-danger"
:title="$t('Warning').toLocaleUpperCase()"
closable
:aria-close-label="$t('Close')"
>
<p>
{{ $t("This is a demonstration site to test Mobilizon.") }}
<b>{{ $t("Please do not use it in any real way.") }}</b>
{{
$t(
"This website isn't moderated and the data that you enter will be automatically destroyed every day at 00:01 (Paris timezone)."
)
}}
</p>
</o-message>
</div>
<error v-if="error" :error="error" />
<main id="main" v-else>
<transition name="fade" mode="out-in">
<router-view ref="routerView" />
</transition>
</main>
<mobilizon-footer />
</div>
</template>
<script lang="ts">
import NavBar from "./components/NavBar.vue";
import {
AUTH_ACCESS_TOKEN,
AUTH_USER_EMAIL,
AUTH_USER_ID,
AUTH_USER_ROLE,
} from "./constants";
import {
CURRENT_USER_CLIENT,
UPDATE_CURRENT_USER_CLIENT,
} from "./graphql/user";
import Footer from "./components/Footer.vue";
import { initializeCurrentActor } from "./utils/auth";
import { CONFIG } from "./graphql/config";
import { IConfig } from "./types/config.model";
import { ICurrentUser } from "./types/current-user.model";
import jwt_decode, { JwtPayload } from "jwt-decode";
import { refreshAccessToken } from "./apollo/utils";
import { defineComponent, ref } from "vue";
import {
useApolloClient,
useMutation,
useQuery,
useResult,
} from "@vue/apollo-composable";
import { RouterView } from "vue-router";
import { useAnnouncer } from "@vue-a11y/announcer";
import { useMeta } from "vue-meta";
export default defineComponent({
name: "App",
setup() {
const { result: configResult } = useQuery(CONFIG);
const config = useResult<{ config: IConfig }>(configResult);
const { result } = useQuery(CURRENT_USER_CLIENT);
const currentUser = useResult<{ currentUser: ICurrentUser }>(result);
const { mutate: updateUserClient } = useMutation(
UPDATE_CURRENT_USER_CLIENT
);
const interval: number | undefined = 0;
const routerView = ref<InstanceType<typeof RouterView> | null>(null);
useMeta({ titleTemplate: "%s | Mobilizon" });
const { resolveClient } = useApolloClient();
return {
config,
currentUser,
updateUserClient,
online: false,
interval,
routerView,
resolveClient,
};
},
components: {
NavBar,
error: () =>
import(/* webpackChunkName: "editor" */ "./components/Error.vue"),
"mobilizon-footer": Footer,
},
methods: {
async initializeCurrentUser(): Promise<boolean> {
const userId = localStorage.getItem(AUTH_USER_ID);
const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
const role = localStorage.getItem(AUTH_USER_ROLE);
if (userId && userEmail && accessToken && role) {
this.updateUserClient({
id: userId,
email: userEmail,
isLoggedIn: true,
role,
});
return true;
}
return false;
},
async refreshApp(registration: ServiceWorkerRegistration): Promise<any> {
const worker = registration.waiting;
if (!worker) {
return Promise.resolve();
}
console.debug("Doing worker.skipWaiting().");
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
console.debug("Done worker.skipWaiting().");
if (event.data.error) {
reject(event.data);
} else {
resolve(event.data);
}
};
console.debug("calling skip waiting");
worker?.postMessage({ type: "skip-waiting" }, [channel.port2]);
});
},
showOfflineNetworkWarnin(): void {
// this.$notifier.error(this.$t("You are offline") as string);
},
extractPageTitleFromRoute(route: any): string {
if (route.meta?.announcer?.message) {
return route.meta?.announcer?.message();
}
return document.title;
},
},
async mounted(): Promise<void> {
if (await this.initializeCurrentUser()) {
await initializeCurrentActor(this.resolveClient());
}
this.online = window.navigator.onLine;
window.addEventListener("offline", () => {
this.online = false;
// this.showOfflineNetworkWarning();
console.debug("offline");
});
window.addEventListener("online", () => {
this.online = true;
console.debug("online");
});
document.addEventListener("refreshApp", (event: Event) => {
this.$oruga.snackbar.open({
queue: false,
indefinite: true,
type: "is-secondary",
actionText: this.$t("Update app") as string,
cancelText: this.$t("Ignore") as string,
message: this.$t("A new version is available.") as string,
onAction: async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const detail = event.detail;
const registration = detail as ServiceWorkerRegistration;
try {
await this.refreshApp(registration);
window.location.reload();
} catch (err) {
console.error(err);
this.$notifier.error(
this.$t(
"An error has occured while refreshing the page."
) as string
);
}
},
});
});
this.interval = setInterval(async () => {
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
if (accessToken) {
const token = jwt_decode<JwtPayload>(accessToken);
if (
token?.exp !== undefined &&
new Date(token.exp * 1000 - 60000) < new Date()
) {
refreshAccessToken(this.resolveClient());
}
}
}, 60000);
},
unmounted(): void {
clearInterval(this.interval);
this.interval = 0;
},
async beforeRouteUpdate(to) {
const pageTitle = this.extractPageTitleFromRoute(to);
if (pageTitle) {
const { polite } = useAnnouncer();
polite(
this.$t("Navigated to {pageTitle}", {
pageTitle,
})
);
}
// Set the focus to the router view
// https://marcus.io/blog/accessible-routing-vuejs
setTimeout(() => {
console.log(this.routerView);
// const focusTarget = this.routerView?.$el as HTMLElement;
// if (focusTarget) {
// // Make focustarget programmatically focussable
// focusTarget.setAttribute("tabindex", "-1");
// // Focus element
// focusTarget.focus();
// // Remove tabindex from focustarget.
// // Reason: https://axesslab.com/skip-links/#update-3-a-comment-from-gov-uk
// focusTarget.removeAttribute("tabindex");
// }
}, 0);
},
});
</script>
<style lang="scss">
@import "variables";
/* Icons */
$mdi-font-path: "~@mdi/font/fonts";
@import "../node_modules/@mdi/font/scss/materialdesignicons";
@import "common";
#mobilizon {
min-height: 100vh;
display: flex;
flex-direction: column;
main {
flex-grow: 1;
}
}
.vue-skip-to {
z-index: 40;
}
</style>