Skip to content

PE-8183: snapshots for extensions #2044

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
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
187 changes: 187 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Development Commands

### Setup and Build
```bash
# Initial setup - install dependencies and generate code
scr setup

# Generate code (run after changing .drift files or GraphQL schemas)
flutter pub run build_runner build --delete-conflicting-outputs

# Watch for changes during development
flutter packages pub run build_runner watch

# Check Flutter version compliance
scr check-flutter

# Database schema validation
scr check-db
```

### Testing
```bash
# Run all tests (main app + all packages)
scr test

# Run main app tests only
flutter test

# Run tests for specific package
cd packages/ardrive_ui && flutter test

# Run specific test file
flutter test test/blocs/upload_cubit_test.dart
```

### Running the App
```bash
# Development environment (web)
flutter run -d chrome --dart-define=environment=development

# Production environment (web)
flutter run -d chrome --dart-define=environment=production

# Mobile development
flutter run --flavor=development

# Mobile production
flutter run --flavor=production
```

### Code Quality
```bash
# Analyze code
flutter analyze

# Format code
dart format .

# Lint check (via lefthook pre-commit)
lefthook run pre-commit
```

## Architecture Overview

ArDrive is a Flutter web/mobile application for decentralized file storage on Arweave blockchain.

### Core Architecture Patterns

**State Management**: BLoC pattern with Cubits
- Complex features (uploads, sync, file operations) use event-driven BLoCs
- Simple UI state uses Cubits
- Dependency injection via Provider

**Database**: Drift ORM with SQL generation
- Schema versioning with migrations in `drift_schemas/` (current: version 27)
- Core entities: drives, files, folders, licenses, ARNS records, ANT records, network transactions
- DAOs provide repository pattern for data access
- Database resets for schema versions < 24

**Upload System**: Multi-strategy architecture
- Direct Arweave uploads vs Turbo bundled uploads
- Payment methods: AR tokens or Turbo credits
- Upload handles abstract different upload patterns
- Real-time progress tracking with cancellation

### Key Components

**Authentication**: Multi-wallet support (ArConnect, keyfile, Ethereum)
**Encryption**: End-to-end encryption for private drives
**ARNS Integration**: Decentralized naming system via `ario_sdk`
**GraphQL**: Artemis-generated clients for Arweave gateway queries
**Packages**: Modular local packages in `/packages/` directory:
- `ardrive_ui` - UI Design Library with Storybook
- `ardrive_crypto` - Cryptography utilities
- `ardrive_uploader` - Upload functionality
- `ardrive_utils` - Shared utilities
- `ario_sdk` - ARNS integration
- `arconnect` - Wallet connection
- `pst` - Profit Sharing Token functionality
- `ardrive_logger` - Logging utilities

### File Structure

- `lib/blocs/` - State management (BLoCs/Cubits)
- `lib/models/` - Database models and DAOs
- `lib/services/` - External integrations (Arweave, payments, auth)
- `lib/pages/` - UI screens and routing
- `lib/components/` - Reusable UI components
- `packages/` - Local modular packages
- `test/` - Unit and integration tests

### Database Schema

When modifying database schema:
1. Update `.drift` files in `lib/models/tables/` or `lib/models/queries/`
2. Run `flutter pub run build_runner build --delete-conflicting-outputs`
3. Update migration logic in `lib/models/database/database.dart` if needed
4. Run `scr check-db` to validate schema changes

### Adding New Features

1. Create BLoC/Cubit in `lib/blocs/`
2. Add models/DAOs if database changes needed
3. Create UI components in `lib/components/` or pages in `lib/pages/`
4. Add tests in corresponding `test/` directories
5. Update routing in `lib/pages/app_router_delegate.dart` if needed

### Commit Message Format

Use conventional commit prefixes (lowercase):
- `fix:` - Bug fixes
- `feat:` - New features
- `perf:` - Performance improvements
- `docs:` - Documentation changes
- `style:` - Formatting changes
- `refactor:` - Code refactoring
- `test:` - Adding missing tests
- `chore:` - Chore tasks

Include detailed changes list after summary line if not self-explanatory.

### Environment Configuration

