Typing indicator

This commit is contained in:
LittleSheep 2025-06-08 01:16:48 +08:00
parent b2a118bbd0
commit 20e6cc4283
7 changed files with 223 additions and 69 deletions

View File

@ -353,5 +353,9 @@
"authDeviceLabelTitle": "Edit Device Label", "authDeviceLabelTitle": "Edit Device Label",
"authDeviceLabelHint": "Enter a name for this device", "authDeviceLabelHint": "Enter a name for this device",
"authDeviceSwipeEditHint": "Swipe left to edit label", "authDeviceSwipeEditHint": "Swipe left to edit label",
"authDeviceSwipeLogoutHint": "Swipe right to logout device" "authDeviceSwipeLogoutHint": "Swipe right to logout device",
"typingHint": {
"one": "{} is typing...",
"other": "{} are typing..."
}
} }

View File

@ -91,6 +91,7 @@ sealed class SnChatMember with _$SnChatMember {
required int notify, required int notify,
required DateTime? joinedAt, required DateTime? joinedAt,
required bool isBot, required bool isBot,
DateTime? lastTyped,
}) = _SnChatMember; }) = _SnChatMember;
factory SnChatMember.fromJson(Map<String, dynamic> json) => factory SnChatMember.fromJson(Map<String, dynamic> json) =>

View File

@ -663,7 +663,7 @@ $SnChatMemberCopyWith<$Res> get sender {
/// @nodoc /// @nodoc
mixin _$SnChatMember { mixin _$SnChatMember {
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get chatRoomId; SnChatRoom? get chatRoom; String get accountId; SnAccount get account; String? get nick; int get role; int get notify; DateTime? get joinedAt; bool get isBot; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get chatRoomId; SnChatRoom? get chatRoom; String get accountId; SnAccount get account; String? get nick; int get role; int get notify; DateTime? get joinedAt; bool get isBot; DateTime? get lastTyped;
/// Create a copy of SnChatMember /// Create a copy of SnChatMember
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -676,16 +676,16 @@ $SnChatMemberCopyWith<SnChatMember> get copyWith => _$SnChatMemberCopyWithImpl<S
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isBot, isBot) || other.isBot == isBot)); return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isBot, isBot) || other.isBot == isBot)&&(identical(other.lastTyped, lastTyped) || other.lastTyped == lastTyped));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot); int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot,lastTyped);
@override @override
String toString() { String toString() {
return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot)'; return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot, lastTyped: $lastTyped)';
} }
@ -696,7 +696,7 @@ abstract mixin class $SnChatMemberCopyWith<$Res> {
factory $SnChatMemberCopyWith(SnChatMember value, $Res Function(SnChatMember) _then) = _$SnChatMemberCopyWithImpl; factory $SnChatMemberCopyWith(SnChatMember value, $Res Function(SnChatMember) _then) = _$SnChatMemberCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot, DateTime? lastTyped
}); });
@ -713,7 +713,7 @@ class _$SnChatMemberCopyWithImpl<$Res>
/// Create a copy of SnChatMember /// Create a copy of SnChatMember
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,}) { @pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,Object? lastTyped = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
@ -728,7 +728,8 @@ as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_no
as int,notify: null == notify ? _self.notify : notify // ignore: cast_nullable_to_non_nullable as int,notify: null == notify ? _self.notify : notify // ignore: cast_nullable_to_non_nullable
as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable
as bool, as bool,lastTyped: freezed == lastTyped ? _self.lastTyped : lastTyped // ignore: cast_nullable_to_non_nullable
as DateTime?,
)); ));
} }
/// Create a copy of SnChatMember /// Create a copy of SnChatMember
@ -760,7 +761,7 @@ $SnAccountCopyWith<$Res> get account {
@JsonSerializable() @JsonSerializable()
class _SnChatMember implements SnChatMember { class _SnChatMember implements SnChatMember {
const _SnChatMember({required this.createdAt, required this.updatedAt, required this.deletedAt, required this.id, required this.chatRoomId, required this.chatRoom, required this.accountId, required this.account, required this.nick, required this.role, required this.notify, required this.joinedAt, required this.isBot}); const _SnChatMember({required this.createdAt, required this.updatedAt, required this.deletedAt, required this.id, required this.chatRoomId, required this.chatRoom, required this.accountId, required this.account, required this.nick, required this.role, required this.notify, required this.joinedAt, required this.isBot, this.lastTyped});
factory _SnChatMember.fromJson(Map<String, dynamic> json) => _$SnChatMemberFromJson(json); factory _SnChatMember.fromJson(Map<String, dynamic> json) => _$SnChatMemberFromJson(json);
@override final DateTime createdAt; @override final DateTime createdAt;
@ -776,6 +777,7 @@ class _SnChatMember implements SnChatMember {
@override final int notify; @override final int notify;
@override final DateTime? joinedAt; @override final DateTime? joinedAt;
@override final bool isBot; @override final bool isBot;
@override final DateTime? lastTyped;
/// Create a copy of SnChatMember /// Create a copy of SnChatMember
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -790,16 +792,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isBot, isBot) || other.isBot == isBot)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMember&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)&&(identical(other.chatRoom, chatRoom) || other.chatRoom == chatRoom)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.role, role) || other.role == role)&&(identical(other.notify, notify) || other.notify == notify)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isBot, isBot) || other.isBot == isBot)&&(identical(other.lastTyped, lastTyped) || other.lastTyped == lastTyped));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot); int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,chatRoomId,chatRoom,accountId,account,nick,role,notify,joinedAt,isBot,lastTyped);
@override @override
String toString() { String toString() {
return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot)'; return 'SnChatMember(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, chatRoomId: $chatRoomId, chatRoom: $chatRoom, accountId: $accountId, account: $account, nick: $nick, role: $role, notify: $notify, joinedAt: $joinedAt, isBot: $isBot, lastTyped: $lastTyped)';
} }
@ -810,7 +812,7 @@ abstract mixin class _$SnChatMemberCopyWith<$Res> implements $SnChatMemberCopyWi
factory _$SnChatMemberCopyWith(_SnChatMember value, $Res Function(_SnChatMember) _then) = __$SnChatMemberCopyWithImpl; factory _$SnChatMemberCopyWith(_SnChatMember value, $Res Function(_SnChatMember) _then) = __$SnChatMemberCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String chatRoomId, SnChatRoom? chatRoom, String accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot, DateTime? lastTyped
}); });
@ -827,7 +829,7 @@ class __$SnChatMemberCopyWithImpl<$Res>
/// Create a copy of SnChatMember /// Create a copy of SnChatMember
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,}) { @override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? chatRoomId = null,Object? chatRoom = freezed,Object? accountId = null,Object? account = null,Object? nick = freezed,Object? role = null,Object? notify = null,Object? joinedAt = freezed,Object? isBot = null,Object? lastTyped = freezed,}) {
return _then(_SnChatMember( return _then(_SnChatMember(
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
@ -842,7 +844,8 @@ as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_no
as int,notify: null == notify ? _self.notify : notify // ignore: cast_nullable_to_non_nullable as int,notify: null == notify ? _self.notify : notify // ignore: cast_nullable_to_non_nullable
as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable as DateTime?,isBot: null == isBot ? _self.isBot : isBot // ignore: cast_nullable_to_non_nullable
as bool, as bool,lastTyped: freezed == lastTyped ? _self.lastTyped : lastTyped // ignore: cast_nullable_to_non_nullable
as DateTime?,
)); ));
} }

View File

@ -167,6 +167,10 @@ _SnChatMember _$SnChatMemberFromJson(Map<String, dynamic> json) =>
? null ? null
: DateTime.parse(json['joined_at'] as String), : DateTime.parse(json['joined_at'] as String),
isBot: json['is_bot'] as bool, isBot: json['is_bot'] as bool,
lastTyped:
json['last_typed'] == null
? null
: DateTime.parse(json['last_typed'] as String),
); );
Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) => Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
@ -184,6 +188,7 @@ Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
'notify': instance.notify, 'notify': instance.notify,
'joined_at': instance.joinedAt?.toIso8601String(), 'joined_at': instance.joinedAt?.toIso8601String(),
'is_bot': instance.isBot, 'is_bot': instance.isBot,
'last_typed': instance.lastTyped?.toIso8601String(),
}; };
_SnChatSummary _$SnChatSummaryFromJson(Map<String, dynamic> json) => _SnChatSummary _$SnChatSummaryFromJson(Map<String, dynamic> json) =>

View File

@ -329,7 +329,7 @@ class ChatListRouteArgs {
/// [_i7.ChatRoomScreen] /// [_i7.ChatRoomScreen]
class ChatRoomRoute extends _i27.PageRouteInfo<ChatRoomRouteArgs> { class ChatRoomRoute extends _i27.PageRouteInfo<ChatRoomRouteArgs> {
ChatRoomRoute({ ChatRoomRoute({
_i28.Key? key, _i29.Key? key,
required String id, required String id,
List<_i27.PageRouteInfo>? children, List<_i27.PageRouteInfo>? children,
}) : super( }) : super(
@ -356,7 +356,7 @@ class ChatRoomRoute extends _i27.PageRouteInfo<ChatRoomRouteArgs> {
class ChatRoomRouteArgs { class ChatRoomRouteArgs {
const ChatRoomRouteArgs({this.key, required this.id}); const ChatRoomRouteArgs({this.key, required this.id});
final _i28.Key? key; final _i29.Key? key;
final String id; final String id;

View File

@ -321,15 +321,46 @@ class ChatRoomScreen extends HookConsumerWidget {
); );
} }
void sendTypingStatus() async { // 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); final wsState = ref.read(websocketStateProvider.notifier);
wsState.sendMessage( wsState.sendMessage(
jsonEncode( jsonEncode(
WebSocketPacket(type: 'messages.typing', data: {'chat_room_id': id}), WebSocketPacket(type: 'messages.typing', data: {'chat_room_id': id}),
), ),
); );
typingDebouncer.value = Timer(const Duration(milliseconds: 1000), () {
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; var isLoading = false;
// Add scroll listener for pagination // Add scroll listener for pagination
@ -352,6 +383,28 @@ class ChatRoomScreen extends HookConsumerWidget {
void onMessage(WebSocketPacket pkt) { void onMessage(WebSocketPacket pkt) {
if (!pkt.type.startsWith('messages')) return; if (!pkt.type.startsWith('messages')) return;
if (['messages.read'].contains(pkt.type)) return; if (['messages.read'].contains(pkt.type)) return;
if (pkt.type == 'messages.typing') {
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!); final message = SnChatMessage.fromJson(pkt.data!);
if (message.chatRoomId != chatRoom.value?.id) return; if (message.chatRoomId != chatRoom.value?.id) return;
switch (pkt.type) { switch (pkt.type) {
@ -425,7 +478,17 @@ class ChatRoomScreen extends HookConsumerWidget {
} }
} }
useEffect(() {}); // Add listener to message controller for typing status
useEffect(() {
void onTextChange() {
if (messageController.text.isNotEmpty) {
sendTypingStatus();
}
}
messageController.addListener(onTextChange);
return () => messageController.removeListener(onTextChange);
}, [messageController]);
final compactHeader = isWideScreen(context); final compactHeader = isWideScreen(context);
@ -666,55 +729,133 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
chatRoom.when( chatRoom.when(
data: data:
(room) => _ChatInput( (room) => Column(
messageController: messageController, mainAxisSize: MainAxisSize.min,
chatRoom: room!, children: [
onSend: sendMessage, AnimatedSwitcher(
onClear: () { duration: const Duration(milliseconds: 300),
if (messageEditingTo.value != null) { switchInCurve: Curves.fastEaseInToSlowEaseOut,
attachments.value.clear(); switchOutCurve: Curves.fastEaseInToSlowEaseOut,
messageController.clear(); transitionBuilder: (
} Widget child,
messageEditingTo.value = null; Animation<double> animation,
messageReplyingTo.value = null; ) {
messageForwardingTo.value = null; return SlideTransition(
}, position: Tween<Offset>(
messageEditingTo: messageEditingTo.value, begin: const Offset(0, -0.3),
messageReplyingTo: messageReplyingTo.value, end: Offset.zero,
messageForwardingTo: messageForwardingTo.value, ).animate(
onPickFile: (bool isPhoto) { CurvedAnimation(
if (isPhoto) { parent: animation,
pickPhotoMedia(); curve: Curves.easeOutCubic,
} else { ),
pickVideoMedia(); ),
} child: SizeTransition(
}, sizeFactor: animation,
attachments: attachments.value, axisAlignment: -1.0,
onUploadAttachment: (_) { child: FadeTransition(
// not going to do anything, only upload when send the message opacity: animation,
}, child: child,
onDeleteAttachment: (index) async { ),
final attachment = attachments.value[index]; ),
if (attachment.isOnCloud) { );
final client = ref.watch(apiClientProvider); },
await client.delete('/files/${attachment.data.id}'); child:
} typingStatuses.value.isNotEmpty
final clone = List.of(attachments.value); ? Container(
clone.removeAt(index); key: const ValueKey('typing-indicator'),
attachments.value = clone; width: double.infinity,
}, padding: const EdgeInsets.symmetric(
onMoveAttachment: (idx, delta) { horizontal: 16,
if (idx + delta < 0 || vertical: 4,
idx + delta >= attachments.value.length) { ),
return; child: Row(
} children: [
final clone = List.of(attachments.value); const Icon(
clone.insert(idx + delta, clone.removeAt(idx)); Symbols.more_horiz,
attachments.value = clone; size: 16,
}, ).padding(horizontal: 8),
onAttachmentsChanged: (newAttachments) { const Gap(8),
attachments.value = newAttachments; Expanded(
}, child: Text(
'typingHint'.plural(
typingStatuses.value.length,
args: [
typingStatuses.value
.map(
(x) =>
x.nick ??
x.account.nick,
)
.join(', '),
],
),
style:
Theme.of(
context,
).textTheme.bodySmall,
),
),
],
),
)
: const SizedBox.shrink(
key: ValueKey('no_typing'),
),
),
_ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
attachments: attachments.value,
onUploadAttachment: (_) {
// not going to do anything, only upload when send the message
},
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud) {
final client = ref.watch(apiClientProvider);
await client.delete(
'/files/${attachment.data.id}',
);
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
),
],
), ),
error: (_, _) => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(), loading: () => const SizedBox.shrink(),

View File

@ -6,7 +6,7 @@ part of 'account_session_sheet.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$authDevicesHash() => r'9b8101167653991314efd37788d8416f414cb9e8'; String _$authDevicesHash() => r'19807110962206a9637075d03cd372233cae2f49';
/// See also [authDevices]. /// See also [authDevices].
@ProviderFor(authDevices) @ProviderFor(authDevices)