Files
tasq/lib/providers/programmer_task_comments_provider.dart
T
redz1029 b307f66f2c Add PM extension (Projects, Work Logs, Comments) + Quill rich-text UI
Projects: Brick-backed model with list/detail screens and nav entry.
Work Logs: per-contributor time ledger, assignee running-clock +
helper logged minutes with Hours/Minutes input; pause-after-save and
helper-deduct prompts.
Comments: threaded comments with file attachments and Quill composer.
Detail screen: tabbed layout (Work Log / Comments / Activity),
editable title + description via edit dialog, Quill-rendered
descriptions with legacy plain-text fallback.
Create dialog: fixed-width (480px max), Quill description editor.
Shared QuillFieldEditor + QuillReadOnly widgets extracted for reuse.
RLS: hardened activity-log actor binding, immutable creator_id/
created_at triggers, SECURITY INVOKER visibility helper,
append-only work-log and scoped comment policies.
Brick migration for project_id FK on programmer_tasks.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-27 16:06:26 +08:00

72 lines
2.5 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart';
import '../models/programmer_task_comment.model.dart';
import 'supabase_provider.dart';
/// Bucket reused for programmer-task comment attachments (public read;
/// authenticated write; owner/privileged delete — already hardened by
/// 20260926120000_task_attachment_write_hardening.sql).
const _attachmentBucket = 'task_attachments';
/// A file selected for upload with a comment.
typedef CommentUpload = ({String name, Uint8List bytes});
/// Comments for a task (newest first), streamed live.
final programmerTaskCommentsProvider =
StreamProvider.family<List<ProgrammerTaskComment>, String>((ref, taskId) {
final client = ref.watch(supabaseClientProvider);
return client
.from('programmer_task_comments')
.stream(primaryKey: ['id'])
.eq('task_id', taskId)
.order('created_at', ascending: false)
.map((rows) => rows.map(ProgrammerTaskComment.fromMap).toList());
});
final programmerTaskCommentsControllerProvider =
Provider<ProgrammerTaskCommentsController>((ref) {
return ProgrammerTaskCommentsController(ref.watch(supabaseClientProvider));
});
class ProgrammerTaskCommentsController {
ProgrammerTaskCommentsController(this._client);
final dynamic _client;
/// Public URL for a stored attachment path.
String attachmentUrl(String path) =>
_client.storage.from(_attachmentBucket).getPublicUrl(path);
/// Adds a comment, uploading any [files] to the attachment bucket first.
/// Requires either a non-empty [body] or at least one file.
Future<void> addComment({
required String taskId,
required String body,
List<CommentUpload> files = const [],
}) async {
final userId = _client.auth.currentUser?.id;
if (userId == null) throw Exception('Not authenticated');
final attachments = <Map<String, dynamic>>[];
for (final file in files) {
final path =
'programmer_tasks/$taskId/comments/${const Uuid().v4()}_${file.name}';
await _client.storage.from(_attachmentBucket).uploadBinary(
path,
file.bytes,
fileOptions: const FileOptions(upsert: true),
);
attachments.add({'path': path, 'name': file.name});
}
await _client.from('programmer_task_comments').insert({
'task_id': taskId,
'author_id': userId,
'body': body,
'attachments': attachments,
});
}
}