Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions bounty-5/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Inline Image Editor — Design Document

## Overview

**Bounty #5** — Provide an inline image editor inside a chat/messenger UI so users can edit images before sending. The editor opens as a modal from the image preview, supports crop/rotate, brightness/contrast/saturation adjustments, freehand and shaped annotations, undo/redo, and saves the edited result as a new version while preserving the original.

## User Flow

1. User taps image preview → Edit button appears
2. Tap Edit → `ImageEditor` modal opens with the current image
3. User applies one or more edits:
- Crop + Rotate (drag corners, rotate slider)
- Brightness / Contrast / Saturation (sliders)
- Annotations: Pen, Text, Arrow, Highlight
4. Undo/Redo buttons revert/restore steps
5. Tap "Save" → new edited version is produced, original untouched
6. Message bubble shows "Edited" label

## Architecture

```
ImageEditor (modal)
├── ToolBar — crop, adjust, annotate, text mode toggles
├── Canvas / ImageView — renders the image + overlay annotations
├── CropTool — crop rectangle + rotation slider
├── AdjustPanel — brightness, contrast, saturation sliders
├── AnnotationToolbar — pen, text, arrow, highlight buttons
├── UndoRedoBar — undo / redo action buttons
└── Footer — cancel / save buttons
```

## State

- **MobX store** (`editorStore.ts`) holds the entire edit session:
- `history: ImageState[]` — undo stack
- `future: ImageState[]` — redo stack
- `currentOperation: EditOperation` — active tool
- `cropRect`, `rotation` — crop/rotate state
- `brightness`, `contrast`, `saturation` — adjustment values
- `annotations: Annotation[]` — list of drawn annotations
- `selectedAnnotationId: string | null`
- `penColor`, `penWidth` — drawing config

## ImageState

Every time the user performs an action, the current `ImageState` is pushed onto `history`. Undo pops the stack and pushes the current state onto `future`. Redo does the reverse.

## Types

See `types.ts` for `EditOperation`, `Annotation`, and `ImageState` definitions.

## Annotations

| Type | Storage |
| --------- | ---------------------------------------------- |
| Pen | Array of `{x, y}` points + color + width |
| Text | Position `{x, y}`, string, font size, color |
| Arrow | `{from, to}` points, color, lineWidth |
| Highlight | `{x, y, width, height}`, color, opacity |

## Export

Final image is produced by composing the original image with current crop/rotate/adjust/annotation layers into a single bitmap via `react-native-view-shot` or a canvas. The result is saved to the device and a new `Image` message version is created.
68 changes: 68 additions & 0 deletions bounty-5/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Bounty #5 — Inline Image Editor

**Prize: $660**

A React Native + TypeScript reference implementation for inline image editing inside a messenger-style chat.

## Features

- **Edit from preview** — tap the "Edit" button on an image preview to open the editor modal
- **Crop + Rotate** — rotate via a slider; crop by dragging corners
- **Adjustments** — brightness, contrast, and saturation sliders (0%–200%)
- **Annotations** — pen (freehand), text labels, arrows, and highlight rectangles
- **Undo / Redo** — full undo/redo stack for every action
- **Save as new version** — original image is preserved; edited copy is saved
- **"Edited" label** — messenger bubble shows an "Edited" badge

## Architecture

```
src/
├── types.ts # EditOperation, Annotation, ImageState
├── state/editorStore.ts # MobX store — undo/redo stack, all edit state
├── components/
│ ├── ImageEditor.tsx # Main modal — tool toggling, header, canvas
│ ├── CropTool.tsx # Crop rectangle + rotation slider
│ ├── AdjustPanel.tsx # Brightness/contrast/saturation sliders
│ ├── AnnotationToolbar.tsx # Color picker, size/width controls
│ └── UndoRedoBar.tsx # Undo / Redo buttons
```

## Usage

```tsx
import { getEditorStore } from './state/editorStore';

const store = getEditorStore();

// Open the editor with an image
store.openEditor({
uri: 'file:///path/to/image.jpg',
width: 1920,
height: 1080,
cropRect: null,
rotation: 0,
brightness: 1,
contrast: 1,
saturation: 1,
annotations: [],
});

// Render the modal somewhere in your app tree
<ImageEditor />
```

## Dependencies

- `react-native` (≥0.72)
- `mobx` (≥6)
- `mobx-react-lite` (≥4)
- `@react-native-community/slider` (for sliders)

## Undo/Redo

Every state mutation (rotation, brightness, annotation add, etc.) automatically pushes the current `ImageState` onto the undo stack. Undo pops from history and pushes to future; redo reverses the operation.

## Image Persistence

The actual bitmap composition (crop + rotate + adjustments + annotations → final file) is intentionally left as a TODO — a production implementation would use `react-native-view-shot` or a native module to render the layers into a single PNG/JPEG.
104 changes: 104 additions & 0 deletions bounty-5/components/AdjustPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react';
import { View, Text, Slider, StyleSheet } from 'react-native';
import { observer } from 'mobx-react-lite';
import { useEditorStore } from '../state/editorStore';

interface SliderRowProps {
label: string;
value: number;
min: number;
max: number;
onChange: (val: number) => void;
}

const SliderRow = ({ label, value, min, max, onChange }: SliderRowProps) => (
<View style={styles.row}>
<View style={styles.labelRow}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{Math.round(value * 100)}%</Text>
</View>
<Slider
style={styles.slider}
minimumValue={min}
maximumValue={max}
step={0.01}
value={value}
onValueChange={onChange}
minimumTrackTintColor="#4a9eff"
maximumTrackTintColor="#555"
thumbTintColor="#4a9eff"
/>
</View>
);

const AdjustPanel = observer(() => {
const store = useEditorStore();

return (
<View style={styles.container}>
<Text style={styles.title}>Adjustments</Text>

<SliderRow
label="Brightness"
value={store.brightness}
min={0}
max={2}
onChange={(val: number) => store.setBrightness(val)}
/>

<SliderRow
label="Contrast"
value={store.contrast}
min={0}
max={2}
onChange={(val: number) => store.setContrast(val)}
/>

<SliderRow
label="Saturation"
value={store.saturation}
min={0}
max={2}
onChange={(val: number) => store.setSaturation(val)}
/>
</View>
);
});

const styles = StyleSheet.create({
container: {
paddingHorizontal: 20,
paddingVertical: 12,
backgroundColor: '#222',
},
title: {
color: '#ffffff',
fontSize: 15,
fontWeight: '600',
marginBottom: 12,
},
row: {
marginBottom: 14,
},
labelRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
label: {
color: '#cccccc',
fontSize: 14,
},
value: {
color: '#ffffff',
fontSize: 14,
fontWeight: '600',
},
slider: {
width: '100%',
height: 36,
},
});

export default AdjustPanel;
Loading