fix: Resolve Flutter compile errors across 15 files

- Fix GroupCategory enum duplication (group.dart re-exports from cluster.dart)
- Fix stitchVideos arg count (use stitchVideosLegacy in quip_recorder)
- Fix enhanced_quip_recorder: getter/field conflict, XFile→File, static calls, CameraLensDirection, setState syntax
- Fix blocking_service: imports, ShareXFiles, SnackBar content, nullable bool
- Fix audio_overlay_service: path_provider import, getOutput() stub, fade icons
- Fix enhanced_beacon_detail_screen: flutter_map v8 interactionOptions, const constraints, Uri.parse
- Fix private_capsule_screen: add api_provider import
- Stub e2ee_device_sync_service (packages not in pubspec)
- Add generic post()/delete() helpers to api_service
- Fix repost_service: remove static, migrate StateNotifier→Notifier (Riverpod 3.x)
- Fix repost_widget: Repost? null assertion, RepostState type, Post field names, timeago, context param
- Fix sojorn_swipeable_post: close if-block, remove undefined _updateChainSetting
- Fix draggable_widget_grid: use this.widget for isEditable/theme/callbacks when ProfileWidget param shadows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Britton 2026-02-17 17:43:30 -06:00
parent 91ff0dc060
commit 0753fd91e6
15 changed files with 143 additions and 1026 deletions

View file

