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
4 changes: 4 additions & 0 deletions assets/svgs/ic_image.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 15 additions & 15 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,21 @@ SPEC CHECKSUMS:
audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe
emoji_picker_flutter: 8e50ec5caac456a23a78637e02c6293ea0ac8771
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
rust_lib_whitenoise: 69ef24b69b2aba78a7ebabc09a504b5a39177d21
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
workmanager_apple: 7bac258335c310689a641e2d66e88d4845d372e9
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
rust_lib_whitenoise: 22de658398f8e36a1a396d35b6b6547a0732e6bb
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778

PODFILE CHECKSUM: e30f02f9d1c72c47bb6344a0a748c9d268180865

Expand Down
119 changes: 119 additions & 0 deletions lib/config/providers/chat_input_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:whitenoise/domain/services/draft_message_service.dart';
import 'package:whitenoise/domain/services/image_picker_service.dart';
import 'package:whitenoise/ui/chat/states/chat_input_state.dart';

class ChatInputNotifier extends FamilyNotifier<ChatInputState, String> {
ChatInputNotifier({
ImagePickerService? imagePickerService,
DraftMessageService? draftMessageService,
Duration draftSaveDelay = const Duration(milliseconds: 500),
}) : _imagePickerService = imagePickerService ?? ImagePickerService(),
_draftMessageService = draftMessageService ?? DraftMessageService(),
_draftSaveDelay = draftSaveDelay;

static final _logger = Logger('ChatInputNotifier');
late final String _groupId;
final ImagePickerService _imagePickerService;
final DraftMessageService _draftMessageService;
final Duration _draftSaveDelay;
Timer? _draftSaveTimer;

@override
ChatInputState build(String groupId) {
_groupId = groupId;
ref.onDispose(() {
_draftSaveTimer?.cancel();
});
return const ChatInputState();
}

Future<String?> loadDraft() async {
state = state.copyWith(isLoadingDraft: true);
try {
final draft = await _draftMessageService.loadDraft(chatId: _groupId);
return draft;
} finally {
state = state.copyWith(isLoadingDraft: false);
}
}

void scheduleDraftSave(String text) {
_draftSaveTimer?.cancel();
_draftSaveTimer = Timer(
_draftSaveDelay,
() => _saveDraft(text),
);
}

Future<void> saveDraftImmediately(String text) async {
_draftSaveTimer?.cancel();
await _saveDraft(text);
}

Future<void> _saveDraft(String text) async {
await _draftMessageService.saveDraft(chatId: _groupId, message: text);
}

void hideMediaSelector() {
state = state.copyWith(showMediaSelector: false);
}

void toggleMediaSelector() {
state = state.copyWith(showMediaSelector: !state.showMediaSelector);
}

Future<void> handleImagesSelected() async {
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider adding error handling incase something goes wrong

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is in the builder if the UI, if an error happens, the image is not shown. Already checked that case in my iphone by hardcoding local paths that don't exist and it works fine

Copy link
Contributor Author

Choose a reason for hiding this comment

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

errorBuilder in ChatInputMediaPreview and in MediaThumbnail

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But now giving a second thought, something else could go wrong before getting to the widget right? I will add error handling here. Thanks

Copy link
Contributor Author

@josefinalliende josefinalliende Oct 17, 2025

Choose a reason for hiding this comment

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

Fixed in c28a784 👍🏻

try {
final imagePaths = await _imagePickerService.pickMultipleImages();
if (imagePaths.isNotEmpty) {
state = state.copyWith(
showMediaSelector: false,
selectedImages: [...state.selectedImages, ...imagePaths],
);
} else {
state = state.copyWith(showMediaSelector: false);
}
} catch (e) {
_logger.warning('Failed to select images for group $_groupId', e);
state = state.copyWith(showMediaSelector: false);
}
}

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

void setSingleLineHeight(double height) {
if (state.singleLineHeight != height) {
state = state.copyWith(singleLineHeight: height);
}
}

void setPreviousEditingMessageContent(String? content) {
state = state.copyWith(previousEditingMessageContent: content);
}

Future<void> clear() async {
_draftSaveTimer?.cancel();
await _draftMessageService.clearDraft(chatId: _groupId);
state = state.copyWith(
showMediaSelector: false,
isLoadingDraft: false,
previousEditingMessageContent: null,
selectedImages: [],
);
}
}

