this repo has no description

feat: update comment handling and UI; refactor to support photo focus and add comment sheet

+496 -135
+2 -2
lib/api.dart
··· 443 443 required String text, 444 444 List<Map<String, dynamic>>? facets, 445 445 required String subject, 446 - Map<String, dynamic>? focus, 446 + String? focus, // Now a String (photo URI) 447 447 String? replyTo, 448 448 }) async { 449 449 final session = await auth.getValidSession(); ··· 462 462 'text': text, 463 463 if (facets != null) 'facets': facets, 464 464 'subject': subject, 465 - if (focus != null) 'focus': focus, 465 + if (focus != null) 'focus': focus, // focus is now a String 466 466 if (replyTo != null) 'replyTo': replyTo, 467 467 'createdAt': DateTime.now().toUtc().toIso8601String(), 468 468 },
+2 -1
lib/providers/gallery_thread_cache_provider.dart
··· 72 72 return List<Map<String, dynamic>>.from(facets); 73 73 } 74 74 75 - Future<bool> createComment({required String text, String? replyTo}) async { 75 + Future<bool> createComment({required String text, String? replyTo, String? focus}) async { 76 76 try { 77 77 final facetsList = await _extractFacets(text); 78 78 final facets = facetsList.isEmpty ? null : facetsList; ··· 81 81 subject: galleryUri, 82 82 replyTo: replyTo, 83 83 facets: facets, 84 + focus: focus, 84 85 ); 85 86 if (uri != null) { 86 87 final thread = await apiService.pollGalleryThreadComments(
+1 -1
lib/providers/gallery_thread_cache_provider.g.dart
··· 6 6 // RiverpodGenerator 7 7 // ************************************************************************** 8 8 9 - String _$galleryThreadHash() => r'c96bf466ccaf8e4856bbc33720d39e68a6405742'; 9 + String _$galleryThreadHash() => r'fa270baa7dd229fdd6b4e2610cf232aed53ed3db'; 10 10 11 11 /// Copied from Dart SDK 12 12 class _SystemHash {
+30 -128
lib/screens/comments_page.dart
··· 7 7 import 'package:grain/screens/hashtag_page.dart'; 8 8 import 'package:grain/screens/profile_page.dart'; 9 9 import 'package:grain/utils.dart'; 10 - import 'package:grain/widgets/app_button.dart'; 10 + import 'package:grain/widgets/add_comment_sheet.dart'; 11 11 import 'package:grain/widgets/app_image.dart'; 12 12 import 'package:grain/widgets/faceted_text.dart'; 13 13 import 'package:grain/widgets/gallery_photo_view.dart'; ··· 24 24 class _CommentsPageState extends ConsumerState<CommentsPage> { 25 25 GalleryPhoto? _selectedPhoto; 26 26 27 - void _showCommentInputSheet( 28 - BuildContext context, 29 - WidgetRef ref, { 30 - String? replyTo, 31 - String? mention, 32 - }) { 33 - showModalBottomSheet( 34 - context: context, 35 - isScrollControlled: true, 36 - builder: (sheetContext) => CommentInputSheet( 37 - initialText: mention ?? '', 38 - onSubmit: (text) async { 39 - await ref 40 - .read(galleryThreadProvider(widget.galleryUri).notifier) 41 - .createComment(text: text.trim(), replyTo: replyTo); 42 - if (!sheetContext.mounted) { 43 - return; 27 + void _showCommentInputSheet(BuildContext context, WidgetRef ref, {Comment? replyToComment}) { 28 + final threadState = ref.read(galleryThreadProvider(widget.galleryUri)); 29 + // Pass replyTo as a Map for compatibility with add_comment_sheet 30 + final replyTo = replyToComment != null 31 + ? { 32 + 'author': replyToComment.author, 33 + 'focus': replyToComment.focus, 34 + 'text': replyToComment.text, 44 35 } 45 - Navigator.of(sheetContext).pop(); 46 - }, 47 - ), 36 + : null; 37 + showAddCommentSheet( 38 + context, 39 + initialText: '', // Always blank 40 + onSubmit: (text) async { 41 + await ref 42 + .read(galleryThreadProvider(widget.galleryUri).notifier) 43 + .createComment(text: text.trim(), replyTo: replyToComment?.uri); 44 + }, 45 + onCancel: () => Navigator.of(context).maybePop(), 46 + gallery: threadState.gallery, 47 + replyTo: replyTo, 48 48 ); 49 49 } 50 50 ··· 121 121 onPhotoTap: (photo) { 122 122 setState(() => _selectedPhoto = photo); 123 123 }, 124 - onReply: (replyTo, {mention}) => _showCommentInputSheet( 125 - context, 126 - ref, 127 - replyTo: replyTo, 128 - mention: mention, 129 - ), 124 + onReply: (comment, {mention}) => 125 + _showCommentInputSheet(context, ref, replyToComment: comment), 130 126 onDelete: (comment) async { 131 127 final confirmed = await showDialog<bool>( 132 128 context: context, ··· 174 170 175 171 if (!context.mounted) return; 176 172 ScaffoldMessenger.of(context).removeCurrentSnackBar(); 173 + await Future.delayed(const Duration(milliseconds: 120)); 174 + if (!context.mounted) return; 177 175 ScaffoldMessenger.of(context).showSnackBar( 178 176 SnackBar( 179 177 content: Text( ··· 224 222 photos: [_selectedPhoto!], 225 223 initialIndex: 0, 226 224 onClose: () => setState(() => _selectedPhoto = null), 225 + showAddCommentButton: false, 227 226 ), 228 227 ), 229 228 ], ··· 231 230 } 232 231 } 233 232 234 - class CommentInputSheet extends StatefulWidget { 235 - final String initialText; 236 - final Future<void> Function(String text) onSubmit; 237 - const CommentInputSheet({super.key, this.initialText = '', required this.onSubmit}); 238 - 239 - @override 240 - State<CommentInputSheet> createState() => _CommentInputSheetState(); 241 - } 242 - 243 - class _CommentInputSheetState extends State<CommentInputSheet> { 244 - late TextEditingController _controller; 245 - bool _posting = false; 246 - String _currentText = ''; 247 - 248 - @override 249 - void initState() { 250 - super.initState(); 251 - _controller = TextEditingController(text: widget.initialText); 252 - _currentText = widget.initialText; 253 - _controller.addListener(_onTextChanged); 254 - } 255 - 256 - void _onTextChanged() { 257 - if (_currentText != _controller.text) { 258 - setState(() { 259 - _currentText = _controller.text; 260 - }); 261 - } 262 - } 263 - 264 - @override 265 - void dispose() { 266 - _controller.removeListener(_onTextChanged); 267 - _controller.dispose(); 268 - super.dispose(); 269 - } 270 - 271 - @override 272 - Widget build(BuildContext context) { 273 - return Padding( 274 - padding: EdgeInsets.only( 275 - bottom: MediaQuery.of(context).viewInsets.bottom, 276 - left: 16, 277 - right: 16, 278 - top: 16, 279 - ), 280 - child: Column( 281 - mainAxisSize: MainAxisSize.min, 282 - children: [ 283 - Row( 284 - mainAxisAlignment: MainAxisAlignment.spaceBetween, 285 - children: [ 286 - AppButton( 287 - label: 'Cancel', 288 - variant: AppButtonVariant.text, 289 - size: AppButtonSize.small, 290 - disabled: _posting, 291 - onPressed: _posting ? null : () => Navigator.pop(context), 292 - ), 293 - AppButton( 294 - borderRadius: 22, 295 - label: 'Post', 296 - loading: _posting, 297 - onPressed: !_posting && _currentText.trim().isNotEmpty 298 - ? () async { 299 - setState(() => _posting = true); 300 - await widget.onSubmit(_currentText.trim()); 301 - } 302 - : null, 303 - ), 304 - ], 305 - ), 306 - const SizedBox(height: 12), 307 - TextField( 308 - controller: _controller, 309 - minLines: 2, 310 - maxLines: 6, 311 - autofocus: true, 312 - decoration: const InputDecoration( 313 - hintText: 'Write your comment...', 314 - border: InputBorder.none, 315 - enabledBorder: InputBorder.none, 316 - focusedBorder: InputBorder.none, 317 - disabledBorder: InputBorder.none, 318 - contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), 319 - ), 320 - ), 321 - ], 322 - ), 323 - ); 324 - } 325 - } 326 - 327 233 class _CommentsList extends StatelessWidget { 328 234 final List<Comment> comments; 329 235 final void Function(GalleryPhoto photo) onPhotoTap; 330 - final void Function(String replyTo, {String? mention}) onReply; 236 + final void Function(Comment replyToComment, {String? mention}) onReply; 331 237 final void Function(Comment comment) onDelete; 332 238 const _CommentsList({ 333 239 required this.comments, ··· 376 282 _CommentTile( 377 283 comment: comment, 378 284 onPhotoTap: onPhotoTap, 379 - onReply: (replyTo, {mention}) { 380 - // Only two levels: replyTo should always be the top-level parent 381 - final parent = _findTopLevelParent(comment, byUri); 382 - onReply(parent.uri, mention: mention); 383 - }, 285 + onReply: (c, {mention}) => onReply(comment, mention: mention), 384 286 onDelete: onDelete, 385 287 ), 386 288 if (repliesByParent[comment.uri] != null) ··· 421 323 class _CommentTile extends StatelessWidget { 422 324 final Comment comment; 423 325 final void Function(GalleryPhoto photo)? onPhotoTap; 424 - final void Function(String replyTo, {String? mention})? onReply; 326 + final void Function(Comment replyToComment, {String? mention})? onReply; 425 327 final void Function(Comment comment)? onDelete; 426 328 const _CommentTile({required this.comment, this.onPhotoTap, this.onReply, this.onDelete}); 427 329 ··· 539 441 onPressed: () { 540 442 final handle = comment.author['handle'] ?? ''; 541 443 final mention = handle.isNotEmpty ? '@$handle ' : ''; 542 - if (onReply != null) onReply!(comment.uri, mention: mention); 444 + if (onReply != null) onReply!(comment, mention: mention); 543 445 }, 544 446 child: Text( 545 447 'Reply',
+15
lib/screens/gallery_page.dart
··· 3 3 import 'package:grain/api.dart'; 4 4 import 'package:grain/models/gallery_photo.dart'; 5 5 import 'package:grain/providers/gallery_cache_provider.dart'; 6 + import 'package:grain/screens/comments_page.dart'; 6 7 import 'package:grain/screens/create_gallery_page.dart'; 7 8 import 'package:grain/screens/hashtag_page.dart'; 8 9 import 'package:grain/screens/profile_page.dart'; ··· 311 312 _selectedPhotoIndex = null; 312 313 }); 313 314 }, 315 + onCommentPosted: (galleryUri) async { 316 + setState(() => _selectedPhoto = null); // Remove overlay 317 + await Future.delayed(const Duration(milliseconds: 200)); 318 + WidgetsBinding.instance.addPostFrameCallback((_) { 319 + if (mounted) { 320 + Navigator.of(context).push( 321 + MaterialPageRoute( 322 + builder: (context) => CommentsPage(galleryUri: galleryUri), 323 + ), 324 + ); 325 + } 326 + }); 327 + }, 328 + gallery: gallery, // Pass the gallery object 314 329 ), 315 330 ), 316 331 ],
+343
lib/widgets/add_comment_sheet.dart
··· 1 + import 'package:flutter/cupertino.dart'; 2 + import 'package:flutter/material.dart'; 3 + import 'package:flutter/services.dart'; 4 + import 'package:grain/api.dart'; 5 + import 'package:grain/widgets/app_image.dart'; 6 + import 'package:grain/widgets/gallery_preview.dart'; 7 + 8 + Future<void> showAddCommentSheet( 9 + BuildContext context, { 10 + String initialText = '', 11 + required Future<void> Function(String text) onSubmit, 12 + VoidCallback? onCancel, 13 + dynamic gallery, // Pass gallery object 14 + dynamic replyTo, // Pass comment/user object if replying to a comment 15 + }) async { 16 + final theme = Theme.of(context); 17 + final controller = TextEditingController(text: initialText); 18 + await showCupertinoSheet( 19 + context: context, 20 + useNestedNavigation: false, 21 + pageBuilder: (context) => Material( 22 + type: MaterialType.transparency, 23 + child: _AddCommentSheet( 24 + controller: controller, 25 + onSubmit: onSubmit, 26 + onCancel: onCancel, 27 + gallery: gallery, 28 + replyTo: replyTo, 29 + ), 30 + ), 31 + ); 32 + SystemChrome.setSystemUIOverlayStyle( 33 + theme.brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, 34 + ); 35 + } 36 + 37 + class _AddCommentSheet extends StatefulWidget { 38 + final TextEditingController controller; 39 + final Future<void> Function(String text) onSubmit; 40 + final VoidCallback? onCancel; 41 + final dynamic gallery; 42 + final dynamic replyTo; 43 + const _AddCommentSheet({ 44 + required this.controller, 45 + required this.onSubmit, 46 + this.onCancel, 47 + this.gallery, 48 + this.replyTo, 49 + }); 50 + 51 + @override 52 + State<_AddCommentSheet> createState() => _AddCommentSheetState(); 53 + } 54 + 55 + class _AddCommentSheetState extends State<_AddCommentSheet> { 56 + bool _posting = false; 57 + String _currentText = ''; 58 + final FocusNode _focusNode = FocusNode(); 59 + 60 + @override 61 + void initState() { 62 + super.initState(); 63 + _currentText = widget.controller.text; 64 + widget.controller.addListener(_onTextChanged); 65 + // Request focus after build 66 + WidgetsBinding.instance.addPostFrameCallback((_) { 67 + _focusNode.requestFocus(); 68 + }); 69 + } 70 + 71 + void _onTextChanged() { 72 + if (_currentText != widget.controller.text) { 73 + setState(() { 74 + _currentText = widget.controller.text; 75 + }); 76 + } 77 + } 78 + 79 + @override 80 + void dispose() { 81 + widget.controller.removeListener(_onTextChanged); 82 + widget.controller.dispose(); 83 + super.dispose(); 84 + } 85 + 86 + @override 87 + Widget build(BuildContext context) { 88 + final theme = Theme.of(context); 89 + final gallery = widget.gallery; 90 + final replyTo = widget.replyTo; 91 + final creator = replyTo != null 92 + ? (replyTo is Map && replyTo['author'] != null ? replyTo['author'] : null) 93 + : gallery?.creator; 94 + final focusPhoto = replyTo != null 95 + ? (replyTo is Map && replyTo['focus'] != null ? replyTo['focus'] : null) 96 + : null; 97 + return CupertinoPageScaffold( 98 + backgroundColor: theme.colorScheme.surface, 99 + navigationBar: CupertinoNavigationBar( 100 + backgroundColor: theme.colorScheme.surface, 101 + border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)), 102 + middle: Text( 103 + replyTo != null ? 'Reply' : 'Add comment', 104 + style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), 105 + ), 106 + leading: CupertinoButton( 107 + padding: EdgeInsets.zero, 108 + onPressed: _posting ? null : widget.onCancel ?? () => Navigator.of(context).maybePop(), 109 + child: Text( 110 + 'Cancel', 111 + style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600), 112 + ), 113 + ), 114 + trailing: CupertinoButton( 115 + padding: EdgeInsets.zero, 116 + onPressed: _posting || _currentText.trim().isEmpty 117 + ? null 118 + : () async { 119 + setState(() => _posting = true); 120 + await widget.onSubmit(_currentText.trim()); 121 + if (mounted) Navigator.of(context).pop(); 122 + setState(() => _posting = false); 123 + }, 124 + child: Row( 125 + mainAxisSize: MainAxisSize.min, 126 + children: [ 127 + Text( 128 + 'Post', 129 + style: TextStyle( 130 + color: _posting || _currentText.trim().isEmpty 131 + ? theme.disabledColor 132 + : theme.colorScheme.primary, 133 + fontWeight: FontWeight.w600, 134 + ), 135 + ), 136 + if (_posting) ...[ 137 + const SizedBox(width: 8), 138 + SizedBox( 139 + width: 16, 140 + height: 16, 141 + child: CircularProgressIndicator( 142 + strokeWidth: 2, 143 + valueColor: AlwaysStoppedAnimation<Color>(theme.colorScheme.primary), 144 + semanticsLabel: 'Posting', 145 + ), 146 + ), 147 + ], 148 + ], 149 + ), 150 + ), 151 + ), 152 + child: SafeArea( 153 + bottom: false, 154 + child: Padding( 155 + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), 156 + child: Column( 157 + children: [ 158 + if ((gallery != null && creator != null && replyTo == null) || 159 + (replyTo != null && creator != null)) ...[ 160 + Row( 161 + crossAxisAlignment: CrossAxisAlignment.start, 162 + children: [ 163 + if ((creator is Map && 164 + creator['avatar'] != null && 165 + creator['avatar'].isNotEmpty) || 166 + (creator is! Map && creator.avatar != null && creator.avatar.isNotEmpty)) 167 + ClipOval( 168 + child: AppImage( 169 + url: creator is Map ? creator['avatar'] : creator.avatar, 170 + width: 40, 171 + height: 40, 172 + fit: BoxFit.cover, 173 + ), 174 + ) 175 + else 176 + CircleAvatar( 177 + radius: 20, 178 + backgroundColor: theme.colorScheme.surfaceContainerHighest, 179 + child: Icon(Icons.person, size: 20, color: theme.iconTheme.color), 180 + ), 181 + Expanded( 182 + child: Padding( 183 + padding: const EdgeInsets.only(left: 20.0), 184 + child: Column( 185 + crossAxisAlignment: CrossAxisAlignment.start, 186 + children: [ 187 + Row( 188 + children: [ 189 + Text( 190 + creator is Map 191 + ? (creator['displayName'] ?? '') 192 + : (creator.displayName ?? ''), 193 + style: theme.textTheme.bodyMedium?.copyWith( 194 + fontWeight: FontWeight.bold, 195 + ), 196 + ), 197 + if ((creator is Map 198 + ? (creator['handle'] ?? '') 199 + : (creator.handle ?? '')) 200 + .isNotEmpty) ...[ 201 + const SizedBox(width: 8), 202 + Text( 203 + '@${creator is Map ? creator['handle'] : creator.handle}', 204 + style: theme.textTheme.bodySmall?.copyWith( 205 + color: theme.hintColor, 206 + ), 207 + ), 208 + ], 209 + ], 210 + ), 211 + const SizedBox(height: 8), 212 + if (replyTo != null && 213 + replyTo is Map && 214 + replyTo['text'] != null && 215 + (replyTo['text'] as String).isNotEmpty) ...[ 216 + Padding( 217 + padding: const EdgeInsets.only(bottom: 8.0), 218 + child: Text( 219 + replyTo['text'], 220 + style: theme.textTheme.bodySmall?.copyWith( 221 + color: theme.hintColor, 222 + ), 223 + maxLines: 3, 224 + overflow: TextOverflow.ellipsis, 225 + ), 226 + ), 227 + ], 228 + if (replyTo != null && focusPhoto != null) ...[ 229 + SizedBox( 230 + height: 64, 231 + child: AspectRatio( 232 + aspectRatio: 233 + (focusPhoto.aspectRatio != null && 234 + focusPhoto.aspectRatio.width > 0 && 235 + focusPhoto.aspectRatio.height > 0) 236 + ? focusPhoto.aspectRatio.width / focusPhoto.aspectRatio.height 237 + : 1.0, 238 + child: AppImage( 239 + url: focusPhoto.thumb?.isNotEmpty == true 240 + ? focusPhoto.thumb 241 + : focusPhoto.fullsize, 242 + fit: BoxFit.cover, 243 + borderRadius: BorderRadius.circular(8), 244 + ), 245 + ), 246 + ), 247 + ] else if (replyTo == null) ...[ 248 + Column( 249 + crossAxisAlignment: CrossAxisAlignment.start, 250 + children: [ 251 + Row( 252 + children: [ 253 + Text( 254 + gallery.title ?? '', 255 + style: theme.textTheme.bodyMedium?.copyWith( 256 + fontWeight: FontWeight.w600, 257 + ), 258 + maxLines: 1, 259 + overflow: TextOverflow.ellipsis, 260 + ), 261 + ], 262 + ), 263 + const SizedBox(height: 4), 264 + Row( 265 + children: [ 266 + Align( 267 + alignment: Alignment.centerLeft, 268 + child: SizedBox( 269 + height: 64, 270 + child: GalleryPreview(gallery: gallery), 271 + ), 272 + ), 273 + ], 274 + ), 275 + ], 276 + ), 277 + ], 278 + ], 279 + ), 280 + ), 281 + ), 282 + ], 283 + ), 284 + const SizedBox(height: 16), 285 + Divider(height: 1, thickness: 1, color: theme.dividerColor), 286 + const SizedBox(height: 16), 287 + ], 288 + Expanded( 289 + child: Row( 290 + crossAxisAlignment: CrossAxisAlignment.start, 291 + children: [ 292 + // Current user avatar 293 + if (apiService.currentUser?.avatar != null && 294 + (apiService.currentUser?.avatar?.isNotEmpty ?? false)) 295 + Padding( 296 + padding: const EdgeInsets.only(right: 8.0, top: 4.0), 297 + child: ClipOval( 298 + child: AppImage( 299 + url: apiService.currentUser!.avatar!, 300 + width: 40, 301 + height: 40, 302 + fit: BoxFit.cover, 303 + ), 304 + ), 305 + ) 306 + else 307 + Padding( 308 + padding: const EdgeInsets.only(right: 8.0, top: 4.0), 309 + child: CircleAvatar( 310 + radius: 20, 311 + backgroundColor: theme.colorScheme.surfaceContainerHighest, 312 + child: Icon(Icons.person, size: 20, color: theme.iconTheme.color), 313 + ), 314 + ), 315 + // Text input 316 + Expanded( 317 + child: TextField( 318 + controller: widget.controller, 319 + focusNode: _focusNode, 320 + maxLines: 6, 321 + minLines: 2, 322 + style: theme.textTheme.bodyMedium, 323 + decoration: InputDecoration( 324 + hintText: 'Add a comment', 325 + hintStyle: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor), 326 + border: InputBorder.none, 327 + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), 328 + isDense: true, 329 + filled: false, 330 + ), 331 + ), 332 + ), 333 + ], 334 + ), 335 + ), 336 + const SizedBox(height: 24), 337 + ], 338 + ), 339 + ), 340 + ), 341 + ); 342 + } 343 + }
+103 -3
lib/widgets/gallery_photo_view.dart
··· 1 1 import 'package:flutter/material.dart'; 2 + import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 + import 'package:grain/api.dart'; 2 4 import 'package:grain/models/gallery_photo.dart'; 5 + import 'package:grain/providers/gallery_thread_cache_provider.dart'; 6 + import 'package:grain/widgets/add_comment_sheet.dart'; 3 7 import 'package:grain/widgets/app_image.dart'; 4 8 5 - class GalleryPhotoView extends StatefulWidget { 9 + class GalleryPhotoView extends ConsumerStatefulWidget { 6 10 final List<GalleryPhoto> photos; 7 11 final int initialIndex; 8 12 final VoidCallback? onClose; 13 + final void Function(String galleryUri)? onCommentPosted; 14 + final dynamic gallery; 15 + final bool showAddCommentButton; 9 16 const GalleryPhotoView({ 10 17 super.key, 11 18 required this.photos, 12 19 required this.initialIndex, 13 20 this.onClose, 21 + this.onCommentPosted, 22 + this.gallery, 23 + this.showAddCommentButton = true, 14 24 }); 15 25 16 26 @override 17 - State<GalleryPhotoView> createState() => _GalleryPhotoViewState(); 27 + ConsumerState<GalleryPhotoView> createState() => _GalleryPhotoViewState(); 18 28 } 19 29 20 - class _GalleryPhotoViewState extends State<GalleryPhotoView> { 30 + class _GalleryPhotoViewState extends ConsumerState<GalleryPhotoView> { 21 31 late PageController _controller; 22 32 late int _currentIndex; 23 33 ··· 80 90 photo.alt!, 81 91 style: const TextStyle(color: Colors.white, fontSize: 16), 82 92 textAlign: TextAlign.center, 93 + ), 94 + ), 95 + if (widget.showAddCommentButton) 96 + SafeArea( 97 + top: false, 98 + child: Padding( 99 + padding: const EdgeInsets.only(top: 8, left: 16, right: 16, bottom: 8), 100 + child: SizedBox( 101 + width: double.infinity, 102 + child: ElevatedButton( 103 + style: ElevatedButton.styleFrom( 104 + backgroundColor: Colors.grey[900], 105 + foregroundColor: Colors.white, 106 + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), 107 + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), 108 + textStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), 109 + elevation: 0, 110 + ), 111 + onPressed: () async { 112 + final photo = widget.photos[_currentIndex]; 113 + final creator = widget.gallery?.creator; 114 + final replyTo = { 115 + 'author': creator != null 116 + ? { 117 + 'avatar': creator.avatar, 118 + 'displayName': creator.displayName, 119 + 'handle': creator.handle, 120 + } 121 + : {'avatar': null, 'displayName': '', 'handle': ''}, 122 + 'focus': photo, 123 + 'text': '', 124 + }; 125 + bool commentPosted = false; 126 + await showAddCommentSheet( 127 + context, 128 + gallery: null, 129 + replyTo: replyTo, 130 + initialText: '', 131 + onSubmit: (text) async { 132 + final photo = widget.photos[_currentIndex]; 133 + final gallery = widget.gallery; 134 + final subject = gallery?.uri; 135 + final focus = photo.uri; 136 + if (subject == null || focus == null) { 137 + return; 138 + } 139 + // Use the provider's createComment method 140 + final notifier = ref.read(galleryThreadProvider(subject).notifier); 141 + final success = await notifier.createComment( 142 + text: text, 143 + focus: focus, 144 + ); 145 + if (success) commentPosted = true; 146 + // Sheet will pop itself 147 + }, 148 + ); 149 + // After sheet closes, notify parent if a comment was posted 150 + if (commentPosted && widget.gallery?.uri != null) { 151 + widget.onClose?.call(); // Remove GalleryPhotoView overlay 152 + widget.onCommentPosted?.call(widget.gallery!.uri); 153 + } 154 + }, 155 + child: Row( 156 + mainAxisAlignment: MainAxisAlignment.start, 157 + children: [ 158 + if (apiService.currentUser?.avatar != null && 159 + (apiService.currentUser?.avatar?.isNotEmpty ?? false)) 160 + ClipOval( 161 + child: AppImage( 162 + url: apiService.currentUser!.avatar!, 163 + width: 32, 164 + height: 32, 165 + fit: BoxFit.cover, 166 + ), 167 + ) 168 + else 169 + CircleAvatar( 170 + radius: 16, 171 + backgroundColor: Colors.grey[800], 172 + child: Icon(Icons.person, size: 16, color: Colors.white), 173 + ), 174 + const SizedBox(width: 12), 175 + Text( 176 + 'Add a comment', 177 + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), 178 + ), 179 + ], 180 + ), 181 + ), 182 + ), 83 183 ), 84 184 ), 85 185 ],