From f80930685c93274fcb6b3d9f8efb2501d6d9841f Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:15:44 +0530 Subject: [PATCH] refactor: fix lints --- lib/core/providers/app_providers.dart | 45 +++++---- lib/core/services/api_service.dart | 24 ++--- lib/core/services/streaming_helper.dart | 80 +++++++++------- lib/core/utils/reasoning_parser.dart | 26 +++-- lib/core/utils/tool_calls_parser.dart | 32 ++++--- .../chat/providers/chat_providers.dart | 95 +++++++++---------- lib/features/chat/views/chat_page.dart | 29 ------ .../widgets/assistant_message_widget.dart | 82 +++++++++------- .../chat/widgets/enhanced_attachment.dart | 4 +- .../chat/widgets/modern_chat_input.dart | 45 ++++----- .../navigation/widgets/chats_drawer.dart | 69 +++++++------- lib/shared/widgets/measure_size.dart | 12 ++- 12 files changed, 277 insertions(+), 266 deletions(-) diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index aa92067..ac6c74e 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -209,7 +209,9 @@ final socketServiceProvider = Provider((ref) { final activeServer = ref.watch(activeServerProvider); final token = ref.watch(authTokenProvider3); - final transportMode = ref.watch(appSettingsProvider).socketTransportMode; // 'auto' or 'ws' + final transportMode = ref + .watch(appSettingsProvider) + .socketTransportMode; // 'auto' or 'ws' return activeServer.maybeWhen( data: (server) { @@ -223,7 +225,9 @@ final socketServiceProvider = Provider((ref) { // ignore unawaited_futures s.connect(); ref.onDispose(() { - try { s.dispose(); } catch (_) {} + try { + s.dispose(); + } catch (_) {} }); return s; }, @@ -373,7 +377,8 @@ final defaultModelAutoSelectionProvider = Provider((ref) { } // Fallback: keep current selection or pick first available - selected ??= ref.read(selectedModelProvider) ?? + selected ??= + ref.read(selectedModelProvider) ?? (models.isNotEmpty ? models.first : null); if (selected != null) { @@ -481,11 +486,11 @@ final conversationsProvider = FutureProvider>((ref) async { conversationMap[conversation.id] = conversation.copyWith( folderId: folderIdToUse, ); - final _idPreview = conversation.id.length > 8 + final idPreview = conversation.id.length > 8 ? conversation.id.substring(0, 8) : conversation.id; foundation.debugPrint( - 'DEBUG: Updated conversation $_idPreview with folderId: $folderIdToUse (explicit: ${explicitFolderId != null})', + 'DEBUG: Updated conversation $idPreview with folderId: $folderIdToUse (explicit: ${explicitFolderId != null})', ); } else { conversationMap[conversation.id] = conversation; @@ -547,11 +552,11 @@ final conversationsProvider = FutureProvider>((ref) async { // Use map to prevent duplicates - this will overwrite if ID already exists conversationMap[toAdd.id] = toAdd; existingIds.add(toAdd.id); - final _idPreview = toAdd.id.length > 8 + final idPreview = toAdd.id.length > 8 ? toAdd.id.substring(0, 8) : toAdd.id; foundation.debugPrint( - 'DEBUG: Added missing conversation from folder fetch: $_idPreview -> folder ${folder.id}', + 'DEBUG: Added missing conversation from folder fetch: $idPreview -> folder ${folder.id}', ); } else { // Create a minimal placeholder if not returned by folder API @@ -566,11 +571,11 @@ final conversationsProvider = FutureProvider>((ref) async { // Use map to prevent duplicates conversationMap[convId] = placeholder; existingIds.add(convId); - final _idPreview = convId.length > 8 + final idPreview = convId.length > 8 ? convId.substring(0, 8) : convId; foundation.debugPrint( - 'DEBUG: Added placeholder conversation for missing ID: $_idPreview -> folder ${folder.id}', + 'DEBUG: Added placeholder conversation for missing ID: $idPreview -> folder ${folder.id}', ); } } @@ -694,16 +699,18 @@ final defaultModelProvider = FutureProvider((ref) async { if (userDefaultModelId != null && userDefaultModelId.isNotEmpty) { try { // Exact ID match only - selectedModel = - models.firstWhere((model) => model.id == userDefaultModelId); + selectedModel = models.firstWhere( + (model) => model.id == userDefaultModelId, + ); foundation.debugPrint( 'DEBUG: Found user default model by ID: ${selectedModel.name}', ); } catch (e) { // Attempt a one-time migration if the stored value was a model name // from older versions. Only migrate on exact, unique name match. - final nameMatches = - models.where((m) => m.name == userDefaultModelId).toList(); + final nameMatches = models + .where((m) => m.name == userDefaultModelId) + .toList(); if (nameMatches.length == 1) { selectedModel = nameMatches.first; foundation.debugPrint( @@ -719,7 +726,8 @@ final defaultModelProvider = FutureProvider((ref) async { 'DEBUG: User default model "$userDefaultModelId" not found by ID and ' 'no unique name match. Ignoring.', ); - selectedModel = null; // Will fall back to server default or first model + selectedModel = + null; // Will fall back to server default or first model } } } @@ -732,14 +740,17 @@ final defaultModelProvider = FutureProvider((ref) async { if (defaultModelId != null && defaultModelId.isNotEmpty) { // Find the model that matches the default model ID (ID only) try { - selectedModel = - models.firstWhere((model) => model.id == defaultModelId); + selectedModel = models.firstWhere( + (model) => model.id == defaultModelId, + ); foundation.debugPrint( 'DEBUG: Found server default model by ID: ${selectedModel.name}', ); } catch (e) { // If server returned a name instead of ID, attempt exact name match. - final byName = models.where((m) => m.name == defaultModelId).toList(); + final byName = models + .where((m) => m.name == defaultModelId) + .toList(); if (byName.length == 1) { selectedModel = byName.first; foundation.debugPrint( diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index d19fd95..abf6921 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -392,11 +392,11 @@ class ApiService { debugPrint( '🔍 DEBUG: Sample chat data fields: ${chatData.keys.toList()}', ); - final _sampleStr = chatData.toString(); - final _preview = _sampleStr.length > 200 - ? _sampleStr.substring(0, 200) - : _sampleStr; - debugPrint('🔍 DEBUG: Sample chat data: $_preview...'); + final samplePreviewSource = chatData.toString(); + final preview = samplePreviewSource.length > 200 + ? samplePreviewSource.substring(0, 200) + : samplePreviewSource; + debugPrint('🔍 DEBUG: Sample chat data: $preview...'); } final conversation = _parseOpenWebUIChat(chatData); @@ -475,8 +475,8 @@ class ApiService { // Debug logging for folder assignment if (folderId != null) { - final _idPreview = id.length > 8 ? id.substring(0, 8) : id; - debugPrint('🔍 DEBUG: Conversation $_idPreview has folderId: $folderId'); + final idPreview = id.length > 8 ? id.substring(0, 8) : id; + debugPrint('🔍 DEBUG: Conversation $idPreview has folderId: $folderId'); } debugPrint( @@ -3357,11 +3357,11 @@ class ApiService { } else if (response.data is Map) { DebugLogger.log(' Object keys: ${(response.data as Map).keys}'); } - final _dataStr = response.data.toString(); - final _dataPreview = _dataStr.length > 200 - ? _dataStr.substring(0, 200) - : _dataStr; - DebugLogger.log(' Sample data: $_dataPreview...'); + final dataSampleSource = response.data.toString(); + final dataPreview = dataSampleSource.length > 200 + ? dataSampleSource.substring(0, 200) + : dataSampleSource; + DebugLogger.log(' Sample data: $dataPreview...'); } catch (e) { debugPrint('❌ $endpoint - Error: $e'); } diff --git a/lib/core/services/streaming_helper.dart b/lib/core/services/streaming_helper.dart index 88afa08..58b62d1 100644 --- a/lib/core/services/streaming_helper.dart +++ b/lib/core/services/streaming_helper.dart @@ -33,7 +33,8 @@ StreamSubscription attachUnifiedChunkedStreaming({ // Message update callbacks required void Function(String) appendToLastMessage, required void Function(String) replaceLastMessageContent, - required void Function(ChatMessage Function(ChatMessage)) updateLastMessageWith, + required void Function(ChatMessage Function(ChatMessage)) + updateLastMessageWith, required void Function() finishStreaming, required List Function() getMessages, }) { @@ -71,7 +72,7 @@ StreamSubscription attachUnifiedChunkedStreaming({ bool suppressSocketContent = suppressSocketContentInitially; bool usingDynamicChannel = usingDynamicChannelInitially; - void _updateImagesFromCurrentContent() { + void updateImagesFromCurrentContent() { try { final msgs = getMessages(); if (msgs.isEmpty || msgs.last.role != 'assistant') return; @@ -236,13 +237,13 @@ StreamSubscription attachUnifiedChunkedStreaming({ for (final call in tc) { if (call is Map) { final fn = call['function']; - final name = - (fn is Map && fn['name'] is String) - ? fn['name'] as String - : null; + final name = (fn is Map && fn['name'] is String) + ? fn['name'] as String + : null; if (name is String && name.isNotEmpty) { final msgs = getMessages(); - final exists = (msgs.isNotEmpty) && + final exists = + (msgs.isNotEmpty) && RegExp( r']*\bname=\"' + RegExp.escape(name) + @@ -262,20 +263,20 @@ StreamSubscription attachUnifiedChunkedStreaming({ final content = delta['content']?.toString() ?? ''; if (content.isNotEmpty) { appendToLastMessage(content); - _updateImagesFromCurrentContent(); + updateImagesFromCurrentContent(); } } } } catch (_) { if (s.isNotEmpty) { appendToLastMessage(s); - _updateImagesFromCurrentContent(); + updateImagesFromCurrentContent(); } } } else { if (s.isNotEmpty) { appendToLastMessage(s); - _updateImagesFromCurrentContent(); + updateImagesFromCurrentContent(); } } } else if (line is Map) { @@ -320,10 +321,12 @@ StreamSubscription attachUnifiedChunkedStreaming({ : null; if (name is String && name.isNotEmpty) { final msgs = getMessages(); - final exists = (msgs.isNotEmpty) && + final exists = + (msgs.isNotEmpty) && RegExp( r']*\bname=\"' + - RegExp.escape(name) + r'\"', + RegExp.escape(name) + + r'\"', multiLine: true, ).hasMatch(msgs.last.content); if (!exists) { @@ -353,10 +356,12 @@ StreamSubscription attachUnifiedChunkedStreaming({ : null; if (name is String && name.isNotEmpty) { final msgs = getMessages(); - final exists = (msgs.isNotEmpty) && + final exists = + (msgs.isNotEmpty) && RegExp( r']*\bname=\"' + - RegExp.escape(name) + r'\"', + RegExp.escape(name) + + r'\"', multiLine: true, ).hasMatch(msgs.last.content); if (!exists) { @@ -372,7 +377,7 @@ StreamSubscription attachUnifiedChunkedStreaming({ final content = delta['content']?.toString() ?? ''; if (content.isNotEmpty) { appendToLastMessage(content); - _updateImagesFromCurrentContent(); + updateImagesFromCurrentContent(); } } } @@ -409,7 +414,9 @@ StreamSubscription attachUnifiedChunkedStreaming({ final list = chatObj['messages']; if (list is List) { final target = list.firstWhere( - (m) => (m is Map && (m['id']?.toString() == assistantMessageId)), + (m) => + (m is Map && + (m['id']?.toString() == assistantMessageId)), orElse: () => null, ); if (target != null) { @@ -431,7 +438,8 @@ StreamSubscription attachUnifiedChunkedStreaming({ final history = chatObj['history']; if (history is Map && history['messages'] is Map) { final Map messagesMap = - (history['messages'] as Map).cast(); + (history['messages'] as Map) + .cast(); final msg = messagesMap[assistantMessageId]; if (msg is Map) { final rawContent = msg['content']; @@ -454,7 +462,8 @@ StreamSubscription attachUnifiedChunkedStreaming({ replaceLastMessageContent(content); } } - } catch (_) {} finally { + } catch (_) { + } finally { finishStreaming(); } }); @@ -483,21 +492,23 @@ StreamSubscription attachUnifiedChunkedStreaming({ } if (content.isNotEmpty) { // Replace current assistant message with a readable error - replaceLastMessageContent('⚠️ ' + content); + replaceLastMessageContent('⚠️ $content'); } } catch (_) {} // Ensure UI exits streaming state finishStreaming(); - } else if ((type == 'chat:message:delta' || type == 'message') && payload != null) { + } else if ((type == 'chat:message:delta' || type == 'message') && + payload != null) { // Incremental message content over socket; respect suppression on SSE-driven flows if (!suppressSocketContent) { final content = payload['content']?.toString() ?? ''; if (content.isNotEmpty) { appendToLastMessage(content); - _updateImagesFromCurrentContent(); + updateImagesFromCurrentContent(); } } - } else if ((type == 'chat:message' || type == 'replace') && payload != null) { + } else if ((type == 'chat:message' || type == 'replace') && + payload != null) { // Full message replacement over socket; respect suppression on SSE-driven flows if (!suppressSocketContent) { final content = payload['content']?.toString() ?? ''; @@ -600,10 +611,9 @@ StreamSubscription attachUnifiedChunkedStreaming({ } else if (type == 'event:status' && payload != null) { final status = payload['status']?.toString() ?? ''; if (status.isNotEmpty) { - updateLastMessageWith((m) => m.copyWith(metadata: { - ...?m.metadata, - 'status': status, - })); + updateLastMessageWith( + (m) => m.copyWith(metadata: {...?m.metadata, 'status': status}), + ); } } else if (type == 'event:tool' && payload != null) { // Accept files from both 'result' and 'files' @@ -624,7 +634,7 @@ StreamSubscription attachUnifiedChunkedStreaming({ final content = payload['content']?.toString() ?? ''; if (content.isNotEmpty) { appendToLastMessage(content); - _updateImagesFromCurrentContent(); + updateImagesFromCurrentContent(); } } } catch (_) {} @@ -640,7 +650,7 @@ StreamSubscription attachUnifiedChunkedStreaming({ final content = payload['content']?.toString() ?? ''; if (content.isNotEmpty) { appendToLastMessage(content); - _updateImagesFromCurrentContent(); + updateImagesFromCurrentContent(); } } } catch (_) {} @@ -656,7 +666,9 @@ StreamSubscription attachUnifiedChunkedStreaming({ } catch (_) {} try { final msgs = getMessages(); - if (msgs.isNotEmpty && msgs.last.role == 'assistant' && msgs.last.isStreaming) { + if (msgs.isNotEmpty && + msgs.last.role == 'assistant' && + msgs.last.isStreaming) { finishStreaming(); } } catch (_) {} @@ -681,17 +693,21 @@ StreamSubscription attachUnifiedChunkedStreaming({ } } - if (isSearching && (chunk.contains('[/SEARCHING]') || chunk.contains('Search complete'))) { + if (isSearching && + (chunk.contains('[/SEARCHING]') || + chunk.contains('Search complete'))) { isSearching = false; updateLastMessageWith( (message) => message.copyWith(metadata: {'webSearchActive': false}), ); - effectiveChunk = effectiveChunk.replaceAll('[SEARCHING]', '').replaceAll('[/SEARCHING]', ''); + effectiveChunk = effectiveChunk + .replaceAll('[SEARCHING]', '') + .replaceAll('[/SEARCHING]', ''); } if (effectiveChunk.trim().isNotEmpty) { appendToLastMessage(effectiveChunk); - _updateImagesFromCurrentContent(); + updateImagesFromCurrentContent(); } }, onDone: () async { diff --git a/lib/core/utils/reasoning_parser.dart b/lib/core/utils/reasoning_parser.dart index 4871111..b3172db 100644 --- a/lib/core/utils/reasoning_parser.dart +++ b/lib/core/utils/reasoning_parser.dart @@ -49,7 +49,9 @@ class ReasoningParser { if (openingIdx >= 0 && !content.contains('')) { final after = content.substring(openingIdx); // Try to extract optional summary - final summaryMatch = RegExp(r'([^<]*)<\/summary>').firstMatch(after); + final summaryMatch = RegExp( + r'([^<]*)<\/summary>', + ).firstMatch(after); final summary = (summaryMatch?.group(1) ?? '').trim(); final reasoning = after .replaceAll(RegExp(r'^]*>'), '') @@ -80,7 +82,11 @@ class ReasoningParser { for (final pair in tagPairs) { final start = RegExp.escape(pair[0]); final end = RegExp.escape(pair[1]); - final tagRegex = RegExp('($start)([\s\S]*?)($end)', multiLine: true, dotAll: true); + final tagRegex = RegExp( + '($start)(.*?)($end)', + multiLine: true, + dotAll: true, + ); final match = tagRegex.firstMatch(content); if (match != null) { final reasoning = (match.group(2) ?? '').trim(); @@ -144,7 +150,8 @@ class ReasoningParser { if (nextDetails == -1 && nextRawStart == -1) { nextIdx = -1; kind = 'none'; - } else if (nextDetails != -1 && (nextRawStart == -1 || nextDetails < nextRawStart)) { + } else if (nextDetails != -1 && + (nextRawStart == -1 || nextDetails < nextRawStart)) { nextIdx = nextDetails; kind = 'details'; } else { @@ -219,7 +226,9 @@ class ReasoningParser { if (depth != 0) { // Unclosed; treat as streaming partial final after = content.substring(openEnd + 1); - final summaryMatch = RegExp(r'([^<]*)<\/summary>').firstMatch(after); + final summaryMatch = RegExp( + r'([^<]*)<\/summary>', + ).firstMatch(after); final summary = (summaryMatch?.group(1) ?? '').trim(); final reasoning = after .replaceAll(RegExp(r'^\s*[\s\S]*?<\/summary>'), '') @@ -238,8 +247,13 @@ class ReasoningParser { break; } else { // Closed block: extract inner content - final inner = content.substring(openEnd + 1, i - 10); // without - final sumMatch = RegExp(r'([^<]*)<\/summary>').firstMatch(inner); + final inner = content.substring( + openEnd + 1, + i - 10, + ); // without + final sumMatch = RegExp( + r'([^<]*)<\/summary>', + ).firstMatch(inner); final summary = (sumMatch?.group(1) ?? '').trim(); final reasoning = inner .replaceAll(RegExp(r'[\s\S]*?<\/summary>'), '') diff --git a/lib/core/utils/tool_calls_parser.dart b/lib/core/utils/tool_calls_parser.dart index 1d80012..7da2365 100644 --- a/lib/core/utils/tool_calls_parser.dart +++ b/lib/core/utils/tool_calls_parser.dart @@ -47,6 +47,7 @@ class ToolCallsParser { .replaceAll('&', '&') .replaceAll('&', '&'); } + /// Represents a mixed stream of text and tool-call entries in original order /// as they appeared in the content. static List? segments(String content) { @@ -97,7 +98,9 @@ class ToolCallsParser { i = nextOpen + 8; // '' + i = (nextClose != -1) + ? nextClose + 10 + : content.length; // '' } } @@ -105,17 +108,16 @@ class ToolCallsParser { if (isToolCalls) { // Decode attributes for tool call tile - dynamic _decode(String? s) { - if (s == null || s.isEmpty) return null; + dynamic decodeAttribute(String? source) { + if (source == null || source.isEmpty) return null; try { - final unescaped = _unescapeHtml(s); + final unescaped = _unescapeHtml(source); return json.decode(unescaped); } catch (_) { - // If JSON decode fails, return unescaped string for display try { - return _unescapeHtml(s); + return _unescapeHtml(source); } catch (_) { - return s; + return source; } } } @@ -123,9 +125,9 @@ class ToolCallsParser { final id = (attrs['id'] ?? ''); final name = (attrs['name'] ?? 'tool'); final done = (attrs['done'] == 'true'); - final args = _decode(attrs['arguments']); - final result = _decode(attrs['result']); - final files = _decode(attrs['files']); + final args = decodeAttribute(attrs['arguments']); + final result = decodeAttribute(attrs['result']); + final files = decodeAttribute(attrs['files']); segs.add( ToolCallsSegment.entry( @@ -207,7 +209,9 @@ class ToolCallsParser { if (parsed == null) return content; final buf = StringBuffer(); for (final c in parsed.toolCalls) { - buf.writeln(c.done ? 'Tool Executed: ${c.name}' : 'Running tool: ${c.name}…'); + buf.writeln( + c.done ? 'Tool Executed: ${c.name}' : 'Running tool: ${c.name}…', + ); final args = _prettyMaybe(c.arguments, max: 400); final res = _prettyMaybe(c.result, max: 800); if (args.isNotEmpty) { @@ -239,8 +243,8 @@ class ToolCallsParser { /// Sanitize assistant/user content before sending to the API, mirroring /// the web client's `processDetails` behavior: - /// - Remove
and
blocks - /// - Replace
...
blocks with the + /// - Remove <details type="reasoning"> and <details type="code_interpreter"> blocks + /// - Replace <details type="tool_calls" ...>...</details> blocks with the /// JSON-serialized `result` attribute (as a quoted string) when available; /// otherwise replace with an empty string. static String sanitizeForApi(String content) { @@ -251,7 +255,7 @@ class ToolCallsParser { for (final t in removeTypes) { content = content.replaceAll( RegExp( - ']*>[\\s\\S]*?<\\/details>', + ']*>[\\s\\S]*?
', multiLine: true, dotAll: true, ), diff --git a/lib/features/chat/providers/chat_providers.dart b/lib/features/chat/providers/chat_providers.dart index 3cb161a..6bc0677 100644 --- a/lib/features/chat/providers/chat_providers.dart +++ b/lib/features/chat/providers/chat_providers.dart @@ -142,7 +142,7 @@ class ChatMessagesNotifier extends StateNotifier> { final msgId = last.id; final chatId = activeConv?.id; if (apiSvc != null && chatId != null && chatId.isNotEmpty) { - final resp = await apiSvc.dio.get('/api/v1/chats/' + chatId); + final resp = await apiSvc.dio.get('/api/v1/chats/$chatId'); final data = resp.data as Map; String content = ''; final chatObj = data['chat'] as Map?; @@ -948,11 +948,11 @@ Future regenerateMessage( final bool isBackgroundWebSearchPre = webSearchEnabled; // Dispatch using unified send pipeline (background tools flow) - final bool _isBackgroundFlowPre = + final bool isBackgroundFlowPre = isBackgroundToolsFlowPre || isBackgroundWebSearchPre || imageGenerationEnabled; - final bool _passSocketSession = wantSessionBinding && _isBackgroundFlowPre; + final bool passSocketSession = wantSessionBinding && isBackgroundFlowPre; final response = api!.sendMessage( messages: conversationMessages, model: selectedModel.id, @@ -961,7 +961,7 @@ Future regenerateMessage( enableWebSearch: webSearchEnabled, enableImageGeneration: imageGenerationEnabled, modelItem: modelItem, - sessionIdOverride: _passSocketSession ? socketSessionId : null, + sessionIdOverride: passSocketSession ? socketSessionId : null, toolServers: toolServers, backgroundTasks: bgTasks, responseMessageId: assistantMessageId, @@ -971,7 +971,7 @@ Future regenerateMessage( final sessionId = response.sessionId; // New unified streaming path via helper; bypass old inline socket block - final bool _isBackgroundFlow = + final bool isBackgroundFlow = isBackgroundToolsFlowPre || isBackgroundWebSearchPre || imageGenerationEnabled || @@ -982,7 +982,7 @@ Future regenerateMessage( ) { final mergedMeta = { if (m.metadata != null) ...m.metadata!, - 'backgroundFlow': _isBackgroundFlow, + 'backgroundFlow': isBackgroundFlow, if (isBackgroundWebSearchPre) 'webSearchFlow': true, if (imageGenerationEnabled) 'imageGenerationFlow': true, }; @@ -990,11 +990,11 @@ Future regenerateMessage( }); } catch (_) {} - final _sendStreamSub = attachUnifiedChunkedStreaming( + final sendStreamSub = attachUnifiedChunkedStreaming( stream: stream, webSearchEnabled: webSearchEnabled, - isBackgroundFlow: _isBackgroundFlow, - suppressSocketContentInitially: !_isBackgroundFlow, + isBackgroundFlow: isBackgroundFlow, + suppressSocketContentInitially: !isBackgroundFlow, usingDynamicChannelInitially: false, assistantMessageId: assistantMessageId, modelId: selectedModel.id, @@ -1014,7 +1014,7 @@ Future regenerateMessage( ref.read(chatMessagesProvider.notifier).finishStreaming(), getMessages: () => ref.read(chatMessagesProvider), ); - ref.read(chatMessagesProvider.notifier).setMessageStream(_sendStreamSub); + ref.read(chatMessagesProvider.notifier).setMessageStream(sendStreamSub); return; } catch (e) { rethrow; @@ -1482,7 +1482,7 @@ Future _sendMessageInternal( if (socketService != null) { // Activity-based watchdog for chat/channel events (resets on activity) - final _chatWatchdog = InactivityWatchdog( + final chatWatchdog = InactivityWatchdog( window: const Duration(minutes: 5), onTimeout: () { try { @@ -1510,7 +1510,7 @@ Future _sendMessageInternal( DebugLogger.stream('Socket chat-events: type=$type'); // Any chat event indicates activity; reset inactivity watchdog // (watchdog defined below, near handler registration) - _chatWatchdog.ping(); + chatWatchdog.ping(); if (type == 'chat:completion' && payload != null) { if (payload is Map) { // Provider may emit tool_calls at the top level @@ -1529,9 +1529,7 @@ Future _sendMessageInternal( final exists = (msgs.isNotEmpty) && RegExp( - r']*\bname=\"' + - RegExp.escape(name) + - r'\"', + ']*\\bname="${RegExp.escape(name)}"', multiLine: true, ).hasMatch(msgs.last.content); if (!exists) { @@ -1567,9 +1565,7 @@ Future _sendMessageInternal( final exists = (msgs.isNotEmpty) && RegExp( - r']*\bname=\"' + - RegExp.escape(name) + - r'\"', + ']*\\bname="${RegExp.escape(name)}"', multiLine: true, ).hasMatch(msgs.last.content); if (!exists) { @@ -1628,8 +1624,8 @@ Future _sendMessageInternal( socketService.offChatEvents(); } catch (_) {} try { - _chatWatchdog.ping(); // ensure timer exists - _chatWatchdog.stop(); + chatWatchdog.ping(); // ensure timer exists + chatWatchdog.stop(); } catch (_) {} // Notify server that chat is completed (mirrors web client) @@ -1744,7 +1740,7 @@ Future _sendMessageInternal( // Normal path: finish now ref.read(chatMessagesProvider.notifier).finishStreaming(); try { - _chatWatchdog.stop(); + chatWatchdog.stop(); } catch (_) {} } } @@ -1767,7 +1763,7 @@ Future _sendMessageInternal( final s = line.trim(); // Dynamic channel activity try { - _chatWatchdog.ping(); + chatWatchdog.ping(); } catch (_) {} DebugLogger.stream( 'Socket [$channel] line=${s.length > 160 ? '${s.substring(0, 160)}…' : s}', @@ -1850,9 +1846,7 @@ Future _sendMessageInternal( final exists = (msgs.isNotEmpty) && RegExp( - r']*\\bname=\"' + - RegExp.escape(name) + - r'\"', + ']*\\bname="${RegExp.escape(name)}"', multiLine: true, ).hasMatch(msgs.last.content); if (!exists) { @@ -1944,7 +1938,7 @@ Future _sendMessageInternal( if (content.isNotEmpty) { ref .read(chatMessagesProvider.notifier) - .replaceLastMessageContent('⚠️ ' + content); + .replaceLastMessageContent('⚠️ $content'); } } catch (_) {} ref.read(chatMessagesProvider.notifier).finishStreaming(); @@ -2060,7 +2054,7 @@ Future _sendMessageInternal( .read(chatMessagesProvider.notifier) .appendToLastMessage(content); _updateImagesFromCurrentContent(ref); - _chatWatchdog.ping(); + chatWatchdog.ping(); } } } catch (_) {} @@ -2068,7 +2062,7 @@ Future _sendMessageInternal( socketService.onChannelEvents(channelEventsHandler); // Start activity watchdog - _chatWatchdog.ping(); + chatWatchdog.ping(); } // Prepare streaming and background handling @@ -2123,14 +2117,14 @@ Future _sendMessageInternal( // Helpers were defined above - int _chunkSeq = 0; + int chunkSeq = 0; final streamSubscription = persistentController.stream.listen( (chunk) { - _chunkSeq += 1; + chunkSeq += 1; try { persistentService.updateStreamProgress( streamId, - chunkSequence: _chunkSeq, + chunkSequence: chunkSeq, appendedContent: chunk, ); } catch (_) {} @@ -3030,7 +3024,7 @@ void _attachSocketStreamingHandlers({ final api = ref.read(apiServiceProvider); // Activity-based watchdog for socket-driven streaming (resets on activity) - final _socketWatchdog = InactivityWatchdog( + final socketWatchdog = InactivityWatchdog( window: const Duration(minutes: 5), onTimeout: () { try { @@ -3054,7 +3048,7 @@ void _attachSocketStreamingHandlers({ if (line is String) { final s = line.trim(); // Any socket line is activity - _socketWatchdog.ping(); + socketWatchdog.ping(); if (s == '[DONE]' || s == 'DONE') { try { socketService.offEvent(channel); @@ -3072,7 +3066,7 @@ void _attachSocketStreamingHandlers({ ); } catch (_) {} ref.read(chatMessagesProvider.notifier).finishStreaming(); - _socketWatchdog.stop(); + socketWatchdog.stop(); return; } if (s.startsWith('data:')) { @@ -3094,7 +3088,7 @@ void _attachSocketStreamingHandlers({ ); } catch (_) {} ref.read(chatMessagesProvider.notifier).finishStreaming(); - _socketWatchdog.stop(); + socketWatchdog.stop(); return; } try { @@ -3118,9 +3112,7 @@ void _attachSocketStreamingHandlers({ final exists = (msgs.isNotEmpty) && RegExp( - r']*\bname=\"' + - RegExp.escape(name) + - r'\"', + ']*\\bname="${RegExp.escape(name)}"', multiLine: true, ).hasMatch(msgs.last.content); if (!exists) { @@ -3157,13 +3149,13 @@ void _attachSocketStreamingHandlers({ } } } else if (line is Map) { - _socketWatchdog.ping(); + socketWatchdog.ping(); if (line['done'] == true) { try { socketService.offEvent(channel); } catch (_) {} ref.read(chatMessagesProvider.notifier).finishStreaming(); - _socketWatchdog.stop(); + socketWatchdog.stop(); return; } } @@ -3172,7 +3164,7 @@ void _attachSocketStreamingHandlers({ socketService.onEvent(channel, handler); // Start activity watchdog now that handler is attached - _socketWatchdog.ping(); + socketWatchdog.ping(); } void chatHandler(Map ev) { @@ -3198,9 +3190,7 @@ void _attachSocketStreamingHandlers({ final exists = (msgs.isNotEmpty) && RegExp( - r']*\bname=\"' + - RegExp.escape(name) + - r'\"', + ']*\\bname="${RegExp.escape(name)}"', multiLine: true, ).hasMatch(msgs.last.content); if (!exists) { @@ -3235,9 +3225,7 @@ void _attachSocketStreamingHandlers({ final exists = (msgs.isNotEmpty) && RegExp( - r']*\bname=\"' + - RegExp.escape(name) + - r'\"', + ']*\\bname="${RegExp.escape(name)}"', multiLine: true, ).hasMatch(msgs.last.content); if (!exists) { @@ -3267,7 +3255,7 @@ void _attachSocketStreamingHandlers({ socketService.offChatEvents(); } catch (_) {} try { - _socketWatchdog.stop(); + socketWatchdog.stop(); } catch (_) {} try { unawaited( @@ -3421,7 +3409,7 @@ void _attachSocketStreamingHandlers({ socketService.onChatEvents(chatHandler); socketService.onChannelEvents(channelEventsHandler); // Start activity watchdog for chat/channel events - _socketWatchdog.ping(); + socketWatchdog.ping(); } // ========== Tool Servers (OpenAPI) Helpers ========== @@ -3495,8 +3483,9 @@ Map? _resolveRef( final section = components?[type]; if (section is Map) { final schema = section[name]; - if (schema is Map) + if (schema is Map) { return Map.from(schema); + } } return null; } @@ -3515,12 +3504,14 @@ Map _resolveSchemaSimple( final out = {}; if (type is String) { out['type'] = type; - if (schema['description'] != null) + if (schema['description'] != null) { out['description'] = schema['description']; + } if (type == 'object') { out['properties'] = {}; - if (schema['required'] is List) + if (schema['required'] is List) { out['required'] = List.from(schema['required']); + } final props = schema['properties']; if (props is Map) { props.forEach((k, v) { diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 985c59f..67fca0f 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -557,15 +557,6 @@ class _ChatPageState extends ConsumerState { }); } - // TODO: Implement select all functionality when needed - // void _selectAllMessages() { - // final messages = ref.read(chatMessagesProvider); - // setState(() { - // _selectedMessageIds.clear(); - // _selectedMessageIds.addAll(messages.map((m) => m.id)); - // }); - // } - void _clearSelection() { setState(() { _selectedMessageIds.clear(); @@ -752,8 +743,6 @@ class _ChatPageState extends ConsumerState { modelName: displayModelName, onCopy: () => _copyMessage(message.content), onRegenerate: () => _regenerateMessage(message), - onLike: () => _likeMessage(message), - onDislike: () => _dislikeMessage(message), ); } else { messageWidget = assistant.AssistantMessageWidget( @@ -763,8 +752,6 @@ class _ChatPageState extends ConsumerState { modelName: displayModelName, onCopy: () => _copyMessage(message.content), onRegenerate: () => _regenerateMessage(message), - onLike: () => _likeMessage(message), - onDislike: () => _dislikeMessage(message), ); } @@ -840,14 +827,6 @@ class _ChatPageState extends ConsumerState { // Inline editing handled by UserMessageBubble. Dialog flow removed. - void _likeMessage(dynamic message) { - // TODO: Implement message liking - } - - void _dislikeMessage(dynamic message) { - // TODO: Implement message disliking - } - Widget _buildEmptyState(ThemeData theme) { final l10n = AppLocalizations.of(context)!; final currentUserAsync = ref.watch(currentUserProvider); @@ -1575,13 +1554,6 @@ class _ChatPageState extends ConsumerState { }); } - // TODO: Implement chat options when needed - // void _showChatOptions() { - // ScaffoldMessenger.of( - // context, - // ).showSnackBar(const SnackBar(content: Text('Chat options coming soon!'))); - // } - void _deleteSelectedMessages() { final selectedMessages = _getSelectedMessages(); if (selectedMessages.isEmpty) return; @@ -1594,7 +1566,6 @@ class _ChatPageState extends ConsumerState { isDestructive: true, ).then((confirmed) async { if (confirmed == true) { - // TODO: Implement message removal // for (final selectedMessage in selectedMessages) { // ref.read(chatMessagesProvider.notifier).removeMessage(selectedMessage.id); // } diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index fc54b2e..c51ab24 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -181,10 +181,12 @@ class _AssistantMessageWidgetState extends ConsumerState final isExpanded = _expandedToolIds.contains(tc.id); final theme = context.conduitTheme; - String _pretty(dynamic v, {int max = 1200}) { + String pretty(dynamic v, {int max = 1200}) { try { - final pretty = const JsonEncoder.withIndent(' ').convert(v); - return pretty.length > max ? '${pretty.substring(0, max)}\n…' : pretty; + final formatted = const JsonEncoder.withIndent(' ').convert(v); + return formatted.length > max + ? '${formatted.substring(0, max)}\n…' + : formatted; } catch (_) { final s = v?.toString() ?? ''; return s.length > max ? '${s.substring(0, max)}…' : s; @@ -233,7 +235,9 @@ class _AssistantMessageWidgetState extends ConsumerState ), const SizedBox(width: Spacing.xs), Icon( - tc.done ? Icons.build_circle_outlined : Icons.play_circle_outline, + tc.done + ? Icons.build_circle_outlined + : Icons.play_circle_outline, size: 14, color: theme.buttonPrimary, ), @@ -281,7 +285,7 @@ class _AssistantMessageWidgetState extends ConsumerState ), const SizedBox(height: Spacing.xxs), SelectableText( - _pretty(tc.arguments), + pretty(tc.arguments), style: TextStyle( fontSize: AppTypography.bodySmall, color: theme.textSecondary, @@ -303,7 +307,7 @@ class _AssistantMessageWidgetState extends ConsumerState ), const SizedBox(height: Spacing.xxs), SelectableText( - _pretty(tc.result), + pretty(tc.result), style: TextStyle( fontSize: AppTypography.bodySmall, color: theme.textSecondary, @@ -315,8 +319,9 @@ class _AssistantMessageWidgetState extends ConsumerState ], ), ), - crossFadeState: - isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, + crossFadeState: isExpanded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, duration: const Duration(milliseconds: 200), ), ], @@ -331,7 +336,7 @@ class _AssistantMessageWidgetState extends ConsumerState // Determine if media (attachments or generated images) is rendered above. final hasMediaAbove = (widget.message.attachmentIds?.isNotEmpty ?? false) || - (widget.message.files?.isNotEmpty ?? false); + (widget.message.files?.isNotEmpty ?? false); bool firstToolSpacerAdded = false; int idx = 0; for (final seg in _segments) { @@ -363,7 +368,7 @@ class _AssistantMessageWidgetState extends ConsumerState } bool get _hasRenderableSegments { - bool _textRenderable(String t) { + bool textRenderable(String t) { String cleaned = t; // Hide tool_calls blocks entirely cleaned = cleaned.replaceAll( @@ -398,7 +403,7 @@ class _AssistantMessageWidgetState extends ConsumerState if (seg.isTool && seg.toolCall != null) return true; if (seg.isReasoning && seg.reasoning != null) return true; final text = seg.text ?? ''; - if (_textRenderable(text)) return true; + if (textRenderable(text)) return true; } return false; } @@ -507,7 +512,8 @@ class _AssistantMessageWidgetState extends ConsumerState ), ); }, - child: (widget.isStreaming && + child: + (widget.isStreaming && !_hasRenderableSegments && _allowTypingIndicator) ? KeyedSubtree( @@ -566,8 +572,18 @@ class _AssistantMessageWidgetState extends ConsumerState ); // Remove raw ... or ... tags in text cleaned = cleaned - .replaceAll(RegExp(r'[\s\S]*?<\/think>', multiLine: true, dotAll: true), '') - .replaceAll(RegExp(r'[\s\S]*?<\/reasoning>', multiLine: true, dotAll: true), ''); + .replaceAll( + RegExp(r'[\s\S]*?<\/think>', multiLine: true, dotAll: true), + '', + ) + .replaceAll( + RegExp( + r'[\s\S]*?<\/reasoning>', + multiLine: true, + dotAll: true, + ), + '', + ); // If there's an unclosed
, drop the tail to avoid raw tags. final lastOpen = cleaned.lastIndexOf(' maxWidth: 500, maxHeight: 400, ), - disableAnimation: false, // Keep animations enabled to prevent black display + disableAnimation: + false, // Keep animations enabled to prevent black display ); }, ), @@ -722,7 +739,8 @@ class _AssistantMessageWidgetState extends ConsumerState maxWidth: imageCount == 2 ? 245 : 160, maxHeight: imageCount == 2 ? 245 : 160, ), - disableAnimation: false, // Keep animations enabled to prevent black display + disableAnimation: + false, // Keep animations enabled to prevent black display ); }).toList(), ), @@ -764,13 +782,10 @@ class _AssistantMessageWidgetState extends ConsumerState Widget dot(Duration delay) { return Container( - width: dotSize, - height: dotSize, - decoration: BoxDecoration( - color: dotColor, - shape: BoxShape.circle, - ), - ) + width: dotSize, + height: dotSize, + decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle), + ) .animate(onPlay: (controller) => controller.repeat()) .then(delay: delay) .scale( @@ -816,13 +831,10 @@ class _AssistantMessageWidgetState extends ConsumerState Widget dot(Duration delay) { return Container( - width: dotSize, - height: dotSize, - decoration: BoxDecoration( - color: dotColor, - shape: BoxShape.circle, - ), - ) + width: dotSize, + height: dotSize, + decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle), + ) .animate(onPlay: (controller) => controller.repeat()) .then(delay: delay) .scale( @@ -859,8 +871,6 @@ class _AssistantMessageWidgetState extends ConsumerState ); } - - Widget _buildActionButtons() { final isErrorMessage = widget.message.content.contains('⚠️') || @@ -914,7 +924,8 @@ class _AssistantMessageWidgetState extends ConsumerState String headerText() { final l10n = AppLocalizations.of(context)!; final hasSummary = rc.summary.isNotEmpty; - final isThinkingSummary = rc.summary.trim().toLowerCase() == 'thinking…' || + final isThinkingSummary = + rc.summary.trim().toLowerCase() == 'thinking…' || rc.summary.trim().toLowerCase() == 'thinking...'; if (widget.isStreaming) { return hasSummary ? rc.summary : l10n.thinking; @@ -1012,8 +1023,9 @@ class _AssistantMessageWidgetState extends ConsumerState ), ), ), - crossFadeState: - isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, + crossFadeState: isExpanded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, duration: const Duration(milliseconds: 200), ), ], diff --git a/lib/features/chat/widgets/enhanced_attachment.dart b/lib/features/chat/widgets/enhanced_attachment.dart index c2371cc..86b7dff 100644 --- a/lib/features/chat/widgets/enhanced_attachment.dart +++ b/lib/features/chat/widgets/enhanced_attachment.dart @@ -131,7 +131,9 @@ class _EnhancedAttachmentState extends ConsumerState { if (path == null) return; final filename = (_fileInfo?['filename'] ?? _fileInfo?['name'] ?? 'file') .toString(); - await Share.shareXFiles([XFile(path, name: filename)]); + await SharePlus.instance.share( + ShareParams(files: [XFile(path, name: filename)]), + ); } String _fileIconFor(String filename) { diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index ded08a1..fb2740f 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -151,7 +151,6 @@ class _ModernChatInputState extends ConsumerState final FocusNode _focusNode = FocusNode(); bool _isRecording = false; bool _isExpanded = true; // Start expanded for better UX - // TODO: Implement voice input functionality // final String _voiceInputText = ''; bool _hasText = false; // track locally without rebuilding on each keystroke StreamSubscription? _voiceStreamSubscription; @@ -414,8 +413,6 @@ class _ModernChatInputState extends ConsumerState }); } - final bool showPlaceholder = - !_hasText && !_focusNode.hasFocus && !_isRecording; final Brightness brightness = Theme.of(context).brightness; final Color outlineColor = (_focusNode.hasFocus || _hasText) ? context.conduitTheme.inputBorderFocused.withValues(alpha: 0.6) @@ -425,16 +422,6 @@ class _ModernChatInputState extends ConsumerState ); final Color composerSurface = context.conduitTheme.inputBackground; final Color placeholderColor = context.conduitTheme.inputPlaceholder; - final Color badgeBackground = showPlaceholder - ? placeholderColor.withValues(alpha: 0.12) - : composerSurface.withValues(alpha: 0.3); - final Color badgeBorder = showPlaceholder - ? Colors.transparent - : outlineColor.withValues(alpha: 0.35); - final Color badgeIconColor = showPlaceholder - ? placeholderColor - : context.conduitTheme.textPrimary.withValues(alpha: 0.75); - return Container( // Transparent wrapper so rounded corners are visible against page background color: Colors.transparent, @@ -679,21 +666,21 @@ class _ModernChatInputState extends ConsumerState ), ), if (!_isExpanded) ...[ - const SizedBox(width: Spacing.sm), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (voiceAvailable) ...[ - _buildVoiceButton(voiceAvailable), - const SizedBox(width: Spacing.xs), - ], - _buildPrimaryButton( - _hasText, - isGenerating, - stopGeneration, - ), + const SizedBox(width: Spacing.sm), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (voiceAvailable) ...[ + _buildVoiceButton(voiceAvailable), + const SizedBox(width: Spacing.xs), ], - ), + _buildPrimaryButton( + _hasText, + isGenerating, + stopGeneration, + ), + ], + ), ], ], ), @@ -1017,7 +1004,7 @@ class _ModernChatInputState extends ConsumerState // Append tools button at the end (always visible) - rowChildren..add( + rowChildren.add( _buildIconButton( icon: Platform.isIOS ? CupertinoIcons.wrench @@ -1605,6 +1592,7 @@ class _ModernChatInputState extends ConsumerState if (!widget.enabled) return; try { final ok = await _voiceService.initialize(); + if (!mounted) return; if (!ok) { _showVoiceUnavailable( AppLocalizations.of(context)?.errorMessage ?? @@ -1614,6 +1602,7 @@ class _ModernChatInputState extends ConsumerState } // Centralized permission + start final stream = await _voiceService.beginListening(); + if (!mounted) return; setState(() { _isRecording = true; _baseTextAtStart = _controller.text; diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 3de0067..7fa7e58 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -819,15 +819,18 @@ class _ChatsDrawerState extends ConsumerState { String folderId, String folderName, ) async { + final l10n = AppLocalizations.of(context)!; final confirmed = await ThemedDialogs.confirm( context, - title: AppLocalizations.of(context)!.deleteFolderTitle, - message: AppLocalizations.of(context)!.deleteFolderMessage, - confirmText: AppLocalizations.of(context)!.delete, + title: l10n.deleteFolderTitle, + message: l10n.deleteFolderMessage, + confirmText: l10n.delete, isDestructive: true, ); + if (!mounted) return; if (!confirmed) return; + final deleteFolderError = l10n.failedToDeleteFolder; try { final api = ref.read(apiServiceProvider); if (api == null) throw Exception('No API service'); @@ -837,16 +840,13 @@ class _ChatsDrawerState extends ConsumerState { ref.invalidate(conversationsProvider); } catch (_) { if (!mounted) return; - UiUtils.showMessage( - this.context, - AppLocalizations.of(context)!.failedToDeleteFolder, - isError: true, - ); + UiUtils.showMessage(this.context, deleteFolderError, isError: true); } } Widget _buildUnfileDropTarget() { final theme = context.conduitTheme; + final l10n = AppLocalizations.of(context)!; final isHover = _dragHoverFolderId == '__UNFILE__'; return DragTarget<_DragConversationData>( onWillAcceptWithDetails: (details) { @@ -874,11 +874,7 @@ class _ChatsDrawerState extends ConsumerState { } } catch (_) { if (mounted) { - UiUtils.showMessage( - context, - AppLocalizations.of(context)!.failedToMoveChat, - isError: true, - ); + UiUtils.showMessage(context, l10n.failedToMoveChat, isError: true); } } }, @@ -1149,14 +1145,14 @@ class _ChatsDrawerState extends ConsumerState { final dynamic authUser = ref.watch(authUserProvider); final user = userFromProfile ?? authUser; - String _initial(String name) { + String initialFor(String name) { if (name.isEmpty) return 'U'; final ch = name.characters.first; return ch.toUpperCase(); } final displayName = deriveUserDisplayName(user); - final initial = _initial(displayName); + final initial = initialFor(displayName); return Padding( padding: const EdgeInsets.fromLTRB(Spacing.sm, 0, Spacing.sm, Spacing.sm), child: Column( @@ -1273,13 +1269,16 @@ class _ChatsDrawerState extends ConsumerState { onTap: () async { HapticFeedback.lightImpact(); Navigator.pop(sheetContext); + final pinErrorMessage = AppLocalizations.of( + context, + )!.failedToUpdatePin; try { await chat.pinConversation(ref, conv.id, !isPinned); } catch (_) { if (!mounted) return; UiUtils.showMessage( this.context, - AppLocalizations.of(context)!.failedToUpdatePin, + pinErrorMessage, isError: true, ); } @@ -1305,13 +1304,16 @@ class _ChatsDrawerState extends ConsumerState { onTap: () async { HapticFeedback.lightImpact(); Navigator.pop(sheetContext); + final archiveErrorMessage = AppLocalizations.of( + context, + )!.failedToUpdateArchive; try { await chat.archiveConversation(ref, conv.id, !isArchived); } catch (_) { if (!mounted) return; UiUtils.showMessage( this.context, - AppLocalizations.of(context)!.failedToUpdateArchive, + archiveErrorMessage, isError: true, ); } @@ -1360,18 +1362,20 @@ class _ChatsDrawerState extends ConsumerState { String conversationId, String currentTitle, ) async { + final l10n = AppLocalizations.of(context)!; final newName = await ThemedDialogs.promptTextInput( context, - title: AppLocalizations.of(context)!.renameChat, - hintText: AppLocalizations.of(context)!.enterChatName, + title: l10n.renameChat, + hintText: l10n.enterChatName, initialValue: currentTitle, - confirmText: AppLocalizations.of(context)!.save, - cancelText: AppLocalizations.of(context)!.cancel, + confirmText: l10n.save, + cancelText: l10n.cancel, ); - + if (!mounted) return; if (newName == null) return; if (newName.isEmpty || newName == currentTitle) return; + final renameError = l10n.failedToRenameChat; try { final api = ref.read(apiServiceProvider); if (api == null) throw Exception('No API service'); @@ -1387,11 +1391,7 @@ class _ChatsDrawerState extends ConsumerState { } } catch (_) { if (!mounted) return; - UiUtils.showMessage( - this.context, - AppLocalizations.of(context)!.failedToRenameChat, - isError: true, - ); + UiUtils.showMessage(this.context, renameError, isError: true); } } @@ -1399,15 +1399,18 @@ class _ChatsDrawerState extends ConsumerState { BuildContext context, String conversationId, ) async { + final l10n = AppLocalizations.of(context)!; final confirmed = await ThemedDialogs.confirm( context, - title: AppLocalizations.of(context)!.deleteChatTitle, - message: AppLocalizations.of(context)!.deleteChatMessage, - confirmText: AppLocalizations.of(context)!.delete, + title: l10n.deleteChatTitle, + message: l10n.deleteChatMessage, + confirmText: l10n.delete, isDestructive: true, ); + if (!mounted) return; if (!confirmed) return; + final deleteError = l10n.failedToDeleteChat; try { final api = ref.read(apiServiceProvider); if (api == null) throw Exception('No API service'); @@ -1422,11 +1425,7 @@ class _ChatsDrawerState extends ConsumerState { ref.invalidate(conversationsProvider); } catch (_) { if (!mounted) return; - UiUtils.showMessage( - this.context, - AppLocalizations.of(context)!.failedToDeleteChat, - isError: true, - ); + UiUtils.showMessage(this.context, deleteError, isError: true); } } } diff --git a/lib/shared/widgets/measure_size.dart b/lib/shared/widgets/measure_size.dart index 8e77a0b..2c0756c 100644 --- a/lib/shared/widgets/measure_size.dart +++ b/lib/shared/widgets/measure_size.dart @@ -7,22 +7,24 @@ class MeasureSize extends SingleChildRenderObjectWidget { final OnWidgetSizeChange onChange; const MeasureSize({super.key, required this.onChange, required Widget child}) - : super(child: child); + : super(child: child); @override RenderObject createRenderObject(BuildContext context) { - return _MeasureSizeRenderObject(onChange); + return MeasureSizeRenderObject(onChange); } @override void updateRenderObject( - BuildContext context, covariant _MeasureSizeRenderObject renderObject) { + BuildContext context, + covariant MeasureSizeRenderObject renderObject, + ) { renderObject.onChange = onChange; } } -class _MeasureSizeRenderObject extends RenderProxyBox { - _MeasureSizeRenderObject(this.onChange); +class MeasureSizeRenderObject extends RenderProxyBox { + MeasureSizeRenderObject(this.onChange); OnWidgetSizeChange onChange; Size? _oldSize;