11 KiB
OWL Research Report #001 - Initial Codebase Analysis
Date: 2026-04-15
This report covers the initial research into all goals outlined in the README, except comms integration (deferred).
1. Extension Strategy - Surviving Upstream Merges
Codebase Overview
Cinny is a Vite 5 + React 18 SPA using TypeScript, Jotai for state, @vanilla-extract/css for styling, and the folds design system. Key layout:
src/
app/
components/ # Reusable React components (48 feature components)
features/ # Feature modules (17 major features)
hooks/ # Custom React hooks (~40 files)
pages/ # Page-level components (routing/auth)
plugins/ # Extension plugins (emoji, markdown, calls, etc.)
state/ # State management (Jotai atoms)
styles/ # CSS (Vanilla Extract)
utils/ # Utilities (matrix, room, sanitization, etc.)
client/ # Matrix client initialization
colors.css.ts # Theme color definitions
index.tsx # Application entry point
No Plugin System
Cinny has no formal plugin/extension architecture. All imports are static. The plugins/ directory is just a naming convention for utility modules, not a registration system.
Recommended Isolation Strategy
All custom owl code lives in a single src/owl/ directory tree. React and Vite don't care where files live — imports are just paths. This keeps everything in one place that upstream will never touch, making it easy to see the full scope of our fork and trivial to manage across merges.
src/owl/
components/ # Our custom UI components (YouTubeEmbedCard, etc.)
features/ # Our custom feature modules
hooks/ # Our custom hooks
plugins/ # Our custom plugins
state/ # Our custom Jotai atoms & settings
styles/ # Our custom CSS (Vanilla Extract)
utils/ # Our custom utilities (sanitizer overrides, etc.)
Upstream files import our code via relative paths like import { YouTubeEmbed } from '../../../owl/components/YouTubeEmbed'. These one-liner injection points are the only upstream modifications needed and are easy to re-apply after a merge.
Injection Points (unavoidable upstream edits)
These are the upstream files we'll need to add small imports/calls into. Each edit should be minimal — ideally a single import + one-liner call to owl code, keeping the actual logic in src/owl/.
| File | Why | Change Size |
|---|---|---|
src/app/state/settings.ts |
Change defaults (twitterEmoji, theme) and add new settings (youtubeEmbed) | ~5 lines |
src/app/utils/sanitize.ts |
Allow external img src URLs | ~10 lines |
src/app/plugins/react-custom-html-parser.tsx |
Render external images directly | ~10 lines |
src/app/components/RenderMessageContent.tsx |
Import & call owl YouTubeEmbedCard | ~5 lines |
src/colors.css.ts |
Add custom themes (if needed) | ~lines for theme |
src/app/hooks/useTheme.ts |
Register custom themes (if needed) | ~5 lines |
src/app/pages/Router.tsx |
Only if adding new routes | ~5 lines |
Merge Strategy
- All logic lives in
src/owl/— upstream never touches it - Upstream edits are kept to minimal injection points (imports + short calls)
- On upstream merge, only the injection points can conflict — re-apply them by hand if needed
- Consider a
src/owl/INJECTIONS.mdfile listing every upstream file we touch and why, so re-applying after merge is mechanical
2. Theming - What CSS Can and Cannot Change
What Themes Control
Themes in Cinny are color-only. The createTheme() system from vanilla-extract exposes these token categories:
- Background: Container, ContainerHover, ContainerActive, ContainerLine, OnContainer
- Surface: Same pattern
- SurfaceVariant: Same pattern
- Primary: Main, MainHover, MainActive, MainLine, OnMain, Container variants, OnContainer
- Secondary: Same as Primary
- Success / Warning / Critical: Same as Primary
- Other: FocusRing, Shadow, Overlay
What Themes CANNOT Change
- Layout/positioning - Component layout is hardcoded in vanilla-extract style objects, not CSS variables
- Icon choices - Icons are imported React components, not themeable
- Behavior - Popup positioning, menu structure, click handlers are all JS
- Spacing/sizing - Uses
foldsdesign tokens (config.space,config.radii, etc.) which are not part of the theme contract
Verdict
Themes alone cannot fix the reaction popup UX or change icons/menus. They can only change colors. Layout, positioning, and behavioral changes require component modifications.
3. Reaction Popup UX
Current Mechanism
The reaction picker is controlled by src/app/features/room/message/Message.tsx:
- A toolbar (
MessageOptionsBase) appears on message hover, positionedtop: -30px; right: 0(absolute) viasrc/app/features/room/message/styles.css.ts - Clicking the emoji button captures
getBoundingClientRect()and opens aPopOutcomponent withposition="Bottom"andalign="End" - The emoji board renders inside this PopOut popup
- There's also a context menu with
MessageQuickReactionsshowing 4 recent emojis
What Would Need to Change
To move reactions to a more sensible position:
- styles.css.ts - Change
MessageOptionsBasepositioning (top/right/bottom) - Message.tsx - Change
PopOutprops (position,align) or replace PopOut with inline rendering - Optionally: render the emoji board inline below the message instead of as a floating popup
This is a component-level change, not achievable through themes.
4. Twitter Emoji (Twemoji) as Default
Current State
- Setting exists:
twitterEmojiinsrc/app/state/settings.ts(line 26) - Default is
false(line 60) - Font files exist:
public/font/Twemoji.Mozilla.v15.1.0.ttfand.woff2 - Applied via CSS custom property
--font-emojiinsrc/app/pages/client/ClientNonUIFeatures.tsx(lines 30-40)
How to Make It Default
One-line change in src/app/state/settings.ts:
// Line 60: change from
twitterEmoji: false,
// to
twitterEmoji: true,
New users get defaultSettings merged with their (empty) localStorage. Existing users who never touched the toggle will also get the new default via the merge logic in getSettings() (lines 86-93).
Merge risk: LOW - Single line change in a defaults object.
5. Default Theme for New Users
Current Defaults
In src/app/state/settings.ts (lines 52-56):
themeId: undefined, // no manual override
useSystemTheme: true, // follow OS preference
lightThemeId: undefined, // fallback: LightTheme
darkThemeId: undefined, // fallback: DarkTheme
Theme resolution in src/app/hooks/useTheme.ts:
- If
useSystemThemeis true (default): detects OS preference, picks light/dark fallback - If disabled: uses
themeId, falls back to LightTheme
Available Themes
| ID | Name |
|---|---|
light-theme |
Light (default light) |
silver-theme |
Silver |
dark-theme |
Dark (default dark) |
butter-theme |
Butter |
How to Set a Default Theme
Option A - Force a specific theme:
useSystemTheme: false,
themeId: 'butter-theme', // or any theme ID
Option B - Change the dark/light fallbacks (keeps system detection):
darkThemeId: 'butter-theme', // dark mode users get Butter
lightThemeId: 'silver-theme', // light mode users get Silver
Merge risk: LOW - Changes to defaults object only.
6. External IMG Tags in Messages
Current Behavior
External image URLs are blocked at two levels:
- Sanitization (
src/app/utils/sanitize.ts, lines 108-127):transformImgTagconverts any<img>with non-mxc://src into an<a>link - React parser (
src/app/plugins/react-custom-html-parser.tsx, lines 476-494): Non-mxc images are rendered as links, not<img>elements
This is a deliberate privacy measure for federated Matrix — loading external images reveals user IPs to image hosts.
What Needs to Change
Since owl.cx is a single-server instance where this threat model doesn't apply:
- sanitize.ts: Modify
transformImgTagto allowhttps://andhttp://src URLs through as<img>tags instead of converting to<a> - react-custom-html-parser.tsx: Modify the
imghandler to render external URLs directly with<img src={originalUrl}>instead of converting to links
Suggested Approach
Add an owl setting (e.g., allowExternalImages: true) and conditionally bypass the mxc-only restriction. This keeps the change isolated and opt-in.
Merge risk: MEDIUM - Touching security-sensitive sanitization code. Keep changes minimal and well-isolated.
7. YouTube Link Embeds
Current State
- No YouTube handling exists in the codebase
iframeis NOT in the allowed tags in sanitize.ts- URL previews exist (
UrlPreviewCard.tsx) but only render Open Graph metadata cards (title, image, description) — no embeds - Settings exist:
urlPreviewandencUrlPreviewbooleans
What Needs to Be Built
Recommended approach: Create a YouTubeEmbedCard component that slots into the existing URL preview system.
-
Create
src/owl/components/YouTubeEmbedCard.tsx:- Detect YouTube URLs via regex (
youtube.com/watch?v=,youtu.be/) - Extract video ID
- Render
<iframe src="https://www.youtube.com/embed/{ID}" ...> - Include sandbox attributes for security
- Detect YouTube URLs via regex (
-
Modify
src/app/components/RenderMessageContent.tsx:- In
renderUrlsPreview, check if URL is YouTube before renderingUrlPreviewCard - If YouTube and embed enabled, render
YouTubeEmbedCardinstead
- In
-
Add setting in
src/app/state/settings.ts:youtubeEmbed: boolean(default: true for owl)
-
No need to modify sanitize.ts for this — the iframe is rendered by our component, not parsed from message HTML.
Merge risk: LOW-MEDIUM - Most code is new files. Only RenderMessageContent.tsx and settings.ts need upstream modifications.
Priority Recommendations
| Task | Effort | Risk | Recommendation |
|---|---|---|---|
| Twemoji default | 1 line | Low | Do immediately |
| Default theme | 2-3 lines | Low | Do immediately |
| External images | ~30 lines | Medium | Do soon, add owl setting |
| YouTube embeds | New component + ~20 lines changes | Low-Medium | Build as isolated component |
| Reaction UX | Component restructure | Medium | Plan carefully, touches upstream |
The first two are trivial defaults changes. External images and YouTube embeds can be built mostly in isolation. The reaction UX rework is the most invasive change and should be planned carefully to minimize merge conflict surface.