Chat quote and reply

This commit is contained in:
LittleSheep 2024-11-18 22:33:03 +08:00
parent 9f7a3082cb
commit 5032cccf38
9 changed files with 279 additions and 116 deletions

View File

@ -119,12 +119,20 @@ class ChatMessageController extends ChangeNotifier {
} }
Future<void> _addUnconfirmedMessage(SnChatMessage message) async { Future<void> _addUnconfirmedMessage(SnChatMessage message) async {
SnChatMessage? quoteEvent;
if (message.body['quote_event'] != null) {
quoteEvent = await getMessage(message.body['quote_event'] as int);
}
final attachmentRid = List<String>.from( final attachmentRid = List<String>.from(
message.body['attachments']?.cast<String>() ?? [], message.body['attachments']?.cast<String>() ?? [],
); );
final attachments = await _attach.getMultiple(attachmentRid); final attachments = await _attach.getMultiple(attachmentRid);
message = message.copyWith( message = message.copyWith(
preload: SnChatMessagePreload(attachments: attachments), preload: SnChatMessagePreload(
quoteEvent: quoteEvent,
attachments: attachments,
),
); );
messages.insert(0, message); messages.insert(0, message);
@ -133,12 +141,20 @@ class ChatMessageController extends ChangeNotifier {
} }
Future<void> _addMessage(SnChatMessage message) async { Future<void> _addMessage(SnChatMessage message) async {
SnChatMessage? quoteEvent;
if (message.body['quote_event'] != null) {
quoteEvent = await getMessage(message.body['quote_event'] as int);
}
final attachmentRid = List<String>.from( final attachmentRid = List<String>.from(
message.body['attachments']?.cast<String>() ?? [], message.body['attachments']?.cast<String>() ?? [],
); );
final attachments = await _attach.getMultiple(attachmentRid); final attachments = await _attach.getMultiple(attachmentRid);
message = message.copyWith( message = message.copyWith(
preload: SnChatMessagePreload(attachments: attachments), preload: SnChatMessagePreload(
quoteEvent: quoteEvent,
attachments: attachments,
),
); );
final idx = messages.indexWhere((e) => e.uuid == message.uuid); final idx = messages.indexWhere((e) => e.uuid == message.uuid);
@ -199,8 +215,8 @@ class ChatMessageController extends ChangeNotifier {
final body = { final body = {
'text': content, 'text': content,
'algorithm': 'plain', 'algorithm': 'plain',
if (quoteId != null) 'quote_id': quoteId, if (quoteId != null) 'quote_event': quoteId,
if (relatedId != null) 'related_id': relatedId, if (relatedId != null) 'quote_event': relatedId,
if (attachments != null && attachments.isNotEmpty) if (attachments != null && attachments.isNotEmpty)
'attachments': attachments, 'attachments': attachments,
}; };
@ -269,6 +285,42 @@ class ChatMessageController extends ChangeNotifier {
} }
} }
/// Get a single event from the current channel
/// If it was not found in local storage we will look it up in remote
Future<SnChatMessage?> getMessage(int id) async {
SnChatMessage? out;
if (_box != null && _box!.containsKey(id)) {
out = _box!.get(id);
}
if (out == null) {
try {
final resp = await _sn.client
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
out = SnChatMessage.fromJson(resp.data);
_saveMessageToLocal([out]);
} catch (_) {
// ignore, maybe not found
}
}
// Preload some related things if found
if (out != null) {
await _ud.listAccount([out.sender.accountId]);
final attachments = await _attach.getMultiple(
out.body['attachments']?.cast<String>() ?? [],
);
out = out.copyWith(
preload: SnChatMessagePreload(
attachments: attachments,
),
);
}
return out;
}
/// Get message from local storage first, then from the server. /// Get message from local storage first, then from the server.
/// Will not check local storage is up to date with the server. /// Will not check local storage is up to date with the server.
/// If you need to do the sync, do the `checkUpdate` instead. /// If you need to do the sync, do the `checkUpdate` instead.
@ -300,9 +352,18 @@ class ChatMessageController extends ChangeNotifier {
out.expand((e) => (e.body['attachments'] as List<dynamic>?) ?? []), out.expand((e) => (e.body['attachments'] as List<dynamic>?) ?? []),
); );
final attachments = await _attach.getMultiple(attachmentRid); final attachments = await _attach.getMultiple(attachmentRid);
// Putting preload back to data
for (var i = 0; i < out.length; i++) { for (var i = 0; i < out.length; i++) {
// Preload related events (quoted)
SnChatMessage? quoteEvent;
if (out[i].body['quote_event'] != null) {
quoteEvent = await getMessage(out[i].body['quote_event'] as int);
}
out[i] = out[i].copyWith( out[i] = out[i].copyWith(
preload: SnChatMessagePreload( preload: SnChatMessagePreload(
quoteEvent: quoteEvent,
attachments: attachments attachments: attachments
.where( .where(
(ele) => (ele) =>

View File

@ -25,6 +25,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
SnChannel? _channel; SnChannel? _channel;
final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
late final ChatMessageController _messageController; late final ChatMessageController _messageController;
Future<void> _fetchChannel() async { Future<void> _fetchChannel() async {
@ -117,6 +118,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
hasMerged: canMergePrevious, hasMerged: canMergePrevious,
isPending: _messageController.unconfirmedMessages isPending: _messageController.unconfirmedMessages
.contains(message.uuid), .contains(message.uuid),
onReply: () {
_inputGlobalKey.currentState?.setReply(message);
},
); );
}, },
), ),
@ -124,8 +128,10 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
if (!_messageController.isPending) if (!_messageController.isPending)
Material( Material(
elevation: 2, elevation: 2,
child: ChatMessageInput(controller: _messageController) child: ChatMessageInput(
.padding(bottom: MediaQuery.of(context).padding.bottom), key: _inputGlobalKey,
controller: _messageController,
).padding(bottom: MediaQuery.of(context).padding.bottom),
), ),
], ],
); );