The app uses three environments (development, staging, production) with config files in `assets/config/`. Use `--dart-define=environment=<env>` to specify environment when running.

### Code Generation

The codebase uses several code generation tools:
- **Artemis**: GraphQL client generation from schema in `lib/services/arweave/graphql/`
- **Drift**: Database schema and DAO generation from `.drift` files
- **JSON Serialization**: Model serialization via `json_annotation`

Build configuration in `build.yaml` specifies output paths and options.

### Key Dependencies

- **Flutter SDK**: 3.19.6 (exact version required)
- **Dart SDK**: >=3.2.0 <4.0.0
- **State Management**: flutter_bloc ^8.1.1
- **Database**: drift ^2.12.1
- **GraphQL**: artemis ^7.0.0-beta.13
- **Testing**: mocktail, bloc_test, golden tests
- **Mobile**: Firebase integration (Crashlytics, Core)

### Development Tools

- **Lefthook**: Git hooks for pre-commit/pre-push validation
- **Script Runner**: Access to custom scripts via `scr` command
- **Flutter Lints**: Code quality enforcement
- **Golden Tests**: UI regression testing support

### Custom Gateway

For testing with custom Arweave gateways, set `flutter.arweaveGatewayUrl` in browser localStorage:
```js
localStorage.setItem('flutter.arweaveGatewayUrl', '"https://my.custom.url"');
```

### Release Process

- **Staging**: All changes to `dev` branch auto-deploy to staging.ardrive.io
- **Production**: Merge `dev` to `master`, create GitHub release with `v*` tag pattern
- **Preview Builds**: PRs to `dev` trigger shareable preview builds
58 changes: 52 additions & 6 deletions lib/blocs/drive_attach/drive_attach_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,30 @@ class DriveAttachCubit extends Cubit<DriveAttachState> {
}
}

Future<bool> _checkForSnapshots(String driveId) async {
try {
final snapshotsStream = _arweave.getAllSnapshotsOfDrive(
driveId,
null, // No lastBlockHeight filter for checking
ownerAddress: null, // Allow snapshots from any owner
);

final snapshots = await snapshotsStream.take(1).toList();
final hasSnapshots = snapshots.isNotEmpty;

if (hasSnapshots) {
logger.i('Drive $driveId has snapshots - will enable performance optimization');
} else {
logger.d('Drive $driveId has no snapshots - will use standard sync');
}

return hasSnapshots;
} catch (e) {
logger.w('Error checking for snapshots on drive $driveId: $e');
return false;
}
}

