Implement service worker offline page
Also, make sure it can be translated.
This commit is contained in:
parent
7cec56fb50
commit
af96e3db5f
9 changed files with 601 additions and 115 deletions
27
src/App.vue
27
src/App.vue
|
|
@ -121,6 +121,33 @@ export default {
|
|||
|
||||
// Set language
|
||||
this.$i18n.locale = this.$store.state.language || "en";
|
||||
|
||||
// Store offline strings.
|
||||
// TODO - Better way to to this? The service worker needs to be able to get these, OR we need multiple translated version of offline.html
|
||||
//
|
||||
// Inspired by https://gist.github.com/JamesMessinger/a0d6389a5d0e3a24814b
|
||||
var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB;
|
||||
var open = indexedDB.open("ServiceWorker", 1);
|
||||
// Create the schema
|
||||
open.onupgradeneeded = () => {
|
||||
var db = open.result;
|
||||
var store = db.createObjectStore("offline_strings", {keyPath: "id"});
|
||||
};
|
||||
open.onsuccess = () => {
|
||||
// Start a new transaction
|
||||
var db = open.result;
|
||||
var tx = db.transaction("offline_strings", "readwrite");
|
||||
var store = tx.objectStore("offline_strings");
|
||||
|
||||
store.put({id: "offline_title", translation: this.$t("offline.offline_title")});
|
||||
store.put({id: "offline_message", translation: this.$t("offline.offline_message")});
|
||||
store.put({id: "offline_redirect", translation: this.$t("offline.offline_redirect")});
|
||||
|
||||
// Close the db when the transaction is done
|
||||
tx.oncomplete = function() {
|
||||
db.close();
|
||||
};
|
||||
}
|
||||
},
|
||||
showNotification() {
|
||||
if(document.visibilityState === "hidden") {
|
||||
|
|
|
|||
|
|
@ -541,5 +541,10 @@
|
|||
"generated_with_ai": "Generated with AI.",
|
||||
"modified": "Modified.",
|
||||
"older_than_n_months": "Older than {n} months."
|
||||
},
|
||||
"offline": {
|
||||
"offline_title": "Having trouble connecting?",
|
||||
"offline_message": "Redirect to an alternate link to join the room",
|
||||
"offline_redirect": "Redirect me"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -259,5 +259,10 @@
|
|||
},
|
||||
"delete_post": {
|
||||
"confirm_text_desc": "Tätä toimintoa ei voi perua."
|
||||
},
|
||||
"offline": {
|
||||
"offline_title": "Ongelmia yhdistämisessä?",
|
||||
"offline_message": "Siirry vaihtoehtoiseen linkkiin liittyäksesi huoneeseen",
|
||||
"offline_redirect": "Siirrä minut"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
55
src/components/MigratingView.vue
Normal file
55
src/components/MigratingView.vue
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<div>
|
||||
<transition name="slow-fade">
|
||||
<div
|
||||
class="text-center d-flex flex-column goodbye-wrapper"
|
||||
>
|
||||
<div class="quote">Migrating...</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "MigratingView",
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
.author {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
}
|
||||
|
||||
.slow-fade-enter-active,
|
||||
.slow-fade-leave-active {
|
||||
transition: opacity 2.5s;
|
||||
}
|
||||
.slow-fade-enter, .slow-fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
.goodbye-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
color: black;
|
||||
background-color: white;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,126 +1,141 @@
|
|||
import Home from '../components/Home.vue'
|
||||
import Chat from '../components/Chat.vue'
|
||||
import Join from '../components/Join.vue'
|
||||
import Login from '../components/Login.vue'
|
||||
import Profile from '../components/Profile.vue'
|
||||
import CreateRoom from '../components/CreateRoom.vue'
|
||||
import GetLink from '../components/GetLink.vue'
|
||||
import Create from '../components/Create.vue'
|
||||
import CreateChannel from '../components/CreateChannel.vue'
|
||||
import CreateFileDrop from '../components/CreateFileDrop.vue'
|
||||
import User from '../models/user'
|
||||
import util from '../plugins/utils'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import Home from "../components/Home.vue";
|
||||
import Chat from "../components/Chat.vue";
|
||||
import Join from "../components/Join.vue";
|
||||
import Login from "../components/Login.vue";
|
||||
import Profile from "../components/Profile.vue";
|
||||
import CreateRoom from "../components/CreateRoom.vue";
|
||||
import GetLink from "../components/GetLink.vue";
|
||||
import Create from "../components/Create.vue";
|
||||
import CreateChannel from "../components/CreateChannel.vue";
|
||||
import CreateFileDrop from "../components/CreateFileDrop.vue";
|
||||
import User from "../models/user";
|
||||
import util from "../plugins/utils";
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import { defineAsyncComponent } from "vue";
|
||||
import { STORE_KEY_SETTINGS, STORE_KEY_USER } from "../store";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
path: "/",
|
||||
name: "Home",
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: '/room/:join(join/)?:roomId?',
|
||||
name: 'Chat',
|
||||
path: "/room/:join(join/)?:roomId?",
|
||||
name: "Chat",
|
||||
component: Chat,
|
||||
meta: {
|
||||
includeRoom: true,
|
||||
includeFavicon: true
|
||||
includeFavicon: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/info',
|
||||
name: 'RoomInfo',
|
||||
component: () => import('../components/RoomInfo.vue'),
|
||||
path: "/info",
|
||||
name: "RoomInfo",
|
||||
component: () => import("../components/RoomInfo.vue"),
|
||||
props: true,
|
||||
meta: {
|
||||
title: 'Info',
|
||||
title: "Info",
|
||||
includeRoom: true,
|
||||
includeFavicon: true
|
||||
}
|
||||
includeFavicon: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'Profile',
|
||||
path: "/profile",
|
||||
name: "Profile",
|
||||
component: Profile,
|
||||
meta: {
|
||||
title: 'Profile',
|
||||
includeFavicon: true
|
||||
}
|
||||
title: "Profile",
|
||||
includeFavicon: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/createroom',
|
||||
name: 'CreateRoom',
|
||||
path: "/createroom",
|
||||
name: "CreateRoom",
|
||||
component: CreateRoom,
|
||||
meta: {
|
||||
title: 'Create room'
|
||||
}
|
||||
title: "Create room",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/getlink',
|
||||
name: 'GetLink',
|
||||
path: "/getlink",
|
||||
name: "GetLink",
|
||||
component: GetLink,
|
||||
},
|
||||
{
|
||||
path: '/create',
|
||||
name: 'Create',
|
||||
path: "/create",
|
||||
name: "Create",
|
||||
component: Create,
|
||||
},
|
||||
{
|
||||
path: '/createchannel',
|
||||
name: 'CreateChannel',
|
||||
path: "/createchannel",
|
||||
name: "CreateChannel",
|
||||
component: CreateChannel,
|
||||
},
|
||||
{
|
||||
path: '/createfiledrop',
|
||||
name: 'CreateFileDrop',
|
||||
path: "/createfiledrop",
|
||||
name: "CreateFileDrop",
|
||||
component: CreateFileDrop,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: Login,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/join/:join(join/)?:roomId?',
|
||||
name: 'Join',
|
||||
path: "/join/:join(join/)?:roomId?",
|
||||
name: "Join",
|
||||
component: Join,
|
||||
props: route => {
|
||||
props: (route) => {
|
||||
return {
|
||||
roomNeedsKnock: (route.query.knock ? true : false),
|
||||
roomDisplayName: route.query.roomName
|
||||
}
|
||||
}
|
||||
roomNeedsKnock: route.query.knock ? true : false,
|
||||
roomDisplayName: route.query.roomName,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/user/:userId?',
|
||||
name: 'User',
|
||||
component: Join
|
||||
path: "/user/:userId?",
|
||||
name: "User",
|
||||
component: Join,
|
||||
},
|
||||
{
|
||||
path: '/invite/:roomId?',
|
||||
name: 'Invite',
|
||||
component: defineAsyncComponent(() => import('../components/Invite.vue')),
|
||||
path: "/invite/:roomId?",
|
||||
name: "Invite",
|
||||
component: defineAsyncComponent(() => import("../components/Invite.vue")),
|
||||
meta: {
|
||||
title: 'Add Friends'
|
||||
}
|
||||
title: "Add Friends",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/goodbye',
|
||||
name: 'Goodbye',
|
||||
component: defineAsyncComponent(() => import('../components/QuoteView.vue')),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
path: "/goodbye",
|
||||
name: "Goodbye",
|
||||
component: defineAsyncComponent(() => import("../components/QuoteView.vue")),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/migrating",
|
||||
name: "Migrating",
|
||||
component: defineAsyncComponent(() => import("../components/MigratingView.vue")),
|
||||
props: false,
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: routes
|
||||
routes: routes,
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const publicPages = ['/login', '/createroom', '/getlink', '/create', '/createchannel', '/createfiledrop'];
|
||||
const publicPages = [
|
||||
"/login",
|
||||
"/createroom",
|
||||
"/getlink",
|
||||
"/create",
|
||||
"/createchannel",
|
||||
"/createfiledrop",
|
||||
"/migrating",
|
||||
];
|
||||
var authRequired = !publicPages.includes(to.path);
|
||||
const loggedIn = router.app.$store.state.auth.user;
|
||||
|
||||
|
|
@ -129,14 +144,46 @@ router.beforeEach((to, from, next) => {
|
|||
const lang = to.query.lang;
|
||||
// Check if valid translation
|
||||
if (router.app.$i18n.messages[lang]) {
|
||||
router.app.$store.commit('setLanguage', lang);
|
||||
router.app.$store.commit("setLanguage", lang);
|
||||
if (router.app.$i18n) {
|
||||
router.app.$i18n.locale = to.query.lang;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (to.name == 'Chat' || to.name == 'Join') {
|
||||
if (to.query && to.query.migrate) {
|
||||
try {
|
||||
if (window.opener) {
|
||||
console.log("Start listening for migration messages");
|
||||
window.addEventListener("message", function (e) {
|
||||
// if (e.origin !== origin) { // TODO - What should we check here?
|
||||
// return
|
||||
// }
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data !== null) {
|
||||
router.app.$store
|
||||
.dispatch("migrate", data)
|
||||
.then(() => {
|
||||
console.log("Migration success");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Migration error", error);
|
||||
})
|
||||
.finally(() => {
|
||||
window.opener.postMessage(JSON.stringify({ cmd: "migrationDone" }), "*");
|
||||
router.app.$navigation.push({ name: "Home" }, -1);
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
});
|
||||
window.opener.postMessage(JSON.stringify({ cmd: "getMigrationData" }), "*");
|
||||
}
|
||||
return next("/migrating");
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
if (to.name == "Chat" || to.name == "Join") {
|
||||
if (!to.params.roomId && to.hash) {
|
||||
// Public rooms start with '#', confuses the router. If hash but no roomId param, set it.
|
||||
to.params.roomId = to.hash;
|
||||
|
|
@ -147,62 +194,84 @@ router.beforeEach((to, from, next) => {
|
|||
//Invite to public room
|
||||
authRequired = false;
|
||||
}
|
||||
} else if (to.name == 'Home') {
|
||||
} else if (to.name == "Home") {
|
||||
if (to.params.roomId !== undefined) {
|
||||
const roomId = to.params.roomId ? util.sanitizeRoomId(to.params.roomId) : null;
|
||||
router.app.$matrix.setCurrentRoomId(roomId);
|
||||
}
|
||||
} else if (to.name == 'User') {
|
||||
} else if (to.name == "User") {
|
||||
if (to.params.userId) {
|
||||
let roomId = util.sanitizeUserId(to.params.userId);
|
||||
if (roomId && !roomId.startsWith("@")) {
|
||||
// Not a full username. Assume local name on this server.
|
||||
return router.app.$config.load().then((config) => {
|
||||
const domain = config.defaultMatrixDomainPart;
|
||||
if (!domain) throw new Error("No domain part for user invite!");
|
||||
roomId = "@" + roomId + ":" + domain;
|
||||
router.app.$matrix.setCurrentRoomId(roomId);
|
||||
}).catch(err => console.error(err)).finally(() => next());
|
||||
return router.app.$config
|
||||
.load()
|
||||
.then((config) => {
|
||||
const domain = config.defaultMatrixDomainPart;
|
||||
if (!domain) throw new Error("No domain part for user invite!");
|
||||
roomId = "@" + roomId + ":" + domain;
|
||||
router.app.$matrix.setCurrentRoomId(roomId);
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
.finally(() => next());
|
||||
} else {
|
||||
router.app.$matrix.setCurrentRoomId(roomId);
|
||||
authRequired = false;
|
||||
}
|
||||
}
|
||||
} else if (to.name == 'Invite') {
|
||||
} else if (to.name == "Invite") {
|
||||
if (to.params.roomId) {
|
||||
const roomId = util.sanitizeRoomId(to.params.roomId);
|
||||
router.app.$matrix.setCurrentRoomId(roomId);
|
||||
}
|
||||
} else if (to.name == 'CreateRoom') {
|
||||
return router.app.$config.load().then((config) => {
|
||||
if (config.hide_add_room_on_home || !config.roomTypes.includes("group_chat")) {
|
||||
next('/');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}).catch(err => { console.error(err); next('/'); });
|
||||
} else if (to.name == 'CreateChannel') {
|
||||
return router.app.$config.load().then((config) => {
|
||||
if (!config.roomTypes.includes("channel")) {
|
||||
next('/');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}).catch(err => { console.error(err); next('/'); });
|
||||
} else if (to.name == 'CreateFileDrop') {
|
||||
return router.app.$config.load().then((config) => {
|
||||
if (!config.roomTypes.includes("file_drop")) {
|
||||
next('/');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}).catch(err => { console.error(err); next('/'); });
|
||||
} else if (to.name == "CreateRoom") {
|
||||
return router.app.$config
|
||||
.load()
|
||||
.then((config) => {
|
||||
if (config.hide_add_room_on_home || !config.roomTypes.includes("group_chat")) {
|
||||
next("/");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
next("/");
|
||||
});
|
||||
} else if (to.name == "CreateChannel") {
|
||||
return router.app.$config
|
||||
.load()
|
||||
.then((config) => {
|
||||
if (!config.roomTypes.includes("channel")) {
|
||||
next("/");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
next("/");
|
||||
});
|
||||
} else if (to.name == "CreateFileDrop") {
|
||||
return router.app.$config
|
||||
.load()
|
||||
.then((config) => {
|
||||
if (!config.roomTypes.includes("file_drop")) {
|
||||
next("/");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
next("/");
|
||||
});
|
||||
}
|
||||
|
||||
// trying to access a restricted page + not logged in
|
||||
// redirect to login page
|
||||
if (authRequired && !loggedIn) {
|
||||
next('/login');
|
||||
next("/login");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
|
@ -225,12 +294,21 @@ router.getRoomLink = function (alias, roomId, roomName, mode, autojoin, knock, m
|
|||
const autoJoinSegment = autojoin ? "join/" : "";
|
||||
let queryString = "";
|
||||
if (Object.entries(params).length > 0) {
|
||||
queryString = "?" + Object.entries(params)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join('&')
|
||||
queryString =
|
||||
"?" +
|
||||
Object.entries(params)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join("&");
|
||||
}
|
||||
return (mirror ? (window.location.protocol + "//" + mirror) : window.location.origin) + window.location.pathname + "#/room/" + autoJoinSegment + encodeURIComponent(util.sanitizeRoomId(alias || roomId)) + queryString;
|
||||
}
|
||||
return (
|
||||
(mirror ? window.location.protocol + "//" + mirror : window.location.origin) +
|
||||
window.location.pathname +
|
||||
"#/room/" +
|
||||
autoJoinSegment +
|
||||
encodeURIComponent(util.sanitizeRoomId(alias || roomId)) +
|
||||
queryString
|
||||
);
|
||||
};
|
||||
|
||||
router.getDMLink = function (user, config, mirror) {
|
||||
let userId = user.user_id;
|
||||
|
|
@ -238,7 +316,9 @@ router.getDMLink = function (user, config, mirror) {
|
|||
// Using default server, don't include it in the link
|
||||
userId = User.localPart(user.user_id);
|
||||
}
|
||||
return `${(mirror ? (window.location.protocol + "//" + mirror) : window.location.origin) + window.location.pathname}#/user/${encodeURIComponent(userId)}`
|
||||
}
|
||||
return `${
|
||||
(mirror ? window.location.protocol + "//" + mirror : window.location.origin) + window.location.pathname
|
||||
}#/user/${encodeURIComponent(userId)}`;
|
||||
};
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -141,7 +141,27 @@ export default {
|
|||
}
|
||||
return this.legacyCryptoStore;
|
||||
},
|
||||
login(user, registrationFlowHandler, createUser = false) {
|
||||
migrate(user) {
|
||||
return util.getMatrixBaseUrl(user, this.$config)
|
||||
.then((baseUrl) => {
|
||||
console.log("Create temp client", user);
|
||||
const tempMatrixClient = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
idBaseUrl: this.$config.identityServer,
|
||||
});
|
||||
user.device_id = tempMatrixClient.getSessionId();
|
||||
return this.refreshAccessToken(tempMatrixClient, user.refresh_token);
|
||||
}).then(() => {
|
||||
console.log("Refresh done, normal login");
|
||||
return this.login(user, undefined, false, true);
|
||||
});
|
||||
},
|
||||
login(user, registrationFlowHandler, createUser = false, isMigration = false) {
|
||||
if (!isMigration && window.location.href.includes("migrate=1")) {
|
||||
console.log("Migrate, so no login!");
|
||||
return Promise.reject("Migrating, so no login");
|
||||
}
|
||||
|
||||
return util.getMatrixBaseUrl(user, this.$config).then((baseUrl) => {
|
||||
const tempMatrixClient = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
|
|
@ -510,12 +530,16 @@ export default {
|
|||
},
|
||||
|
||||
onSessionRefresh(refreshToken) {
|
||||
return this.refreshAccessToken(this.matrixClient, refreshToken);
|
||||
},
|
||||
|
||||
refreshAccessToken(matrixClient, refreshToken) {
|
||||
if (this.tokenRefreshPromise) {
|
||||
return this.tokenRefreshPromise;
|
||||
}
|
||||
const now = Date.now();
|
||||
this.tokenRefreshPromise =
|
||||
this.matrixClient.http.request(sdk.Method.Post, "/refresh", undefined, { refresh_token: refreshToken}, { prefix: sdk.ClientPrefix.V3, inhibitLogoutEmit: true }).then((result) => {
|
||||
matrixClient.http.request(sdk.Method.Post, "/refresh", undefined, { refresh_token: refreshToken}, { prefix: sdk.ClientPrefix.V3, inhibitLogoutEmit: true }).then((result) => {
|
||||
// Store new one!
|
||||
var user = this.$store.state.auth.user;
|
||||
user.access_token = result.access_token;
|
||||
|
|
|
|||
|
|
@ -136,6 +136,33 @@ const store = createStore({
|
|||
this.$matrix.logout();
|
||||
commit('logout');
|
||||
},
|
||||
migrate({ state }, migrationData) {
|
||||
if (migrationData && migrationData.type) {
|
||||
let storage = migrationData.type == "session" ? sessionStorage : localStorage;
|
||||
if (migrationData.settings) {
|
||||
try {
|
||||
const settings = JSON.parse(migrationData.settings);
|
||||
storage.setItem(STORE_KEY_SETTINGS, migrationData.settings);
|
||||
state.useLocalStorage = migrationData.type != "session";
|
||||
} catch (error) {
|
||||
console.error("Failed to migrate settings", error);
|
||||
}
|
||||
}
|
||||
if (migrationData.user) {
|
||||
const user = JSON.parse(migrationData.user);
|
||||
state.auth.user = user;
|
||||
return this.$matrix.migrate(user).then(
|
||||
(user) => {
|
||||
return Promise.resolve(user);
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return Promise.reject("Migration error");
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
storage: state => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue