Compare commits
No commits in common. "a3c8dafff9599bf1aa5ecad1b5d92c342e9086b1" and "f26edce071b3feaa395c2baefddde4bca7da8992" have entirely different histories.
a3c8dafff9
...
f26edce071
@ -281,10 +281,6 @@
|
|||||||
"one": "{} attachment",
|
"one": "{} attachment",
|
||||||
"other": "{} attachments"
|
"other": "{} attachments"
|
||||||
},
|
},
|
||||||
"messageTyping": {
|
|
||||||
"one": "{} is typing...",
|
|
||||||
"other": "{} are typing..."
|
|
||||||
},
|
|
||||||
"fieldAttachmentRandomId": "Random ID",
|
"fieldAttachmentRandomId": "Random ID",
|
||||||
"fieldAttachmentAlt": "Alternative text",
|
"fieldAttachmentAlt": "Alternative text",
|
||||||
"addAttachmentFromAlbum": "Add from album",
|
"addAttachmentFromAlbum": "Add from album",
|
||||||
|
@ -279,10 +279,6 @@
|
|||||||
"one": "{} 个附件",
|
"one": "{} 个附件",
|
||||||
"other": "{} 个附件"
|
"other": "{} 个附件"
|
||||||
},
|
},
|
||||||
"messageTyping": {
|
|
||||||
"one": "{} 正在输入",
|
|
||||||
"other": "{} 正在输入"
|
|
||||||
},
|
|
||||||
"fieldAttachmentRandomId": "访问 ID",
|
"fieldAttachmentRandomId": "访问 ID",
|
||||||
"fieldAttachmentAlt": "概述文字",
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@ -12,7 +11,6 @@ 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';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/types/websocket.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class ChatMessageController extends ChangeNotifier {
|
class ChatMessageController extends ChangeNotifier {
|
||||||
@ -38,7 +36,8 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
|
|
||||||
int? messageTotal;
|
int? messageTotal;
|
||||||
|
|
||||||
bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
|
bool get isAllLoaded =>
|
||||||
|
messageTotal != null && messages.length >= messageTotal!;
|
||||||
|
|
||||||
String? _boxKey;
|
String? _boxKey;
|
||||||
SnChannel? channel;
|
SnChannel? channel;
|
||||||
@ -51,10 +50,8 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
/// Stored as a list of nonce to provide the loading state
|
/// Stored as a list of nonce to provide the loading state
|
||||||
final List<String> unconfirmedMessages = List.empty(growable: true);
|
final List<String> unconfirmedMessages = List.empty(growable: true);
|
||||||
|
|
||||||
Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
Box<SnChatMessage>? get _box =>
|
||||||
|
(_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
||||||
final List<SnChannelMember> typingMembers = List.empty(growable: true);
|
|
||||||
final Map<int, Timer> typingInactiveTimer = {};
|
|
||||||
|
|
||||||
Future<void> initialize(SnChannel chan) async {
|
Future<void> initialize(SnChannel chan) async {
|
||||||
channel = chan;
|
channel = chan;
|
||||||
@ -81,17 +78,22 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
if (event.payload?['channel_id'] != channel?.id) break;
|
if (event.payload?['channel_id'] != channel?.id) break;
|
||||||
final member = SnChannelMember.fromJson(event.payload!['member']);
|
final member = SnChannelMember.fromJson(event.payload!['member']);
|
||||||
if (member.id == profile?.id) break;
|
if (member.id == profile?.id) break;
|
||||||
if (!typingMembers.any((x) => x.id == member.id)) {
|
// TODO impl typing users
|
||||||
typingMembers.add(member);
|
// if (!_typingUsers.any((x) => x.id == member.id)) {
|
||||||
print('Typing member: ${typingMembers.map((ele) => member.id)}');
|
// setState(() {
|
||||||
notifyListeners();
|
// _typingUsers.add(member);
|
||||||
}
|
// });
|
||||||
typingInactiveTimer[member.id]?.cancel();
|
// }
|
||||||
typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
|
// _typingInactiveTimer[member.id]?.cancel();
|
||||||
typingMembers.removeWhere((x) => x.id == member.id);
|
// _typingInactiveTimer[member.id] = Timer(
|
||||||
typingInactiveTimer.remove(member.id);
|
// const Duration(seconds: 3),
|
||||||
notifyListeners();
|
// () {
|
||||||
});
|
// setState(() {
|
||||||
|
// _typingUsers.removeWhere((x) => x.id == member.id);
|
||||||
|
// _typingInactiveTimer.remove(member.id);
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -99,35 +101,6 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer? _typingNotifyTimer;
|
|
||||||
bool _typingStatus = false;
|
|
||||||
|
|
||||||
Future<void> _sendTypingStatusPackage() async {
|
|
||||||
_ws.conn?.sink.add(jsonEncode(
|
|
||||||
WebSocketPackage(
|
|
||||||
method: 'status.typing',
|
|
||||||
endpoint: 'im',
|
|
||||||
payload: {
|
|
||||||
'channel_id': channel!.id,
|
|
||||||
},
|
|
||||||
).toJson(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
void pingTypingStatus() {
|
|
||||||
if (!_typingStatus) {
|
|
||||||
_sendTypingStatusPackage();
|
|
||||||
_typingStatus = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_typingNotifyTimer == null || !_typingNotifyTimer!.isActive) {
|
|
||||||
_typingNotifyTimer?.cancel();
|
|
||||||
_typingNotifyTimer = Timer(const Duration(milliseconds: 1850), () {
|
|
||||||
_typingStatus = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
|
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
|
||||||
if (_box == null) return;
|
if (_box == null) return;
|
||||||
await _box!.putAll({
|
await _box!.putAll({
|
||||||
@ -194,7 +167,8 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'messages.edit':
|
case 'messages.edit':
|
||||||
if (message.relatedEventId != null) {
|
if (message.relatedEventId != null) {
|
||||||
final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
|
final idx =
|
||||||
|
messages.indexWhere((x) => x.id == message.relatedEventId);
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
final newBody = message.body;
|
final newBody = message.body;
|
||||||
newBody.remove('related_event');
|
newBody.remove('related_event');
|
||||||
@ -233,7 +207,8 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
'algorithm': 'plain',
|
'algorithm': 'plain',
|
||||||
if (quoteId != null) 'quote_event': quoteId,
|
if (quoteId != null) 'quote_event': quoteId,
|
||||||
if (relatedId != null) 'related_event': relatedId,
|
if (relatedId != null) 'related_event': relatedId,
|
||||||
if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
|
if (attachments != null && attachments.isNotEmpty)
|
||||||
|
'attachments': attachments,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock the message locally
|
// Mock the message locally
|
||||||
@ -330,7 +305,8 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
|
|
||||||
if (out == null) {
|
if (out == null) {
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
final resp = await _sn.client
|
||||||
|
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
||||||
out = SnChatMessage.fromJson(resp.data);
|
out = SnChatMessage.fromJson(resp.data);
|
||||||
_saveMessageToLocal([out]);
|
_saveMessageToLocal([out]);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@ -365,7 +341,9 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
bool forceRemote = false,
|
bool forceRemote = false,
|
||||||
}) async {
|
}) async {
|
||||||
late List<SnChatMessage> out;
|
late List<SnChatMessage> out;
|
||||||
if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
|
if (_box != null &&
|
||||||
|
(_box!.length >= take + offset || forceLocal) &&
|
||||||
|
!forceRemote) {
|
||||||
out = _box!.keys
|
out = _box!.keys
|
||||||
.toList()
|
.toList()
|
||||||
.cast<int>()
|
.cast<int>()
|
||||||
@ -408,7 +386,8 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
quoteEvent: quoteEvent,
|
quoteEvent: quoteEvent,
|
||||||
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(),
|
||||||
),
|
),
|
||||||
@ -416,7 +395,10 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preload sender accounts
|
// Preload sender accounts
|
||||||
final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
|
final accountId = out
|
||||||
|
.where((ele) => ele.sender.accountId >= 0)
|
||||||
|
.map((ele) => ele.sender.accountId)
|
||||||
|
.toSet();
|
||||||
await _ud.listAccount(accountId);
|
await _ud.listAccount(accountId);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
@ -104,7 +104,7 @@ class PostWriteMedia {
|
|||||||
if (attachment != null) {
|
if (attachment != null) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||||
if (width != null && height != null && !kIsWeb) {
|
if (width != null && height != null) {
|
||||||
return ResizeImage(
|
return ResizeImage(
|
||||||
provider,
|
provider,
|
||||||
width: width,
|
width: width,
|
||||||
|
@ -87,7 +87,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
try {
|
try {
|
||||||
final resp = await sn.client.request(
|
final resp = await sn.client.request(
|
||||||
widget.editingChannelAlias != null
|
widget.editingChannelAlias != null
|
||||||
? '/cgi/im/channels/$scope/${_editingChannel!.id}'
|
? '/cgi/im/channels/$scope/${widget.editingChannelAlias}'
|
||||||
: '/cgi/im/channels/$scope',
|
: '/cgi/im/channels/$scope',
|
||||||
data: payload,
|
data: payload,
|
||||||
options: Options(
|
options: Options(
|
||||||
|
@ -17,7 +17,6 @@ import 'package:surface/types/chat.dart';
|
|||||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
||||||
import 'package:surface/widgets/chat/chat_message.dart';
|
import 'package:surface/widgets/chat/chat_message.dart';
|
||||||
import 'package:surface/widgets/chat/chat_message_input.dart';
|
import 'package:surface/widgets/chat/chat_message_input.dart';
|
||||||
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
|
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
@ -336,17 +335,11 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
if (!_messageController.isPending)
|
if (!_messageController.isPending)
|
||||||
Material(
|
Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: Column(
|
child: ChatMessageInput(
|
||||||
children: [
|
|
||||||
ChatTypingIndicator(controller: _messageController),
|
|
||||||
ChatMessageInput(
|
|
||||||
key: _inputGlobalKey,
|
key: _inputGlobalKey,
|
||||||
otherMember: _otherMember,
|
otherMember: _otherMember,
|
||||||
controller: _messageController,
|
controller: _messageController,
|
||||||
),
|
).padding(bottom: MediaQuery.of(context).padding.bottom),
|
||||||
Gap(MediaQuery.of(context).padding.bottom),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -173,8 +173,8 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
DateFormat('y/M/d').format(DateTime.now().copyWith(
|
DateFormat('y/M/d').format(DateTime.now().copyWith(
|
||||||
month: kSpecialDays[ele]?.$1,
|
month: kSpecialDays[ele]!.$1,
|
||||||
day: kSpecialDays[ele]?.$2,
|
day: kSpecialDays[ele]!.$2,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -82,15 +82,24 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _isSubmitting = true);
|
setState(() => _isSubmitting = true);
|
||||||
|
|
||||||
|
List<int> markList = List.empty(growable: true);
|
||||||
|
for (final element in _notifications) {
|
||||||
|
if (element.id <= 0) continue;
|
||||||
|
if (element.readAt != null) continue;
|
||||||
|
markList.add(element.id);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.put('/cgi/id/notifications/read/all');
|
await sn.client.put('/cgi/id/notifications/read', data: {
|
||||||
|
'messages': markList,
|
||||||
|
});
|
||||||
_notifications.clear();
|
_notifications.clear();
|
||||||
_fetchNotifications();
|
_fetchNotifications();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar(
|
context.showSnackbar(
|
||||||
'notificationMarkAllReadPrompt'.plural(resp.data['count']),
|
'notificationMarkAllReadPrompt'.plural(markList.length),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
@ -88,9 +88,9 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
|
Tab(icon: const Icon(Symbols.home)),
|
||||||
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
|
Tab(icon: const Icon(Symbols.group)),
|
||||||
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
|
Tab(icon: const Icon(Symbols.settings)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -15,10 +15,10 @@ class AttachmentList extends StatefulWidget {
|
|||||||
final List<SnAttachment?> data;
|
final List<SnAttachment?> data;
|
||||||
final bool bordered;
|
final bool bordered;
|
||||||
final bool gridded;
|
final bool gridded;
|
||||||
|
final bool noGrow;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
final double? maxHeight;
|
final double? maxHeight;
|
||||||
final double? minWidth;
|
final double? minWidth;
|
||||||
final double? maxWidth;
|
|
||||||
final EdgeInsets? padding;
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const AttachmentList({
|
const AttachmentList({
|
||||||
@ -26,10 +26,10 @@ class AttachmentList extends StatefulWidget {
|
|||||||
required this.data,
|
required this.data,
|
||||||
this.bordered = false,
|
this.bordered = false,
|
||||||
this.gridded = false,
|
this.gridded = false,
|
||||||
|
this.noGrow = false,
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
this.maxHeight,
|
this.maxHeight,
|
||||||
this.minWidth,
|
this.minWidth,
|
||||||
this.maxWidth,
|
|
||||||
this.padding,
|
this.padding,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
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:provider/provider.dart';
|
import 'package:provider/provider.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';
|
||||||
|
@ -158,7 +158,8 @@ class ChatMessage extends StatelessWidget {
|
|||||||
AttachmentList(
|
AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
bordered: true,
|
bordered: true,
|
||||||
// gridded: true,
|
gridded: true,
|
||||||
|
noGrow: true,
|
||||||
maxHeight: 560,
|
maxHeight: 560,
|
||||||
minWidth: 480,
|
minWidth: 480,
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
@ -11,6 +11,7 @@ import 'package:surface/providers/user_directory.dart';
|
|||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/chat.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 {
|
||||||
@ -32,16 +33,6 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
final TextEditingController _contentController = TextEditingController();
|
final TextEditingController _contentController = TextEditingController();
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_contentController.addListener(() {
|
|
||||||
if (_contentController.text.isNotEmpty) {
|
|
||||||
widget.controller.pingTypingStatus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void setReply(SnChatMessage? value) {
|
void setReply(SnChatMessage? value) {
|
||||||
setState(() => _replyingMessage = value);
|
setState(() => _replyingMessage = value);
|
||||||
}
|
}
|
||||||
@ -174,6 +165,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
? Container(
|
? Container(
|
||||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
@ -213,6 +205,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
? Container(
|
? Container(
|
||||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
import 'package:surface/controllers/chat_message_controller.dart';
|
|
||||||
import 'package:surface/providers/user_directory.dart';
|
|
||||||
|
|
||||||
class ChatTypingIndicator extends StatelessWidget {
|
|
||||||
final ChatMessageController controller;
|
|
||||||
|
|
||||||
const ChatTypingIndicator({super.key, required this.controller});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
|
||||||
|
|
||||||
return StyledWidget(controller.typingMembers.isEmpty
|
|
||||||
? const SizedBox.shrink()
|
|
||||||
: Container(
|
|
||||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.more_horiz, weight: 600, size: 20),
|
|
||||||
const Gap(8),
|
|
||||||
Text(
|
|
||||||
'messageTyping'.plural(controller.typingMembers.length, args: [
|
|
||||||
controller.typingMembers
|
|
||||||
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
|
||||||
? ele.nick!
|
|
||||||
: ud.getAccountFromCache(ele.accountId)?.name ?? 'unknown')
|
|
||||||
.join(', '),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.height(controller.typingMembers.isNotEmpty ? 38 : 0, animate: true)
|
|
||||||
.animate(
|
|
||||||
const Duration(milliseconds: 300),
|
|
||||||
Curves.fastLinearToSlowEaseIn,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user