void submit() async {
final driveId = driveIdController.text;
final driveName = driveNameController.text;
Expand Down Expand Up @@ -158,15 +182,37 @@ class DriveAttachCubit extends Cubit<DriveAttachState> {
profileKey: _profileKey,
);

emit(DriveAttachSuccess());

// Check for snapshots to provide better user feedback
final hasSnapshots = await _checkForSnapshots(driveId);

// Don't emit success yet - go straight to syncing state

/// Wait for the sync to finish before syncing the newly attached drive.
await _syncBloc.waitCurrentSync();

/// Then, sync and select the newly attached drive.
unawaited(_syncBloc
.startSync()
.then((value) => _drivesBloc.selectDrive(driveId)));
/// Show syncing state while the drive syncs
emit(DriveAttachSyncing(hasSnapshots: hasSnapshots));

/// Start the sync in the background
/// Don't await it so the user can close the modal if they want
_syncBloc.syncSingleDrive(driveId).then((_) {
if (!isClosed) {
/// Select the drive after sync completes
_drivesBloc.selectDrive(driveId);
}
}).catchError((err) {
logger.e('Error during background sync of attached drive', err);
});

// Give the UI time to show the syncing state before allowing the user to close
await Future.delayed(const Duration(seconds: 2));

// Check if still in syncing state (user hasn't closed the modal)
if (!isClosed && state is DriveAttachSyncing) {
/// Emit success to indicate the attach is complete
/// The sync continues in the background
emit(DriveAttachSuccess());
}
Comment on lines +207 to +215
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fishy here, wondering if is there a chance we leave the user stuck


PlausibleEventTracker.trackAttachDrive(
drivePrivacy: drivePrivacy,
Expand Down
9 changes: 9 additions & 0 deletions lib/blocs/drive_attach/drive_attach_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ class DriveAttachInProgress extends DriveAttachState {}

class DriveAttachSuccess extends DriveAttachState {}

class DriveAttachSyncing extends DriveAttachState {
final bool hasSnapshots;

DriveAttachSyncing({this.hasSnapshots = false});

@override
List<Object> get props => [hasSnapshots];
}

class DriveAttachFailure extends DriveAttachState {}

class DriveAttachInvalidDriveKey extends DriveAttachState {}
9 changes: 2 additions & 7 deletions lib/blocs/drives/drives_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'package:ardrive/models/models.dart';
import 'package:ardrive/user/repositories/user_preferences_repository.dart';
import 'package:ardrive/utils/user_utils.dart';
import 'package:ardrive_utils/ardrive_utils.dart';
import 'package:drift/drift.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:rxdart/rxdart.dart';
Expand Down Expand Up @@ -50,14 +49,10 @@ class DrivesCubit extends Cubit<DrivesState> {

_drivesSubscription =
Rx.combineLatest3<List<Drive>, List<FolderEntry>, void, List<Drive>>(
_driveDao.allDrives(
order: (drives) {
return OrderBy([OrderingTerm.asc(drives.name)]);
},
).watch(),
_driveDao.allDrives().watch(),
_driveDao.ghostFolders().watch(),
_profileCubit.stream.startWith(ProfileCheckingAvailability()),
(drives, _, __) => drives,
(drives, _, __) => drives..sort((a, b) => a.name.compareTo(b.name)),
).listen((drives) async {
final state = this.state;

Expand Down
36 changes: 36 additions & 0 deletions lib/components/drive_attach_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,42 @@ class _DriveAttachFormState extends State<DriveAttachForm> {
);
}

if (state is DriveAttachSyncing) {
return ArDriveStandardModalNew(
hasCloseButton: true,
title: 'Syncing drive...',
content: SizedBox(
width: kMediumDialogWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 16),
const CircularProgressIndicator(),
const SizedBox(height: 24),
Text(
state.hasSnapshots
? 'Snapshots detected! Using optimized sync...\n\nThis should be quick!'
: 'Please wait while we sync the drive contents.\n\nThis may take a moment for large drives.',
textAlign: TextAlign.center,
style: ArDriveTypography.body.bodyRegular(),
),
const SizedBox(height: 16),
Text(
'You can close this modal and continue using ArDrive.\nThe sync will continue in the background.',
textAlign: TextAlign.center,
style: ArDriveTypography.body.smallRegular(
color: ArDriveTheme.of(context)
.themeData
.colors
.themeFgSubtle,
),
),
],
),
),
);
}

return ArDriveStandardModalNew(
title: appLocalizationsOf(context).attachDriveEmphasized,
content: SizedBox(
Expand Down
14 changes: 12 additions & 2 deletions lib/components/progress_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,33 @@ class ProgressBar extends StatefulWidget {

class _ProgressBarState extends State<ProgressBar> {
late double _percentage;
double _lastPercentage = 0;
DateTime _lastUpdate = DateTime.now();

@override
Widget build(BuildContext context) {
return StreamBuilder<LinearProgress>(
stream: widget.percentage,
builder: (context, snapshot) {
final now = DateTime.now();
_percentage = snapshot.hasData
? ((snapshot.data!.progress * 100)).roundToDouble() / 100
: 0;

// Disable animation for rapid updates to prevent UI lag
final isRapidUpdate = now.difference(_lastUpdate).inMilliseconds < 200;
final hasSignificantChange = (_percentage - _lastPercentage).abs() > 0.05;

_lastPercentage = _percentage;
_lastUpdate = now;

return LinearPercentIndicator(
animation: true,
animation: !isRapidUpdate && hasSignificantChange,
animateFromLastPercent: true,
lineHeight: 10.0,
barRadius: const Radius.circular(5),
backgroundColor: const Color(0xffFAFAFA),
animationDuration: 1000,
animationDuration: 100,
percent: _percentage,
progressColor: const Color(0xff3C3C3C),
);
Expand Down
Loading
Loading