Skip to content
Merged
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
97 changes: 85 additions & 12 deletions lib/config/providers/chat_input_provider.dart
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:whitenoise/config/providers/active_pubkey_provider.dart';
import 'package:whitenoise/domain/models/media_file_upload.dart';
import 'package:whitenoise/domain/services/draft_message_service.dart';
import 'package:whitenoise/domain/services/image_picker_service.dart';
import 'package:whitenoise/src/rust/api/media_files.dart' as rust_media_files;
import 'package:whitenoise/ui/chat/states/chat_input_state.dart';

import 'package:whitenoise/utils/pubkey_formatter.dart';

class ChatInputNotifier extends FamilyNotifier<ChatInputState, String> {
ChatInputNotifier({
ImagePickerService? imagePickerService,
DraftMessageService? draftMessageService,
Duration draftSaveDelay = const Duration(milliseconds: 500),
Future<rust_media_files.MediaFile> Function({
required String accountPubkey,
required String groupId,
required String filePath,
})?
uploadMediaFn,
}) : _imagePickerService = imagePickerService ?? ImagePickerService(),
_draftMessageService = draftMessageService ?? DraftMessageService(),
_draftSaveDelay = draftSaveDelay;
_draftSaveDelay = draftSaveDelay,
_uploadMediaFn = uploadMediaFn ?? rust_media_files.uploadChatMedia;

static final _logger = Logger('ChatInputNotifier');
late final String _groupId;
final ImagePickerService _imagePickerService;
final DraftMessageService _draftMessageService;
final Duration _draftSaveDelay;
final Future<rust_media_files.MediaFile> Function({
required String accountPubkey,
required String groupId,
required String filePath,
})
_uploadMediaFn;
Timer? _draftSaveTimer;

@override
Expand Down Expand Up @@ -66,30 +84,85 @@ class ChatInputNotifier extends FamilyNotifier<ChatInputState, String> {
}

Future<void> handleImagesSelected() async {
final accountPubkey = ref.read(activePubkeyProvider);
final accountHexPubkey = PubkeyFormatter(pubkey: accountPubkey).toHex() ?? '';
if (accountHexPubkey.isEmpty) {
state = state.copyWith(showMediaSelector: false);
return;
}
try {
final imagePaths = await _imagePickerService.pickMultipleImages();
if (imagePaths.isNotEmpty) {
state = state.copyWith(
showMediaSelector: false,
selectedImages: [...state.selectedImages, ...imagePaths],
);
} else {
if (imagePaths.isEmpty) {
state = state.copyWith(showMediaSelector: false);
return;
}
final uploadingItems =
imagePaths
.map(
(path) => MediaFileUpload.uploading(filePath: path),
)
.toList();

state = state.copyWith(
showMediaSelector: false,
selectedMedia: [...state.selectedMedia, ...uploadingItems],
);
for (final path in imagePaths) {
unawaited(_uploadImage(filePath: path, accountHexPubkey: accountHexPubkey));
}
} catch (e) {
_logger.warning('Failed to select images for group $_groupId', e);
state = state.copyWith(showMediaSelector: false);
return;
}
}

Future<void> _uploadImage({required String filePath, required String accountHexPubkey}) async {
try {
final mediaFile = await _uploadMediaFn(
accountPubkey: accountHexPubkey,
groupId: _groupId,
filePath: filePath,
);
final updatedMedia =
state.selectedMedia.map((item) {
return item.maybeWhen(
uploading:
(path) =>
path == filePath
? MediaFileUpload.uploaded(file: mediaFile, originalFilePath: filePath)
: item,
orElse: () => item,
);
}).toList();

state = state.copyWith(selectedMedia: updatedMedia);
} catch (e, st) {
_logger.severe('Failed to upload image: $filePath', e, st);
final updatedMedia =
state.selectedMedia.map((item) {
return item.maybeWhen(
uploading:
(path) =>
path == filePath
? MediaFileUpload.failed(filePath: path, error: e.toString())
: item,
orElse: () => item,
);
}).toList();

state = state.copyWith(selectedMedia: updatedMedia);
}
}

void removeImage(int index) {
if (index < 0 || index >= state.selectedImages.length) {
if (index < 0 || index >= state.selectedMedia.length) {
_logger.warning('Invalid image index: $index');
return;
}
final updatedImages = List<String>.from(state.selectedImages);
updatedImages.removeAt(index);
state = state.copyWith(selectedImages: updatedImages);
final updatedMedia = List<MediaFileUpload>.from(state.selectedMedia);
updatedMedia.removeAt(index);
state = state.copyWith(selectedMedia: updatedMedia);
}

void setSingleLineHeight(double height) {
Expand All @@ -109,7 +182,7 @@ class ChatInputNotifier extends FamilyNotifier<ChatInputState, String> {
showMediaSelector: false,
isLoadingDraft: false,
previousEditingMessageContent: null,
selectedImages: [],
selectedMedia: [],
);
}
}
Expand Down
3 changes: 2 additions & 1 deletion lib/ui/chat/states/chat_input_state.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:whitenoise/domain/models/media_file_upload.dart';

part 'chat_input_state.freezed.dart';

Expand All @@ -7,7 +8,7 @@ class ChatInputState with _$ChatInputState {
const factory ChatInputState({
@Default(false) bool isLoadingDraft,
@Default(false) bool showMediaSelector,
@Default([]) List<String> selectedImages,
@Default([]) List<MediaFileUpload> selectedMedia,
double? singleLineHeight,
String? previousEditingMessageContent,
}) = _ChatInputState;
Expand Down
54 changes: 27 additions & 27 deletions lib/ui/chat/states/chat_input_state.freezed.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions lib/ui/chat/widgets/chat_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ class _ChatInputState extends ConsumerState<ChatInput> with WidgetsBindingObserv

final isEditing = chatState.editingMessage[widget.groupId] != null;
final content = _textController.text.trim();
if (content.isEmpty && chatInputState.selectedImages.isEmpty) return;
if (content.isEmpty && chatInputState.selectedMedia.isEmpty) return;

widget.onSend(content, isEditing);

Expand Down Expand Up @@ -234,14 +234,14 @@ class _ChatInputState extends ConsumerState<ChatInput> with WidgetsBindingObserv
},
),
ChatInputMediaPreview(
imagePaths: chatInputState.selectedImages,
mediaItems: chatInputState.selectedMedia,
onRemoveImage: _removeImage,
onAddMore: _handleImagesSelected,
isReply: chatState.replyingTo[widget.groupId] != null,
),
Row(
children: [
if (chatInputState.selectedImages.isEmpty)
if (chatInputState.selectedMedia.isEmpty)
GestureDetector(
onTap: _toggleMediaSelector,
child: Padding(
Expand Down Expand Up @@ -277,7 +277,7 @@ class _ChatInputState extends ConsumerState<ChatInput> with WidgetsBindingObserv
textController: _textController,
singleLineHeight: chatInputState.singleLineHeight,
onSend: _sendMessage,
hasImages: chatInputState.selectedImages.isNotEmpty,
hasImages: chatInputState.selectedMedia.isNotEmpty,
),
],
),
Expand Down
Loading