Add version files and update imports for trip model; enhance error handling
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import "package:supabase_flutter/supabase_flutter.dart";
|
||||
|
||||
enum ChannelKind { text, operations, voice, announcement, unknown }
|
||||
|
||||
abstract class BaseChannel {
|
||||
BaseChannel({
|
||||
required this.client,
|
||||
required this.id,
|
||||
required this.organizationId,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.slug,
|
||||
required this.kind,
|
||||
required this.isPrivate,
|
||||
required this.position,
|
||||
});
|
||||
|
||||
final SupabaseClient client;
|
||||
final String id;
|
||||
final String organizationId;
|
||||
final String name;
|
||||
final String description;
|
||||
final String slug;
|
||||
final ChannelKind kind;
|
||||
final bool isPrivate;
|
||||
final int position;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"id": id,
|
||||
"organization_id": organizationId,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"slug": slug,
|
||||
"type": kind.name,
|
||||
"is_private": isPrivate,
|
||||
"position": position,
|
||||
};
|
||||
}
|
||||
|
||||
static ChannelKind channelKindFromApi(Object? rawType) {
|
||||
final value = (rawType ?? "").toString().trim().toLowerCase();
|
||||
switch (value) {
|
||||
case "text":
|
||||
return ChannelKind.text;
|
||||
case "operations":
|
||||
return ChannelKind.operations;
|
||||
case "voice":
|
||||
return ChannelKind.voice;
|
||||
case "announcement":
|
||||
return ChannelKind.announcement;
|
||||
default:
|
||||
return ChannelKind.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, dynamic> asMap(Object? value) {
|
||||
if (value is Map<String, dynamic>) return value;
|
||||
if (value is Map) return value.cast<String, dynamic>();
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
|
||||
static List<dynamic> asList(Object? value) {
|
||||
if (value is List) return value;
|
||||
return const <dynamic>[];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
import "package:bus_running_record/models/channels/base_channel.dart";
|
||||
import "package:bus_running_record/models/operations/stop.dart";
|
||||
import "package:supabase_flutter/supabase_flutter.dart";
|
||||
|
||||
class OperationsSchedule {
|
||||
const OperationsSchedule({
|
||||
required this.id,
|
||||
required this.version,
|
||||
required this.sourceFileName,
|
||||
required this.uploadedAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final int version;
|
||||
final String sourceFileName;
|
||||
final DateTime? uploadedAt;
|
||||
|
||||
factory OperationsSchedule.fromMap(Map<String, dynamic> map) {
|
||||
return OperationsSchedule(
|
||||
id: (map["id"] ?? "").toString(),
|
||||
version: (map["version"] as num?)?.toInt() ?? 1,
|
||||
sourceFileName: (map["source_file_name"] ?? "").toString(),
|
||||
uploadedAt: DateTime.tryParse((map["uploaded_at"] ?? "").toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OperationsTrip {
|
||||
const OperationsTrip({
|
||||
required this.id,
|
||||
required this.scheduleId,
|
||||
required this.tripNumber,
|
||||
required this.dutyNumber,
|
||||
required this.busWorkNumber,
|
||||
required this.direction,
|
||||
required this.sortOrder,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String scheduleId;
|
||||
final String tripNumber;
|
||||
final String dutyNumber;
|
||||
final String busWorkNumber;
|
||||
final String direction;
|
||||
final int sortOrder;
|
||||
String get dutyKey => "$dutyNumber::$busWorkNumber";
|
||||
|
||||
factory OperationsTrip.fromMap(Map<String, dynamic> map) {
|
||||
return OperationsTrip(
|
||||
id: (map["id"] ?? "").toString(),
|
||||
scheduleId: (map["schedule_id"] ?? "").toString(),
|
||||
tripNumber: (map["trip_number"] ?? "").toString(),
|
||||
dutyNumber: (map["duty_number"] ?? "").toString(),
|
||||
busWorkNumber: (map["bus_work_number"] ?? "").toString(),
|
||||
direction: (map["direction"] ?? "").toString(),
|
||||
sortOrder: (map["sort_order"] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OperationsScheduledStop extends Stop {
|
||||
const OperationsScheduledStop({
|
||||
required this.id,
|
||||
required this.tripId,
|
||||
required this.sequence,
|
||||
required super.name,
|
||||
required this.scheduledTime,
|
||||
super.alias,
|
||||
super.aliasSource,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String tripId;
|
||||
final int sequence;
|
||||
final String? scheduledTime;
|
||||
}
|
||||
|
||||
class OperationsStopUpdate {
|
||||
const OperationsStopUpdate({
|
||||
required this.status,
|
||||
required this.actualTime,
|
||||
required this.vehicleId,
|
||||
required this.notes,
|
||||
});
|
||||
|
||||
final String? status;
|
||||
final String? actualTime;
|
||||
final String? vehicleId;
|
||||
final String? notes;
|
||||
}
|
||||
|
||||
class OperationsStop {
|
||||
const OperationsStop({required this.scheduledStop, this.update});
|
||||
|
||||
final OperationsScheduledStop scheduledStop;
|
||||
final OperationsStopUpdate? update;
|
||||
|
||||
String get id => scheduledStop.id;
|
||||
String get tripId => scheduledStop.tripId;
|
||||
int get sequence => scheduledStop.sequence;
|
||||
String get name => scheduledStop.name;
|
||||
String get displayName => scheduledStop.displayName;
|
||||
String? get aliasSource => scheduledStop.aliasSource;
|
||||
String? get scheduledTime => scheduledStop.scheduledTime;
|
||||
String? get status => update?.status;
|
||||
String? get actualTime => update?.actualTime;
|
||||
String? get vehicleId => update?.vehicleId;
|
||||
String? get notes => update?.notes;
|
||||
}
|
||||
|
||||
class OperationsTripSnapshot {
|
||||
const OperationsTripSnapshot({
|
||||
required this.trip,
|
||||
required this.stops,
|
||||
required this.scheduledStops,
|
||||
});
|
||||
|
||||
final OperationsTrip trip;
|
||||
final List<OperationsStop> stops;
|
||||
final List<OperationsScheduledStop> scheduledStops;
|
||||
}
|
||||
|
||||
class OperationsDutySnapshot {
|
||||
const OperationsDutySnapshot({
|
||||
required this.dutyNumber,
|
||||
required this.busWorkNumber,
|
||||
required this.trips,
|
||||
});
|
||||
|
||||
final String dutyNumber;
|
||||
final String busWorkNumber;
|
||||
final List<OperationsTripSnapshot> trips;
|
||||
}
|
||||
|
||||
class OperationsStopAlias {
|
||||
const OperationsStopAlias({
|
||||
required this.rawStopName,
|
||||
required this.aliasStopName,
|
||||
required this.source,
|
||||
});
|
||||
|
||||
final String rawStopName;
|
||||
final String aliasStopName;
|
||||
final String source;
|
||||
}
|
||||
|
||||
class OperationsSnapshot {
|
||||
const OperationsSnapshot({
|
||||
required this.schedule,
|
||||
required this.trips,
|
||||
required this.duties,
|
||||
required this.scheduledStops,
|
||||
required this.stopAliasesByRawName,
|
||||
required this.stopAliases,
|
||||
});
|
||||
|
||||
final OperationsSchedule? schedule;
|
||||
final List<OperationsTripSnapshot> trips;
|
||||
final List<OperationsDutySnapshot> duties;
|
||||
final List<OperationsScheduledStop> scheduledStops;
|
||||
final Map<String, String> stopAliasesByRawName;
|
||||
final List<OperationsStopAlias> stopAliases;
|
||||
}
|
||||
|
||||
class OperationsChannel extends BaseChannel {
|
||||
OperationsChannel({
|
||||
required super.client,
|
||||
required super.id,
|
||||
required super.organizationId,
|
||||
required super.name,
|
||||
required super.description,
|
||||
required super.slug,
|
||||
required super.isPrivate,
|
||||
required super.position,
|
||||
}) : super(kind: ChannelKind.operations);
|
||||
|
||||
factory OperationsChannel.fromApi({
|
||||
required SupabaseClient client,
|
||||
required Map<String, dynamic> map,
|
||||
}) {
|
||||
return OperationsChannel(
|
||||
client: client,
|
||||
id: (map["id"] ?? "").toString(),
|
||||
organizationId: (map["organization_id"] ?? "").toString(),
|
||||
name: (map["name"] ?? "").toString(),
|
||||
description: (map["description"] ?? map["topic"] ?? "").toString(),
|
||||
slug: (map["slug"] ?? "").toString(),
|
||||
isPrivate: map["is_private"] == true,
|
||||
position: (map["position"] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Future<OperationsSnapshot> loadSnapshot() async {
|
||||
final scheduleRow = await client
|
||||
.from("operations_schedules")
|
||||
.select("id, version, source_file_name, uploaded_at")
|
||||
.eq("channel_id", id)
|
||||
.eq("is_active", true)
|
||||
.order("uploaded_at", ascending: false)
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
if (scheduleRow == null) {
|
||||
return const OperationsSnapshot(
|
||||
schedule: null,
|
||||
trips: <OperationsTripSnapshot>[],
|
||||
duties: <OperationsDutySnapshot>[],
|
||||
scheduledStops: <OperationsScheduledStop>[],
|
||||
stopAliasesByRawName: <String, String>{},
|
||||
stopAliases: <OperationsStopAlias>[],
|
||||
);
|
||||
}
|
||||
|
||||
final stopAliases = await listStopAliases();
|
||||
final stopAliasesByRawName = _aliasesByRawStopName(stopAliases);
|
||||
final aliasesByNormalizedName = <String, String>{};
|
||||
final aliasSourceByNormalizedName = <String, String>{};
|
||||
for (final entry in stopAliasesByRawName.entries) {
|
||||
aliasesByNormalizedName[Stop.normalizeName(entry.key)] = entry.value;
|
||||
}
|
||||
for (final alias in stopAliases) {
|
||||
aliasSourceByNormalizedName[Stop.normalizeName(alias.rawStopName)] =
|
||||
alias.source;
|
||||
}
|
||||
|
||||
final schedule = OperationsSchedule.fromMap(BaseChannel.asMap(scheduleRow));
|
||||
final tripRows = await client
|
||||
.from("operations_trips")
|
||||
.select(
|
||||
"id, schedule_id, trip_number, duty_number, bus_work_number, direction, sort_order",
|
||||
)
|
||||
.eq("schedule_id", schedule.id)
|
||||
.order("sort_order", ascending: true);
|
||||
|
||||
final trips = BaseChannel.asList(
|
||||
tripRows,
|
||||
).map((row) => OperationsTrip.fromMap(BaseChannel.asMap(row))).toList();
|
||||
|
||||
if (trips.isEmpty) {
|
||||
return OperationsSnapshot(
|
||||
schedule: schedule,
|
||||
trips: const <OperationsTripSnapshot>[],
|
||||
duties: const <OperationsDutySnapshot>[],
|
||||
scheduledStops: const <OperationsScheduledStop>[],
|
||||
stopAliasesByRawName: stopAliasesByRawName,
|
||||
stopAliases: stopAliases,
|
||||
);
|
||||
}
|
||||
|
||||
final tripIds = trips.map((trip) => trip.id).toList();
|
||||
final stopRows = await client
|
||||
.from("operations_trip_stops")
|
||||
.select("id, trip_id, stop_sequence, stop_name, scheduled_time")
|
||||
.filter("trip_id", "in", "(${tripIds.join(",")})")
|
||||
.order("trip_id", ascending: true)
|
||||
.order("stop_sequence", ascending: true);
|
||||
|
||||
final stopIds = BaseChannel.asList(stopRows)
|
||||
.map((row) => (BaseChannel.asMap(row)["id"] ?? "").toString())
|
||||
.where((value) => value.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
List<dynamic> updateRows = const <dynamic>[];
|
||||
if (stopIds.isNotEmpty) {
|
||||
updateRows = await client
|
||||
.from("operations_stop_updates")
|
||||
.select("trip_stop_id, status, actual_time, vehicle_id, notes")
|
||||
.filter("trip_stop_id", "in", "(${stopIds.join(",")})");
|
||||
}
|
||||
|
||||
final updatesByStopId = <String, Map<String, dynamic>>{};
|
||||
for (final row in BaseChannel.asList(updateRows)) {
|
||||
final map = BaseChannel.asMap(row);
|
||||
final tripStopId = (map["trip_stop_id"] ?? "").toString();
|
||||
if (tripStopId.isEmpty) continue;
|
||||
updatesByStopId[tripStopId] = map;
|
||||
}
|
||||
|
||||
final scheduledStopsByTripId = <String, List<OperationsScheduledStop>>{};
|
||||
final allScheduledStops = <OperationsScheduledStop>[];
|
||||
for (final row in BaseChannel.asList(stopRows)) {
|
||||
final map = BaseChannel.asMap(row);
|
||||
final stopId = (map["id"] ?? "").toString();
|
||||
final tripId = (map["trip_id"] ?? "").toString();
|
||||
final scheduledStop = OperationsScheduledStop(
|
||||
id: stopId,
|
||||
tripId: tripId,
|
||||
sequence: (map["stop_sequence"] as num?)?.toInt() ?? 0,
|
||||
name: (map["stop_name"] ?? "").toString().trim(),
|
||||
alias: aliasesByNormalizedName[
|
||||
Stop.normalizeName((map["stop_name"] ?? "").toString())],
|
||||
aliasSource: aliasSourceByNormalizedName[
|
||||
Stop.normalizeName((map["stop_name"] ?? "").toString())],
|
||||
scheduledTime: (map["scheduled_time"] ?? "").toString().trim().isEmpty
|
||||
? null
|
||||
: (map["scheduled_time"] ?? "").toString(),
|
||||
);
|
||||
allScheduledStops.add(scheduledStop);
|
||||
scheduledStopsByTripId
|
||||
.putIfAbsent(tripId, () => <OperationsScheduledStop>[])
|
||||
.add(scheduledStop);
|
||||
}
|
||||
|
||||
final snapshots = trips.map((trip) {
|
||||
final scheduledStops =
|
||||
scheduledStopsByTripId[trip.id] ?? const <OperationsScheduledStop>[];
|
||||
final stops = scheduledStops
|
||||
.map((scheduledStop) {
|
||||
final update = updatesByStopId[scheduledStop.id];
|
||||
final hasUpdate = update != null;
|
||||
return OperationsStop(
|
||||
scheduledStop: scheduledStop,
|
||||
update: !hasUpdate
|
||||
? null
|
||||
: OperationsStopUpdate(
|
||||
status: (update["status"] ?? "").toString().trim().isEmpty
|
||||
? null
|
||||
: (update["status"] ?? "").toString(),
|
||||
actualTime:
|
||||
(update["actual_time"] ?? "")
|
||||
.toString()
|
||||
.trim()
|
||||
.isEmpty
|
||||
? null
|
||||
: (update["actual_time"] ?? "").toString(),
|
||||
vehicleId:
|
||||
(update["vehicle_id"] ?? "").toString().trim().isEmpty
|
||||
? null
|
||||
: (update["vehicle_id"] ?? "").toString(),
|
||||
notes: (update["notes"] ?? "").toString().trim().isEmpty
|
||||
? null
|
||||
: (update["notes"] ?? "").toString(),
|
||||
),
|
||||
);
|
||||
})
|
||||
.toList(growable: false);
|
||||
return OperationsTripSnapshot(
|
||||
trip: trip,
|
||||
stops: stops,
|
||||
scheduledStops: scheduledStops,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final groupedByDuty = <String, List<OperationsTripSnapshot>>{};
|
||||
for (final snapshot in snapshots) {
|
||||
groupedByDuty
|
||||
.putIfAbsent(snapshot.trip.dutyKey, () => <OperationsTripSnapshot>[])
|
||||
.add(snapshot);
|
||||
}
|
||||
final duties = groupedByDuty.entries
|
||||
.map((entry) {
|
||||
final dutyTrips = entry.value;
|
||||
final firstTrip = dutyTrips.first.trip;
|
||||
return OperationsDutySnapshot(
|
||||
dutyNumber: firstTrip.dutyNumber,
|
||||
busWorkNumber: firstTrip.busWorkNumber,
|
||||
trips: dutyTrips,
|
||||
);
|
||||
})
|
||||
.toList(growable: false);
|
||||
|
||||
return OperationsSnapshot(
|
||||
schedule: schedule,
|
||||
trips: snapshots,
|
||||
duties: duties,
|
||||
scheduledStops: allScheduledStops,
|
||||
stopAliasesByRawName: stopAliasesByRawName,
|
||||
stopAliases: stopAliases,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<OperationsStopAlias>> listStopAliases() async {
|
||||
final rows = await client
|
||||
.from("operations_stop_aliases")
|
||||
.select("raw_stop_name, alias_stop_name, source")
|
||||
.eq("channel_id", id);
|
||||
|
||||
final aliases = <OperationsStopAlias>[];
|
||||
for (final row in BaseChannel.asList(rows)) {
|
||||
final map = BaseChannel.asMap(row);
|
||||
final raw = (map["raw_stop_name"] ?? "").toString().trim();
|
||||
final alias = (map["alias_stop_name"] ?? "").toString().trim();
|
||||
final source = (map["source"] ?? "").toString().trim();
|
||||
if (raw.isEmpty || alias.isEmpty) continue;
|
||||
aliases.add(
|
||||
OperationsStopAlias(
|
||||
rawStopName: raw,
|
||||
aliasStopName: alias,
|
||||
source: source.isEmpty ? "user" : source,
|
||||
),
|
||||
);
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
Map<String, String> _aliasesByRawStopName(List<OperationsStopAlias> aliases) {
|
||||
final byRawStopName = <String, String>{};
|
||||
for (final alias in aliases) {
|
||||
byRawStopName[alias.rawStopName] = alias.aliasStopName;
|
||||
}
|
||||
return byRawStopName;
|
||||
}
|
||||
|
||||
Future<void> markStopCompleted({
|
||||
required String tripStopId,
|
||||
required bool completed,
|
||||
String? actualTime,
|
||||
String? vehicleId,
|
||||
String? notes,
|
||||
}) async {
|
||||
final userId = client.auth.currentUser?.id;
|
||||
if (userId == null || userId.isEmpty) {
|
||||
throw StateError(
|
||||
"Cannot update stop status without an authenticated user.",
|
||||
);
|
||||
}
|
||||
|
||||
String? persistedActualTime = actualTime;
|
||||
if (completed &&
|
||||
(persistedActualTime == null || persistedActualTime.isEmpty)) {
|
||||
final now = DateTime.now();
|
||||
final hour = now.hour.toString().padLeft(2, "0");
|
||||
final minute = now.minute.toString().padLeft(2, "0");
|
||||
persistedActualTime = "$hour:$minute";
|
||||
}
|
||||
|
||||
await client.from("operations_stop_updates").upsert({
|
||||
"trip_stop_id": tripStopId,
|
||||
"status": completed ? "completed" : null,
|
||||
"actual_time": completed ? persistedActualTime : null,
|
||||
"vehicle_id": vehicleId,
|
||||
"notes": notes,
|
||||
"updated_by": userId,
|
||||
}, onConflict: "trip_stop_id");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import "package:bus_running_record/models/channels/base_channel.dart";
|
||||
import "package:supabase_flutter/supabase_flutter.dart";
|
||||
|
||||
class TextChannelMessage {
|
||||
const TextChannelMessage({
|
||||
required this.id,
|
||||
required this.channelId,
|
||||
required this.authorUserId,
|
||||
required this.content,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String channelId;
|
||||
final String authorUserId;
|
||||
final String content;
|
||||
final DateTime? createdAt;
|
||||
|
||||
factory TextChannelMessage.fromMap(Map<String, dynamic> map) {
|
||||
return TextChannelMessage(
|
||||
id: (map["id"] ?? "").toString(),
|
||||
channelId: (map["channel_id"] ?? "").toString(),
|
||||
authorUserId: (map["author_user_id"] ?? "").toString(),
|
||||
content: (map["content"] ?? "").toString(),
|
||||
createdAt: DateTime.tryParse((map["created_at"] ?? "").toString()),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"id": id,
|
||||
"channel_id": channelId,
|
||||
"author_user_id": authorUserId,
|
||||
"content": content,
|
||||
"created_at": createdAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TextChannel extends BaseChannel {
|
||||
TextChannel({
|
||||
required super.client,
|
||||
required super.id,
|
||||
required super.organizationId,
|
||||
required super.name,
|
||||
required super.description,
|
||||
required super.slug,
|
||||
required super.isPrivate,
|
||||
required super.position,
|
||||
}) : super(kind: ChannelKind.text);
|
||||
|
||||
factory TextChannel.fromApi({
|
||||
required SupabaseClient client,
|
||||
required Map<String, dynamic> map,
|
||||
}) {
|
||||
return TextChannel(
|
||||
client: client,
|
||||
id: (map["id"] ?? "").toString(),
|
||||
organizationId: (map["organization_id"] ?? "").toString(),
|
||||
name: (map["name"] ?? "").toString(),
|
||||
description: (map["description"] ?? map["topic"] ?? "").toString(),
|
||||
slug: (map["slug"] ?? "").toString(),
|
||||
isPrivate: map["is_private"] == true,
|
||||
position: (map["position"] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<TextChannelMessage>> listMessages({int limit = 50}) async {
|
||||
final rows = await client
|
||||
.from("messages")
|
||||
.select("id, channel_id, author_user_id, content, created_at")
|
||||
.eq("channel_id", id)
|
||||
.order("created_at", ascending: true)
|
||||
.limit(limit);
|
||||
|
||||
return BaseChannel.asList(rows)
|
||||
.map((row) => TextChannelMessage.fromMap(BaseChannel.asMap(row)))
|
||||
.where((message) => message.id.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<TextChannelMessage?> sendMessage(String content) async {
|
||||
final trimmed = content.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
|
||||
final authorUserId = client.auth.currentUser?.id;
|
||||
if (authorUserId == null || authorUserId.isEmpty) {
|
||||
throw StateError("Cannot send message without an authenticated user.");
|
||||
}
|
||||
|
||||
final inserted = await client
|
||||
.from("messages")
|
||||
.insert({
|
||||
"channel_id": id,
|
||||
"author_user_id": authorUserId,
|
||||
"content": trimmed,
|
||||
})
|
||||
.select("id, channel_id, author_user_id, content, created_at")
|
||||
.single();
|
||||
|
||||
return TextChannelMessage.fromMap(BaseChannel.asMap(inserted));
|
||||
}
|
||||
|
||||
RealtimeChannel subscribeToMessages({
|
||||
required void Function() onMessageChanged,
|
||||
void Function(RealtimeSubscribeStatus status, Object? error)? onStatus,
|
||||
}) {
|
||||
final topic = "messages:$id";
|
||||
return client.channel(topic)
|
||||
..onPostgresChanges(
|
||||
event: PostgresChangeEvent.all,
|
||||
schema: "public",
|
||||
table: "messages",
|
||||
filter: PostgresChangeFilter(
|
||||
type: PostgresChangeFilterType.eq,
|
||||
column: "channel_id",
|
||||
value: id,
|
||||
),
|
||||
callback: (_) => onMessageChanged(),
|
||||
)
|
||||
..subscribe((status, [error]) {
|
||||
onStatus?.call(status, error);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> unsubscribe(RealtimeChannel channel) async {
|
||||
await client.removeChannel(channel);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user