View File

@ -91,9 +91,9 @@ class SnChatMessage with _$SnChatMessage {
class SnChatMessagePreload with _$SnChatMessagePreload { class SnChatMessagePreload with _$SnChatMessagePreload {
const SnChatMessagePreload._(); const SnChatMessagePreload._();
@HiveType(typeId: 5)
const factory SnChatMessagePreload({ const factory SnChatMessagePreload({
@HiveField(0) List<SnAttachment?>? attachments, List<SnAttachment?>? attachments,
SnChatMessage? quoteEvent,
}) = _SnChatMessagePreload; }) = _SnChatMessagePreload;
factory SnChatMessagePreload.fromJson(Map<String, dynamic> json) => factory SnChatMessagePreload.fromJson(Map<String, dynamic> json) =>

View File

@ -1540,8 +1540,8 @@ SnChatMessagePreload _$SnChatMessagePreloadFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$SnChatMessagePreload { mixin _$SnChatMessagePreload {
@HiveField(0)
List<SnAttachment?>? get attachments => throw _privateConstructorUsedError; List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
SnChatMessage? get quoteEvent => throw _privateConstructorUsedError;
/// Serializes this SnChatMessagePreload to a JSON map. /// Serializes this SnChatMessagePreload to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -1559,7 +1559,9 @@ abstract class $SnChatMessagePreloadCopyWith<$Res> {
$Res Function(SnChatMessagePreload) then) = $Res Function(SnChatMessagePreload) then) =
_$SnChatMessagePreloadCopyWithImpl<$Res, SnChatMessagePreload>; _$SnChatMessagePreloadCopyWithImpl<$Res, SnChatMessagePreload>;
@useResult @useResult
$Res call({@HiveField(0) List<SnAttachment?>? attachments}); $Res call({List<SnAttachment?>? attachments, SnChatMessage? quoteEvent});
$SnChatMessageCopyWith<$Res>? get quoteEvent;
} }
/// @nodoc /// @nodoc
@ -1579,14 +1581,33 @@ class _$SnChatMessagePreloadCopyWithImpl<$Res,
@override @override
$Res call({ $Res call({
Object? attachments = freezed, Object? attachments = freezed,
Object? quoteEvent = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
attachments: freezed == attachments attachments: freezed == attachments
? _value.attachments ? _value.attachments
: attachments // ignore: cast_nullable_to_non_nullable : attachments // ignore: cast_nullable_to_non_nullable
as List<SnAttachment?>?, as List<SnAttachment?>?,
quoteEvent: freezed == quoteEvent
? _value.quoteEvent
: quoteEvent // ignore: cast_nullable_to_non_nullable
as SnChatMessage?,
) as $Val); ) as $Val);
} }
/// Create a copy of SnChatMessagePreload
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatMessageCopyWith<$Res>? get quoteEvent {
if (_value.quoteEvent == null) {
return null;
}
return $SnChatMessageCopyWith<$Res>(_value.quoteEvent!, (value) {
return _then(_value.copyWith(quoteEvent: value) as $Val);
});
}
} }
/// @nodoc /// @nodoc
@ -1597,7 +1618,10 @@ abstract class _$$SnChatMessagePreloadImplCopyWith<$Res>
__$$SnChatMessagePreloadImplCopyWithImpl<$Res>; __$$SnChatMessagePreloadImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({@HiveField(0) List<SnAttachment?>? attachments}); $Res call({List<SnAttachment?>? attachments, SnChatMessage? quoteEvent});
@override
$SnChatMessageCopyWith<$Res>? get quoteEvent;
} }
/// @nodoc /// @nodoc
@ -1614,22 +1638,26 @@ class __$$SnChatMessagePreloadImplCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? attachments = freezed, Object? attachments = freezed,
Object? quoteEvent = freezed,
}) { }) {
return _then(_$SnChatMessagePreloadImpl( return _then(_$SnChatMessagePreloadImpl(
attachments: freezed == attachments attachments: freezed == attachments
? _value._attachments ? _value._attachments
: attachments // ignore: cast_nullable_to_non_nullable : attachments // ignore: cast_nullable_to_non_nullable
as List<SnAttachment?>?, as List<SnAttachment?>?,
quoteEvent: freezed == quoteEvent
? _value.quoteEvent
: quoteEvent // ignore: cast_nullable_to_non_nullable
as SnChatMessage?,
)); ));
} }
} }
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
@HiveType(typeId: 5)
class _$SnChatMessagePreloadImpl extends _SnChatMessagePreload { class _$SnChatMessagePreloadImpl extends _SnChatMessagePreload {
const _$SnChatMessagePreloadImpl( const _$SnChatMessagePreloadImpl(
{@HiveField(0) final List<SnAttachment?>? attachments}) {final List<SnAttachment?>? attachments, this.quoteEvent})
: _attachments = attachments, : _attachments = attachments,
super._(); super._();
@ -1638,7 +1666,6 @@ class _$SnChatMessagePreloadImpl extends _SnChatMessagePreload {
final List<SnAttachment?>? _attachments; final List<SnAttachment?>? _attachments;
@override @override
@HiveField(0)
List<SnAttachment?>? get attachments { List<SnAttachment?>? get attachments {
final value = _attachments; final value = _attachments;
if (value == null) return null; if (value == null) return null;
@ -1647,9 +1674,12 @@ class _$SnChatMessagePreloadImpl extends _SnChatMessagePreload {
return EqualUnmodifiableListView(value); return EqualUnmodifiableListView(value);
} }
@override
final SnChatMessage? quoteEvent;
@override @override
String toString() { String toString() {
return 'SnChatMessagePreload(attachments: $attachments)'; return 'SnChatMessagePreload(attachments: $attachments, quoteEvent: $quoteEvent)';
} }
@override @override
@ -1658,13 +1688,15 @@ class _$SnChatMessagePreloadImpl extends _SnChatMessagePreload {
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$SnChatMessagePreloadImpl && other is _$SnChatMessagePreloadImpl &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other._attachments, _attachments)); .equals(other._attachments, _attachments) &&
(identical(other.quoteEvent, quoteEvent) ||
other.quoteEvent == quoteEvent));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(runtimeType,
runtimeType, const DeepCollectionEquality().hash(_attachments)); const DeepCollectionEquality().hash(_attachments), quoteEvent);
/// Create a copy of SnChatMessagePreload /// Create a copy of SnChatMessagePreload
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -1686,16 +1718,17 @@ class _$SnChatMessagePreloadImpl extends _SnChatMessagePreload {
abstract class _SnChatMessagePreload extends SnChatMessagePreload { abstract class _SnChatMessagePreload extends SnChatMessagePreload {
const factory _SnChatMessagePreload( const factory _SnChatMessagePreload(
{@HiveField(0) final List<SnAttachment?>? attachments}) = {final List<SnAttachment?>? attachments,
_$SnChatMessagePreloadImpl; final SnChatMessage? quoteEvent}) = _$SnChatMessagePreloadImpl;
const _SnChatMessagePreload._() : super._(); const _SnChatMessagePreload._() : super._();
factory _SnChatMessagePreload.fromJson(Map<String, dynamic> json) = factory _SnChatMessagePreload.fromJson(Map<String, dynamic> json) =
_$SnChatMessagePreloadImpl.fromJson; _$SnChatMessagePreloadImpl.fromJson;
@override @override
@HiveField(0)
List<SnAttachment?>? get attachments; List<SnAttachment?>? get attachments;
@override
SnChatMessage? get quoteEvent;
/// Create a copy of SnChatMessagePreload /// Create a copy of SnChatMessagePreload
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View File

@ -204,41 +204,6 @@ class SnChatMessageImplAdapter extends TypeAdapter<_$SnChatMessageImpl> {
typeId == other.typeId; typeId == other.typeId;
} }
class SnChatMessagePreloadImplAdapter
extends TypeAdapter<_$SnChatMessagePreloadImpl> {
@override
final int typeId = 5;
@override
_$SnChatMessagePreloadImpl read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return _$SnChatMessagePreloadImpl(
attachments: (fields[0] as List?)?.cast<SnAttachment?>(),
);
}
@override
void write(BinaryWriter writer, _$SnChatMessagePreloadImpl obj) {
writer
..writeByte(1)
..writeByte(0)
..write(obj.attachments);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SnChatMessagePreloadImplAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// ************************************************************************** // **************************************************************************
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
@ -374,10 +339,14 @@ _$SnChatMessagePreloadImpl _$$SnChatMessagePreloadImplFromJson(
? null ? null
: SnAttachment.fromJson(e as Map<String, dynamic>)) : SnAttachment.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
quoteEvent: json['quote_event'] == null
? null
: SnChatMessage.fromJson(json['quote_event'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$$SnChatMessagePreloadImplToJson( Map<String, dynamic> _$$SnChatMessagePreloadImplToJson(
_$SnChatMessagePreloadImpl instance) => _$SnChatMessagePreloadImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'attachments': instance.attachments?.map((e) => e?.toJson()).toList(), 'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
'quote_event': instance.quoteEvent?.toJson(),
}; };

View File

@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
@ -8,18 +9,23 @@ import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/attachment/attachment_list.dart'; import 'package:surface/widgets/attachment/attachment_list.dart';
import 'package:surface/widgets/markdown_content.dart'; import 'package:surface/widgets/markdown_content.dart';
import 'package:swipe_to/swipe_to.dart';
class ChatMessage extends StatelessWidget { class ChatMessage extends StatelessWidget {
final SnChatMessage data; final SnChatMessage data;
final bool isCompact;
final bool isMerged; final bool isMerged;
final bool hasMerged; final bool hasMerged;
final bool isPending; final bool isPending;
final Function()? onReply;
const ChatMessage({ const ChatMessage({
super.key, super.key,
required this.data, required this.data,
this.isCompact = false,
this.isMerged = false, this.isMerged = false,
this.hasMerged = false, this.hasMerged = false,
this.isPending = false, this.isPending = false,
this.onReply,
}); });
@override @override
@ -29,59 +35,92 @@ class ChatMessage extends StatelessWidget {
final dateFormatter = DateFormat('MM/dd HH:mm'); final dateFormatter = DateFormat('MM/dd HH:mm');
return Column( return SwipeTo(
crossAxisAlignment: CrossAxisAlignment.start, key: Key('chat-message-${data.id}'),
children: [ iconOnLeftSwipe: Symbols.reply,
Row( swipeSensitivity: 20,
crossAxisAlignment: CrossAxisAlignment.start, onLeftSwipe: onReply != null ? (_) => onReply!() : null,
children: [ child: Column(
if (!isMerged) crossAxisAlignment: CrossAxisAlignment.start,
AccountImage( children: [
content: user?.avatar, Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged && !isCompact)
AccountImage(
content: user?.avatar,
)
else if (isMerged)
const Gap(40),
const Gap(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged)
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
if (isCompact)
AccountImage(
content: user?.avatar,
radius: 12,
).padding(right: 6),
Text(
(data.sender.nick?.isNotEmpty ?? false)
? data.sender.nick!
: user!.nick,
).bold(),
const Gap(6),
Text(
dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
if (isCompact) const Gap(4),
if (data.preload?.quoteEvent != null)
StyledWidget(Container(
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
padding: const EdgeInsets.only(
left: 4,
right: 4,
top: 8,
bottom: 6,
),
child: ChatMessage(
data: data.preload!.quoteEvent!,
isCompact: true,
),
)).padding(bottom: 4, top: isMerged ? 4 : 2),
if (data.body['text'] != null)
MarkdownTextContent(
content: data.body['text'],
isAutoWarp: true,
),
],
),
) )
else ],
const Gap(40), ).opacity(isPending ? 0.5 : 1),
const Gap(8), if (data.preload?.attachments?.isNotEmpty ?? false)
Expanded( AttachmentList(
child: Column( data: data.preload!.attachments!,
crossAxisAlignment: CrossAxisAlignment.start, bordered: true,
children: [ noGrow: true,
if (!isMerged) maxHeight: 520,
Row( listPadding: const EdgeInsets.only(top: 8),
crossAxisAlignment: CrossAxisAlignment.baseline, ),
textBaseline: TextBaseline.alphabetic, if (!hasMerged && !isCompact) const Gap(12),
children: [ ],
Text( ),
(data.sender.nick?.isNotEmpty ?? false)
? data.sender.nick!
: user!.nick,
).bold(),
const Gap(6),
Text(
dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
if (data.body['text'] != null)
MarkdownTextContent(
content: data.body['text'],
isAutoWarp: true,
),
],
),
)
],
).opacity(isPending ? 0.5 : 1),
if (data.preload?.attachments?.isNotEmpty ?? false)
AttachmentList(
data: data.preload!.attachments!,
bordered: true,
noGrow: true,
maxHeight: 520,
listPadding: const EdgeInsets.only(top: 8),
),
if (!hasMerged) const Gap(8),
],
); );
} }
} }

View File

@ -8,7 +8,9 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart'; import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/controllers/post_write_controller.dart'; import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart'; import 'package:surface/widgets/post/post_media_pending_list.dart';
class ChatMessageInput extends StatefulWidget { class ChatMessageInput extends StatefulWidget {
@ -16,16 +18,22 @@ class ChatMessageInput extends StatefulWidget {
const ChatMessageInput({super.key, required this.controller}); const ChatMessageInput({super.key, required this.controller});
@override @override
State<ChatMessageInput> createState() => _ChatMessageInputState(); State<ChatMessageInput> createState() => ChatMessageInputState();
} }
class _ChatMessageInputState extends State<ChatMessageInput> { class ChatMessageInputState extends State<ChatMessageInput> {
bool _isBusy = false; bool _isBusy = false;
double? _progress; double? _progress;
SnChatMessage? _replyingMessage;
final TextEditingController _contentController = TextEditingController(); final TextEditingController _contentController = TextEditingController();
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
void setReply(SnChatMessage? value) {
setState(() => _replyingMessage = value);
}
Future<void> _sendMessage() async { Future<void> _sendMessage() async {
if (_isBusy) return; if (_isBusy) return;
@ -69,6 +77,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
attach.putCache( attach.putCache(
_attachments.where((e) => e.attachment != null).map((e) => e.attachment!), _attachments.where((e) => e.attachment != null).map((e) => e.attachment!),
noCheck: true,
); );
// Send the message // Send the message
@ -80,9 +89,11 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
.where((e) => e.attachment != null) .where((e) => e.attachment != null)
.map((e) => e.attachment!.rid) .map((e) => e.attachment!.rid)
.toList(), .toList(),
quoteId: _replyingMessage?.id,
); );
_contentController.clear(); _contentController.clear();
_attachments.clear(); _attachments.clear();
_replyingMessage = null;
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
@ -134,10 +145,45 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
setState(() => _attachments.removeAt(idx)); setState(() => _attachments.removeAt(idx));
}, },
onUpdateBusy: (state) => setState(() => _isBusy = state), onUpdateBusy: (state) => setState(() => _isBusy = state),
).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate( ),
const Duration(milliseconds: 300), ).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate(
Curves.fastEaseInToSlowEaseOut), const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
), SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Padding(
padding: _replyingMessage != null
? const EdgeInsets.only(top: 8)
: EdgeInsets.zero,
child: _replyingMessage != null
? MaterialBanner(
padding: const EdgeInsets.only(left: 16.0),
leading: const Icon(Symbols.reply),
content: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_replyingMessage?.body['text'] != null)
MarkdownTextContent(
content: _replyingMessage?.body['text'],
),
],
),
),
actions: [
TextButton(
child: Text('cancel'.tr()),
onPressed: () {
setState(() => _replyingMessage = null);
},
),
],
)
: const SizedBox.shrink(),
),
).height(_replyingMessage != null ? 54 + 8 : 0, animate: true).animate(
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SizedBox( SizedBox(
height: 56, height: 56,
child: Row( child: Row(

View File

@ -516,10 +516,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_native_splash name: flutter_native_splash
sha256: ee5c9bd2b74ea8676442fd4ab876b5d41681df49276488854d6c81a5377c0ef1 sha256: "1152ab0067ca5a2ebeb862fe0a762057202cceb22b7e62692dcbabf6483891bb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.4.3"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -1343,6 +1343,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.1" version: "0.4.1"
swipe_to:
dependency: "direct main"
description:
name: swipe_to
sha256: "58f61031803ece9b0efe09006809e78904c640c6d42d48715d1d1c3c28f8499a"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
synchronized: synchronized:
dependency: transitive dependency: transitive
description: description:

View File

@ -78,6 +78,7 @@ dependencies:
isar_flutter_libs: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1
hive: ^2.2.3 hive: ^2.2.3
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
swipe_to: ^1.0.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: