mobilizon/js/src/App.vue

331 lines
8.6 KiB
Vue
Raw Normal View History

<template>
<div id="mobilizon">
<!-- <VueAnnouncer />
<VueSkipTo to="#main" :label="t('Skip to main content')" /> -->
<NavBar />
<div v-if="isDemoMode">
<o-notification
class="container mx-auto"
variant="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-notification>
</div>
<ErrorComponent v-if="error" :error="error" />
<main id="main" class="px-2 py-4" v-else>
<router-view></router-view>
</main>
<mobilizon-footer />
</div>
</template>
<script lang="ts" setup>
import NavBar from "@/components/NavBar.vue";
import {
AUTH_ACCESS_TOKEN,
AUTH_USER_EMAIL,
AUTH_USER_ID,
AUTH_USER_ROLE,
} from "@/constants";
import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user";
import MobilizonFooter from "@/components/PageFooter.vue";
import jwt_decode, { JwtPayload } from "jwt-decode";
import { refreshAccessToken } from "@/apollo/utils";
import {
reactive,
ref,
provide,
onUnmounted,
onMounted,
onBeforeMount,
inject,
defineAsyncComponent,
computed,
watch,
onBeforeUnmount,
} from "vue";
import { LocationType } from "@/types/user-location.model";
import { useMutation, useQuery } from "@vue/apollo-composable";
import {
initializeCurrentActor,
NoIdentitiesException,
} from "@/utils/identity";
import { useI18n } from "vue-i18n";
import { Snackbar } from "@/plugins/snackbar";
import { Notifier } from "@/plugins/notifier";
import { CONFIG } from "@/graphql/config";
import { IConfig } from "@/types/config.model";
import { useRouter } from "vue-router";
import RouteName from "@/router/name";
const { result: configResult } = useQuery<{ config: IConfig }>(
CONFIG,
undefined,
{ fetchPolicy: "cache-only" }
);
const config = computed(() => configResult.value?.config);
const ErrorComponent = defineAsyncComponent(
() => import("@/components/ErrorComponent.vue")
);
const { t } = useI18n({ useScope: "global" });
const location = computed(() => config.value?.location);
const userLocation = reactive<LocationType>({
lon: undefined,
lat: undefined,
name: undefined,
picture: undefined,
isIPLocation: true,
accuracy: 100,
});
const updateUserLocation = (newLocation: LocationType) => {
userLocation.lat = newLocation.lat;
userLocation.lon = newLocation.lon;
userLocation.name = newLocation.name;
userLocation.picture = newLocation.picture;
userLocation.isIPLocation = newLocation.isIPLocation;
userLocation.accuracy = newLocation.accuracy;
};
updateUserLocation({
lat: location.value?.latitude,
lon: location.value?.longitude,
name: "", // config.ipLocation.country.name,
isIPLocation: true,
accuracy: 150, // config.ipLocation.location.accuracy_radius * 1.5 || 150,
});
provide("userLocation", {
userLocation,
updateUserLocation,
});
// const routerView = ref("routerView");
const error = ref<Error | null>(null);
const online = ref(true);
const interval = ref<number>(0);
const notifier = inject<Notifier>("notifier");
interval.value = window.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();
2019-01-18 14:47:10 +01:00
}
}
}, 60000) as unknown as number;
onBeforeMount(async () => {
if (initializeCurrentUser()) {
try {
await initializeCurrentActor();
} catch (err) {
if (err instanceof NoIdentitiesException) {
await router.push({
name: RouteName.REGISTER_PROFILE,
params: {
email: localStorage.getItem(AUTH_USER_EMAIL),
userAlreadyActivated: "true",
},
});
}
}
}
});
const snackbar = inject<Snackbar>("snackbar");
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
onMounted(() => {
online.value = window.navigator.onLine;
window.addEventListener("offline", () => {
online.value = false;
showOfflineNetworkWarning();
console.debug("offline");
});
window.addEventListener("online", () => {
online.value = true;
console.debug("online");
});
document.addEventListener("refreshApp", (event: Event) => {
snackbar?.open({
queue: false,
indefinite: true,
variant: "dark",
actionText: t("Update app"),
cancelText: t("Ignore"),
message: t("A new version is available."),
onAction: async () => {
const registration = (
event as unknown as { detail: ServiceWorkerRegistration }
).detail;
try {
await refreshApp(registration);
window.location.reload();
} catch (err) {
console.error(err);
notifier?.error(t("An error has occured while refreshing the page."));
}
},
});
});
darkModePreference.addEventListener("change", changeTheme);
});
onUnmounted(() => {
clearInterval(interval.value);
interval.value = 0;
});
const { mutate: updateCurrentUser } = useMutation(UPDATE_CURRENT_USER_CLIENT);
const initializeCurrentUser = () => {
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) {
updateCurrentUser({
id: userId,
email: userEmail,
isLoggedIn: true,
role,
});
return true;
}
return false;
};
const refreshApp = async (
registration: ServiceWorkerRegistration
): Promise<any> => {
const worker = registration.waiting;
if (!worker) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
if (event.data.error) {
reject(event.data);
} else {
resolve(event.data);
}
};
worker?.postMessage({ type: "skip-waiting" }, [channel.port2]);
});
};
const showOfflineNetworkWarning = (): void => {
notifier?.error(t("You are offline"));
};
// const extractPageTitleFromRoute = (routeWatched: RouteLocation): string => {
// if (routeWatched.meta?.announcer?.message) {
// return routeWatched.meta?.announcer?.message();
// }
// return document.title;
// };
// watch(route, (routeWatched) => {
// const pageTitle = extractPageTitleFromRoute(routeWatched);
// if (pageTitle) {
// // this.$announcer.polite(
// // t("Navigated to {pageTitle}", {
// // pageTitle,
// // }) as string
// // );
// }
// // Set the focus to the router view
// // https://marcus.io/blog/accessible-routing-vuejs
// setTimeout(() => {
// const focusTarget = (
// routerView.value?.$refs?.componentFocusTarget !== undefined
// ? routerView.value?.$refs?.componentFocusTarget
// : routerView.value?.$el
// ) as HTMLElement;
// if (focusTarget && focusTarget instanceof Element) {
// // 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);
// });
const router = useRouter();
watch(config, async (configWatched: IConfig | undefined) => {
if (configWatched) {
const { statistics } = await import("@/services/statistics");
statistics(configWatched?.analytics, {
router,
version: configWatched.version,
});
}
});
const isDemoMode = computed(() => config.value?.demoMode);
const changeTheme = () => {
console.debug("changing theme");
if (
localStorage.getItem("theme") === "dark" ||
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
};
onBeforeUnmount(() => {
darkModePreference.removeEventListener("change", changeTheme);
});
</script>
<style lang="scss">
#mobilizon {
min-height: 100vh;
display: flex;
flex-direction: column;
main {
flex-grow: 1;
}
}
.vue-skip-to {
z-index: 40;
}
</style>