Merge branch 'feat/footer-share-ci' into dev
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s

CI keeps the 'test once per code state' plan but records commit and
git tree as image labels instead of extra :sha-/:tree- tags, so the
registry only holds dev, latest, vX.Y.Z and stable; the plan test now
asserts the expected decisions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-30 21:29:56 +01:00
commit 141f87f3a0
22 files changed
+735 -61

No files matched your search

@@ -0,0 +1,87 @@
import { describe, it, expect } from 'vitest';
import { formatSharedThought } from '../shareTarget';
describe('formatSharedThought', () => {
it('formats pure text correctly', () => {
expect(formatSharedThought({ text: 'Spontaner Einfall' })).toBe('Spontaner Einfall');
});
it('formats pure url correctly', () => {
expect(formatSharedThought({ url: 'https://example.com/article' })).toBe('https://example.com/article');
});
it('combines title and url when text is empty', () => {
expect(formatSharedThought({ title: 'Toller Artikel', url: 'https://example.com/article' }))
.toBe('Toller Artikel\nhttps://example.com/article');
});
it('does not duplicate URL when text already contains it', () => {
const input = {
title: 'Artikel',
text: 'Lies das hier: https://example.com/article unbedingt!',
url: 'https://example.com/article',
};
expect(formatSharedThought(input))
.toBe('Artikel\nLies das hier: https://example.com/article unbedingt!');
});
it('does not duplicate URL when text is exactly the URL', () => {
const input = {
title: 'News',
text: 'https://news.example.com',
url: 'https://news.example.com',
};
expect(formatSharedThought(input))
.toBe('News\nhttps://news.example.com');
});
it('does not duplicate title if text starts with title', () => {
const input = {
title: 'MindSift Update',
text: 'MindSift Update ist jetzt verfügbar.',
};
expect(formatSharedThought(input))
.toBe('MindSift Update ist jetzt verfügbar.');
});
it('combines title, text and distinct URL', () => {
const input = {
title: 'Empfehlung',
text: 'Ein großartiges Tool für schnelles Erfassen.',
url: 'https://mindsift.app',
};
expect(formatSharedThought(input))
.toBe('Empfehlung\nEin großartiges Tool für schnelles Erfassen.\nhttps://mindsift.app');
});
it('handles image with text', () => {
const input = {
title: 'Foto vom Ausflug',
fileName: 'berge.jpg',
hasMedia: true,
};
expect(formatSharedThought(input))
.toBe('Foto vom Ausflug\n(Bild geteilt – Bilder im Posteingang werden noch nicht unterstützt)');
});
it('handles image only with filename', () => {
const input = {
fileName: 'screenshot.png',
hasMedia: true,
};
expect(formatSharedThought(input))
.toBe('[screenshot.png] (Bild geteilt – Bilder im Posteingang werden noch nicht unterstützt)');
});
it('handles image only without filename', () => {
const input = {
hasMedia: true,
};
expect(formatSharedThought(input))
.toBe('(Bild geteilt – Bilder im Posteingang werden noch nicht unterstützt)');
});
it('returns empty string if nothing is provided', () => {
expect(formatSharedThought({})).toBe('');
});
});
+117
View File
@@ -0,0 +1,117 @@
export interface SharedData {
id?: number;
title?: string;
text?: string;
url?: string;
fileName?: string;
hasMedia?: boolean;
createdAt?: number;
}
const SHARE_DB_NAME = 'mindsift-share-target';
const SHARE_STORE_NAME = 'shared_items';
/**
* Combines title, text, url and media note into a single thought string.
* Ensures URLs are not duplicated if already present in the shared text or title.
*/
export function formatSharedThought(share: SharedData): string {
const title = (share.title || '').trim();
const text = (share.text || '').trim();
const url = (share.url || '').trim();
const mediaNote = '(Bild geteilt – Bilder im Posteingang werden noch nicht unterstützt)';
let content = '';
// Check if url is already contained in text or title
const urlInText = url !== '' && text.includes(url);
const urlInTitle = url !== '' && title.includes(url);
// Combine title and text
if (title && text) {
if (text === title) {
content = text;
} else if (text.startsWith(title)) {
content = text;
} else {
content = `${title}\n${text}`;
}
} else if (title) {
content = title;
} else if (text) {
content = text;
}
// Append URL if not already included in text or title
if (url && !urlInText && !urlInTitle) {
if (content) {
content = `${content}\n${url}`;
} else {
content = url;
}
}
// Media note handling
if (share.hasMedia || share.fileName) {
if (content) {
content = `${content}\n${mediaNote}`;
} else if (share.fileName) {
content = `[${share.fileName}] ${mediaNote}`;
} else {
content = mediaNote;
}
}
return content.trim();
}
/**
* Retrieves and removes all pending shared items stored in IndexedDB by the Service Worker.
*/
export async function consumePendingShares(): Promise<SharedData[]> {
if (typeof indexedDB === 'undefined') return [];
return new Promise((resolve) => {
try {
const req = indexedDB.open(SHARE_DB_NAME, 1);
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains(SHARE_STORE_NAME)) {
db.createObjectStore(SHARE_STORE_NAME, { autoIncrement: true, keyPath: 'id' });
}
};
req.onsuccess = () => {
const db = req.result;
if (!db.objectStoreNames.contains(SHARE_STORE_NAME)) {
db.close();
return resolve([]);
}
const tx = db.transaction(SHARE_STORE_NAME, 'readwrite');
const store = tx.objectStore(SHARE_STORE_NAME);
const getAllReq = store.getAll();
getAllReq.onsuccess = () => {
const items: SharedData[] = getAllReq.result || [];
store.clear();
tx.oncomplete = () => {
db.close();
resolve(items);
};
tx.onerror = () => {
db.close();
resolve(items);
};
};
getAllReq.onerror = () => {
db.close();
resolve([]);
};
};
req.onerror = () => resolve([]);
} catch {
resolve([]);
}
});
}
+2
View File
@@ -20,6 +20,8 @@ export interface AuthStatus {
hasImprintContent?: boolean;
privacyUrl?: string;
hasPrivacyContent?: boolean;
customLinkLabel?: string;
customLinkUrl?: string;
};
// Voice input: own Whisper server, the browser's recognition, or switched off (STT_* environment)
voice?: { mode: VoiceMode; maxSeconds: number };