feat: Implement Electron window state persistence, update checking with splash screen and banner, and external link metadata fetching.
All checks were successful
Build and Release / build-and-release (push) Successful in 12m18s

This commit is contained in:
Bryan1029384756
2026-02-12 00:00:17 -06:00
parent 40ad86bbcb
commit 0da09ebb2f
6 changed files with 207 additions and 8 deletions

View File

@@ -346,6 +346,44 @@ app.whenReady().then(async () => {
} }
}); });
// Flatpak update check
ipcMain.handle('check-flatpak-update', async () => {
const isFlatpak = fs.existsSync('/.flatpak-info') || !!process.env.FLATPAK_ID;
if (!isFlatpak) return { isFlatpak: false };
try {
const yaml = await httpGet('https://gitea.moyettes.com/Moyettes/DiscordClone/releases/download/latest/latest-linux.yml');
if (!yaml) return { isFlatpak: true, updateAvailable: false };
const versionMatch = yaml.match(/^version:\s*(.+)$/m);
if (!versionMatch) return { isFlatpak: true, updateAvailable: false };
const latestVersion = versionMatch[1].trim();
const currentVersion = app.getVersion();
// Semver comparison: determine if update exists and its severity
const latest = latestVersion.split('.').map(Number);
const current = currentVersion.split('.').map(Number);
let updateAvailable = false;
let updateType = 'patch'; // 'major', 'minor', or 'patch'
for (let i = 0; i < Math.max(latest.length, current.length); i++) {
const l = latest[i] || 0;
const c = current[i] || 0;
if (l > c) {
updateAvailable = true;
updateType = i === 0 ? 'major' : i === 1 ? 'minor' : 'patch';
break;
}
if (l < c) break;
}
return { isFlatpak: true, updateAvailable, updateType, latestVersion, currentVersion };
} catch (err) {
console.error('Flatpak update check error:', err.message);
return { isFlatpak: true, updateAvailable: false };
}
});
ipcMain.handle('open-external', async (event, url) => { ipcMain.handle('open-external', async (event, url) => {
await shell.openExternal(url); await shell.openExternal(url);
}); });

View File

