* mobile: add invitations, network creation, and settings (parity phase 1) Surface backend capabilities that already existed in the mobile API client but had no UI: - NetworkListScreen now lists pending invitations with an Accept action and a header "+" to create a network; empty state offers creation instead of pointing users to desktop. - New CreateNetworkSheet and use-invitations hooks (accept invite, create network) following the existing react-query patterns. - SettingsScreen replaces its placeholder with an email-notifications toggle (optimistic, mirrors desktop), app version, and sign out. - Wire the previously-unreachable Settings row into the Drawer. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * mobile: network member management and avatars (parity phase 2) - New NetworkSettingsScreen (reachable from the stream-list header) lists members with admin remove, an invite-by-email sheet, and pending invitations with revoke — backed by new use-member-management hooks. - Avatars: add avatar_object_id to HumanSchema, uploadAvatar/deleteAvatar/ getAvatarDownloadUrl client methods (raw PUT via expo-file-system), a use-avatar-url hook, and image rendering in the shared Avatar component. AccountScreen gains a tap-to-change profile picture via expo-image-picker. - auth-store gains refreshUser to pick up avatar changes. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * mobile: task particles — view, edit, and compose (parity phase 3) Bring the task particle to parity with desktop's richer model: - Replace the thin `quest` schema with desktop's `task` model (ChecklistItem, TaskProperties: title/notes/checklist/assigned_to/done) in the discriminated union, the Firestore converter, and consumers (StreamCard, FallbackParticleView). - New TaskParticleView renders an editable card (round done checkbox, title, notes, checklist with add/toggle/edit/remove, assignee chips) persisting each edit to Firestore; an 8s dwell auto-advances and field focus suspends playback. Wired into StreamView's render switch. - Compose: a task button in the ComposeDock opens a TaskComposeSheet (createTaskParticle helper). Gated off in the new-stream flow, where a stream's first particle must be text or media. Note: particles are written client-side to Firestore, matching desktop; Orion's REST validator still only accepts `quest`, which is a pre-existing inconsistency to reconcile backend-side separately. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * mobile: paper and file particle views (parity phase 4) - Extract the shared markdown renderer/theme out of TextParticleView into a reusable MarkdownBody component (DRY). - PaperParticleView renders desktop-authored documents (title + markdown) with a length-based dwell. - FileParticleView shows name/size and a Download action that opens a signed URL via the OS. - Both wired into StreamView's render switch; FallbackParticleView is now a true catch-all for unknown/folder types only. Deferred (documented for a follow-up phase): composing papers/files from mobile, particle attachments + lightbox, and link previews in text. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * mobile: billing & usage in network settings (parity phase 5) Surface the network plan, daily usage, and Stripe management — all backed by client methods that already existed. New use-billing hooks and a BillingSection (mirroring desktop): every member sees the plan + usage summary; admins get cadence selection + "Upgrade to Pro" (checkout) and "Manage subscription" (portal), opening Stripe in the system browser. Added to NetworkSettingsScreen. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * mobile: let users dismiss the keyboard from a task card Focusing a task field opened the keyboard with no way out — it covered the card and the stream's tap-to-advance zones. Now: - A "Done" pill appears at the card's top-right while editing (reusing the existing `editing` flag) and calls Keyboard.dismiss(); the title row reserves space so the pill never overlaps a long title. - The card ScrollView gains keyboardDismissMode (interactive on iOS, on-drag on Android) so dragging the card also dismisses the keyboard. Dismissing blurs the active field, which flips `editing` off and resumes the dwell timer and tap navigation automatically. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * decrease clutter in stream-view * format * format * consolidate avatar --------- Co-authored-by: Claude <[email protected]>
166 lines
4.8 KiB
TypeScript
166 lines
4.8 KiB
TypeScript
import { Fragment, type ReactNode } from 'react';
|
|
import { Platform, type ViewStyle } from 'react-native';
|
|
import { Renderer, useMarkdown, type MarkedStyles } from 'react-native-marked';
|
|
|
|
// Shared markdown rendering for text and paper particles. Mirrors the desktop
|
|
// Crepe palette (markdown-editor.css `--crepe-*`) so a message reads the same
|
|
// on both surfaces: white-on-transparent text, a blue accent, pink inline
|
|
// code, and a near-opaque dark surface behind code blocks and tables.
|
|
//
|
|
// Known gap vs desktop: fenced code blocks aren't syntax-highlighted (Crepe
|
|
// uses CodeMirror; react-native-marked only exposes the language tag). They
|
|
// render as plain monospace on the dark surface, which is acceptable for v1.
|
|
const TEXT_COLOR = 'rgba(255,255,255,0.92)';
|
|
const ACCENT = '#60a5fa';
|
|
const SURFACE = 'rgba(24,24,28,0.96)';
|
|
const OUTLINE = 'rgba(255,255,255,0.2)';
|
|
const MONO = Platform.OS === 'ios' ? 'Menlo' : 'monospace';
|
|
|
|
// react-native-marked doesn't render GFM task-list checkboxes (marked strips
|
|
// the `[ ]`/`[x]` into token flags the parser ignores), so a write/read drift
|
|
// shows up as bullets with no box. Swap the marker for a checkbox glyph before
|
|
// parsing — read-only, matching desktop's bullet-free checkboxes.
|
|
const TASK_ITEM_RE = /^(\s*)[-*+] \[([ xX])\] /gm;
|
|
|
|
function withTaskCheckboxes(markdown: string): string {
|
|
return markdown.replace(
|
|
TASK_ITEM_RE,
|
|
(_match, indent: string, mark: string) =>
|
|
`${indent}${mark === ' ' ? '☐' : '☑'} `,
|
|
);
|
|
}
|
|
|
|
const MARKDOWN_THEME = {
|
|
colors: {
|
|
text: TEXT_COLOR,
|
|
link: ACCENT,
|
|
code: SURFACE,
|
|
border: OUTLINE,
|
|
},
|
|
};
|
|
|
|
const MARKDOWN_STYLES: MarkedStyles = {
|
|
text: { color: TEXT_COLOR, fontSize: 18, lineHeight: 28 },
|
|
li: { color: TEXT_COLOR, fontSize: 18, lineHeight: 28 },
|
|
strong: { fontWeight: '700' },
|
|
em: { fontStyle: 'italic' },
|
|
strikethrough: {
|
|
textDecorationLine: 'line-through',
|
|
color: 'rgba(255,255,255,0.6)',
|
|
},
|
|
// fontStyle "normal" cancels react-native-marked's italic-by-default for
|
|
// links and inline code (desktop renders neither italic).
|
|
link: { color: ACCENT, fontStyle: 'normal' },
|
|
// borderBottomWidth 0 removes the library's default heading underline rule,
|
|
// which desktop's headings don't have.
|
|
h1: {
|
|
color: '#ffffff',
|
|
fontSize: 28,
|
|
lineHeight: 34,
|
|
fontWeight: '700',
|
|
marginTop: 8,
|
|
marginBottom: 8,
|
|
borderBottomWidth: 0,
|
|
},
|
|
h2: {
|
|
color: '#ffffff',
|
|
fontSize: 24,
|
|
lineHeight: 30,
|
|
fontWeight: '700',
|
|
marginTop: 8,
|
|
marginBottom: 6,
|
|
borderBottomWidth: 0,
|
|
},
|
|
h3: {
|
|
color: '#ffffff',
|
|
fontSize: 20,
|
|
lineHeight: 26,
|
|
fontWeight: '600',
|
|
marginTop: 6,
|
|
marginBottom: 4,
|
|
},
|
|
h4: {
|
|
color: '#ffffff',
|
|
fontSize: 18,
|
|
lineHeight: 24,
|
|
fontWeight: '600',
|
|
marginTop: 6,
|
|
marginBottom: 4,
|
|
},
|
|
h5: {
|
|
color: '#ffffff',
|
|
fontSize: 16,
|
|
lineHeight: 22,
|
|
fontWeight: '600',
|
|
marginTop: 4,
|
|
marginBottom: 2,
|
|
},
|
|
h6: {
|
|
color: 'rgba(255,255,255,0.7)',
|
|
fontSize: 15,
|
|
lineHeight: 20,
|
|
fontWeight: '600',
|
|
marginTop: 4,
|
|
marginBottom: 2,
|
|
},
|
|
codespan: {
|
|
color: '#fca5a5',
|
|
fontFamily: MONO,
|
|
fontStyle: 'normal',
|
|
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
},
|
|
code: {
|
|
backgroundColor: SURFACE,
|
|
borderColor: OUTLINE,
|
|
borderWidth: 1,
|
|
borderRadius: 8,
|
|
padding: 12,
|
|
marginVertical: 6,
|
|
},
|
|
blockquote: {
|
|
borderLeftWidth: 3,
|
|
borderLeftColor: OUTLINE,
|
|
paddingLeft: 12,
|
|
marginVertical: 6,
|
|
opacity: 0.85,
|
|
},
|
|
// hr is left to the library default, which already draws a 1px rule in the
|
|
// themed border color (OUTLINE).
|
|
table: { borderWidth: 1, borderColor: OUTLINE, marginVertical: 6 },
|
|
tableRow: { borderColor: OUTLINE },
|
|
tableCell: { borderColor: OUTLINE, padding: 8 },
|
|
};
|
|
|
|
// react-native-marked feeds fenced code blocks the `em` (italic, proportional)
|
|
// text style, so out of the box code renders italic in the body font. Override
|
|
// `code` to apply a monospace, non-italic style instead — matching desktop's
|
|
// code blocks.
|
|
const CODE_TEXT_STYLE = {
|
|
color: TEXT_COLOR,
|
|
fontFamily: MONO,
|
|
fontSize: 15,
|
|
lineHeight: 22,
|
|
};
|
|
|
|
class MarkdownRenderer extends Renderer {
|
|
code(text: string, language?: string, containerStyle?: ViewStyle): ReactNode {
|
|
return super.code(text, language, containerStyle, CODE_TEXT_STYLE);
|
|
}
|
|
}
|
|
|
|
const MARKDOWN_RENDERER = new MarkdownRenderer();
|
|
|
|
/**
|
|
* Renders GFM markdown using the shared Flowy palette. `useMarkdown` returns an
|
|
* array of block nodes; we splat them into a Fragment so they nest cleanly
|
|
* inside a parent ScrollView (vs. the library's own FlatList-based component).
|
|
*/
|
|
export function MarkdownBody({ content }: { content: string }) {
|
|
const nodes = useMarkdown(withTaskCheckboxes(content), {
|
|
renderer: MARKDOWN_RENDERER,
|
|
theme: MARKDOWN_THEME,
|
|
styles: MARKDOWN_STYLES,
|
|
});
|
|
return <Fragment>{nodes}</Fragment>;
|
|
}
|