implement update prompt, clean service worker handling

This commit is contained in:
marlon-erler
2026-09-24 22:07:36 +02:00
parent ecf72a7318
commit a63488d62a
10 changed files with 625 additions and 930 deletions
+1 -1
View File
@@ -122,7 +122,7 @@ function SettingsInfoPane(
{InfoTile(
"build",
coreViewModel.translations.settings.version,
settingsViewModel.coreViewModel.BUILD,
settingsViewModel.coreViewModel.version,
)}
</div>
</div>
+179 -166
View File
@@ -19,185 +19,198 @@ export function HomePage(
chatListViewModel: ChatListViewModel,
) {
// sections
const updateText = React.createProxyState([coreViewModel.latestVersion], ()=>coreViewModel.translations.homePage.updateButton(coreViewModel.latestVersion.value));
const overviewSection = (
<div id="overview-section">
<h2>{coreViewModel.translations.homePage.overviewHeadline}</h2>
<div id="overview-section">
<h2>{coreViewModel.translations.homePage.overviewHeadline}</h2>
<label class="tile flex-no">
<span class="icon">cell_tower</span>
<div>
<span>
{coreViewModel.translations.homePage.serverAddress}
</span>
<input
list="previous-connection-list"
placeholder={
coreViewModel.translations.homePage
.serverAddressPlaceholder
}
bind:value={connectionViewModel.serverAddressInput}
on:enter={connectionViewModel.connect}
></input>
<datalist
hidden
id="previous-connection-list"
children:append={[
connectionViewModel.previousAddresses,
StringToOption,
]}
></datalist>
</div>
</label>
<label class="tile flex-no">
<span class="icon">cell_tower</span>
<div>
<span>
{coreViewModel.translations.homePage.serverAddress}
</span>
<input
list="previous-connection-list"
placeholder={
coreViewModel.translations.homePage
.serverAddressPlaceholder
}
bind:value={connectionViewModel.serverAddressInput}
on:enter={connectionViewModel.connect}
></input>
<datalist
hidden
id="previous-connection-list"
children:append={[
connectionViewModel.previousAddresses,
StringToOption,
]}
></datalist>
</div>
</label>
<div class="flex-row">
<button
class="danger flex justify-center"
aria-label={
coreViewModel.translations.homePage.disconnectAudioLabel
}
on:click={connectionViewModel.disconnect}
toggle:disabled={connectionViewModel.cannotDisonnect}
>
<span class="icon">link_off</span>
</button>
<button
class="flex justify-center"
aria-label={
coreViewModel.translations.homePage
.manageConnectionsAudioLabel
}
on:click={connectionViewModel.showConnectionModal}
toggle:disabled={
connectionViewModel.hasNoPreviousConnections
}
>
<span class="icon">history</span>
</button>
<button
class="primary flex justify-center"
aria-label={
coreViewModel.translations.homePage.connectAudioLabel
}
on:click={connectionViewModel.connect}
toggle:disabled={connectionViewModel.cannotConnect}
>
<span class="icon">link</span>
</button>
</div>
<div class="flex-row">
<button
class="danger flex justify-center"
aria-label={
coreViewModel.translations.homePage.disconnectAudioLabel
}
on:click={connectionViewModel.disconnect}
toggle:disabled={connectionViewModel.cannotDisonnect}
>
<span class="icon">link_off</span>
</button>
<button
class="flex justify-center"
aria-label={
coreViewModel.translations.homePage
.manageConnectionsAudioLabel
}
on:click={connectionViewModel.showConnectionModal}
toggle:disabled={
connectionViewModel.hasNoPreviousConnections
}
>
<span class="icon">history</span>
</button>
<button
class="primary flex justify-center"
aria-label={
coreViewModel.translations.homePage.connectAudioLabel
}
on:click={connectionViewModel.connect}
toggle:disabled={connectionViewModel.cannotConnect}
>
<span class="icon">link</span>
</button>
</div>
<hr></hr>
<hr></hr>
<label class="tile flex-no">
<span class="icon">account_circle</span>
<div>
<span>
{coreViewModel.translations.homePage.yourNameLabel}
</span>
<input
placeholder={
coreViewModel.translations.homePage
.yourNamePlaceholder
}
bind:value={settingsViewModel.usernameInput}
on:enter={settingsViewModel.setName}
></input>
</div>
</label>
<div class="flex-row justify-end">
<button
class="width-50"
on:click={settingsViewModel.setName}
toggle:disabled={settingsViewModel.cannotSetName}
aria-label={
coreViewModel.translations.homePage
.setNameButtonAudioLabel
}
>
{coreViewModel.translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<label class="tile flex-no">
<span class="icon">account_circle</span>
<div>
<span>
{coreViewModel.translations.homePage.yourNameLabel}
</span>
<input
placeholder={
coreViewModel.translations.homePage
.yourNamePlaceholder
}
bind:value={settingsViewModel.usernameInput}
on:enter={settingsViewModel.setName}
></input>
</div>
</label>
<div class="flex-row justify-end">
<button
class="width-50"
on:click={settingsViewModel.setName}
toggle:disabled={settingsViewModel.cannotSetName}
aria-label={
coreViewModel.translations.homePage
.setNameButtonAudioLabel
}
>
{coreViewModel.translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<hr></hr>
{HomePageButton(
settingsViewModel.showSettingsModal,
coreViewModel.translations.homePage.settingsButton,
"settings",
)}
{HomePageButton(
fileTransferViewModel.showDirectionSelectionModal,
coreViewModel.translations.homePage.transferDataButton,
"sync_alt",
)}
{HomePageButton(
storageViewModel.showStorageModal,
coreViewModel.translations.homePage.manageStorageButton,
"hard_drive",
)}
{HomePageButton(
settingsViewModel.showSettingsModal,
coreViewModel.translations.homePage.settingsButton,
"settings",
)}
{HomePageButton(
fileTransferViewModel.showDirectionSelectionModal,
coreViewModel.translations.homePage.transferDataButton,
"sync_alt",
)}
{HomePageButton(
storageViewModel.showStorageModal,
coreViewModel.translations.homePage.manageStorageButton,
"hard_drive",
)}
<div class="mobile-only">
<hr></hr>
<button
class="primary"
on:click={coreViewModel.update}
toggle:hidden={coreViewModel.noUpdateAvailable}
>
<span
subscribe:innerText={updateText}
></span>
<span class="icon">update</span>
</button>
<div class="flex-row justify-end">
<button class="ghost width-50" on:click={scrollToChat}>
{coreViewModel.translations.homePage.scrollToChatButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
);
<div class="mobile-only">
<hr></hr>
const chatSection = (
<div id="chat-section">
<h2>{coreViewModel.translations.homePage.chatsHeadline}</h2>
<div class="flex-row justify-end">
<button class="ghost width-50" on:click={scrollToChat}>
{coreViewModel.translations.homePage.scrollToChatButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
);
<div class="flex-row width-input">
<input
placeholder={
coreViewModel.translations.homePage.addChatPlaceholder
}
aria-label={
coreViewModel.translations.homePage.addChatAudioLabel
}
bind:value={chatListViewModel.newChatPrimaryChannel}
on:enter={chatListViewModel.createChat}
></input>
<button
class="primary"
aria-label={
coreViewModel.translations.homePage.addChatButton
}
on:click={chatListViewModel.createChat}
toggle:disabled={chatListViewModel.cannotCreateChat}
>
<span class="icon">add</span>
</button>
</div>
const chatSection = (
<div id="chat-section">
<h2>{coreViewModel.translations.homePage.chatsHeadline}</h2>
<div
id="chat-grid"
children:append={[
chatListViewModel.chatViewModels,
ChatViewModelToChatEntry,
]}
></div>
</div>
);
<div class="flex-row width-input">
<input
placeholder={
coreViewModel.translations.homePage.addChatPlaceholder
}
aria-label={
coreViewModel.translations.homePage.addChatAudioLabel
}
bind:value={chatListViewModel.newChatPrimaryChannel}
on:enter={chatListViewModel.createChat}
></input>
<button
class="primary"
aria-label={
coreViewModel.translations.homePage.addChatButton
}
on:click={chatListViewModel.createChat}
toggle:disabled={chatListViewModel.cannotCreateChat}
>
<span class="icon">add</span>
</button>
</div>
// methods
function scrollToChat() {
chatSection.scrollIntoView();
}
<div
id="chat-grid"
children:append={[
chatListViewModel.chatViewModels,
ChatViewModelToChatEntry,
]}
></div>
</div>
);
// final
return (
<article id="home-page">
<div>
{overviewSection}
{chatSection}
</div>
</article>
// methods
function scrollToChat() {
chatSection.scrollIntoView();
}
// final
return (
<article id="home-page">
<div>
{overviewSection}
{chatSection}
</div>
</article>
);
}
+6
View File
@@ -73,6 +73,8 @@ const englishTranslations = {
manageStorageButton: "Manage storage",
transferDataButton: "Transfer or export data",
updateButton: (version: string) => `Update to ${version}`,
scrollToChatButton: "Chats",
///
@@ -411,6 +413,8 @@ export const allTranslations: {
settingsButton: "Einstellungen",
manageStorageButton: "Daten verwalten",
transferDataButton: "Daten übertragen",
updateButton: (version: string) => `Aktualisieren: ${version}`,
scrollToChatButton: "Chats",
@@ -716,6 +720,8 @@ export const allTranslations: {
settingsButton: "Ajustes",
manageStorageButton: "Gestionar almacenamiento",
transferDataButton: "Enviar o exportar archivos",
updateButton: (version: string) => `Actualizar a ${version}`,
scrollToChatButton: "Chats",
+93 -49
View File
@@ -9,7 +9,13 @@ import ConnectionModel from "../../Model/Global/connectionModel";
import ChatListModel from "../../Model/Chat/chatListModel";
export default class CoreViewModel {
readonly BUILD = "Build 26.09.24.D";
swRegistration: ServiceWorkerRegistration|undefined = undefined;
version: React.State<string> = new React.State("");
latestVersion: React.State<string> = new React.State("");
noUpdateAvailable: React.State<boolean> = React.createProxyState([this.latestVersion], () => {
return (this.latestVersion.value == "" || this.latestVersion.value == this.version.value)
})
translations: Translations;
@@ -84,34 +90,72 @@ export default class CoreViewModel {
// DRAG & DROP
draggedObject: React.State<any> = new React.State<any>(undefined);
// OFFLINE MODE & UPDATE
configureServiceWorker = async (): Promise<void> => {
if ("serviceWorker" in navigator) {
try {
this.swRegistration = await navigator.serviceWorker.register(
"sw.js",
{
scope: "/",
},
);
if (this.swRegistration.active) {
console.log("Service worker active");
}
} catch (error) {
console.error(`Could not install service worker: ${error}`);
}
}
fetch("/version").then(async response => {
this.version.value = await response.text();
});
fetch("/latestVersion").then(async response => {
this.latestVersion.value = (await response.text()).replace("\n", "");
console.log("LATEST", this.latestVersion.value);
})
}
update = async (): Promise<void> => {
if (!this.swRegistration) return;
await this.swRegistration.update();
window.location.reload();
}
// init
constructor(
public readonly storageModel: StorageModel,
public readonly settingsModel: SettingsModel,
public readonly connectionModel: ConnectionModel,
public readonly chatListModel: ChatListModel,
public readonly fileTransferModel: FileTransferModel,
public readonly storageModel: StorageModel,
public readonly settingsModel: SettingsModel,
public readonly connectionModel: ConnectionModel,
public readonly chatListModel: ChatListModel,
public readonly fileTransferModel: FileTransferModel,
) {
this.translations =
allTranslations[settingsModel.language] || allTranslations.en;
this.translations =
allTranslations[settingsModel.language] || allTranslations.en;
document.body.addEventListener("keydown", this.handleKeyDown);
document.body.addEventListener("keydown", this.handleKeyDown);
window.onpopstate = () => {
if (!this.context) return;
this.closeContext(this.context.contextId, true);
};
window.onpopstate = () => {
if (!this.context) return;
this.closeContext(this.context.contextId, true);
};
this.startChron();
this.chronHandlerManager.setHandler(
"core-view-model",
this.handleChron,
);
this.configureServiceWorker();
this.noUpdateAvailable.subscribe(() => {
console.log(this.noUpdateAvailable.value, this.latestVersion.value, this.version.value);
})
this.startChron();
this.chronHandlerManager.setHandler(
"core-view-model",
this.handleChron,
);
}
// util
static checkIsKeystroke(e: KeyboardEvent): boolean {
return (e.metaKey || e.altKey) && e.ctrlKey;
return (e.metaKey || e.altKey) && e.ctrlKey;
}
}
@@ -120,19 +164,19 @@ export class Context {
keystrokes = new Map<string, () => void>();
handleKeystroke = (e: KeyboardEvent): boolean => {
const fn: (() => void) | undefined = this.keystrokes.get(
e.key.toLowerCase(),
);
if (!fn) return false;
fn();
return true;
const fn: (() => void) | undefined = this.keystrokes.get(
e.key.toLowerCase(),
);
if (!fn) return false;
fn();
return true;
};
close = (): void => {};
handleContextClose = (fromHistoryEvent: boolean): void => {};
registerKeyStroke = (key: string, fn: () => void): void => {
this.keystrokes.set(key, fn);
this.keystrokes.set(key, fn);
};
constructor(public contextDebugDescription: string) {}
@@ -143,45 +187,45 @@ export class ContextHost<T> extends Context {
currentContext = new React.State<Context | undefined>(undefined);
get isOpen(): boolean {
return false;
return false;
}
get contextSelection(): T | undefined {
return undefined;
return undefined;
}
registerContext = (key: T, context: Context): void => {
this.contexts.set(key, context);
this.contexts.set(key, context);
};
closeCurrentContext = (): void => {
if (this.currentContext.value) {
this.coreViewModel.closeContext(
this.currentContext.value.contextId,
);
}
this.currentContext.value = undefined;
if (this.currentContext.value) {
this.coreViewModel.closeContext(
this.currentContext.value.contextId,
);
}
this.currentContext.value = undefined;
};
updateContexts = (): void => {
if (this.isOpen == false) return;
if (this.isOpen == false) return;
const selection = this.contextSelection;
if (!selection) return;
const selectedContext: Context | undefined =
this.contexts.get(selection);
if (!selectedContext) return;
if (selectedContext != this.currentContext.value) {
this.closeCurrentContext();
}
const selection = this.contextSelection;
if (!selection) return;
const selectedContext: Context | undefined =
this.contexts.get(selection);
if (!selectedContext) return;
if (selectedContext != this.currentContext.value) {
this.closeCurrentContext();
}
this.coreViewModel.context = selectedContext;
this.currentContext.value = selectedContext;
this.coreViewModel.context = selectedContext;
this.currentContext.value = selectedContext;
};
constructor(
contextDebugDescription: string,
public coreViewModel: CoreViewModel,
contextDebugDescription: string,
public coreViewModel: CoreViewModel,
) {
super(contextDebugDescription);
super(contextDebugDescription);
}
}