fix(push): reinstalled app replaces its old subscription; devices listed and removable
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 2m8s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 2m8s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f76b0f34ff
commit
5b636b66e2
9 files changed
+123
-14
No files matched your search
@@ -11,6 +11,7 @@ import {
|
||||
MessageSquare,
|
||||
Globe,
|
||||
Send,
|
||||
X,
|
||||
AlertCircle,
|
||||
Palette,
|
||||
Upload,
|
||||
@@ -50,7 +51,7 @@ import { SessionsPanel } from './SessionsPanel';
|
||||
import { ToggleSwitch } from '../../components/ToggleSwitch';
|
||||
import { setLinkPreviewsEnabled, useLinkPreviewsEnabled } from '../../lib/linkPreview';
|
||||
import { isJournalEnabled, setJournalEnabled } from '../../lib/journal';
|
||||
import { currentPushSubscription, disablePush, enablePush, pushSupported } from '../../lib/push';
|
||||
import { currentPushSubscription, disablePush, enablePush, pushSupported, removePushDevice } from '../../lib/push';
|
||||
import { FormSheet } from '../../components/FormSheet';
|
||||
|
||||
interface UserSettingsViewProps {
|
||||
@@ -301,13 +302,28 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
// Web push: devices of this account and whether this browser is one of them
|
||||
const [pushDevices, setPushDevices] = useState<PushDevice[]>([]);
|
||||
const [pushHere, setPushHere] = useState(false);
|
||||
const [pushEndpointHere, setPushEndpointHere] = useState<string | null>(null);
|
||||
const [pushBusy, setPushBusy] = useState(false);
|
||||
const [pushMsg, setPushMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
|
||||
const reloadPush = useCallback(async () => {
|
||||
const [devices, sub] = await Promise.all([api.getPushDevices().catch(() => []), currentPushSubscription()]);
|
||||
setPushDevices(devices);
|
||||
setPushHere(Boolean(sub && devices.some((d) => d.endpoint === sub.endpoint)));
|
||||
setPushEndpointHere(sub?.endpoint ?? null);
|
||||
}, []);
|
||||
// An old installation or a device you no longer use: remove it from the list
|
||||
const removeDevice = async (endpoint: string) => {
|
||||
setPushBusy(true);
|
||||
setPushMsg(null);
|
||||
try {
|
||||
await removePushDevice(endpoint);
|
||||
await reloadPush();
|
||||
} catch (err) {
|
||||
setPushMsg({ type: 'err', text: err instanceof Error ? err.message : 'Gerät konnte nicht entfernt werden' });
|
||||
} finally {
|
||||
setPushBusy(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
void reloadPush();
|
||||
}, [reloadPush]);
|
||||
@@ -1783,10 +1799,36 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
{pushDevices.length > 0 && (
|
||||
<p className="pl-6 text-[11px] text-faint">
|
||||
Aktiv auf {pushDevices.length} {pushDevices.length === 1 ? 'Gerät' : 'Geräten'}:{' '}
|
||||
{pushDevices.map((d) => d.device || 'Gerät').join(', ')}
|
||||
</p>
|
||||
<div className="pl-6 space-y-1">
|
||||
<p className="text-[11px] text-faint">
|
||||
Aktiv auf {pushDevices.length} {pushDevices.length === 1 ? 'Gerät' : 'Geräten'}. Alte Installationen
|
||||
verschwinden beim nächsten Push von selbst oder lassen sich hier entfernen.
|
||||
</p>
|
||||
<ul className="border border-border divide-y divide-border bg-panel">
|
||||
{pushDevices.map((d) => (
|
||||
<li key={d.id} className="flex items-center justify-between gap-2 pl-2.5 pr-1 py-1">
|
||||
<span className="min-w-0 text-[11px] text-main truncate">
|
||||
{d.device || 'Gerät'}
|
||||
<span className="text-faint">
|
||||
{' · seit '}
|
||||
{new Date(d.createdAt).toLocaleDateString('de-DE')}
|
||||
{d.endpoint === pushEndpointHere && ' · dieses Gerät'}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void removeDevice(d.endpoint)}
|
||||
disabled={pushBusy || demoMode}
|
||||
title="Gerät entfernen"
|
||||
aria-label={`${d.device || 'Gerät'} entfernen`}
|
||||
className="h-9 w-9 sm:h-7 sm:w-7 shrink-0 flex items-center justify-center text-faint hover:text-status-err disabled:opacity-40"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{pushMsg && (
|
||||
<p className={`pl-6 text-[11px] ${pushMsg.type === 'ok' ? 'text-status-ok' : 'text-status-err'}`}>{pushMsg.text}</p>
|
||||
|
||||
@@ -734,8 +734,8 @@ export const api = {
|
||||
return res.devices || [];
|
||||
},
|
||||
|
||||
async subscribePush(sub: PushSubscriptionJSON, device: string): Promise<void> {
|
||||
await request('/api/v1/push/subscribe', { method: 'POST', body: JSON.stringify({ ...sub, device }) });
|
||||
async subscribePush(sub: PushSubscriptionJSON, device: string, replaces?: string): Promise<void> {
|
||||
await request('/api/v1/push/subscribe', { method: 'POST', body: JSON.stringify({ ...sub, device, replaces: replaces || '' }) });
|
||||
},
|
||||
|
||||
async unsubscribePush(endpoint: string): Promise<void> {
|
||||
|
||||
@@ -64,7 +64,40 @@ export async function enablePush(): Promise<void> {
|
||||
throw new Error('Der Browser hat die Anmeldung beim Push-Dienst abgelehnt (Berechtigung blockiert oder Push-Dienst nicht erreichbar)');
|
||||
}
|
||||
}
|
||||
await api.subscribePush(sub.toJSON(), deviceName());
|
||||
// The address this browser registered before is dead once it has a new one (e.g. after reinstalling
|
||||
// the app): the server drops it right away instead of counting it as another device
|
||||
const previous = readStoredEndpoint();
|
||||
await api.subscribePush(sub.toJSON(), deviceName(), previous && previous !== sub.endpoint ? previous : undefined);
|
||||
storeEndpoint(sub.endpoint);
|
||||
}
|
||||
|
||||
const ENDPOINT_KEY = 'mindsift_push_endpoint';
|
||||
|
||||
function readStoredEndpoint(): string | null {
|
||||
try {
|
||||
return localStorage.getItem(ENDPOINT_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function storeEndpoint(endpoint: string | null): void {
|
||||
try {
|
||||
if (endpoint) localStorage.setItem(ENDPOINT_KEY, endpoint);
|
||||
else localStorage.removeItem(ENDPOINT_KEY);
|
||||
} catch {
|
||||
// storage blocked: the dead device is removed on the next push instead
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes one of the own devices (e.g. an old installation) on the server. */
|
||||
export async function removePushDevice(endpoint: string): Promise<void> {
|
||||
const here = await currentPushSubscription();
|
||||
if (here?.endpoint === endpoint) {
|
||||
await disablePush();
|
||||
return;
|
||||
}
|
||||
await api.unsubscribePush(endpoint);
|
||||
}
|
||||
|
||||
export async function disablePush(): Promise<void> {
|
||||
@@ -73,4 +106,5 @@ export async function disablePush(): Promise<void> {
|
||||
const endpoint = sub.endpoint;
|
||||
await sub.unsubscribe().catch(() => false);
|
||||
await api.unsubscribePush(endpoint).catch(() => {});
|
||||
storeEndpoint(null);
|
||||
}
|
||||
Reference in new issue
Block a user