@@ -32,6 +32,10 @@ contextBridge.exposeInMainWorld('appSettings', {
set: (key, value) => ipcRenderer.invoke('set-setting', key, value), set: (key, value) => ipcRenderer.invoke('set-setting', key, value),
}); });
contextBridge.exposeInMainWorld('updateAPI', {
checkFlatpakUpdate: () => ipcRenderer.invoke('check-flatpak-update'),
});
contextBridge.exposeInMainWorld('sessionPersistence', { contextBridge.exposeInMainWorld('sessionPersistence', {
save: (data) => ipcRenderer.invoke('save-session', data), save: (data) => ipcRenderer.invoke('save-session', data),
load: () => ipcRenderer.invoke('load-session'), load: () => ipcRenderer.invoke('load-session'),

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { TitleBarUpdateIcon } from './UpdateBanner';
const TitleBar = () => { const TitleBar = () => {
return ( return (
@@ -26,6 +27,7 @@ const TitleBar = () => {
</div> </div>
<div className="titlebar-title">Discord Clone</div> <div className="titlebar-title">Discord Clone</div>
<div className="titlebar-buttons"> <div className="titlebar-buttons">
<TitleBarUpdateIcon />
<button <button
className="titlebar-btn titlebar-minimize" className="titlebar-btn titlebar-minimize"
onClick={() => window.windowControls?.minimize()} onClick={() => window.windowControls?.minimize()}

View File

@@ -0,0 +1,95 @@
import React, { useState, useEffect, createContext, useContext } from 'react';
import updateIcon from '../assets/icons/update.svg';
const RELEASE_URL = 'https://gitea.moyettes.com/Moyettes/DiscordClone/releases/tag/latest';
const UpdateContext = createContext(null);
export function useUpdateCheck() {
return useContext(UpdateContext);
}
export function UpdateProvider({ children }) {
const [state, setState] = useState(null);
useEffect(() => {
if (!window.updateAPI) return;
window.updateAPI.checkFlatpakUpdate().then((result) => {
if (!result?.updateAvailable) return;
setState(result);
}).catch(() => {});
}, []);
return (
<UpdateContext.Provider value={state}>
{children}
{state && (state.updateType === 'major' || state.updateType === 'minor') && (
<ForcedUpdateModal latestVersion={state.latestVersion} />
)}
</UpdateContext.Provider>
);
}
function ForcedUpdateModal({ latestVersion }) {
const handleDownload = () => {
window.cryptoAPI?.openExternal(RELEASE_URL);
};
return (
<div className="forced-update-overlay">
<div className="forced-update-modal">
<h2>Update Required</h2>
<p>
A new version (v{latestVersion}) is available. This update is required to continue using the app.
</p>
<button className="forced-update-btn" onClick={handleDownload}>
Download Update
</button>
</div>
</div>
);
}
export function TitleBarUpdateIcon() {
const update = useUpdateCheck();
if (!update) return null;
const handleClick = () => {
window.cryptoAPI?.openExternal(RELEASE_URL);
};
return (
<button
className="titlebar-btn"
onClick={handleClick}
aria-label={`Update available: v${update.latestVersion}`}
title={`Update available: v${update.latestVersion}`}
style={{ borderRight: '1px solid var(--app-frame-border)' }}
>
<div style={{ marginRight: '12px' }}>
<div style={{
width: 20,
height: 20,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}>
<img
src={updateIcon}
alt=""
style={{
width: 20,
height: 20,
transform: 'translateX(-1000px)',
filter: `drop-shadow(1000px 0 0 #3ba55c)`,
}}
/>
</div>
</div>
</button>
);
}

View File

@@ -2816,6 +2816,63 @@ body {
width: 200px; width: 200px;
} }
/* ============================================
FORCED UPDATE MODAL (Flatpak)
============================================ */
.forced-update-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 10002;
}
.forced-update-modal {
background-color: var(--bg-secondary);
border-radius: 8px;
padding: 32px;
width: 400px;
max-width: 90vw;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.forced-update-modal h2 {
color: var(--header-primary);
font-size: 20px;
font-weight: 700;
margin: 0 0 8px;
}
.forced-update-modal p {
color: var(--header-secondary);
font-size: 14px;
margin: 0 0 24px;
line-height: 1.4;
}
.forced-update-btn {
background-color: var(--brand-experiment);
color: white;
border: none;
border-radius: 3px;
padding: 10px 24px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
transition: background-color 0.15s;
}
.forced-update-btn:hover {
background-color: var(--brand-experiment-hover);
}
.drag-overlay-category { .drag-overlay-category {
padding: 8px 12px; padding: 8px 12px;
background-color: var(--bg-secondary); background-color: var(--bg-secondary);

View File

@@ -8,6 +8,7 @@ import './index.css';
import { ThemeProvider } from './contexts/ThemeContext'; import { ThemeProvider } from './contexts/ThemeContext';
import { VoiceProvider } from './contexts/VoiceContext'; import { VoiceProvider } from './contexts/VoiceContext';
import { UpdateProvider } from './components/UpdateBanner';
import TitleBar from './components/TitleBar'; import TitleBar from './components/TitleBar';
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
@@ -15,14 +16,16 @@ const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider> <ThemeProvider>
<ConvexProvider client={convex}> <UpdateProvider>
<VoiceProvider> <ConvexProvider client={convex}>
<TitleBar /> <VoiceProvider>
<HashRouter> <TitleBar />
<App /> <HashRouter>
</HashRouter> <App />
</VoiceProvider> </HashRouter>
</ConvexProvider> </VoiceProvider>
</ConvexProvider>
</UpdateProvider>
</ThemeProvider> </ThemeProvider>
</React.StrictMode>, </React.StrictMode>,
); );