@ -1,25 +1,6 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'cluster.dart' show GroupCategory;
enum GroupCategory { export 'cluster.dart' show GroupCategory;
general('General', 'general'),
hobby('Hobby', 'hobby'),
sports('Sports', 'sports'),
professional('Professional', 'professional'),
localBusiness('Local Business', 'local_business'),
support('Support', 'support'),
education('Education', 'education');
const GroupCategory(this.displayName, this.value);
final String displayName;
final String value;
static GroupCategory fromString(String value) {
return GroupCategory.values.firstWhere(
(cat) => cat.value == value,
orElse: () => GroupCategory.general,
);
}
}
enum GroupRole { enum GroupRole {
owner('Owner'), owner('Owner'),

View file

@ -41,7 +41,7 @@ class _EnhancedBeaconDetailScreenState extends State<EnhancedBeaconDetailScreen>
options: MapOptions( options: MapOptions(
initialCenter: LatLng(widget.beacon.lat, widget.beacon.lng), initialCenter: LatLng(widget.beacon.lat, widget.beacon.lng),
initialZoom: 15.0, initialZoom: 15.0,
interactiveFlags: InteractiveFlag.none, interactionOptions: const InteractionOptions(flags: InteractiveFlag.none),
), ),
children: [ children: [
TileLayer( TileLayer(
@ -483,7 +483,7 @@ class _EnhancedBeaconDetailScreenState extends State<EnhancedBeaconDetailScreen>
color: AppTheme.navyBlue, color: AppTheme.navyBlue,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Center( child: Center(
child: Text( child: Text(
'${index + 1}', '${index + 1}',
style: TextStyle( style: TextStyle(
@ -759,7 +759,7 @@ class _EnhancedBeaconDetailScreenState extends State<EnhancedBeaconDetailScreen>
], ],
), ),
), ),
const Icon( Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
color: Colors.grey[400], color: Colors.grey[400],
size: 16, size: 16,
@ -778,7 +778,7 @@ class _EnhancedBeaconDetailScreenState extends State<EnhancedBeaconDetailScreen>
} }
void _callEmergency() async { void _callEmergency() async {
const url = 'tel:911'; final url = Uri.parse('tel:911');
if (await canLaunchUrl(url)) { if (await canLaunchUrl(url)) {
await launchUrl(url); await launchUrl(url);
} }

View file

@ -768,7 +768,7 @@ class _CreateGroupFormState extends ConsumerState<_CreateGroupForm> {
await api.createGroup( await api.createGroup(
name: _nameCtrl.text.trim(), name: _nameCtrl.text.trim(),
description: _descCtrl.text.trim(), description: _descCtrl.text.trim(),
category: group_models.GroupCategory.general, category: GroupCategory.general,
isPrivate: _privacy, isPrivate: _privacy,
); );
widget.onCreated(); widget.onCreated();

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import '../../models/cluster.dart'; import '../../models/cluster.dart';
import '../../providers/api_provider.dart';
import '../../services/api_service.dart'; import '../../services/api_service.dart';
import '../../services/auth_service.dart'; import '../../services/auth_service.dart';
import '../../services/capsule_security_service.dart'; import '../../services/capsule_security_service.dart';

View file

@ -166,7 +166,7 @@ class _FeedsojornScreenState extends ConsumerState<FeedsojornScreen> {
} }
void _sharePost(Post post) { void _sharePost(Post post) {
final text = post.content.isNotEmpty ? post.content : 'Check this out on Sojorn'; final text = post.body.isNotEmpty ? post.body : 'Check this out on Sojorn';
Share.share(text, subject: 'Shared from Sojorn'); Share.share(text, subject: 'Shared from Sojorn');
} }
@ -311,7 +311,7 @@ class _FeedsojornScreenState extends ConsumerState<FeedsojornScreen> {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'items: ${_feedItems.length} | ads: ${adIndices.length}', 'items: ${_feedItems.length} | ads: ${adIndices.length}',
style: const TextStyle( style: TextStyle(
color: SojornColors.basicWhite.withValues(alpha: 0.7), color: SojornColors.basicWhite.withValues(alpha: 0.7),
fontSize: 11, fontSize: 11,
), ),
@ -321,7 +321,7 @@ class _FeedsojornScreenState extends ConsumerState<FeedsojornScreen> {
adIndices.isEmpty adIndices.isEmpty
? 'ad positions: none' ? 'ad positions: none'
: 'ad positions: ${adIndices.join(', ')}', : 'ad positions: ${adIndices.join(', ')}',
style: const TextStyle( style: TextStyle(
color: SojornColors.basicWhite.withValues(alpha: 0.7), color: SojornColors.basicWhite.withValues(alpha: 0.7),
fontSize: 11, fontSize: 11,
), ),

View file

@ -42,7 +42,6 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
DateTime? _segmentStartTime; DateTime? _segmentStartTime;
Timer? _progressTicker; Timer? _progressTicker;
Duration _currentSegmentDuration = Duration.zero; Duration _currentSegmentDuration = Duration.zero;
Duration _totalRecordedDuration = Duration.zero;
// Speed Control // Speed Control
double _playbackSpeed = 1.0; double _playbackSpeed = 1.0;
@ -166,7 +165,7 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
// Auto-stop at max duration // Auto-stop at max duration
Timer(const Duration(milliseconds: 100), () { Timer(const Duration(milliseconds: 100), () {
if (get _totalRecordedDuration >= _maxDuration) { if (_totalRecordedDuration >= _maxDuration) {
_stopRecording(); _stopRecording();
} }
}); });
@ -187,7 +186,6 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
// Save current segment // Save current segment
_segmentDurations.add(_currentSegmentDuration); _segmentDurations.add(_currentSegmentDuration);
_totalRecordedDuration = get _totalRecordedDuration;
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pause recording'))); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pause recording')));
@ -200,7 +198,7 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
try { try {
await _cameraController!.resumeVideoRecording(); await _cameraController!.resumeVideoRecording();
setState(() => { setState(() {
_isPaused = false; _isPaused = false;
_segmentStartTime = DateTime.now(); _segmentStartTime = DateTime.now();
_currentSegmentDuration = Duration.zero; _currentSegmentDuration = Duration.zero;
@ -234,12 +232,10 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
// Add segment if it has content // Add segment if it has content
if (_currentSegmentDuration.inMilliseconds > 500) { // Minimum 0.5 seconds if (_currentSegmentDuration.inMilliseconds > 500) { // Minimum 0.5 seconds
_recordedSegments.add(videoFile); _recordedSegments.add(File(videoFile.path));
_segmentDurations.add(_currentSegmentDuration); _segmentDurations.add(_currentSegmentDuration);
} }
_totalRecordedDuration = get _totalRecordedDuration;
// Auto-process if we have segments // Auto-process if we have segments
if (_recordedSegments.isNotEmpty) { if (_recordedSegments.isNotEmpty) {
_processVideo(); _processVideo();
@ -258,8 +254,7 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
setState(() => _isProcessing = true); setState(() => _isProcessing = true);
try { try {
final videoStitchingService = VideoStitchingService(); final finalFile = await VideoStitchingService.stitchVideos(
final finalFile = await videoStitchingService.stitchVideos(
_recordedSegments, _recordedSegments,
_segmentDurations, _segmentDurations,
_selectedFilter, _selectedFilter,
@ -267,7 +262,7 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
_showTextOverlay ? { _showTextOverlay ? {
'text': _overlayText, 'text': _overlayText,
'size': _textSize, 'size': _textSize,
'color': _textColor.value.toHex(), 'color': '#${_textColor.value.toRadixString(16).padLeft(8, '0')}',
'position': _textPositionY, 'position': _textPositionY,
} : null, } : null,
audioOverlayPath: _selectedAudio?.path, audioOverlayPath: _selectedAudio?.path,
@ -318,7 +313,7 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
try { try {
final camera = _cameras.firstWhere( final camera = _cameras.firstWhere(
(c) => c.lensDirection == (_isRearCamera ? CameraLensDirection.back : CameraDirection.front), (c) => c.lensDirection == (_isRearCamera ? CameraLensDirection.back : CameraLensDirection.front),
orElse: () => _cameras.first orElse: () => _cameras.first
); );
@ -357,7 +352,6 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
_recordedSegments.clear(); _recordedSegments.clear();
_segmentDurations.clear(); _segmentDurations.clear();
_currentSegmentDuration = Duration.zero; _currentSegmentDuration = Duration.zero;
_totalRecordedDuration = Duration.zero;
}); });
} }
@ -481,7 +475,7 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
Container( Container(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: get _totalRecordedDuration.inMilliseconds / _maxDuration.inMilliseconds, value: _totalRecordedDuration.inMilliseconds / _maxDuration.inMilliseconds,
backgroundColor: Colors.white24, backgroundColor: Colors.white24,
valueColor: AlwaysStoppedAnimation<Color>( valueColor: AlwaysStoppedAnimation<Color>(
_isPaused ? Colors.orange : Colors.red, _isPaused ? Colors.orange : Colors.red,
@ -495,7 +489,7 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
children: [ children: [
// Duration // Duration
Text( Text(
_formatDuration(get _totalRecordedDuration), _formatDuration(_totalRecordedDuration),
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 16, fontSize: 16,
@ -737,7 +731,6 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
max: 48, max: 48,
divisions: 4, divisions: 4,
label: '${_textSize.toInt()}', label: '${_textSize.toInt()}',
labelStyle: const TextStyle(color: Colors.white70),
activeColor: AppTheme.navyBlue, activeColor: AppTheme.navyBlue,
inactiveColor: Colors.white24, inactiveColor: Colors.white24,
onChanged: (value) => setState(() => _textSize = value), onChanged: (value) => setState(() => _textSize = value),
@ -751,11 +744,11 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
min: 0.0, min: 0.0,
max: 1.0, max: 1.0,
label: _textPositionY == 0.0 ? 'Top' : 'Bottom', label: _textPositionY == 0.0 ? 'Top' : 'Bottom',
labelStyle: const TextStyle(color: Colors.white70),
activeColor: AppTheme.navyBlue, activeColor: AppTheme.navyBlue,
inactiveColor: Colors.white24, inactiveColor: Colors.white24,
onChanged: (value) => setState(() => _textPositionY = value), onChanged: (value) => setState(() => _textPositionY = value),
), ),
),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),

View file

@ -196,7 +196,7 @@ class _QuipRecorderScreenState extends State<QuipRecorderScreen>
if (_recordedSegments.length == 1) { if (_recordedSegments.length == 1) {
finalFile = _recordedSegments.first; finalFile = _recordedSegments.first;
} else { } else {
finalFile = await VideoStitchingService.stitchVideos(_recordedSegments); finalFile = await VideoStitchingService.stitchVideosLegacy(_recordedSegments);
} }
if (finalFile != null && mounted) { if (finalFile != null && mounted) {

View file

@ -176,6 +176,16 @@ class ApiService {
return _callGoApi(path, method: 'GET', queryParams: queryParams); return _callGoApi(path, method: 'GET', queryParams: queryParams);
} }
/// Simple POST request helper
Future<Map<String, dynamic>> post(String path, Map<String, dynamic> body) async {
return _callGoApi(path, method: 'POST', body: body);
}
/// Simple DELETE request helper
Future<Map<String, dynamic>> delete(String path) async {
return _callGoApi(path, method: 'DELETE');
}
Future<void> resendVerificationEmail(String email) async { Future<void> resendVerificationEmail(String email) async {
await _callGoApi('/auth/resend-verification', await _callGoApi('/auth/resend-verification',

View file

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'media/ffmpeg.dart'; import 'media/ffmpeg.dart';
@ -92,10 +93,10 @@ class AudioOverlayService {
try { try {
final command = "-i '${audioFile.path}' -f null -"; final command = "-i '${audioFile.path}' -f null -";
final session = await FFmpegKit.execute(command); final session = await FFmpegKit.execute(command);
final logs = await session.getAllLogs(); final output = await session.getOutput() ?? '';
final logs = output.split('\n');
for (final log in logs) { for (final message in logs) {
final message = log.getMessage();
if (message.contains('Duration:')) { if (message.contains('Duration:')) {
// Parse duration from FFmpeg output // Parse duration from FFmpeg output
final durationMatch = RegExp(r'Duration: (\d{2}):(\d{2}):(\d{2}\.\d{2})').firstMatch(message); final durationMatch = RegExp(r'Duration: (\d{2}):(\d{2}):(\d{2}\.\d{2})').firstMatch(message);
@ -442,7 +443,7 @@ class _AudioOverlayControlsState extends State<AudioOverlayControls> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
Icons.fade_in, Icons.volume_up,
color: Colors.white, color: Colors.white,
size: 16, size: 16,
), ),
@ -475,7 +476,7 @@ class _AudioOverlayControlsState extends State<AudioOverlayControls> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
Icons.fade_out, Icons.volume_down,
color: Colors.white, color: Colors.white,
size: 16, size: 16,
), ),

View file

@ -1,8 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
class BlockingService { class BlockingService {
@ -30,8 +33,8 @@ class BlockingService {
await file.writeAsString(const JsonEncoder.withIndent(' ').convert(exportData)); await file.writeAsString(const JsonEncoder.withIndent(' ').convert(exportData));
// Share the file // Share the file
final result = await Share.shareXFiles([file.path]); final result = await Share.shareXFiles([XFile(file.path)]);
return result.status == ShareResultStatus.done; return result.status == ShareResultStatus.success;
} catch (e) { } catch (e) {
print('Error exporting blocked users to JSON: $e'); print('Error exporting blocked users to JSON: $e');
return false; return false;
@ -54,8 +57,8 @@ class BlockingService {
await file.writeAsString(csvContent.toString()); await file.writeAsString(csvContent.toString());
// Share the file // Share the file
final result = await Share.shareXFiles([file.path]); final result = await Share.shareXFiles([XFile(file.path)]);
return result.status == ShareResultStatus.done; return result.status == ShareResultStatus.success;
} catch (e) { } catch (e) {
print('Error exporting blocked users to CSV: $e'); print('Error exporting blocked users to CSV: $e');
return false; return false;
@ -279,7 +282,7 @@ class _BlockManagementScreenState extends State<BlockManagementScreen> {
// This would typically come from your API service // This would typically come from your API service
// For now, we'll use a placeholder // For now, we'll use a placeholder
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final blockedUsersJson = prefs.getString(_blockedUsersJsonKey); final blockedUsersJson = prefs.getString(BlockingService._blockedUsersJsonKey);
if (blockedUsersJson != null) { if (blockedUsersJson != null) {
final blockedUsersList = jsonDecode(blockedUsersJson) as List<dynamic>; final blockedUsersList = jsonDecode(blockedUsersJson) as List<dynamic>;
@ -299,10 +302,10 @@ class _BlockManagementScreenState extends State<BlockManagementScreen> {
Future<void> _saveBlockedUsers() async { Future<void> _saveBlockedUsers() async {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setString(_blockedUsersJsonKey, jsonEncode(_blockedUsers)); await prefs.setString(BlockingService._blockedUsersJsonKey, jsonEncode(_blockedUsers));
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: 'Failed to save blocked users'), const SnackBar(content: Text('Failed to save blocked users')),
); );
} }
} }
@ -353,21 +356,21 @@ class _BlockManagementScreenState extends State<BlockManagementScreen> {
final validatedUsers = await BlockingService.validateBlockedUsers(importedUsers); final validatedUsers = await BlockingService.validateBlockedUsers(importedUsers);
setState(() { setState(() {
_blockedUsers = {..._blockedUsers, ...validatedUsers}.toSet().toList()}; _blockedUsers = {..._blockedUsers, ...validatedUsers}.toList();
}); });
await _saveBlockedUsers(); await _saveBlockedUsers();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: 'Successfully imported ${validatedUsers.length} users', content: Text('Successfully imported ${validatedUsers.length} users'),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: 'Failed to import: $e', content: Text('Failed to import: $e'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@ -397,7 +400,7 @@ class _BlockManagementScreenState extends State<BlockManagementScreen> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
user: const Text('Cancel'), child: const Text('Cancel'),
), ),
], ],
), ),
@ -412,17 +415,17 @@ class _BlockManagementScreenState extends State<BlockManagementScreen> {
try { try {
final success = await format.exportFunction!(_blockedUsers); final success = await format.exportFunction!(_blockedUsers);
if (success) { if (success == true) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: 'Successfully exported ${_blockedUsers.length} users', content: Text('Successfully exported ${_blockedUsers.length} users'),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: 'Export cancelled or failed', content: const Text('Export cancelled or failed'),
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
), ),
); );
@ -430,7 +433,7 @@ class _BlockManagementScreenState extends State<BlockManagementScreen> {
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: 'Export failed: $e', content: Text('Export failed: $e'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );

View file

@ -1,869 +1,6 @@
import 'dart:convert'; // E2EE device sync service stub (encrypt/pointycastle/qr_flutter not in pubspec)
import 'dart:typed_data'; // Full implementation deferred until those packages are added.
import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart';
import 'package:pointycastle/export.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sojorn/services/api_service.dart';
class E2EEDeviceSyncService { class E2EEDeviceSyncService {
static const String _devicesKey = 'e2ee_devices'; // TODO: implement when encrypt, pointycastle, qr_flutter are added to pubspec
static const String _currentDeviceKey = 'e2ee_current_device';
static const String _keysKey = 'e2ee_keys';
/// Device information for E2EE
class DeviceInfo {
final String id;
final String name;
final String type; // mobile, desktop, web
final String publicKey;
final DateTime lastSeen;
final bool isActive;
final Map<String, dynamic>? metadata;
DeviceInfo({
required this.id,
required this.name,
required this.type,
required this.publicKey,
required this.lastSeen,
this.isActive = true,
this.metadata,
});
factory DeviceInfo.fromJson(Map<String, dynamic> json) {
return DeviceInfo(
id: json['id'] ?? '',
name: json['name'] ?? '',
type: json['type'] ?? '',
publicKey: json['public_key'] ?? '',
lastSeen: DateTime.parse(json['last_seen']),
isActive: json['is_active'] ?? true,
metadata: json['metadata'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'type': type,
'public_key': publicKey,
'last_seen': lastSeen.toIso8601String(),
'is_active': isActive,
'metadata': metadata,
};
}
}
/// E2EE key pair
class E2EEKeyPair {
final String privateKey;
final String publicKey;
final String keyId;
final DateTime createdAt;
final DateTime? expiresAt;
final String algorithm; // RSA, ECC, etc.
E2EEKeyPair({
required this.privateKey,
required this.publicKey,
required this.keyId,
required this.createdAt,
this.expiresAt,
this.algorithm = 'RSA',
});
factory E2EEKeyPair.fromJson(Map<String, dynamic> json) {
return E2EEKeyPair(
privateKey: json['private_key'] ?? '',
publicKey: json['public_key'] ?? '',
keyId: json['key_id'] ?? '',
createdAt: DateTime.parse(json['created_at']),
expiresAt: json['expires_at'] != null ? DateTime.parse(json['expires_at']) : null,
algorithm: json['algorithm'] ?? 'RSA',
);
}
Map<String, dynamic> toJson() {
return {
'private_key': privateKey,
'public_key': publicKey,
'key_id': keyId,
'created_at': createdAt.toIso8601String(),
'expires_at': expiresAt?.toIso8601String(),
'algorithm': algorithm,
};
}
}
/// QR code data for device verification
class QRVerificationData {
final String deviceId;
final String publicKey;
final String timestamp;
final String signature;
final String userId;
QRVerificationData({
required this.deviceId,
required this.publicKey,
required this.timestamp,
required this.signature,
required this.userId,
});
factory QRVerificationData.fromJson(Map<String, dynamic> json) {
return QRVerificationData(
deviceId: json['device_id'] ?? '',
publicKey: json['public_key'] ?? '',
timestamp: json['timestamp'] ?? '',
signature: json['signature'] ?? '',
userId: json['user_id'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'device_id': deviceId,
'public_key': publicKey,
'timestamp': timestamp,
'signature': signature,
'user_id': userId,
};
}
String toBase64() {
return base64Encode(utf8.encode(jsonEncode(toJson())));
}
factory QRVerificationData.fromBase64(String base64String) {
final json = jsonDecode(utf8.decode(base64Decode(base64String)));
return QRVerificationData.fromJson(json);
}
}
/// Generate new E2EE key pair
static Future<E2EEKeyPair> generateKeyPair() async {
try {
// Generate RSA key pair
final keyPair = RSAKeyGenerator().generateKeyPair(2048);
final privateKey = keyPair.privateKey as RSAPrivateKey;
final publicKey = keyPair.publicKey as RSAPublicKey;
// Convert to PEM format
final privatePem = privateKey.toPem();
final publicPem = publicKey.toPem();
// Generate key ID
final keyId = _generateKeyId();
return E2EEKeyPair(
privateKey: privatePem,
publicKey: publicPem,
keyId: keyId,
createdAt: DateTime.now(),
algorithm: 'RSA',
);
} catch (e) {
throw Exception('Failed to generate E2EE key pair: $e');
}
}
/// Register current device
static Future<DeviceInfo> registerDevice({
required String userId,
required String deviceName,
required String deviceType,
Map<String, dynamic>? metadata,
}) async {
try {
// Generate key pair for this device
final keyPair = await generateKeyPair();
// Create device info
final device = DeviceInfo(
id: _generateDeviceId(),
name: deviceName,
type: deviceType,
publicKey: keyPair.publicKey,
lastSeen: DateTime.now(),
metadata: metadata,
);
// Save to local storage
await _saveCurrentDevice(device);
await _saveKeyPair(keyPair);
// Register with server
await _registerDeviceWithServer(userId, device, keyPair);
return device;
} catch (e) {
throw Exception('Failed to register device: $e');
}
}
/// Get QR verification data for current device
static Future<QRVerificationData> getQRVerificationData(String userId) async {
try {
final device = await _getCurrentDevice();
if (device == null) {
throw Exception('No device registered');
}
final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
final signature = await _signData(device.id + timestamp + userId);
return QRVerificationData(
deviceId: device.id,
publicKey: device.publicKey,
timestamp: timestamp,
signature: signature,
userId: userId,
);
} catch (e) {
throw Exception('Failed to generate QR data: $e');
}
}
/// Verify and add device from QR code
static Future<bool> verifyAndAddDevice(String qrData, String currentUserId) async {
try {
final qrVerificationData = QRVerificationData.fromBase64(qrData);
// Verify signature
final isValid = await _verifySignature(
qrVerificationData.deviceId + qrVerificationData.timestamp + qrVerificationData.userId,
qrVerificationData.signature,
qrVerificationData.publicKey,
);
if (!isValid) {
throw Exception('Invalid QR code signature');
}
// Check if timestamp is recent (within 5 minutes)
final timestamp = int.parse(qrVerificationData.timestamp);
final now = DateTime.now().millisecondsSinceEpoch();
if (now - timestamp > 5 * 60 * 1000) { // 5 minutes
throw Exception('QR code expired');
}
// Add device to user's device list
final device = DeviceInfo(
id: qrVerificationData.deviceId,
name: 'QR Linked Device',
type: 'unknown',
publicKey: qrVerificationData.publicKey,
lastSeen: DateTime.now(),
);
await _addDeviceToUser(currentUserId, device);
return true;
} catch (e) {
print('Failed to verify QR device: $e');
return false;
}
}
/// Sync keys between devices
static Future<bool> syncKeys(String userId) async {
try {
// Get all devices for user
final devices = await _getUserDevices(userId);
// Get current device
final currentDevice = await _getCurrentDevice();
if (currentDevice == null) {
throw Exception('No current device found');
}
// Sync keys with server
final response = await ApiService.instance.post('/api/e2ee/sync-keys', {
'device_id': currentDevice.id,
'devices': devices.map((d) => d.toJson()).toList(),
});
if (response['success'] == true) {
// Update local device list
final updatedDevices = (response['devices'] as List<dynamic>?)
?.map((d) => DeviceInfo.fromJson(d as Map<String, dynamic>))
.toList() ?? [];
await _saveUserDevices(userId, updatedDevices);
return true;
}
return false;
} catch (e) {
print('Failed to sync keys: $e');
return false;
}
}
/// Encrypt message for specific device
static Future<String> encryptMessageForDevice({
required String message,
required String targetDeviceId,
required String userId,
}) async {
try {
// Get target device's public key
final devices = await _getUserDevices(userId);
final targetDevice = devices.firstWhere(
(d) => d.id == targetDeviceId,
orElse: () => throw Exception('Target device not found'),
);
// Get current device's private key
final currentKeyPair = await _getCurrentKeyPair();
if (currentKeyPair == null) {
throw Exception('No encryption keys available');
}
// Encrypt message
final encryptedData = await _encryptWithPublicKey(
message,
targetDevice.publicKey,
);
return encryptedData;
} catch (e) {
throw Exception('Failed to encrypt message: $e');
}
}
/// Decrypt message from any device
static Future<String> decryptMessage({
required String encryptedMessage,
required String userId,
}) async {
try {
// Get current device's private key
final currentKeyPair = await _getCurrentKeyPair();
if (currentKeyPair == null) {
throw Exception('No decryption keys available');
}
// Decrypt message
final decryptedData = await _decryptWithPrivateKey(
encryptedMessage,
currentKeyPair.privateKey,
);
return decryptedData;
} catch (e) {
throw Exception('Failed to decrypt message: $e');
}
}
/// Remove device
static Future<bool> removeDevice(String userId, String deviceId) async {
try {
// Remove from server
final response = await ApiService.instance.delete('/api/e2ee/devices/$deviceId');
if (response['success'] == true) {
// Remove from local storage
final devices = await _getUserDevices(userId);
devices.removeWhere((d) => d.id == deviceId);
await _saveUserDevices(userId, devices);
// If removing current device, clear local data
final currentDevice = await _getCurrentDevice();
if (currentDevice?.id == deviceId) {
await _clearLocalData();
}
return true;
}
return false;
} catch (e) {
print('Failed to remove device: $e');
return false;
}
}
/// Get all user devices
static Future<List<DeviceInfo>> getUserDevices(String userId) async {
return await _getUserDevices(userId);
}
/// Get current device info
static Future<DeviceInfo?> getCurrentDevice() async {
return await _getCurrentDevice();
}
// Private helper methods
static String _generateDeviceId() {
return 'device_${DateTime.now().millisecondsSinceEpoch}_${_generateRandomString(8)}';
}
static String _generateKeyId() {
return 'key_${DateTime.now().millisecondsSinceEpoch}_${_generateRandomString(8)}';
}
static String _generateRandomString(int length) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
final random = Random.secure();
return String.fromCharCodes(Iterable.generate(
length,
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
));
}
static Future<void> _saveCurrentDevice(DeviceInfo device) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_currentDeviceKey, jsonEncode(device.toJson()));
}
static Future<DeviceInfo?> _getCurrentDevice() async {
final prefs = await SharedPreferences.getInstance();
final deviceJson = prefs.getString(_currentDeviceKey);
if (deviceJson != null) {
return DeviceInfo.fromJson(jsonDecode(deviceJson));
}
return null;
}
static Future<void> _saveKeyPair(E2EEKeyPair keyPair) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keysKey, jsonEncode(keyPair.toJson()));
}
static Future<E2EEKeyPair?> _getCurrentKeyPair() async {
final prefs = await SharedPreferences.getInstance();
final keysJson = prefs.getString(_keysKey);
if (keysJson != null) {
return E2EEKeyPair.fromJson(jsonDecode(keysJson));
}
return null;
}
static Future<void> _saveUserDevices(String userId, List<DeviceInfo> devices) async {
final prefs = await SharedPreferences.getInstance();
final key = '${_devicesKey}_$userId';
await prefs.setString(key, jsonEncode(devices.map((d) => d.toJson()).toList()));
}
static Future<List<DeviceInfo>> _getUserDevices(String userId) async {
final prefs = await SharedPreferences.getInstance();
final key = '${_devicesKey}_$userId';
final devicesJson = prefs.getString(key);
if (devicesJson != null) {
final devicesList = jsonDecode(devicesJson) as List<dynamic>;
return devicesList.map((d) => DeviceInfo.fromJson(d as Map<String, dynamic>)).toList();
}
return [];
}
static Future<void> _addDeviceToUser(String userId, DeviceInfo device) async {
final devices = await _getUserDevices(userId);
devices.add(device);
await _saveUserDevices(userId, devices);
}
static Future<void> _registerDeviceWithServer(String userId, DeviceInfo device, E2EEKeyPair keyPair) async {
final response = await ApiService.instance.post('/api/e2ee/register-device', {
'user_id': userId,
'device': device.toJson(),
'public_key': keyPair.publicKey,
'key_id': keyPair.keyId,
});
if (response['success'] != true) {
throw Exception('Failed to register device with server');
}
}
static Future<String> _signData(String data) async {
// This would use the current device's private key to sign data
// For now, return a mock signature
final bytes = utf8.encode(data);
final digest = sha256.convert(bytes);
return base64Encode(digest.bytes);
}
static Future<bool> _verifySignature(String data, String signature, String publicKey) async {
// This would verify the signature using the public key
// For now, return true
return true;
}
static Future<String> _encryptWithPublicKey(String message, String publicKey) async {
try {
// Parse public key
final parser = RSAKeyParser();
final rsaPublicKey = parser.parse(publicKey) as RSAPublicKey;
// Encrypt
final encrypter = Encrypter(rsaPublicKey);
final encrypted = encrypter.encrypt(message);
return encrypted.base64;
} catch (e) {
throw Exception('Encryption failed: $e');
}
}
static Future<String> _decryptWithPrivateKey(String encryptedMessage, String privateKey) async {
try {
// Parse private key
final parser = RSAKeyParser();
final rsaPrivateKey = parser.parse(privateKey) as RSAPrivateKey;
// Decrypt
final encrypter = Encrypter(rsaPrivateKey);
final decrypted = encrypter.decrypt64(encryptedMessage);
return decrypted;
} catch (e) {
throw Exception('Decryption failed: $e');
}
}
static Future<void> _clearLocalData() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_currentDeviceKey);
await prefs.remove(_keysKey);
}
}
/// QR Code Display Widget
class E2EEQRCodeWidget extends StatelessWidget {
final String qrData;
final String title;
final String description;
const E2EEQRCodeWidget({
super.key,
required this.qrData,
required this.title,
required this.description,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 8),
Text(
description,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: QrImageView(
data: qrData,
version: QrVersions.auto,
size: 200.0,
backgroundColor: Colors.white,
),
),
const SizedBox(height: 16),
Text(
'Scan this code with another device to link it',
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
textAlign: TextAlign.center,
),
],
),
);
}
}
/// Device List Widget
class E2EEDeviceListWidget extends StatelessWidget {
final List<E2EEDeviceSyncService.DeviceInfo> devices;
final Function(String)? onRemoveDevice;
final Function(String)? onVerifyDevice;
const E2EEDeviceListWidget({
super.key,
required this.devices,
this.onRemoveDevice,
this.onVerifyDevice,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
children: [
const Icon(
Icons.devices,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
const Text(
'Linked Devices',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Text(
'${devices.length} devices',
style: TextStyle(
color: Colors.grey[400],
fontSize: 12,
),
),
],
),
),
// Device list
if (devices.isEmpty)
Container(
padding: const EdgeInsets.all(32),
child: Column(
children: [
Icon(
Icons.device_unknown,
color: Colors.grey[600],
size: 48,
),
const SizedBox(height: 16),
Text(
'No devices linked',
style: TextStyle(
color: Colors.grey[400],
fontSize: 16,
),
),
const SizedBox(height: 8),
Text(
'Link devices to enable E2EE chat sync',
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
)
else
...devices.asMap().entries.map((entry) {
final index = entry.key;
final device = entry.value;
return _buildDeviceItem(device, index);
}).toList(),
],
),
);
}
Widget _buildDeviceItem(E2EEDeviceSyncService.DeviceInfo device, int index) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey[800]!,
width: 1,
),
),
),
child: Row(
children: [
// Device icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _getDeviceTypeColor(device.type),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
_getDeviceTypeIcon(device.type),
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
// Device info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
device.name,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
Text(
'${device.type} • Last seen ${_formatLastSeen(device.lastSeen)}',
style: TextStyle(
color: Colors.grey[400],
fontSize: 12,
),
),
],
),
),
// Status indicator
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: device.isActive ? Colors.green : Colors.grey,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
// Actions
if (onRemoveDevice != null || onVerifyDevice != null)
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Colors.white),
color: Colors.white,
onSelected: (value) {
switch (value) {
case 'remove':
onRemoveDevice!(device.id);
break;
case 'verify':
onVerifyDevice!(device.id);
break;
}
},
itemBuilder: (context) => [
if (onVerifyDevice != null)
const PopupMenuItem(
value: 'verify',
child: Row(
children: [
Icon(Icons.verified, size: 16),
SizedBox(width: 8),
Text('Verify'),
],
),
),
if (onRemoveDevice != null)
const PopupMenuItem(
value: 'remove',
child: Row(
children: [
Icon(Icons.delete, size: 16, color: Colors.red),
SizedBox(width: 8),
Text('Remove', style: TextStyle(color: Colors.red)),
],
),
),
],
),
],
),
);
}
Color _getDeviceTypeColor(String type) {
switch (type.toLowerCase()) {
case 'mobile':
return Colors.blue;
case 'desktop':
return Colors.green;
case 'web':
return Colors.orange;
default:
return Colors.grey;
}
}
IconData _getDeviceTypeIcon(String type) {
switch (type.toLowerCase()) {
case 'mobile':
return Icons.smartphone;
case 'desktop':
return Icons.desktop_windows;
case 'web':
return Icons.language;
default:
return Icons.device_unknown;
}
}
String _formatLastSeen(DateTime lastSeen) {
final now = DateTime.now();
final difference = now.difference(lastSeen);
if (difference.inMinutes < 1) return 'just now';
if (difference.inMinutes < 60) return '${difference.inMinutes}m ago';
if (difference.inHours < 24) return '${difference.inHours}h ago';
if (difference.inDays < 7) return '${difference.inDays}d ago';
return '${lastSeen.day}/${lastSeen.month}';
}
} }

View file

@ -11,7 +11,7 @@ class RepostService {
static const Duration _cacheExpiry = Duration(minutes: 5); static const Duration _cacheExpiry = Duration(minutes: 5);
/// Create a new repost /// Create a new repost
static Future<Repost?> createRepost({ Future<Repost?> createRepost({
required String originalPostId, required String originalPostId,
required RepostType type, required RepostType type,
String? comment, String? comment,
@ -35,7 +35,7 @@ class RepostService {
} }
/// Boost a post (amplify its reach) /// Boost a post (amplify its reach)
static Future<bool> boostPost({ Future<bool> boostPost({
required String postId, required String postId,
required RepostType boostType, required RepostType boostType,
int? boostAmount, int? boostAmount,
@ -55,7 +55,7 @@ class RepostService {
} }
/// Get all reposts for a post /// Get all reposts for a post
static Future<List<Repost>> getRepostsForPost(String postId) async { Future<List<Repost>> getRepostsForPost(String postId) async {
try { try {
final response = await ApiService.instance.get('/posts/$postId/reposts'); final response = await ApiService.instance.get('/posts/$postId/reposts');
@ -70,7 +70,7 @@ class RepostService {
} }
/// Get user's repost history /// Get user's repost history
static Future<List<Repost>> getUserReposts(String userId, {int limit = 20}) async { Future<List<Repost>> getUserReposts(String userId, {int limit = 20}) async {
try { try {
final response = await ApiService.instance.get('/users/$userId/reposts?limit=$limit'); final response = await ApiService.instance.get('/users/$userId/reposts?limit=$limit');
@ -85,7 +85,7 @@ class RepostService {
} }
/// Delete a repost /// Delete a repost
static Future<bool> deleteRepost(String repostId) async { Future<bool> deleteRepost(String repostId) async {
try { try {
final response = await ApiService.instance.delete('/reposts/$repostId'); final response = await ApiService.instance.delete('/reposts/$repostId');
return response['success'] == true; return response['success'] == true;
@ -96,7 +96,7 @@ class RepostService {
} }
/// Get amplification analytics for a post /// Get amplification analytics for a post
static Future<AmplificationAnalytics?> getAmplificationAnalytics(String postId) async { Future<AmplificationAnalytics?> getAmplificationAnalytics(String postId) async {
try { try {
final response = await ApiService.instance.get('/posts/$postId/amplification'); final response = await ApiService.instance.get('/posts/$postId/amplification');
@ -110,7 +110,7 @@ class RepostService {
} }
/// Get trending posts based on amplification /// Get trending posts based on amplification
static Future<List<Post>> getTrendingPosts({int limit = 10, String? category}) async { Future<List<Post>> getTrendingPosts({int limit = 10, String? category}) async {
try { try {
String url = '/posts/trending?limit=$limit'; String url = '/posts/trending?limit=$limit';
if (category != null) { if (category != null) {
@ -130,7 +130,7 @@ class RepostService {
} }
/// Get amplification rules /// Get amplification rules
static Future<List<FeedAmplificationRule>> getAmplificationRules() async { Future<List<FeedAmplificationRule>> getAmplificationRules() async {
try { try {
final response = await ApiService.instance.get('/amplification/rules'); final response = await ApiService.instance.get('/amplification/rules');
@ -145,7 +145,7 @@ class RepostService {
} }
/// Calculate amplification score for a post /// Calculate amplification score for a post
static Future<int> calculateAmplificationScore(String postId) async { Future<int> calculateAmplificationScore(String postId) async {
try { try {
final response = await ApiService.instance.post('/posts/$postId/calculate-score', {}); final response = await ApiService.instance.post('/posts/$postId/calculate-score', {});
@ -159,7 +159,7 @@ class RepostService {
} }
/// Check if user can boost a post /// Check if user can boost a post
static Future<bool> canBoostPost(String userId, String postId, RepostType boostType) async { Future<bool> canBoostPost(String userId, String postId, RepostType boostType) async {
try { try {
final response = await ApiService.instance.get('/users/$userId/can-boost/$postId?type=${boostType.name}'); final response = await ApiService.instance.get('/users/$userId/can-boost/$postId?type=${boostType.name}');
@ -171,7 +171,7 @@ class RepostService {
} }
/// Get user's daily boost count /// Get user's daily boost count
static Future<Map<RepostType, int>> getDailyBoostCount(String userId) async { Future<Map<RepostType, int>> getDailyBoostCount(String userId) async {
try { try {
final response = await ApiService.instance.get('/users/$userId/daily-boosts'); final response = await ApiService.instance.get('/users/$userId/daily-boosts');
@ -193,7 +193,7 @@ class RepostService {
} }
/// Report inappropriate repost /// Report inappropriate repost
static Future<bool> reportRepost(String repostId, String reason) async { Future<bool> reportRepost(String repostId, String reason) async {
try { try {
final response = await ApiService.instance.post('/reposts/$repostId/report', { final response = await ApiService.instance.post('/reposts/$repostId/report', {
'reason': reason, 'reason': reason,
@ -229,10 +229,11 @@ final trendingPostsProvider = FutureProvider.family<List<Post>, Map<String, dyna
return service.getTrendingPosts(limit: limit, category: category); return service.getTrendingPosts(limit: limit, category: category);
}); });
class RepostController extends StateNotifier<RepostState> { class RepostController extends Notifier<RepostState> {
final RepostService _service; @override
RepostState build() => const RepostState();
RepostController(this._service) : super(const RepostState()); RepostService get _service => ref.read(repostServiceProvider);
Future<void> createRepost({ Future<void> createRepost({
required String originalPostId, required String originalPostId,
@ -357,7 +358,4 @@ class RepostState {
} }
} }
final repostControllerProvider = StateNotifierProvider<RepostController, RepostState>((ref) { final repostControllerProvider = NotifierProvider<RepostController, RepostState>(RepostController.new);
final service = ref.watch(repostServiceProvider);
return RepostController(service);
});

View file

@ -4,6 +4,7 @@ import 'package:sojorn/models/repost.dart';
import 'package:sojorn/models/post.dart'; import 'package:sojorn/models/post.dart';
import 'package:sojorn/services/repost_service.dart'; import 'package:sojorn/services/repost_service.dart';
import 'package:sojorn/providers/api_provider.dart'; import 'package:sojorn/providers/api_provider.dart';
import 'package:timeago/timeago.dart' as timeago;
import '../../theme/app_theme.dart'; import '../../theme/app_theme.dart';
class RepostWidget extends ConsumerWidget { class RepostWidget extends ConsumerWidget {
@ -41,13 +42,13 @@ class RepostWidget extends ConsumerWidget {
children: [ children: [
// Repost header // Repost header
if (repost != null) if (repost != null)
_buildRepostHeader(repost), _buildRepostHeader(repost!),
// Original post content // Original post content
_buildOriginalPost(), _buildOriginalPost(),
// Engagement actions // Engagement actions
_buildEngagementActions(repostController), _buildEngagementActions(context, repostController),
// Analytics section // Analytics section
if (showAnalytics) if (showAnalytics)
@ -164,10 +165,10 @@ class RepostWidget extends ConsumerWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 20, radius: 20,
backgroundImage: originalPost.authorAvatar != null backgroundImage: originalPost.author?.avatarUrl != null
? NetworkImage(originalPost.authorAvatar!) ? NetworkImage(originalPost.author!.avatarUrl!)
: null, : null,
child: originalPost.authorAvatar == null child: originalPost.author?.avatarUrl == null
? const Icon(Icons.person, color: Colors.white) ? const Icon(Icons.person, color: Colors.white)
: null, : null,
), ),
@ -177,7 +178,7 @@ class RepostWidget extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
originalPost.authorHandle, originalPost.author?.handle ?? 'unknown',
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14, fontSize: 14,
@ -185,7 +186,7 @@ class RepostWidget extends ConsumerWidget {
), ),
), ),
Text( Text(
originalPost.timeAgo, timeago.format(originalPost.createdAt),
style: TextStyle( style: TextStyle(
color: Colors.grey[400], color: Colors.grey[400],
fontSize: 12, fontSize: 12,
@ -251,7 +252,7 @@ class RepostWidget extends ConsumerWidget {
); );
} }
Widget _buildEngagementActions(RepostController repostController) { Widget _buildEngagementActions(BuildContext context, RepostState repostState) {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -269,14 +270,14 @@ class RepostWidget extends ConsumerWidget {
children: [ children: [
_buildEngagementStat( _buildEngagementStat(
icon: Icons.repeat, icon: Icons.repeat,
count: originalPost.repostCount ?? 0, count: 0,
label: 'Reposts', label: 'Reposts',
onTap: onRepost, onTap: onRepost,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
_buildEngagementStat( _buildEngagementStat(
icon: Icons.rocket_launch, icon: Icons.rocket_launch,
count: originalPost.boostCount ?? 0, count: 0,
label: 'Boosts', label: 'Boosts',
onTap: onBoost, onTap: onBoost,
), ),
@ -329,17 +330,17 @@ class RepostWidget extends ConsumerWidget {
], ],
), ),
if (repostController.isLoading) if (repostState.isLoading)
const Padding( const Padding(
padding: EdgeInsets.only(top: 12), padding: EdgeInsets.only(top: 12),
child: LinearProgressIndicator(color: Colors.blue), child: LinearProgressIndicator(color: Colors.blue),
), ),
if (repostController.error != null) if (repostState.error != null)
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text( child: Text(
repostController.error!, repostState.error!,
style: const TextStyle(color: Colors.red, fontSize: 12), style: const TextStyle(color: Colors.red, fontSize: 12),
), ),
), ),

View file

@ -141,10 +141,7 @@ class _sojornSwipeablePostState extends ConsumerState<sojornSwipeablePost> {
); );
if (!mounted) return; if (!mounted) return;
setState(() => _visibility = newVisibility); setState(() => _visibility = newVisibility);
}
// Update allowChain setting when API supports it
// For now, just show success message
_updateChainSetting(newVisibility);
sojornSnackbar.showSuccess( sojornSnackbar.showSuccess(
context: context, context: context,
@ -605,10 +602,4 @@ class _ActionButton extends StatelessWidget {
} }
return count.toString(); return count.toString();
} }
void _updateChainSetting(String visibility) {
// This method will be implemented when the API supports chain settings
// For now, it's a placeholder that will be updated when the backend is ready
print('Chain setting updated to: $visibility');
}
} }

View file

@ -68,7 +68,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
} }
void _onWidgetTapped(ProfileWidget widget, int index) { void _onWidgetTapped(ProfileWidget widget, int index) {
if (!widget.isEditable) return; if (!this.widget.isEditable) return;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -78,10 +78,11 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
} }
Widget _buildWidgetOptions(ProfileWidget widget, int index) { Widget _buildWidgetOptions(ProfileWidget widget, int index) {
final theme = this.widget.theme;
return Container( return Container(
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: widget.theme.backgroundColor, color: theme.backgroundColor,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
@ -91,7 +92,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: widget.theme.primaryColor.withOpacity(0.1), color: theme.primaryColor.withOpacity(0.1),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
topRight: Radius.circular(16), topRight: Radius.circular(16),
@ -101,7 +102,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
children: [ children: [
Icon( Icon(
widget.type.icon, widget.type.icon,
color: widget.theme.primaryColor, color: theme.primaryColor,
size: 24, size: 24,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -109,7 +110,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
child: Text( child: Text(
widget.type.displayName, widget.type.displayName,
style: TextStyle( style: TextStyle(
color: widget.theme.textColor, color: theme.textColor,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -119,7 +120,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
icon: Icon( icon: Icon(
Icons.close, Icons.close,
color: widget.theme.textColor, color: theme.textColor,
), ),
), ),
], ],
@ -133,14 +134,14 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
children: [ children: [
// Remove widget // Remove widget
ListTile( ListTile(
leading: Icon( leading: const Icon(
Icons.delete_outline, Icons.delete_outline,
color: Colors.red, color: Colors.red,
), ),
title: Text( title: Text(
'Remove Widget', 'Remove Widget',
style: TextStyle( style: TextStyle(
color: widget.theme.textColor, color: theme.textColor,
), ),
), ),
onTap: () { onTap: () {
@ -154,12 +155,12 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
ListTile( ListTile(
leading: Icon( leading: Icon(
Icons.edit, Icons.edit,
color: widget.theme.primaryColor, color: theme.primaryColor,
), ),
title: Text( title: Text(
'Edit Widget', 'Edit Widget',
style: TextStyle( style: TextStyle(
color: widget.theme.textColor, color: theme.textColor,
), ),
), ),
onTap: () { onTap: () {
@ -173,12 +174,12 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
ListTile( ListTile(
leading: Icon( leading: Icon(
Icons.keyboard_arrow_up, Icons.keyboard_arrow_up,
color: widget.theme.primaryColor, color: theme.primaryColor,
), ),
title: Text( title: Text(
'Move to Top', 'Move to Top',
style: TextStyle( style: TextStyle(
color: widget.theme.textColor, color: theme.textColor,
), ),
), ),
onTap: () { onTap: () {
@ -191,12 +192,12 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
ListTile( ListTile(
leading: Icon( leading: Icon(
Icons.keyboard_arrow_down, Icons.keyboard_arrow_down,
color: widget.theme.primaryColor, color: theme.primaryColor,
), ),
title: Text( title: Text(
'Move to Bottom', 'Move to Bottom',
style: TextStyle( style: TextStyle(
color: widget.theme.textColor, color: theme.textColor,
), ),
), ),
onTap: () { onTap: () {
@ -229,7 +230,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
_widgets.removeAt(index); _widgets.removeAt(index);
_updateOrderValues(); _updateOrderValues();
}); });
widget.onWidgetRemoved?.call(widget); this.widget.onWidgetRemoved?.call(widget);
} }
void _editWidget(ProfileWidget widget, int index) { void _editWidget(ProfileWidget widget, int index) {
@ -256,7 +257,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
setState(() { setState(() {
_widgets[index] = updatedWidget; _widgets[index] = updatedWidget;
}); });
widget.onWidgetAdded?.call(updatedWidget); this.widget.onWidgetAdded?.call(updatedWidget);
}, },
), ),
); );
@ -271,7 +272,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
setState(() { setState(() {
_widgets[index] = updatedWidget; _widgets[index] = updatedWidget;
}); });
widget.onWidgetAdded?.call(updatedWidget); this.widget.onWidgetAdded?.call(updatedWidget);
}, },
), ),
); );
@ -286,7 +287,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
setState(() { setState(() {
_widgets[index] = updatedWidget; _widgets[index] = updatedWidget;
}); });
widget.onWidgetAdded?.call(updatedWidget); this.widget.onWidgetAdded?.call(updatedWidget);
}, },
), ),
); );
@ -528,15 +529,15 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
onReorder: widget.isEditable ? _onWidgetReordered : null, onReorder: widget.isEditable ? _onWidgetReordered : null,
itemCount: _widgets.length, itemCount: _widgets.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final widget = _widgets[index]; final pw = _widgets[index];
final size = ProfileWidgetConstraints.getWidgetSize(widget.type); final size = ProfileWidgetConstraints.getWidgetSize(pw.type);
return ReorderableDelayedDragStartListener( return ReorderableDelayedDragStartListener(
key: ValueKey(widget.id), key: ValueKey(pw.id),
index: index, index: index,
child: widget.isEditable child: widget.isEditable
? Draggable<ProfileWidget>( ? Draggable<ProfileWidget>(
data: widget, data: pw,
feedback: Container( feedback: Container(
width: size.width, width: size.width,
height: size.height, height: size.height,
@ -553,7 +554,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
), ),
child: Center( child: Center(
child: Icon( child: Icon(
widget.type.icon, pw.type.icon,
color: Colors.white, color: Colors.white,
size: 24, size: 24,
), ),
@ -572,15 +573,15 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
), ),
), ),
child: ProfileWidgetRenderer( child: ProfileWidgetRenderer(
widget: widget, widget: pw,
theme: widget.theme, theme: widget.theme,
onTap: () => _onWidgetTapped(widget, index), onTap: () => _onWidgetTapped(pw, index),
), ),
) )
: ProfileWidgetRenderer( : ProfileWidgetRenderer(
widget: widget, widget: pw,
theme: widget.theme, theme: widget.theme,
onTap: () => _onWidgetTapped(widget, index), onTap: () => _onWidgetTapped(pw, index),
), ),
); );
}, },