✨ Dynamic chat online counter basis
This commit is contained in:
50
lib/pods/chat/chat_online_count.dart
Normal file
50
lib/pods/chat/chat_online_count.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'dart:async';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
|
||||
part 'chat_online_count.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ChatOnlineCountNotifier extends _$ChatOnlineCountNotifier {
|
||||
@override
|
||||
Future<int> build(String chatroomId) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final ws = ref.watch(websocketProvider);
|
||||
|
||||
// Fetch initial online count
|
||||
final response = await apiClient.get(
|
||||
'/sphere/chat/$chatroomId/members/online',
|
||||
);
|
||||
final initialCount = response.data as int;
|
||||
|
||||
// Listen for websocket status updates
|
||||
final subscription = ws.dataStream.listen((WebSocketPacket packet) {
|
||||
if (packet.type == 'accounts.status.update') {
|
||||
final data = packet.data;
|
||||
if (data != null && data['chat_room_id'] == chatroomId) {
|
||||
final status = SnAccountStatus.fromJson(data['status']);
|
||||
var delta = status.isOnline ? 1 : -1;
|
||||
if (status.clearedAt != null &&
|
||||
status.clearedAt!.isBefore(DateTime.now())) {
|
||||
if (status.isInvisible) delta = 1;
|
||||
}
|
||||
// Update count based on online status
|
||||
state.whenData((currentCount) {
|
||||
final newCount = currentCount + delta;
|
||||
state = AsyncData(
|
||||
newCount.clamp(0, double.infinity).toInt(),
|
||||
); // Ensure non-negative
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
subscription.cancel();
|
||||
});
|
||||
|
||||
return initialCount;
|
||||
}
|
||||
}
|
168
lib/pods/chat/chat_online_count.g.dart
Normal file
168
lib/pods/chat/chat_online_count.g.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'chat_online_count.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatOnlineCountNotifierHash() =>
|
||||
r'254ed141ffd99585d898203b3d2b86c4d18db80d';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$ChatOnlineCountNotifier
|
||||
extends BuildlessAutoDisposeAsyncNotifier<int> {
|
||||
late final String chatroomId;
|
||||
|
||||
FutureOr<int> build(String chatroomId);
|
||||
}
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
@ProviderFor(ChatOnlineCountNotifier)
|
||||
const chatOnlineCountNotifierProvider = ChatOnlineCountNotifierFamily();
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
class ChatOnlineCountNotifierFamily extends Family<AsyncValue<int>> {
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
const ChatOnlineCountNotifierFamily();
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
ChatOnlineCountNotifierProvider call(String chatroomId) {
|
||||
return ChatOnlineCountNotifierProvider(chatroomId);
|
||||
}
|
||||
|
||||
@override
|
||||
ChatOnlineCountNotifierProvider getProviderOverride(
|
||||
covariant ChatOnlineCountNotifierProvider provider,
|
||||
) {
|
||||
return call(provider.chatroomId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'chatOnlineCountNotifierProvider';
|
||||
}
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
class ChatOnlineCountNotifierProvider
|
||||
extends AutoDisposeAsyncNotifierProviderImpl<ChatOnlineCountNotifier, int> {
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
ChatOnlineCountNotifierProvider(String chatroomId)
|
||||
: this._internal(
|
||||
() => ChatOnlineCountNotifier()..chatroomId = chatroomId,
|
||||
from: chatOnlineCountNotifierProvider,
|
||||
name: r'chatOnlineCountNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$chatOnlineCountNotifierHash,
|
||||
dependencies: ChatOnlineCountNotifierFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ChatOnlineCountNotifierFamily._allTransitiveDependencies,
|
||||
chatroomId: chatroomId,
|
||||
);
|
||||
|
||||
ChatOnlineCountNotifierProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.chatroomId,
|
||||
}) : super.internal();
|
||||
|
||||
final String chatroomId;
|
||||
|
||||
@override
|
||||
FutureOr<int> runNotifierBuild(covariant ChatOnlineCountNotifier notifier) {
|
||||
return notifier.build(chatroomId);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(ChatOnlineCountNotifier Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ChatOnlineCountNotifierProvider._internal(
|
||||
() => create()..chatroomId = chatroomId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
chatroomId: chatroomId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<ChatOnlineCountNotifier, int>
|
||||
createElement() {
|
||||
return _ChatOnlineCountNotifierProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ChatOnlineCountNotifierProvider &&
|
||||
other.chatroomId == chatroomId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, chatroomId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ChatOnlineCountNotifierRef on AutoDisposeAsyncNotifierProviderRef<int> {
|
||||
/// The parameter `chatroomId` of this provider.
|
||||
String get chatroomId;
|
||||
}
|
||||
|
||||
class _ChatOnlineCountNotifierProviderElement
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderElement<ChatOnlineCountNotifier, int>
|
||||
with ChatOnlineCountNotifierRef {
|
||||
_ChatOnlineCountNotifierProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get chatroomId =>
|
||||
(origin as ChatOnlineCountNotifierProvider).chatroomId;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
5
lib/pods/chat/chat_rooms.dart
Normal file
5
lib/pods/chat/chat_rooms.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
|
||||
final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
|
||||
|
||||
final flashingMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
219
lib/pods/chat/chat_subscribe.dart
Normal file
219
lib/pods/chat/chat_subscribe.dart
Normal file
@@ -0,0 +1,219 @@
|
||||
import "dart:async";
|
||||
import "dart:convert";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/pods/lifecycle.dart";
|
||||
import "package:island/pods/chat/messages_notifier.dart";
|
||||
import "package:island/pods/websocket.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/widgets/chat/call_button.dart";
|
||||
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||
|
||||
part 'chat_subscribe.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
|
||||
late final String _roomId;
|
||||
late final SnChatRoom _chatRoom;
|
||||
late final SnChatMember _chatIdentity;
|
||||
late final MessagesNotifier _messagesNotifier;
|
||||
|
||||
final List<SnChatMember> _typingStatuses = [];
|
||||
Timer? _typingCleanupTimer;
|
||||
Timer? _typingCooldownTimer;
|
||||
Timer? _periodicSubscribeTimer;
|
||||
StreamSubscription? _wsSubscription;
|
||||
|
||||
@override
|
||||
List<SnChatMember> build(String roomId) {
|
||||
_roomId = roomId;
|
||||
final ws = ref.watch(websocketProvider);
|
||||
final chatRoomAsync = ref.watch(chatroomProvider(roomId));
|
||||
final chatIdentityAsync = ref.watch(chatroomIdentityProvider(roomId));
|
||||
_messagesNotifier = ref.watch(messagesNotifierProvider(roomId).notifier);
|
||||
|
||||
if (chatRoomAsync.isLoading || chatIdentityAsync.isLoading) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (chatRoomAsync.value == null || chatIdentityAsync.value == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
_chatRoom = chatRoomAsync.value!;
|
||||
_chatIdentity = chatIdentityAsync.value!;
|
||||
|
||||
// Subscribe to messages
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.subscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Send initial read receipt
|
||||
sendReadReceipt();
|
||||
|
||||
// Set up WebSocket listener
|
||||
_wsSubscription = ws.dataStream.listen(onMessage);
|
||||
|
||||
// Set up typing status cleanup timer
|
||||
_typingCleanupTimer = Timer.periodic(const Duration(seconds: 5), (_) {
|
||||
if (_typingStatuses.isNotEmpty) {
|
||||
// Remove typing statuses older than 5 seconds
|
||||
final now = DateTime.now();
|
||||
_typingStatuses.removeWhere((member) {
|
||||
final lastTyped =
|
||||
member.lastTyped ??
|
||||
DateTime.now().subtract(const Duration(milliseconds: 1350));
|
||||
return now.difference(lastTyped).inSeconds > 5;
|
||||
});
|
||||
state = List.of(_typingStatuses);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up periodic subscribe timer (every 5 minutes)
|
||||
_periodicSubscribeTimer = Timer.periodic(const Duration(minutes: 5), (_) {
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.subscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Listen to app lifecycle changes
|
||||
ref.listen(appLifecycleStateProvider, (previous, next) {
|
||||
final lifecycleState = next.value;
|
||||
if (lifecycleState == AppLifecycleState.paused ||
|
||||
lifecycleState == AppLifecycleState.inactive) {
|
||||
// Unsubscribe when app goes to background
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.unsubscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (lifecycleState == AppLifecycleState.resumed) {
|
||||
// Resubscribe when app comes back to foreground
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.subscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on dispose
|
||||
ref.onDispose(() {
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.unsubscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
),
|
||||
),
|
||||
);
|
||||
_wsSubscription?.cancel();
|
||||
_typingCleanupTimer?.cancel();
|
||||
_typingCooldownTimer?.cancel();
|
||||
_periodicSubscribeTimer?.cancel();
|
||||
});
|
||||
|
||||
return _typingStatuses;
|
||||
}
|
||||
|
||||
void onMessage(WebSocketPacket pkt) {
|
||||
if (!pkt.type.startsWith('messages')) return;
|
||||
if (['messages.read'].contains(pkt.type)) return;
|
||||
|
||||
if (pkt.type == 'messages.typing' && pkt.data?['sender'] != null) {
|
||||
if (pkt.data?['room_id'] != _chatRoom.id) return;
|
||||
if (pkt.data?['sender_id'] == _chatIdentity.id) return;
|
||||
|
||||
final sender = SnChatMember.fromJson(
|
||||
pkt.data?['sender'],
|
||||
).copyWith(lastTyped: DateTime.now());
|
||||
|
||||
// Check if the sender is already in the typing list
|
||||
final existingIndex = _typingStatuses.indexWhere(
|
||||
(member) => member.id == sender.id,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
// Update the existing entry with new timestamp
|
||||
_typingStatuses[existingIndex] = sender;
|
||||
} else {
|
||||
// Add new typing status
|
||||
_typingStatuses.add(sender);
|
||||
}
|
||||
state = List.of(_typingStatuses);
|
||||
return;
|
||||
}
|
||||
|
||||
final message = SnChatMessage.fromJson(pkt.data!);
|
||||
if (message.chatRoomId != _chatRoom.id) return;
|
||||
switch (pkt.type) {
|
||||
case 'messages.new':
|
||||
case 'messages.update':
|
||||
case 'messages.delete':
|
||||
if (message.type.startsWith('call')) {
|
||||
// Handle the ongoing call.
|
||||
ref.invalidate(ongoingCallProvider(message.chatRoomId));
|
||||
}
|
||||
_messagesNotifier.receiveMessage(message);
|
||||
// Send read receipt for new message
|
||||
sendReadReceipt();
|
||||
}
|
||||
}
|
||||
|
||||
void sendReadReceipt() {
|
||||
// Send websocket packet
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.read',
|
||||
data: {'chat_room_id': _roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void sendTypingStatus() {
|
||||
// Don't send if we're already in a cooldown period
|
||||
if (_typingCooldownTimer != null) return;
|
||||
|
||||
// Send typing status immediately
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.typing',
|
||||
data: {'chat_room_id': _roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
_typingCooldownTimer = Timer(const Duration(milliseconds: 850), () {
|
||||
_typingCooldownTimer = null;
|
||||
});
|
||||
}
|
||||
}
|
176
lib/pods/chat/chat_subscribe.g.dart
Normal file
176
lib/pods/chat/chat_subscribe.g.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'chat_subscribe.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatSubscribeNotifierHash() =>
|
||||
r'10a6b2c687149ebb419e4c96349d8bab1f183ec6';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$ChatSubscribeNotifier
|
||||
extends BuildlessAutoDisposeNotifier<List<SnChatMember>> {
|
||||
late final String roomId;
|
||||
|
||||
List<SnChatMember> build(String roomId);
|
||||
}
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
@ProviderFor(ChatSubscribeNotifier)
|
||||
const chatSubscribeNotifierProvider = ChatSubscribeNotifierFamily();
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
class ChatSubscribeNotifierFamily extends Family<List<SnChatMember>> {
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
const ChatSubscribeNotifierFamily();
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
ChatSubscribeNotifierProvider call(String roomId) {
|
||||
return ChatSubscribeNotifierProvider(roomId);
|
||||
}
|
||||
|
||||
@override
|
||||
ChatSubscribeNotifierProvider getProviderOverride(
|
||||
covariant ChatSubscribeNotifierProvider provider,
|
||||
) {
|
||||
return call(provider.roomId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'chatSubscribeNotifierProvider';
|
||||
}
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
class ChatSubscribeNotifierProvider
|
||||
extends
|
||||
AutoDisposeNotifierProviderImpl<
|
||||
ChatSubscribeNotifier,
|
||||
List<SnChatMember>
|
||||
> {
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
ChatSubscribeNotifierProvider(String roomId)
|
||||
: this._internal(
|
||||
() => ChatSubscribeNotifier()..roomId = roomId,
|
||||
from: chatSubscribeNotifierProvider,
|
||||
name: r'chatSubscribeNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$chatSubscribeNotifierHash,
|
||||
dependencies: ChatSubscribeNotifierFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ChatSubscribeNotifierFamily._allTransitiveDependencies,
|
||||
roomId: roomId,
|
||||
);
|
||||
|
||||
ChatSubscribeNotifierProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.roomId,
|
||||
}) : super.internal();
|
||||
|
||||
final String roomId;
|
||||
|
||||
@override
|
||||
List<SnChatMember> runNotifierBuild(
|
||||
covariant ChatSubscribeNotifier notifier,
|
||||
) {
|
||||
return notifier.build(roomId);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(ChatSubscribeNotifier Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ChatSubscribeNotifierProvider._internal(
|
||||
() => create()..roomId = roomId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
roomId: roomId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeNotifierProviderElement<ChatSubscribeNotifier, List<SnChatMember>>
|
||||
createElement() {
|
||||
return _ChatSubscribeNotifierProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ChatSubscribeNotifierProvider && other.roomId == roomId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, roomId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ChatSubscribeNotifierRef
|
||||
on AutoDisposeNotifierProviderRef<List<SnChatMember>> {
|
||||
/// The parameter `roomId` of this provider.
|
||||
String get roomId;
|
||||
}
|
||||
|
||||
class _ChatSubscribeNotifierProviderElement
|
||||
extends
|
||||
AutoDisposeNotifierProviderElement<
|
||||
ChatSubscribeNotifier,
|
||||
List<SnChatMember>
|
||||
>
|
||||
with ChatSubscribeNotifierRef {
|
||||
_ChatSubscribeNotifierProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get roomId => (origin as ChatSubscribeNotifierProvider).roomId;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
@@ -6,7 +6,7 @@ part of 'chat_summary.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatSummaryHash() => r'87a10e4cefa37dc5fa8eadb175ef1b2bed6070bf';
|
||||
String _$chatSummaryHash() => r'7b79dba7445f634373fbb2ee0ced99b2302097c2';
|
||||
|
||||
/// See also [ChatSummary].
|
||||
@ProviderFor(ChatSummary)
|
@@ -10,13 +10,14 @@ import "package:island/models/chat.dart";
|
||||
import "package:island/models/file.dart";
|
||||
import "package:island/pods/config.dart";
|
||||
import "package:island/pods/database.dart";
|
||||
import "package:island/pods/lifecycle.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/services/file.dart";
|
||||
import "package:island/widgets/alert.dart";
|
||||
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||
import "package:uuid/uuid.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/pods/chat_rooms.dart";
|
||||
import "package:island/pods/chat/chat_rooms.dart";
|
||||
|
||||
part 'messages_notifier.g.dart';
|
||||
|
||||
@@ -66,13 +67,15 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
// Only setup sync and lifecycle listeners if user is a member
|
||||
if (identity != null) {
|
||||
ref.listen(appLifecycleStateProvider, (_, next) {
|
||||
if (next.hasValue && next.value == AppLifecycleState.resumed) {
|
||||
developer.log(
|
||||
'App resumed, syncing messages',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
syncMessages();
|
||||
}
|
||||
next.whenData((state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
developer.log(
|
||||
'App resumed, syncing messages',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
syncMessages();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$messagesNotifierHash() => r'37bab723531a5c5248471ef810b74cf57b3dc237';
|
||||
String _$messagesNotifierHash() => r'4257c9b3792418e913d0bac3ef58e727314635af';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
@@ -7,7 +7,7 @@ part of 'config.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$appSettingsNotifierHash() =>
|
||||
r'b5e9b2ea9b01c236a68669a00eaa563c1fb4efa6';
|
||||
r'3b0967a39a375c664c3fd44cbee7936b8b2f5fec';
|
||||
|
||||
/// See also [AppSettingsNotifier].
|
||||
@ProviderFor(AppSettingsNotifier)
|
||||
|
@@ -2,10 +2,6 @@ import "dart:async";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
|
||||
final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
|
||||
|
||||
final flashingMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
||||
|
||||
final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) {
|
||||
final controller = StreamController<AppLifecycleState>();
|
||||
|
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart' hide ConnectionState;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/chat/call_button.dart';
|
||||
import 'package:island/widgets/chat/call_overlay.dart';
|
||||
|
@@ -11,8 +11,8 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/realm.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat_summary.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/pods/chat/chat_summary.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/realm/realms.dart';
|
||||
|
@@ -17,7 +17,7 @@ import "package:island/widgets/chat/message_item.dart";
|
||||
import "package:island/widgets/response.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/services/responsive.dart";
|
||||
import "package:island/pods/messages_notifier.dart";
|
||||
import "package:island/pods/chat/messages_notifier.dart";
|
||||
|
||||
class PublicRoomPreview extends HookConsumerWidget {
|
||||
final String id;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import "dart:async";
|
||||
import "dart:convert";
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:file_picker/file_picker.dart";
|
||||
import "package:flutter/material.dart";
|
||||
@@ -10,10 +9,11 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/models/file.dart";
|
||||
import "package:island/pods/chat/chat_subscribe.dart";
|
||||
import "package:island/pods/config.dart";
|
||||
import "package:island/pods/messages_notifier.dart";
|
||||
import "package:island/pods/chat/messages_notifier.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/pods/websocket.dart";
|
||||
import "package:island/pods/chat/chat_online_count.dart";
|
||||
import "package:island/services/file.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/services/responsive.dart";
|
||||
@@ -37,33 +37,6 @@ final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
|
||||
|
||||
final flashingMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
||||
|
||||
final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) {
|
||||
final controller = StreamController<AppLifecycleState>();
|
||||
|
||||
final observer = _AppLifecycleObserver((state) {
|
||||
if (controller.isClosed) return;
|
||||
controller.add(state);
|
||||
});
|
||||
WidgetsBinding.instance.addObserver(observer);
|
||||
|
||||
ref.onDispose(() {
|
||||
WidgetsBinding.instance.removeObserver(observer);
|
||||
controller.close();
|
||||
});
|
||||
|
||||
return controller.stream;
|
||||
});
|
||||
|
||||
class _AppLifecycleObserver extends WidgetsBindingObserver {
|
||||
final ValueChanged<AppLifecycleState> onChange;
|
||||
_AppLifecycleObserver(this.onChange);
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
onChange(state);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatRoomScreen extends HookConsumerWidget {
|
||||
final String id;
|
||||
const ChatRoomScreen({super.key, required this.id});
|
||||
@@ -73,6 +46,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
final chatRoom = ref.watch(chatroomProvider(id));
|
||||
final chatIdentity = ref.watch(chatroomIdentityProvider(id));
|
||||
final isSyncing = ref.watch(isSyncingProvider);
|
||||
final onlineCount = ref.watch(chatOnlineCountNotifierProvider(id));
|
||||
|
||||
if (chatIdentity.isLoading || chatRoom.isLoading) {
|
||||
return AppScaffold(
|
||||
@@ -157,7 +131,10 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
|
||||
final messages = ref.watch(messagesNotifierProvider(id));
|
||||
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
|
||||
final ws = ref.watch(websocketProvider);
|
||||
final chatSubscribe = ref.watch(chatSubscribeNotifierProvider(id));
|
||||
final chatSubscribeNotifier = ref.read(
|
||||
chatSubscribeNotifierProvider(id).notifier,
|
||||
);
|
||||
|
||||
final messageController = useTextEditingController();
|
||||
final scrollController = useScrollController();
|
||||
@@ -168,65 +145,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
final attachments = useState<List<UniversalFile>>([]);
|
||||
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
|
||||
|
||||
// Function to send read receipt
|
||||
void sendReadReceipt() async {
|
||||
// Send websocket packet
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.read',
|
||||
data: {'chat_room_id': id},
|
||||
endpoint: 'DysonNetwork.Sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Members who are typing
|
||||
final typingStatuses = useState<List<SnChatMember>>([]);
|
||||
final typingDebouncer = useState<Timer?>(null);
|
||||
|
||||
void sendTypingStatus() {
|
||||
// Don't send if we're already in a cooldown period
|
||||
if (typingDebouncer.value != null) return;
|
||||
|
||||
// Send typing status immediately
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.typing',
|
||||
data: {'chat_room_id': id},
|
||||
endpoint: 'DysonNetwork.Sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
typingDebouncer.value = Timer(const Duration(milliseconds: 850), () {
|
||||
typingDebouncer.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
// Add timer to remove typing status after inactivity
|
||||
useEffect(() {
|
||||
final removeTypingTimer = Timer.periodic(const Duration(seconds: 5), (_) {
|
||||
if (typingStatuses.value.isNotEmpty) {
|
||||
// Remove typing statuses older than 5 seconds
|
||||
final now = DateTime.now();
|
||||
typingStatuses.value =
|
||||
typingStatuses.value.where((member) {
|
||||
final lastTyped =
|
||||
member.lastTyped ??
|
||||
DateTime.now().subtract(const Duration(milliseconds: 1350));
|
||||
return now.difference(lastTyped).inSeconds < 5;
|
||||
}).toList();
|
||||
}
|
||||
});
|
||||
|
||||
return () => removeTypingTimer.cancel();
|
||||
}, []);
|
||||
|
||||
var isLoading = false;
|
||||
|
||||
final listController = useMemoized(() => ListController(), []);
|
||||
@@ -246,79 +164,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
return () => scrollController.removeListener(onScroll);
|
||||
}, [scrollController]);
|
||||
|
||||
// Add websocket listener for new messages
|
||||
useEffect(() {
|
||||
void onMessage(WebSocketPacket pkt) {
|
||||
if (!pkt.type.startsWith('messages')) return;
|
||||
if (['messages.read'].contains(pkt.type)) return;
|
||||
|
||||
if (pkt.type == 'messages.typing' && pkt.data?['sender'] != null) {
|
||||
if (pkt.data?['room_id'] != chatRoom.value?.id) return;
|
||||
if (pkt.data?['sender_id'] == chatIdentity.value?.id) return;
|
||||
|
||||
final sender = SnChatMember.fromJson(
|
||||
pkt.data?['sender'],
|
||||
).copyWith(lastTyped: DateTime.now());
|
||||
|
||||
// Check if the sender is already in the typing list
|
||||
final existingIndex = typingStatuses.value.indexWhere(
|
||||
(member) => member.id == sender.id,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
// Update the existing entry with new timestamp
|
||||
final updatedList = [...typingStatuses.value];
|
||||
updatedList[existingIndex] = sender;
|
||||
typingStatuses.value = updatedList;
|
||||
} else {
|
||||
// Add new typing status
|
||||
typingStatuses.value = [...typingStatuses.value, sender];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final message = SnChatMessage.fromJson(pkt.data!);
|
||||
if (message.chatRoomId != chatRoom.value?.id) return;
|
||||
switch (pkt.type) {
|
||||
case 'messages.new':
|
||||
case 'messages.update':
|
||||
case 'messages.delete':
|
||||
if (message.type.startsWith('call')) {
|
||||
// Handle the ongoing call.
|
||||
ref.invalidate(ongoingCallProvider(message.chatRoomId));
|
||||
}
|
||||
messagesNotifier.receiveMessage(message);
|
||||
// Send read receipt for new message
|
||||
sendReadReceipt();
|
||||
}
|
||||
}
|
||||
|
||||
sendReadReceipt();
|
||||
final subscription = ws.dataStream.listen(onMessage);
|
||||
return () => subscription.cancel();
|
||||
}, [ws, chatRoom]);
|
||||
|
||||
useEffect(() {
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.subscribe',
|
||||
data: {'chat_room_id': id},
|
||||
),
|
||||
),
|
||||
);
|
||||
return () {
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.unsubscribe',
|
||||
data: {'chat_room_id': id},
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
Future<void> pickPhotoMedia() async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
@@ -352,21 +197,19 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
void sendMessage() {
|
||||
if (messageController.text.trim().isNotEmpty ||
|
||||
attachments.value.isNotEmpty) {
|
||||
messagesNotifier
|
||||
.sendMessage(
|
||||
messageController.text.trim(),
|
||||
attachments.value,
|
||||
editingTo: messageEditingTo.value,
|
||||
forwardingTo: messageForwardingTo.value,
|
||||
replyingTo: messageReplyingTo.value,
|
||||
onProgress: (messageId, progress) {
|
||||
attachmentProgress.value = {
|
||||
...attachmentProgress.value,
|
||||
messageId: progress,
|
||||
};
|
||||
},
|
||||
)
|
||||
.then((_) => sendReadReceipt());
|
||||
messagesNotifier.sendMessage(
|
||||
messageController.text.trim(),
|
||||
attachments.value,
|
||||
editingTo: messageEditingTo.value,
|
||||
forwardingTo: messageForwardingTo.value,
|
||||
replyingTo: messageReplyingTo.value,
|
||||
onProgress: (messageId, progress) {
|
||||
attachmentProgress.value = {
|
||||
...attachmentProgress.value,
|
||||
messageId: progress,
|
||||
};
|
||||
},
|
||||
);
|
||||
messageController.clear();
|
||||
messageEditingTo.value = null;
|
||||
messageReplyingTo.value = null;
|
||||
@@ -379,7 +222,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
useEffect(() {
|
||||
void onTextChange() {
|
||||
if (messageController.text.isNotEmpty) {
|
||||
sendTypingStatus();
|
||||
chatSubscribeNotifier.sendTypingStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,15 +557,33 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
bottom:
|
||||
isSyncing
|
||||
? const PreferredSize(
|
||||
preferredSize: Size.fromHeight(2),
|
||||
child: LinearProgressIndicator(
|
||||
bottom: () {
|
||||
final hasProgress = isSyncing;
|
||||
final hasOnlineCount = onlineCount.hasValue;
|
||||
if (!hasProgress && !hasOnlineCount) return null;
|
||||
return PreferredSize(
|
||||
preferredSize: Size.fromHeight(
|
||||
(hasProgress ? 2 : 0) + (hasOnlineCount ? 24 : 0),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (hasProgress)
|
||||
const LinearProgressIndicator(
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
if (hasOnlineCount)
|
||||
Container(
|
||||
height: 24,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'${(onlineCount as AsyncData).value} online',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
@@ -781,7 +642,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
child:
|
||||
typingStatuses.value.isNotEmpty
|
||||
chatSubscribe.isNotEmpty
|
||||
? Container(
|
||||
key: const ValueKey('typing-indicator'),
|
||||
width: double.infinity,
|
||||
@@ -799,9 +660,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
'typingHint'.plural(
|
||||
typingStatuses.value.length,
|
||||
chatSubscribe.length,
|
||||
args: [
|
||||
typingStatuses.value
|
||||
chatSubscribe
|
||||
.map(
|
||||
(x) =>
|
||||
x.nick ??
|
||||
|
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/messages_notifier.dart';
|
||||
import 'package:island/pods/chat/messages_notifier.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/chat/message_list_tile.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
@@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||
|
@@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_popup_card/flutter_popup_card.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/widgets/account/account_nameplate.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/screens/account/profile.dart';
|
||||
import 'package:island/widgets/chat/call_participant_card.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
|
@@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:pretty_diff_text/pretty_diff_text.dart';
|
||||
|
@@ -11,7 +11,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/database/message.dart';
|
||||
import 'package:island/models/embed.dart';
|
||||
import 'package:island/pods/messages_notifier.dart';
|
||||
import 'package:island/pods/chat/messages_notifier.dart';
|
||||
import 'package:island/pods/translate.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/screens/chat/room.dart';
|
||||
|
@@ -6,7 +6,7 @@ import "package:gap/gap.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/pods/messages_notifier.dart";
|
||||
import "package:island/pods/chat/messages_notifier.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/services/responsive.dart";
|
||||
import "package:island/widgets/alert.dart";
|
||||
|
Reference in New Issue
Block a user