♻️ Refactored attachment cache
This commit is contained in:
parent
432705c570
commit
359cd94532
@ -4,6 +4,7 @@ import 'dart:math' as math;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
@ -17,6 +18,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final UserDirectoryProvider _ud;
|
late final UserDirectoryProvider _ud;
|
||||||
late final WebSocketProvider _ws;
|
late final WebSocketProvider _ws;
|
||||||
|
late final SnAttachmentProvider _attach;
|
||||||
|
|
||||||
StreamSubscription? _wsSubscription;
|
StreamSubscription? _wsSubscription;
|
||||||
|
|
||||||
@ -24,6 +26,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
_ud = context.read<UserDirectoryProvider>();
|
_ud = context.read<UserDirectoryProvider>();
|
||||||
_ws = context.read<WebSocketProvider>();
|
_ws = context.read<WebSocketProvider>();
|
||||||
|
_attach = context.read<SnAttachmentProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPending = true;
|
bool isPending = true;
|
||||||
@ -116,12 +119,28 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addUnconfirmedMessage(SnChatMessage message) async {
|
Future<void> _addUnconfirmedMessage(SnChatMessage message) async {
|
||||||
|
final attachmentRid = List<String>.from(
|
||||||
|
message.body['attachments']?.cast<String>() ?? [],
|
||||||
|
);
|
||||||
|
final attachments = await _attach.getMultiple(attachmentRid);
|
||||||
|
message = message.copyWith(
|
||||||
|
preload: SnChatMessagePreload(attachments: attachments),
|
||||||
|
);
|
||||||
|
|
||||||
messages.insert(0, message);
|
messages.insert(0, message);
|
||||||
unconfirmedMessages.add(message.uuid);
|
unconfirmedMessages.add(message.uuid);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addMessage(SnChatMessage message) async {
|
Future<void> _addMessage(SnChatMessage message) async {
|
||||||
|
final attachmentRid = List<String>.from(
|
||||||
|
message.body['attachments']?.cast<String>() ?? [],
|
||||||
|
);
|
||||||
|
final attachments = await _attach.getMultiple(attachmentRid);
|
||||||
|
message = message.copyWith(
|
||||||
|
preload: SnChatMessagePreload(attachments: attachments),
|
||||||
|
);
|
||||||
|
|
||||||
final idx = messages.indexWhere((e) => e.uuid == message.uuid);
|
final idx = messages.indexWhere((e) => e.uuid == message.uuid);
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
unconfirmedMessages.remove(message.uuid);
|
unconfirmedMessages.remove(message.uuid);
|
||||||
@ -182,7 +201,8 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
'algorithm': 'plain',
|
'algorithm': 'plain',
|
||||||
if (quoteId != null) 'quote_id': quoteId,
|
if (quoteId != null) 'quote_id': quoteId,
|
||||||
if (relatedId != null) 'related_id': relatedId,
|
if (relatedId != null) 'related_id': relatedId,
|
||||||
if (attachments != null) 'attachments': attachments,
|
if (attachments != null && attachments.isNotEmpty)
|
||||||
|
'attachments': attachments,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock the message locally
|
// Mock the message locally
|
||||||
@ -257,25 +277,41 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
int offset, {
|
int offset, {
|
||||||
bool forceLocal = false,
|
bool forceLocal = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (_box != null) {
|
late List<SnChatMessage> out;
|
||||||
// Try retrieve these messages from the local storage
|
if (_box != null && (_box!.length >= take + offset || forceLocal)) {
|
||||||
if (_box!.length >= take + offset || forceLocal) {
|
out = _box!.values.skip(offset).take(take).toList();
|
||||||
return _box!.values.skip(offset).take(take).toList();
|
} else {
|
||||||
}
|
final resp = await _sn.client.get(
|
||||||
|
'/cgi/im/channels/${channel!.keyPath}/events',
|
||||||
|
queryParameters: {
|
||||||
|
'take': take,
|
||||||
|
'offset': offset,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
messageTotal = resp.data['count'] as int?;
|
||||||
|
out = List<SnChatMessage>.from(
|
||||||
|
resp.data['data']?.map((e) => SnChatMessage.fromJson(e)) ?? [],
|
||||||
|
);
|
||||||
|
_saveMessageToLocal(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
final resp = await _sn.client.get(
|
// Preload attachments
|
||||||
'/cgi/im/channels/${channel!.keyPath}/events',
|
final attachmentRid = List<String>.from(
|
||||||
queryParameters: {
|
out.expand((e) => (e.body['attachments'] as List<dynamic>?) ?? []),
|
||||||
'take': take,
|
|
||||||
'offset': offset,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
messageTotal = resp.data['count'] as int?;
|
final attachments = await _attach.getMultiple(attachmentRid);
|
||||||
final out = List<SnChatMessage>.from(
|
out = out.reversed
|
||||||
resp.data['data']?.map((e) => SnChatMessage.fromJson(e)) ?? [],
|
.map((ele) => ele.copyWith(
|
||||||
);
|
preload: SnChatMessagePreload(
|
||||||
_saveMessageToLocal(out);
|
attachments: attachments
|
||||||
|
.where((e) =>
|
||||||
|
(ele.body['attachments'] as List<dynamic>?)
|
||||||
|
?.contains(e) ??
|
||||||
|
false)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
|
||||||
// Preload sender accounts
|
// Preload sender accounts
|
||||||
await _ud.listAccount(out.map((ele) => ele.sender.accountId).toSet());
|
await _ud.listAccount(out.map((ele) => ele.sender.accountId).toSet());
|
||||||
|
@ -19,6 +19,14 @@ class SnAttachmentProvider {
|
|||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
|
||||||
|
for (final item in items) {
|
||||||
|
if ((item.isAnalyzed && item.isUploaded) || noCheck) {
|
||||||
|
_cache[item.rid] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<SnAttachment> getOne(String rid, {noCache = false}) async {
|
Future<SnAttachment> getOne(String rid, {noCache = false}) async {
|
||||||
if (!noCache && _cache.containsKey(rid)) {
|
if (!noCache && _cache.containsKey(rid)) {
|
||||||
return _cache[rid]!;
|
return _cache[rid]!;
|
||||||
@ -26,37 +34,48 @@ class SnAttachmentProvider {
|
|||||||
|
|
||||||
final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
|
final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
|
||||||
final out = SnAttachment.fromJson(resp.data);
|
final out = SnAttachment.fromJson(resp.data);
|
||||||
_cache[rid] = out;
|
if (out.isAnalyzed && out.isUploaded) {
|
||||||
|
_cache[rid] = out;
|
||||||
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SnAttachment>> getMultiple(List<String> rids,
|
Future<List<SnAttachment?>> getMultiple(List<String> rids,
|
||||||
{noCache = false}) async {
|
{noCache = false}) async {
|
||||||
final pendingFetch =
|
final result = List<SnAttachment?>.filled(rids.length, null);
|
||||||
noCache ? rids : rids.where((rid) => !_cache.containsKey(rid)).toList();
|
final Map<String, int> randomMapping = {};
|
||||||
|
for (int i = 0; i < rids.length; i++) {
|
||||||
|
final rid = rids[i];
|
||||||
|
if (noCache || !_cache.containsKey(rid)) {
|
||||||
|
randomMapping[rid] = i;
|
||||||
|
} else {
|
||||||
|
result[i] = _cache[rid]!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final pendingFetch = randomMapping.keys;
|
||||||
|
|
||||||
if (pendingFetch.isEmpty) {
|
if (pendingFetch.isNotEmpty) {
|
||||||
return rids.map((rid) => _cache[rid]!).toList();
|
final resp = await _sn.client.get(
|
||||||
|
'/cgi/uc/attachments',
|
||||||
|
queryParameters: {
|
||||||
|
'take': pendingFetch.length,
|
||||||
|
'id': pendingFetch.join(','),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final out = resp.data['data']
|
||||||
|
.map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (final item in out) {
|
||||||
|
if (item.isAnalyzed && item.isUploaded) {
|
||||||
|
_cache[item.rid] = item;
|
||||||
|
}
|
||||||
|
result[randomMapping[item.rid]!] = item;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final resp = await _sn.client.get('/cgi/uc/attachments', queryParameters: {
|
return result;
|
||||||
'take': pendingFetch.length,
|
|
||||||
'id': pendingFetch.join(','),
|
|
||||||
});
|
|
||||||
final out = resp.data['data']
|
|
||||||
.where((e) => e['id'] != 0)
|
|
||||||
.map((e) => SnAttachment.fromJson(e))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
for (final item in out) {
|
|
||||||
_cache[item.rid] = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rids
|
|
||||||
.where((rid) => _cache.containsKey(rid))
|
|
||||||
.map((rid) => _cache[rid]!)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, String> mimetypeOverrides = {
|
static Map<String, String> mimetypeOverrides = {
|
||||||
|
@ -53,7 +53,8 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
preload: SnPostPreload(
|
preload: SnPostPreload(
|
||||||
attachments: attachments
|
attachments: attachments
|
||||||
.where(
|
.where(
|
||||||
(ele) => out[i].body['attachments']?.contains(ele.rid) ?? false,
|
(ele) =>
|
||||||
|
out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
|
@ -80,7 +80,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
_writeController.addAttachments(
|
_writeController.addAttachments(
|
||||||
result.map((e) => PostWriteMedia.fromFile(e)),
|
result.map((e) => PostWriteMedia.fromFile(e)),
|
||||||
);
|
);
|
||||||
setState(() {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
|
|
||||||
part 'chat.freezed.dart';
|
part 'chat.freezed.dart';
|
||||||
@ -79,8 +80,22 @@ class SnChatMessage with _$SnChatMessage {
|
|||||||
@HiveField(8) required SnChannelMember sender,
|
@HiveField(8) required SnChannelMember sender,
|
||||||
@HiveField(9) required int channelId,
|
@HiveField(9) required int channelId,
|
||||||
@HiveField(10) required int senderId,
|
@HiveField(10) required int senderId,
|
||||||
|
SnChatMessagePreload? preload,
|
||||||
}) = _SnChatMessage;
|
}) = _SnChatMessage;
|
||||||
|
|
||||||
factory SnChatMessage.fromJson(Map<String, dynamic> json) =>
|
factory SnChatMessage.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnChatMessageFromJson(json);
|
_$SnChatMessageFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnChatMessagePreload with _$SnChatMessagePreload {
|
||||||
|
const SnChatMessagePreload._();
|
||||||
|
|
||||||
|
@HiveType(typeId: 5)
|
||||||
|
const factory SnChatMessagePreload({
|
||||||
|
@HiveField(0) List<SnAttachment?>? attachments,
|
||||||
|
}) = _SnChatMessagePreload;
|
||||||
|
|
||||||
|
factory SnChatMessagePreload.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnChatMessagePreloadFromJson(json);
|
||||||
|
}
|
||||||
|
@ -1083,6 +1083,7 @@ mixin _$SnChatMessage {
|
|||||||
int get channelId => throw _privateConstructorUsedError;
|
int get channelId => throw _privateConstructorUsedError;
|
||||||
@HiveField(10)
|
@HiveField(10)
|
||||||
int get senderId => throw _privateConstructorUsedError;
|
int get senderId => throw _privateConstructorUsedError;
|
||||||
|
SnChatMessagePreload? get preload => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this SnChatMessage to a JSON map.
|
/// Serializes this SnChatMessage to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@ -1111,10 +1112,12 @@ abstract class $SnChatMessageCopyWith<$Res> {
|
|||||||
@HiveField(7) SnChannel channel,
|
@HiveField(7) SnChannel channel,
|
||||||
@HiveField(8) SnChannelMember sender,
|
@HiveField(8) SnChannelMember sender,
|
||||||
@HiveField(9) int channelId,
|
@HiveField(9) int channelId,
|
||||||
@HiveField(10) int senderId});
|
@HiveField(10) int senderId,
|
||||||
|
SnChatMessagePreload? preload});
|
||||||
|
|
||||||
$SnChannelCopyWith<$Res> get channel;
|
$SnChannelCopyWith<$Res> get channel;
|
||||||
$SnChannelMemberCopyWith<$Res> get sender;
|
$SnChannelMemberCopyWith<$Res> get sender;
|
||||||
|
$SnChatMessagePreloadCopyWith<$Res>? get preload;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -1143,6 +1146,7 @@ class _$SnChatMessageCopyWithImpl<$Res, $Val extends SnChatMessage>
|
|||||||
Object? sender = null,
|
Object? sender = null,
|
||||||
Object? channelId = null,
|
Object? channelId = null,
|
||||||
Object? senderId = null,
|
Object? senderId = null,
|
||||||
|
Object? preload = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
id: null == id
|
id: null == id
|
||||||
@ -1189,6 +1193,10 @@ class _$SnChatMessageCopyWithImpl<$Res, $Val extends SnChatMessage>
|
|||||||
? _value.senderId
|
? _value.senderId
|
||||||
: senderId // ignore: cast_nullable_to_non_nullable
|
: senderId // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
preload: freezed == preload
|
||||||
|
? _value.preload
|
||||||
|
: preload // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnChatMessagePreload?,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1211,6 +1219,20 @@ class _$SnChatMessageCopyWithImpl<$Res, $Val extends SnChatMessage>
|
|||||||
return _then(_value.copyWith(sender: value) as $Val);
|
return _then(_value.copyWith(sender: value) as $Val);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnChatMessage
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnChatMessagePreloadCopyWith<$Res>? get preload {
|
||||||
|
if (_value.preload == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnChatMessagePreloadCopyWith<$Res>(_value.preload!, (value) {
|
||||||
|
return _then(_value.copyWith(preload: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -1232,12 +1254,15 @@ abstract class _$$SnChatMessageImplCopyWith<$Res>
|
|||||||
@HiveField(7) SnChannel channel,
|
@HiveField(7) SnChannel channel,
|
||||||
@HiveField(8) SnChannelMember sender,
|
@HiveField(8) SnChannelMember sender,
|
||||||
@HiveField(9) int channelId,
|
@HiveField(9) int channelId,
|
||||||
@HiveField(10) int senderId});
|
@HiveField(10) int senderId,
|
||||||
|
SnChatMessagePreload? preload});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
$SnChannelCopyWith<$Res> get channel;
|
$SnChannelCopyWith<$Res> get channel;
|
||||||
@override
|
@override
|
||||||
$SnChannelMemberCopyWith<$Res> get sender;
|
$SnChannelMemberCopyWith<$Res> get sender;
|
||||||
|
@override
|
||||||
|
$SnChatMessagePreloadCopyWith<$Res>? get preload;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -1264,6 +1289,7 @@ class __$$SnChatMessageImplCopyWithImpl<$Res>
|
|||||||
Object? sender = null,
|
Object? sender = null,
|
||||||
Object? channelId = null,
|
Object? channelId = null,
|
||||||
Object? senderId = null,
|
Object? senderId = null,
|
||||||
|
Object? preload = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$SnChatMessageImpl(
|
return _then(_$SnChatMessageImpl(
|
||||||
id: null == id
|
id: null == id
|
||||||
@ -1310,6 +1336,10 @@ class __$$SnChatMessageImplCopyWithImpl<$Res>
|
|||||||
? _value.senderId
|
? _value.senderId
|
||||||
: senderId // ignore: cast_nullable_to_non_nullable
|
: senderId // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
preload: freezed == preload
|
||||||
|
? _value.preload
|
||||||
|
: preload // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnChatMessagePreload?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1329,7 +1359,8 @@ class _$SnChatMessageImpl extends _SnChatMessage {
|
|||||||
@HiveField(7) required this.channel,
|
@HiveField(7) required this.channel,
|
||||||
@HiveField(8) required this.sender,
|
@HiveField(8) required this.sender,
|
||||||
@HiveField(9) required this.channelId,
|
@HiveField(9) required this.channelId,
|
||||||
@HiveField(10) required this.senderId})
|
@HiveField(10) required this.senderId,
|
||||||
|
this.preload})
|
||||||
: _body = body,
|
: _body = body,
|
||||||
super._();
|
super._();
|
||||||
|
|
||||||
@ -1375,10 +1406,12 @@ class _$SnChatMessageImpl extends _SnChatMessage {
|
|||||||
@override
|
@override
|
||||||
@HiveField(10)
|
@HiveField(10)
|
||||||
final int senderId;
|
final int senderId;
|
||||||
|
@override
|
||||||
|
final SnChatMessagePreload? preload;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnChatMessage(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, uuid: $uuid, body: $body, type: $type, channel: $channel, sender: $sender, channelId: $channelId, senderId: $senderId)';
|
return 'SnChatMessage(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, uuid: $uuid, body: $body, type: $type, channel: $channel, sender: $sender, channelId: $channelId, senderId: $senderId, preload: $preload)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1401,7 +1434,8 @@ class _$SnChatMessageImpl extends _SnChatMessage {
|
|||||||
(identical(other.channelId, channelId) ||
|
(identical(other.channelId, channelId) ||
|
||||||
other.channelId == channelId) &&
|
other.channelId == channelId) &&
|
||||||
(identical(other.senderId, senderId) ||
|
(identical(other.senderId, senderId) ||
|
||||||
other.senderId == senderId));
|
other.senderId == senderId) &&
|
||||||
|
(identical(other.preload, preload) || other.preload == preload));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@ -1418,7 +1452,8 @@ class _$SnChatMessageImpl extends _SnChatMessage {
|
|||||||
channel,
|
channel,
|
||||||
sender,
|
sender,
|
||||||
channelId,
|
channelId,
|
||||||
senderId);
|
senderId,
|
||||||
|
preload);
|
||||||
|
|
||||||
/// Create a copy of SnChatMessage
|
/// Create a copy of SnChatMessage
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@ -1448,7 +1483,8 @@ abstract class _SnChatMessage extends SnChatMessage {
|
|||||||
@HiveField(7) required final SnChannel channel,
|
@HiveField(7) required final SnChannel channel,
|
||||||
@HiveField(8) required final SnChannelMember sender,
|
@HiveField(8) required final SnChannelMember sender,
|
||||||
@HiveField(9) required final int channelId,
|
@HiveField(9) required final int channelId,
|
||||||
@HiveField(10) required final int senderId}) = _$SnChatMessageImpl;
|
@HiveField(10) required final int senderId,
|
||||||
|
final SnChatMessagePreload? preload}) = _$SnChatMessageImpl;
|
||||||
const _SnChatMessage._() : super._();
|
const _SnChatMessage._() : super._();
|
||||||
|
|
||||||
factory _SnChatMessage.fromJson(Map<String, dynamic> json) =
|
factory _SnChatMessage.fromJson(Map<String, dynamic> json) =
|
||||||
@ -1487,6 +1523,8 @@ abstract class _SnChatMessage extends SnChatMessage {
|
|||||||
@override
|
@override
|
||||||
@HiveField(10)
|
@HiveField(10)
|
||||||
int get senderId;
|
int get senderId;
|
||||||
|
@override
|
||||||
|
SnChatMessagePreload? get preload;
|
||||||
|
|
||||||
/// Create a copy of SnChatMessage
|
/// Create a copy of SnChatMessage
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@ -1495,3 +1533,174 @@ abstract class _SnChatMessage extends SnChatMessage {
|
|||||||
_$$SnChatMessageImplCopyWith<_$SnChatMessageImpl> get copyWith =>
|
_$$SnChatMessageImplCopyWith<_$SnChatMessageImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SnChatMessagePreload _$SnChatMessagePreloadFromJson(Map<String, dynamic> json) {
|
||||||
|
return _SnChatMessagePreload.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnChatMessagePreload {
|
||||||
|
@HiveField(0)
|
||||||
|
List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this SnChatMessagePreload to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of SnChatMessagePreload
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$SnChatMessagePreloadCopyWith<SnChatMessagePreload> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $SnChatMessagePreloadCopyWith<$Res> {
|
||||||
|
factory $SnChatMessagePreloadCopyWith(SnChatMessagePreload value,
|
||||||
|
$Res Function(SnChatMessagePreload) then) =
|
||||||
|
_$SnChatMessagePreloadCopyWithImpl<$Res, SnChatMessagePreload>;
|
||||||
|
@useResult
|
||||||
|
$Res call({@HiveField(0) List<SnAttachment?>? attachments});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnChatMessagePreloadCopyWithImpl<$Res,
|
||||||
|
$Val extends SnChatMessagePreload>
|
||||||
|
implements $SnChatMessagePreloadCopyWith<$Res> {
|
||||||
|
_$SnChatMessagePreloadCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnChatMessagePreload
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? attachments = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
attachments: freezed == attachments
|
||||||
|
? _value.attachments
|
||||||
|
: attachments // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnAttachment?>?,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$SnChatMessagePreloadImplCopyWith<$Res>
|
||||||
|
implements $SnChatMessagePreloadCopyWith<$Res> {
|
||||||
|
factory _$$SnChatMessagePreloadImplCopyWith(_$SnChatMessagePreloadImpl value,
|
||||||
|
$Res Function(_$SnChatMessagePreloadImpl) then) =
|
||||||
|
__$$SnChatMessagePreloadImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({@HiveField(0) List<SnAttachment?>? attachments});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$SnChatMessagePreloadImplCopyWithImpl<$Res>
|
||||||
|
extends _$SnChatMessagePreloadCopyWithImpl<$Res, _$SnChatMessagePreloadImpl>
|
||||||
|
implements _$$SnChatMessagePreloadImplCopyWith<$Res> {
|
||||||
|
__$$SnChatMessagePreloadImplCopyWithImpl(_$SnChatMessagePreloadImpl _value,
|
||||||
|
$Res Function(_$SnChatMessagePreloadImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of SnChatMessagePreload
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? attachments = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$SnChatMessagePreloadImpl(
|
||||||
|
attachments: freezed == attachments
|
||||||
|
? _value._attachments
|
||||||
|
: attachments // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnAttachment?>?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
@HiveType(typeId: 5)
|
||||||
|
class _$SnChatMessagePreloadImpl extends _SnChatMessagePreload {
|
||||||
|
const _$SnChatMessagePreloadImpl(
|
||||||
|
{@HiveField(0) final List<SnAttachment?>? attachments})
|
||||||
|
: _attachments = attachments,
|
||||||
|
super._();
|
||||||
|
|
||||||
|
factory _$SnChatMessagePreloadImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$SnChatMessagePreloadImplFromJson(json);
|
||||||
|
|
||||||
|
final List<SnAttachment?>? _attachments;
|
||||||
|
@override
|
||||||
|
@HiveField(0)
|
||||||
|
List<SnAttachment?>? get attachments {
|
||||||
|
final value = _attachments;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_attachments is EqualUnmodifiableListView) return _attachments;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnChatMessagePreload(attachments: $attachments)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$SnChatMessagePreloadImpl &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._attachments, _attachments));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType, const DeepCollectionEquality().hash(_attachments));
|
||||||
|
|
||||||
|
/// Create a copy of SnChatMessagePreload
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$SnChatMessagePreloadImplCopyWith<_$SnChatMessagePreloadImpl>
|
||||||
|
get copyWith =>
|
||||||
|
__$$SnChatMessagePreloadImplCopyWithImpl<_$SnChatMessagePreloadImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$SnChatMessagePreloadImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _SnChatMessagePreload extends SnChatMessagePreload {
|
||||||
|
const factory _SnChatMessagePreload(
|
||||||
|
{@HiveField(0) final List<SnAttachment?>? attachments}) =
|
||||||
|
_$SnChatMessagePreloadImpl;
|
||||||
|
const _SnChatMessagePreload._() : super._();
|
||||||
|
|
||||||
|
factory _SnChatMessagePreload.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$SnChatMessagePreloadImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
@HiveField(0)
|
||||||
|
List<SnAttachment?>? get attachments;
|
||||||
|
|
||||||
|
/// Create a copy of SnChatMessagePreload
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$SnChatMessagePreloadImplCopyWith<_$SnChatMessagePreloadImpl>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
@ -204,6 +204,41 @@ 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
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
@ -309,6 +344,10 @@ _$SnChatMessageImpl _$$SnChatMessageImplFromJson(Map<String, dynamic> json) =>
|
|||||||
sender: SnChannelMember.fromJson(json['sender'] as Map<String, dynamic>),
|
sender: SnChannelMember.fromJson(json['sender'] as Map<String, dynamic>),
|
||||||
channelId: (json['channel_id'] as num).toInt(),
|
channelId: (json['channel_id'] as num).toInt(),
|
||||||
senderId: (json['sender_id'] as num).toInt(),
|
senderId: (json['sender_id'] as num).toInt(),
|
||||||
|
preload: json['preload'] == null
|
||||||
|
? null
|
||||||
|
: SnChatMessagePreload.fromJson(
|
||||||
|
json['preload'] as Map<String, dynamic>),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$SnChatMessageImplToJson(_$SnChatMessageImpl instance) =>
|
Map<String, dynamic> _$$SnChatMessageImplToJson(_$SnChatMessageImpl instance) =>
|
||||||
@ -324,4 +363,21 @@ Map<String, dynamic> _$$SnChatMessageImplToJson(_$SnChatMessageImpl instance) =>
|
|||||||
'sender': instance.sender.toJson(),
|
'sender': instance.sender.toJson(),
|
||||||
'channel_id': instance.channelId,
|
'channel_id': instance.channelId,
|
||||||
'sender_id': instance.senderId,
|
'sender_id': instance.senderId,
|
||||||
|
'preload': instance.preload?.toJson(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnChatMessagePreloadImpl _$$SnChatMessagePreloadImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SnChatMessagePreloadImpl(
|
||||||
|
attachments: (json['attachments'] as List<dynamic>?)
|
||||||
|
?.map((e) => e == null
|
||||||
|
? null
|
||||||
|
: SnAttachment.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnChatMessagePreloadImplToJson(
|
||||||
|
_$SnChatMessagePreloadImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
|
||||||
};
|
};
|
||||||
|
@ -53,7 +53,7 @@ class SnPost with _$SnPost {
|
|||||||
@freezed
|
@freezed
|
||||||
class SnPostPreload with _$SnPostPreload {
|
class SnPostPreload with _$SnPostPreload {
|
||||||
const factory SnPostPreload({
|
const factory SnPostPreload({
|
||||||
required List<SnAttachment>? attachments,
|
required List<SnAttachment?>? attachments,
|
||||||
}) = _SnPostPreload;
|
}) = _SnPostPreload;
|
||||||
|
|
||||||
factory SnPostPreload.fromJson(Map<String, Object?> json) =>
|
factory SnPostPreload.fromJson(Map<String, Object?> json) =>
|
||||||
|
@ -953,7 +953,7 @@ SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnPostPreload {
|
mixin _$SnPostPreload {
|
||||||
List<SnAttachment>? get attachments => throw _privateConstructorUsedError;
|
List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this SnPostPreload to a JSON map.
|
/// Serializes this SnPostPreload to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@ -971,7 +971,7 @@ abstract class $SnPostPreloadCopyWith<$Res> {
|
|||||||
SnPostPreload value, $Res Function(SnPostPreload) then) =
|
SnPostPreload value, $Res Function(SnPostPreload) then) =
|
||||||
_$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>;
|
_$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({List<SnAttachment>? attachments});
|
$Res call({List<SnAttachment?>? attachments});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -995,7 +995,7 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
|
|||||||
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?>?,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1008,7 +1008,7 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res>
|
|||||||
__$$SnPostPreloadImplCopyWithImpl<$Res>;
|
__$$SnPostPreloadImplCopyWithImpl<$Res>;
|
||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({List<SnAttachment>? attachments});
|
$Res call({List<SnAttachment?>? attachments});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -1030,7 +1030,7 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
|
|||||||
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?>?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1038,15 +1038,15 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$SnPostPreloadImpl implements _SnPostPreload {
|
class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||||
const _$SnPostPreloadImpl({required final List<SnAttachment>? attachments})
|
const _$SnPostPreloadImpl({required final List<SnAttachment?>? attachments})
|
||||||
: _attachments = attachments;
|
: _attachments = attachments;
|
||||||
|
|
||||||
factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$SnPostPreloadImplFromJson(json);
|
_$$SnPostPreloadImplFromJson(json);
|
||||||
|
|
||||||
final List<SnAttachment>? _attachments;
|
final List<SnAttachment?>? _attachments;
|
||||||
@override
|
@override
|
||||||
List<SnAttachment>? get attachments {
|
List<SnAttachment?>? get attachments {
|
||||||
final value = _attachments;
|
final value = _attachments;
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
if (_attachments is EqualUnmodifiableListView) return _attachments;
|
if (_attachments is EqualUnmodifiableListView) return _attachments;
|
||||||
@ -1091,13 +1091,13 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
|
|||||||
|
|
||||||
abstract class _SnPostPreload implements SnPostPreload {
|
abstract class _SnPostPreload implements SnPostPreload {
|
||||||
const factory _SnPostPreload(
|
const factory _SnPostPreload(
|
||||||
{required final List<SnAttachment>? attachments}) = _$SnPostPreloadImpl;
|
{required final List<SnAttachment?>? attachments}) = _$SnPostPreloadImpl;
|
||||||
|
|
||||||
factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
|
factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
|
||||||
_$SnPostPreloadImpl.fromJson;
|
_$SnPostPreloadImpl.fromJson;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<SnAttachment>? get attachments;
|
List<SnAttachment?>? get attachments;
|
||||||
|
|
||||||
/// Create a copy of SnPostPreload
|
/// Create a copy of SnPostPreload
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@ -103,13 +103,15 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
|||||||
_$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
_$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
||||||
_$SnPostPreloadImpl(
|
_$SnPostPreloadImpl(
|
||||||
attachments: (json['attachments'] as List<dynamic>?)
|
attachments: (json['attachments'] as List<dynamic>?)
|
||||||
?.map((e) => SnAttachment.fromJson(e as Map<String, dynamic>))
|
?.map((e) => e == null
|
||||||
|
? null
|
||||||
|
: SnAttachment.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
|
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'attachments': instance.attachments?.map((e) => e.toJson()).toList(),
|
'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
|
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
|
||||||
|
@ -14,7 +14,7 @@ import 'package:surface/widgets/universal_image.dart';
|
|||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class AttachmentItem extends StatelessWidget {
|
class AttachmentItem extends StatelessWidget {
|
||||||
final SnAttachment data;
|
final SnAttachment? data;
|
||||||
final bool isExpandable;
|
final bool isExpandable;
|
||||||
const AttachmentItem({
|
const AttachmentItem({
|
||||||
super.key,
|
super.key,
|
||||||
@ -23,15 +23,19 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context, String heroTag) {
|
Widget _buildContent(BuildContext context, String heroTag) {
|
||||||
final tp = data.mimetype.split('/').firstOrNull;
|
if (data == null) {
|
||||||
|
return const Icon(Symbols.cancel).center();
|
||||||
|
}
|
||||||
|
|
||||||
|
final tp = data!.mimetype.split('/').firstOrNull;
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
switch (tp) {
|
switch (tp) {
|
||||||
case 'image':
|
case 'image':
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: 'attachment-${data.rid}-$heroTag',
|
tag: 'attachment-${data!.rid}-$heroTag',
|
||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(data.rid),
|
sn.getAttachmentUrl(data!.rid),
|
||||||
key: Key('attachment-${data.rid}-$heroTag'),
|
key: Key('attachment-${data!.rid}-$heroTag'),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -45,7 +49,7 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
final uuid = Uuid();
|
final uuid = Uuid();
|
||||||
final heroTag = uuid.v4();
|
final heroTag = uuid.v4();
|
||||||
|
|
||||||
if (data.isMature) {
|
if (data!.isMature) {
|
||||||
return _AttachmentItemSensitiveBlur(
|
return _AttachmentItemSensitiveBlur(
|
||||||
child: _buildContent(context, heroTag),
|
child: _buildContent(context, heroTag),
|
||||||
);
|
);
|
||||||
@ -56,7 +60,7 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
child: _buildContent(context, heroTag),
|
child: _buildContent(context, heroTag),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
AttachmentDetailPopup(data: data, heroTag: heroTag),
|
AttachmentDetailPopup(data: data!, heroTag: heroTag),
|
||||||
rootNavigator: true,
|
rootNavigator: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -6,7 +6,7 @@ import 'package:surface/types/attachment.dart';
|
|||||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||||
|
|
||||||
class AttachmentList extends StatelessWidget {
|
class AttachmentList extends StatelessWidget {
|
||||||
final List<SnAttachment> data;
|
final List<SnAttachment?> data;
|
||||||
final bool? bordered;
|
final bool? bordered;
|
||||||
final double? maxHeight;
|
final double? maxHeight;
|
||||||
final EdgeInsets? listPadding;
|
final EdgeInsets? listPadding;
|
||||||
@ -46,7 +46,7 @@ class AttachmentList extends StatelessWidget {
|
|||||||
borderRadius: kDefaultRadius,
|
borderRadius: kDefaultRadius,
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: data[0].metadata['ratio']?.toDouble() ?? 1,
|
aspectRatio: data[0]?.metadata['ratio']?.toDouble() ?? 1,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: kDefaultRadius,
|
borderRadius: kDefaultRadius,
|
||||||
child: AttachmentItem(data: data[0], isExpandable: true),
|
child: AttachmentItem(data: data[0], isExpandable: true),
|
||||||
@ -62,7 +62,7 @@ class AttachmentList extends StatelessWidget {
|
|||||||
border: Border(top: borderSide, bottom: borderSide),
|
border: Border(top: borderSide, bottom: borderSide),
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: data[0].metadata['ratio']?.toDouble() ?? 1,
|
aspectRatio: data[0]?.metadata['ratio']?.toDouble() ?? 1,
|
||||||
child: AttachmentItem(data: data[0], isExpandable: true),
|
child: AttachmentItem(data: data[0], isExpandable: true),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -86,7 +86,7 @@ class AttachmentList extends StatelessWidget {
|
|||||||
borderRadius: kDefaultRadius,
|
borderRadius: kDefaultRadius,
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: data[idx].metadata['ratio']?.toDouble() ?? 1,
|
aspectRatio: data[idx]?.metadata['ratio']?.toDouble() ?? 1,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: kDefaultRadius,
|
borderRadius: kDefaultRadius,
|
||||||
child:
|
child:
|
||||||
|
@ -6,6 +6,7 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
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/markdown_content.dart';
|
import 'package:surface/widgets/markdown_content.dart';
|
||||||
|
|
||||||
class ChatMessage extends StatelessWidget {
|
class ChatMessage extends StatelessWidget {
|
||||||
@ -63,6 +64,13 @@ class ChatMessage extends StatelessWidget {
|
|||||||
content: data.body['text'],
|
content: data.body['text'],
|
||||||
isAutoWarp: true,
|
isAutoWarp: true,
|
||||||
),
|
),
|
||||||
|
if (data.preload?.attachments?.isNotEmpty ?? false)
|
||||||
|
AttachmentList(
|
||||||
|
data: data.preload!.attachments!,
|
||||||
|
bordered: true,
|
||||||
|
maxHeight: 520,
|
||||||
|
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
),
|
||||||
if (!hasMerged) const Gap(8),
|
if (!hasMerged) const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
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:image_picker/image_picker.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
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/providers/sn_attachment.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||||
|
|
||||||
class ChatMessageInput extends StatefulWidget {
|
class ChatMessageInput extends StatefulWidget {
|
||||||
final ChatMessageController controller;
|
final ChatMessageController controller;
|
||||||
@ -14,15 +20,83 @@ class ChatMessageInput extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ChatMessageInputState extends State<ChatMessageInput> {
|
class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
double? _progress;
|
||||||
|
|
||||||
final TextEditingController _contentController = TextEditingController();
|
final TextEditingController _contentController = TextEditingController();
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
|
||||||
void _sendMessage() {
|
Future<void> _sendMessage() async {
|
||||||
|
if (_isBusy) return;
|
||||||
|
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < _attachments.length; i++) {
|
||||||
|
final media = _attachments[i];
|
||||||
|
if (media.attachment != null) continue; // Already uploaded, skip
|
||||||
|
if (media.isEmpty) continue; // Nothing to do, skip
|
||||||
|
|
||||||
|
final place = await attach.chunkedUploadInitialize(
|
||||||
|
(await media.length())!,
|
||||||
|
media.name,
|
||||||
|
'interactive',
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final item = await attach.chunkedUploadParts(
|
||||||
|
media.toFile()!,
|
||||||
|
place.$1,
|
||||||
|
place.$2,
|
||||||
|
onProgress: (progress) {
|
||||||
|
// Calculate overall progress for attachments
|
||||||
|
setState(() {
|
||||||
|
progress = (i + progress) / _attachments.length;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_attachments[i] = PostWriteMedia(item);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attach.putCache(
|
||||||
|
_attachments.where((e) => e.attachment != null).map((e) => e.attachment!),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send the message
|
||||||
|
// NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type
|
||||||
widget.controller.sendMessage(
|
widget.controller.sendMessage(
|
||||||
'messages.new',
|
'messages.new',
|
||||||
_contentController.text,
|
_contentController.text,
|
||||||
|
attachments: _attachments
|
||||||
|
.where((e) => e.attachment != null)
|
||||||
|
.map((e) => e.attachment!.rid)
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
_contentController.clear();
|
_contentController.clear();
|
||||||
|
_attachments.clear();
|
||||||
|
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<PostWriteMedia> _attachments = List.empty(growable: true);
|
||||||
|
final _imagePicker = ImagePicker();
|
||||||
|
|
||||||
|
void _selectMedia() async {
|
||||||
|
final result = await _imagePicker.pickMultipleMedia();
|
||||||
|
if (result.isEmpty) return;
|
||||||
|
_attachments.addAll(
|
||||||
|
result.map((e) => PostWriteMedia.fromFile(e)),
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -37,6 +111,33 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
if (_isBusy && _progress != null)
|
||||||
|
TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0, end: _progress),
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
builder: (context, value, _) =>
|
||||||
|
LinearProgressIndicator(value: value, minHeight: 2),
|
||||||
|
)
|
||||||
|
else if (_isBusy)
|
||||||
|
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||||
|
Padding(
|
||||||
|
padding: _attachments.isNotEmpty
|
||||||
|
? const EdgeInsets.only(top: 8)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
child: PostMediaPendingListRaw(
|
||||||
|
attachments: _attachments,
|
||||||
|
isBusy: _isBusy,
|
||||||
|
onUpdate: (idx, updatedMedia) async {
|
||||||
|
setState(() => _attachments[idx] = updatedMedia);
|
||||||
|
},
|
||||||
|
onRemove: (idx) async {
|
||||||
|
setState(() => _attachments.removeAt(idx));
|
||||||
|
},
|
||||||
|
onUpdateBusy: (state) => setState(() => _isBusy = state),
|
||||||
|
).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate(
|
||||||
|
const Duration(milliseconds: 300),
|
||||||
|
Curves.fastEaseInToSlowEaseOut),
|
||||||
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 56,
|
height: 56,
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -53,6 +154,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
|
if (_isBusy) return;
|
||||||
_sendMessage();
|
_sendMessage();
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
},
|
},
|
||||||
@ -60,7 +162,14 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: _sendMessage,
|
onPressed: _isBusy ? null : _selectMedia,
|
||||||
|
icon: Icon(
|
||||||
|
Symbols.add_photo_alternate,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _isBusy ? null : _sendMessage,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Symbols.send,
|
Symbols.send,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
@ -61,7 +61,8 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
|||||||
preload: SnPostPreload(
|
preload: SnPostPreload(
|
||||||
attachments: attachments
|
attachments: attachments
|
||||||
.where(
|
.where(
|
||||||
(ele) => out[i].body['attachments']?.contains(ele.rid) ?? false,
|
(ele) =>
|
||||||
|
out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
|
@ -18,54 +18,22 @@ import 'package:surface/widgets/dialog.dart';
|
|||||||
|
|
||||||
class PostMediaPendingList extends StatelessWidget {
|
class PostMediaPendingList extends StatelessWidget {
|
||||||
final PostWriteController controller;
|
final PostWriteController controller;
|
||||||
|
|
||||||
const PostMediaPendingList({super.key, required this.controller});
|
const PostMediaPendingList({super.key, required this.controller});
|
||||||
|
|
||||||
void _cropImage(BuildContext context, int idx) async {
|
Future<void> _handleUpdate(int idx, PostWriteMedia updatedMedia) async {
|
||||||
final media = controller.attachments[idx];
|
|
||||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
|
||||||
? await showCupertinoImageCropper(
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
context,
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
imageProvider: media.getImageProvider(context)!,
|
|
||||||
)
|
|
||||||
: await showMaterialImageCropper(
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
context,
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
imageProvider: media.getImageProvider(context)!,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result == null) return;
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
controller.setIsBusy(true);
|
controller.setIsBusy(true);
|
||||||
|
try {
|
||||||
final rawBytes =
|
controller.setAttachmentAt(idx, updatedMedia);
|
||||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
} finally {
|
||||||
.buffer
|
controller.setIsBusy(false);
|
||||||
.asUint8List();
|
}
|
||||||
controller.setAttachmentAt(
|
|
||||||
idx,
|
|
||||||
PostWriteMedia.fromBytes(rawBytes, media.name, media.type),
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.setIsBusy(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deleteAttachment(BuildContext context, int idx) async {
|
Future<void> _handleRemove(int idx) async {
|
||||||
final media = controller.attachments[idx];
|
|
||||||
if (media.attachment == null) return;
|
|
||||||
|
|
||||||
controller.setIsBusy(true);
|
controller.setIsBusy(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.delete('/cgi/uc/attachments/${media.attachment!.id}');
|
|
||||||
controller.removeAttachmentAt(idx);
|
controller.removeAttachmentAt(idx);
|
||||||
} catch (err) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
} finally {
|
||||||
controller.setIsBusy(false);
|
controller.setIsBusy(false);
|
||||||
}
|
}
|
||||||
@ -73,108 +41,180 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: controller,
|
listenable: controller,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return Container(
|
return PostMediaPendingListRaw(
|
||||||
constraints: const BoxConstraints(maxHeight: 120),
|
attachments: controller.attachments,
|
||||||
child: ListView.separated(
|
isBusy: controller.isBusy,
|
||||||
scrollDirection: Axis.horizontal,
|
onUpdate: (idx, updatedMedia) => _handleUpdate(idx, updatedMedia),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
onRemove: (idx) => _handleRemove(idx),
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
onUpdateBusy: (state) => controller.setIsBusy(state),
|
||||||
itemCount: controller.attachments.length,
|
|
||||||
itemBuilder: (context, idx) {
|
|
||||||
final media = controller.attachments[idx];
|
|
||||||
return ContextMenuRegion(
|
|
||||||
contextMenu: ContextMenu(
|
|
||||||
entries: [
|
|
||||||
if (media.type == PostWriteMediaType.image &&
|
|
||||||
media.attachment != null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'preview'.tr(),
|
|
||||||
icon: Symbols.preview,
|
|
||||||
onSelected: () {
|
|
||||||
context.pushTransparentRoute(
|
|
||||||
AttachmentDetailPopup(data: media.attachment!),
|
|
||||||
rootNavigator: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (media.type == PostWriteMediaType.image &&
|
|
||||||
media.attachment == null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'crop'.tr(),
|
|
||||||
icon: Symbols.crop,
|
|
||||||
onSelected: () => _cropImage(context, idx),
|
|
||||||
),
|
|
||||||
if (media.attachment != null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'delete'.tr(),
|
|
||||||
icon: Symbols.delete,
|
|
||||||
onSelected: controller.isBusy
|
|
||||||
? null
|
|
||||||
: () => _deleteAttachment(context, idx),
|
|
||||||
),
|
|
||||||
if (media.attachment == null)
|
|
||||||
MenuItem(
|
|
||||||
label: 'delete'.tr(),
|
|
||||||
icon: Symbols.delete,
|
|
||||||
onSelected: () {
|
|
||||||
controller.removeAttachmentAt(idx);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else
|
|
||||||
MenuItem(
|
|
||||||
label: 'unlink'.tr(),
|
|
||||||
icon: Symbols.link_off,
|
|
||||||
onSelected: () {
|
|
||||||
controller.removeAttachmentAt(idx);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: switch (media.type) {
|
|
||||||
PostWriteMediaType.image =>
|
|
||||||
LayoutBuilder(builder: (context, constraints) {
|
|
||||||
return Image(
|
|
||||||
image: media.getImageProvider(
|
|
||||||
context,
|
|
||||||
width: (constraints.maxWidth * devicePixelRatio)
|
|
||||||
.round(),
|
|
||||||
height:
|
|
||||||
(constraints.maxHeight * devicePixelRatio)
|
|
||||||
.round(),
|
|
||||||
)!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
_ => Container(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: const Icon(Symbols.docs).center(),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PostMediaPendingListRaw extends StatelessWidget {
|
||||||
|
final List<PostWriteMedia> attachments;
|
||||||
|
final bool isBusy;
|
||||||
|
final Future<void> Function(int idx, PostWriteMedia updatedMedia)? onUpdate;
|
||||||
|
final Future<void> Function(int idx)? onRemove;
|
||||||
|
final void Function(bool state)? onUpdateBusy;
|
||||||
|
|
||||||
|
const PostMediaPendingListRaw({
|
||||||
|
super.key,
|
||||||
|
required this.attachments,
|
||||||
|
required this.isBusy,
|
||||||
|
this.onUpdate,
|
||||||
|
this.onRemove,
|
||||||
|
this.onUpdateBusy,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> _cropImage(BuildContext context, int idx) async {
|
||||||
|
final media = attachments[idx];
|
||||||
|
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||||
|
? await showCupertinoImageCropper(
|
||||||
|
context,
|
||||||
|
imageProvider: media.getImageProvider(context)!,
|
||||||
|
)
|
||||||
|
: await showMaterialImageCropper(
|
||||||
|
context,
|
||||||
|
imageProvider: media.getImageProvider(context)!,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
final rawBytes =
|
||||||
|
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||||
|
.buffer
|
||||||
|
.asUint8List();
|
||||||
|
|
||||||
|
if (onUpdate != null) {
|
||||||
|
final updatedMedia = PostWriteMedia.fromBytes(
|
||||||
|
rawBytes,
|
||||||
|
media.name,
|
||||||
|
media.type,
|
||||||
|
);
|
||||||
|
await onUpdate!(idx, updatedMedia);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteAttachment(BuildContext context, int idx) async {
|
||||||
|
final media = attachments[idx];
|
||||||
|
if (media.attachment == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
onUpdateBusy?.call(true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.delete('/cgi/uc/attachments/${media.attachment!.id}');
|
||||||
|
onRemove!(idx);
|
||||||
|
} catch (err) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
onUpdateBusy?.call(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 120),
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
|
itemCount: attachments.length,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final media = attachments[idx];
|
||||||
|
return ContextMenuRegion(
|
||||||
|
contextMenu: ContextMenu(
|
||||||
|
entries: [
|
||||||
|
if (media.type == PostWriteMediaType.image &&
|
||||||
|
media.attachment != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'preview'.tr(),
|
||||||
|
icon: Symbols.preview,
|
||||||
|
onSelected: () {
|
||||||
|
context.pushTransparentRoute(
|
||||||
|
AttachmentDetailPopup(data: media.attachment!),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (media.type == PostWriteMediaType.image &&
|
||||||
|
media.attachment == null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'crop'.tr(),
|
||||||
|
icon: Symbols.crop,
|
||||||
|
onSelected: () => _cropImage(context, idx),
|
||||||
|
),
|
||||||
|
if (media.attachment != null && onRemove != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'delete'.tr(),
|
||||||
|
icon: Symbols.delete,
|
||||||
|
onSelected:
|
||||||
|
isBusy ? null : () => _deleteAttachment(context, idx),
|
||||||
|
),
|
||||||
|
if (media.attachment == null && onRemove != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'delete'.tr(),
|
||||||
|
icon: Symbols.delete,
|
||||||
|
onSelected: () {
|
||||||
|
onRemove!(idx);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (onRemove != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'unlink'.tr(),
|
||||||
|
icon: Symbols.link_off,
|
||||||
|
onSelected: () {
|
||||||
|
onRemove!(idx);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: switch (media.type) {
|
||||||
|
PostWriteMediaType.image =>
|
||||||
|
LayoutBuilder(builder: (context, constraints) {
|
||||||
|
return Image(
|
||||||
|
image: media.getImageProvider(
|
||||||
|
context,
|
||||||
|
width: (constraints.maxWidth * devicePixelRatio)
|
||||||
|
.round(),
|
||||||
|
height: (constraints.maxHeight * devicePixelRatio)
|
||||||
|
.round(),
|
||||||
|
)!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
_ => Container(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: const Icon(Symbols.docs).center(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user