From f655a5115a284aa66573c4f3225453f6a4d0efd9 Mon Sep 17 00:00:00 2001 From: timmy Date: Fri, 7 Aug 2026 20:22:21 +0000 Subject: [PATCH] feat: capture shared mobile content (#223) --- frontend/create-issue-sheet.js | 55 ++++++++++++++- frontend/icons/stackchain-192.png | Bin 0 -> 966 bytes frontend/icons/stackchain-512.png | Bin 0 -> 2737 bytes frontend/index.html | 62 ++++++++++++++++ frontend/manifest.webmanifest | 21 ++++++ frontend/service-worker.js | 25 +++++++ src/views.py | 18 ++++- tests/test_my_work.py | 113 ++++++++++++++++++++++++++++++ tests/test_work_pages.py | 22 +++++- 9 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 frontend/icons/stackchain-192.png create mode 100644 frontend/icons/stackchain-512.png create mode 100644 frontend/manifest.webmanifest create mode 100644 frontend/service-worker.js diff --git a/frontend/create-issue-sheet.js b/frontend/create-issue-sheet.js index 6bc2662..7fa1bf1 100644 --- a/frontend/create-issue-sheet.js +++ b/frontend/create-issue-sheet.js @@ -8,8 +8,20 @@ function newIssueOperationId() { return String(Date.now()) + '-' + Math.random().toString(16).slice(2); } +function normalizeSharedContent(value = {}) { + const clean = input => String(input || '').replace(/\s+/g, ' ').trim(); + const text = String(value.text || '').trim().slice(0, 9500); + const textSummary = clean(text); + const sentence = (textSummary.match(/^.{1,80}?[.!?](?:\s|$)/)?.[0] || textSummary.slice(0, 80)).trim(); + const title = (clean(value.title) || sentence).slice(0, 240); + const url = clean(value.url).slice(0, 2000); + const body = text && url && text.includes(url) ? text : [text, url].filter(Boolean).join('\n\n'); + return { title, body }; +} + function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOperationId }) { const storageKey = 'stackchain.issue-capture.v1'; + const sharedStorageKey = 'stackchain.issue-share.v1'; let pending = null; const safeLabelIds = value => Array.from(new Set( (Array.isArray(value) ? value : []).filter(id => Number.isInteger(id) && id > 0) @@ -73,6 +85,42 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp catch (_error) { /* Confirmed creation remains authoritative. */ } } + function pendingSharedContent() { + try { + const parsed = JSON.parse(storage.getItem(sharedStorageKey) || 'null'); + if (!parsed || typeof parsed !== 'object') return null; + const shared = normalizeSharedContent({title: parsed.title, text: parsed.body}); + return shared.title || shared.body ? shared : null; + } catch (_error) { + return null; + } + } + + function acceptSharedContent() { + const shared = pendingSharedContent(); + if (!shared) return loadDraft(); + const accepted = saveDraft({...loadDraft(), ...shared}); + try { storage.removeItem(sharedStorageKey); } + catch (_error) { /* Accepted content is already persisted as the issue draft. */ } + return accepted; + } + + function discardSharedContent() { + try { storage.removeItem(sharedStorageKey); } + catch (_error) { /* The existing issue draft remains authoritative. */ } + } + + function stageSharedContent(value) { + const shared = normalizeSharedContent(value); + if (!shared.title && !shared.body) return {status: 'empty'}; + try { storage.setItem(sharedStorageKey, JSON.stringify(shared)); } + catch (_error) { /* The caller can still use the in-page share payload. */ } + const existing = loadDraft(); + if (existing.title || existing.body) return {status: 'conflict'}; + acceptSharedContent(); + return {status: 'ready'}; + } + function loadLabels(repository) { const encoded = String(repository || '').split('/').map(encodeURIComponent).join('/'); const priorities = new Set(['p0', 'priority-high', 'critical']); @@ -117,7 +165,12 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp return pending; } - return { saveDraft, loadDraft, loadLabels, loadMilestones, submit }; + return { + saveDraft, loadDraft, loadLabels, loadMilestones, submit, + stageSharedContent, pendingSharedContent, acceptSharedContent, discardSharedContent, + }; } +createIssueCapture.normalizeSharedContent = normalizeSharedContent; + if (typeof module !== 'undefined' && module.exports) module.exports = createIssueCapture; diff --git a/frontend/icons/stackchain-192.png b/frontend/icons/stackchain-192.png new file mode 100644 index 0000000000000000000000000000000000000000..3ee495ee4d0e6a5aa5cc7c853fac600e112712ff GIT binary patch literal 966 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 + + Stackchain Dashboard