# 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.md` file 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 `folds` design 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, positioned `top: -30px; right: 0` (absolute) via `src/app/features/room/message/styles.css.ts`
- Clicking the emoji button captures `getBoundingClientRect()` and opens a `PopOut` component with `position="Bottom"` and `align="End"`
- The emoji board renders inside this PopOut popup
- There's also a context menu with `MessageQuickReactions` showing 4 recent emojis

### What Would Need to Change

To move reactions to a more sensible position:

1. **styles.css.ts** - Change `MessageOptionsBase` positioning (top/right/bottom)
2. **Message.tsx** - Change `PopOut` props (`position`, `align`) or replace PopOut with inline rendering
3. 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: `twitterEmoji` in `src/app/state/settings.ts` (line 26)
- **Default is `false`** (line 60)
- Font files exist: `public/font/Twemoji.Mozilla.v15.1.0.ttf` and `.woff2`
- Applied via CSS custom property `--font-emoji` in `src/app/pages/client/ClientNonUIFeatures.tsx` (lines 30-40)

### How to Make It Default

**One-line change** in `src/app/state/settings.ts`:

```typescript
// 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):
```typescript
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 `useSystemTheme` is 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:**
```typescript
useSystemTheme: false,
themeId: 'butter-theme',    // or any theme ID
```

**Option B - Change the dark/light fallbacks (keeps system detection):**
```typescript
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:

1. **Sanitization** (`src/app/utils/sanitize.ts`, lines 108-127): `transformImgTag` converts any `<img>` with non-`mxc://` src into an `<a>` link
2. **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:

1. **sanitize.ts**: Modify `transformImgTag` to allow `https://` and `http://` src URLs through as `<img>` tags instead of converting to `<a>`
2. **react-custom-html-parser.tsx**: Modify the `img` handler 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
- **`iframe` is 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: `urlPreview` and `encUrlPreview` booleans

### What Needs to Be Built

**Recommended approach**: Create a `YouTubeEmbedCard` component that slots into the existing URL preview system.

1. **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

2. **Modify** `src/app/components/RenderMessageContent.tsx`:
   - In `renderUrlsPreview`, check if URL is YouTube before rendering `UrlPreviewCard`
   - If YouTube and embed enabled, render `YouTubeEmbedCard` instead

3. **Add setting** in `src/app/state/settings.ts`:
   - `youtubeEmbed: boolean` (default: true for owl)

4. **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.