final chatInputProvider = NotifierProvider.family<ChatInputNotifier, ChatInputState, String>(
ChatInputNotifier.new,
);
8 changes: 4 additions & 4 deletions lib/domain/services/draft_message_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class DraftMessageService {
static const FlutterSecureStorage _defaultStorage = FlutterSecureStorage();
static const String _draftPrefix = 'draft_message_';

static Future<void> saveDraft({
Future<void> saveDraft({
required String chatId,
required String message,
FlutterSecureStorage? storage,
Expand All @@ -22,7 +22,7 @@ class DraftMessageService {
}
}

static Future<String?> loadDraft({
Future<String?> loadDraft({
required String chatId,
FlutterSecureStorage? storage,
}) async {
Expand All @@ -35,7 +35,7 @@ class DraftMessageService {
}
}

static Future<void> clearDraft({
Future<void> clearDraft({
required String chatId,
FlutterSecureStorage? storage,
}) async {
Expand All @@ -48,7 +48,7 @@ class DraftMessageService {
}
}

static Future<void> clearAllDrafts({FlutterSecureStorage? storage}) async {
Future<void> clearAllDrafts({FlutterSecureStorage? storage}) async {
final secureStorage = storage ?? _defaultStorage;
try {
final allKeys = await secureStorage.readAll();
Expand Down
15 changes: 15 additions & 0 deletions lib/domain/services/image_picker_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,19 @@ class ImagePickerService {
rethrow;
}
}

Future<List<String>> pickMultipleImages() async {
try {
final List<XFile> images = await _imagePicker.pickMultiImage(
maxWidth: 1920,
maxHeight: 1920,
imageQuality: 85,
);

return images.map((image) => image.path).toList();
} catch (e) {
_logger.severe('Failed to pick images: $e');
rethrow;
}
}
}
3 changes: 2 additions & 1 deletion lib/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@
"removeFromGroupConfirmation": "Bist du sicher, dass du {name} aus der Gruppe entfernen möchtest? Sie verlieren den Zugang zu allen Nachrichten und Gruppenaktivitäten. Du musst sie erneut einladen, wenn du sie zurückholen möchtest.",
"removedFromGroupSuccess": "{name} aus Gruppe entfernt",
"failedToRemoveMember": "Fehler beim Entfernen des Mitglieds",
"noUserToStartChatWith": "Kein Benutzer zum Chatten"
"noUserToStartChatWith": "Kein Benutzer zum Chatten",
"photos": "Fotos"
},
"clipboard": {
"emptyTextError": "Kein Text in der Zwischenablage gefunden",
Expand Down
3 changes: 2 additions & 1 deletion lib/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@
"removeFromGroupConfirmation": "Are you sure you want to remove {name} from the group? They'll lose access to all messages and group activity. You'll need to invite them again if you want them back.",
"removedFromGroupSuccess": "{name} removed from group",
"failedToRemoveMember": "Failed to remove member",
"noUserToStartChatWith": "No user to start chat with"
"noUserToStartChatWith": "No user to start chat with",
"photos": "Photos"
},
"clipboard": {
"emptyTextError": "No text found in clipboard",
Expand Down
3 changes: 2 additions & 1 deletion lib/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@
"removeFromGroupConfirmation": "¿Estás seguro de que quieres remover a {name} del grupo? Perderán acceso a todos los mensajes y actividad del grupo. Tendrás que invitarlos de nuevo si los quieres de vuelta.",
"removedFromGroupSuccess": "{name} removido del grupo",
"failedToRemoveMember": "Error al remover miembro",
"noUserToStartChatWith": "No hay usuario para iniciar chat"
"noUserToStartChatWith": "No hay usuario para iniciar chat",
"photos": "Fotos"
},
"clipboard": {
"emptyTextError": "No se encontró texto en el portapapeles",
Expand Down
3 changes: 2 additions & 1 deletion lib/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@
"removeFromGroupConfirmation": "Êtes-vous sûr de vouloir retirer {name} du groupe ? Ils perdront l'accès à tous les messages et activités du groupe. Vous devrez les inviter à nouveau si vous les voulez de retour.",
"removedFromGroupSuccess": "{name} retiré du groupe",
"failedToRemoveMember": "Échec de la suppression du membre",
"noUserToStartChatWith": "Aucun utilisateur avec qui commencer une conversation"
"noUserToStartChatWith": "Aucun utilisateur avec qui commencer une conversation",
"photos": "Photos"
},
"clipboard": {
"emptyTextError": "Aucun texte trouvé dans le presse-papiers",
Expand Down
3 changes: 2 additions & 1 deletion lib/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@
"removeFromGroupConfirmation": "Sei sicuro di voler rimuovere {name} dal gruppo? Perderanno l'accesso a tutti i messaggi e alle attività del gruppo. Dovrai invitarli di nuovo se li vuoi di ritorno.",
"removedFromGroupSuccess": "{name} rimosso dal gruppo",
"failedToRemoveMember": "Impossibile rimuovere il membro",
"noUserToStartChatWith": "Nessun utente con cui iniziare la chat"
"noUserToStartChatWith": "Nessun utente con cui iniziare la chat",
"photos": "Foto"
},
"clipboard": {
"emptyTextError": "Nessun testo trovato negli appunti",
Expand Down
3 changes: 2 additions & 1 deletion lib/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@
"removeFromGroupConfirmation": "Tem certeza de que deseja remover {name} do grupo? Eles perderão o acesso a todas as mensagens e atividades do grupo. Você precisará convidá-los novamente se quiser que voltem.",
"removedFromGroupSuccess": "{name} removido do grupo",
"failedToRemoveMember": "Falha ao remover membro",
"noUserToStartChatWith": "Nenhum usuário para iniciar conversa"
"noUserToStartChatWith": "Nenhum usuário para iniciar conversa",
"photos": "Fotos"
},
"clipboard": {
"emptyTextError": "Nenhum texto encontrado na área de transferência",
Expand Down
3 changes: 2 additions & 1 deletion lib/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@
"removeFromGroupConfirmation": "Вы уверены, что хотите удалить {name} из группы? Они потеряют доступ ко всем сообщениям и активности группы. Вам нужно будет пригласить их снова, если захотите их вернуть.",
"removedFromGroupSuccess": "{name} удалён из группы",
"failedToRemoveMember": "Не удалось удалить участника",
"noUserToStartChatWith": "Нет пользователя для начала чата"
"noUserToStartChatWith": "Нет пользователя для начала чата",
"photos": "Фото"
},
"clipboard": {
"emptyTextError": "Текст в буфере обмена не найден",
Expand Down
3 changes: 2 additions & 1 deletion lib/locales/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@
"removeFromGroupConfirmation": "{name} adlı kişiyi gruptan çıkarmak istediğinizden emin misiniz? Tüm mesajlara ve grup etkinliklerine erişimlerini kaybedecekler. Onları geri istiyorsanız tekrar davet etmeniz gerekecek.",
"removedFromGroupSuccess": "{name} gruptan çıkarıldı",
"failedToRemoveMember": "Üye çıkarma başarısız",
"noUserToStartChatWith": "Sohbet başlatacak kullanıcı yok"
"noUserToStartChatWith": "Sohbet başlatacak kullanıcı yok",
"photos": "Fotoğraflar"
},
"clipboard": {
"emptyTextError": "Panoda metin bulunamadı",
Expand Down
2 changes: 1 addition & 1 deletion lib/models/relay_status.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ extension RelayStatusExt on RelayStatus {
String getIconAsset() {
switch (this) {
case RelayStatus.connected:
return AssetsPaths.icCheckmarkFilledSvg;
return AssetsPaths.icCheckmarkFilled;
case RelayStatus.connecting:
return AssetsPaths.icInProgress;
case RelayStatus.pending:
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/auth_flow/qr_scanner_bottom_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class _QRScannerBottomSheetState extends State<QRScannerBottomSheet> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
WnImage(
AssetsPaths.icCheckmarkFilledSvg,
AssetsPaths.icCheckmarkFilled,
size: 94.w,
color: context.colors.primary,
),
Expand Down
14 changes: 14 additions & 0 deletions lib/ui/chat/states/chat_input_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';

part 'chat_input_state.freezed.dart';

@freezed
class ChatInputState with _$ChatInputState {
const factory ChatInputState({
@Default(false) bool isLoadingDraft,
@Default(false) bool showMediaSelector,
@Default([]) List<String> selectedImages,
double? singleLineHeight,
String? previousEditingMessageContent,
}) = _ChatInputState;
}
Loading