✨ Chat quote and reply
This commit is contained in:
parent
9f7a3082cb
commit
5032cccf38
@ -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) =>
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -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) =>
|
||||||
|
@ -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.
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
@ -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),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
12
pubspec.lock
12
pubspec.lock
@ -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:
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user