forked from eatvio/eatvio_app
feat(chat): add plan order message widget and enhance chat bubble to support message types
This commit is contained in:
@@ -17,3 +17,11 @@ abstract class OAuthVerificationConfirmResponse with _$OAuthVerificationConfirmR
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -11,3 +11,11 @@ abstract class OAuthVerificationRequest with _$OAuthVerificationRequest {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -11,3 +11,11 @@ abstract class OAuthVerificationRequestResponse with _$OAuthVerificationRequestR
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ abstract class ChatMessageModel with _$ChatMessageModel {
|
||||
@JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead,
|
||||
@JsonKey(name: 'is_author_message_auth_user') required bool? isCurrentUserAuthor,
|
||||
@JsonKey(name: 'room_id') required String? roomId,
|
||||
@JsonKey(name: 'type') String? type,
|
||||
@Default(MessageState.sent) @JsonKey(includeToJson: false, includeFromJson: false) MessageState messageState,
|
||||
@Default(null) @JsonKey(includeToJson: false, includeFromJson: false) String? uuid,
|
||||
}) = _ChatMessageModel;
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cub
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/plan_order_message_widget.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
|
||||
@@ -221,14 +222,36 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
state.chatMessages[index].updatedAt.day != state.chatMessages[index + 1].updatedAt.day) ||
|
||||
index + 1 == state.chatMessages.length) {
|
||||
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
|
||||
final message = state.chatMessages[index];
|
||||
final isPlanOrderMessage = message.type == 'plan_order';
|
||||
|
||||
PacketOptions? messageOption;
|
||||
if (isPlanOrderMessage && chosenPlan != null) {
|
||||
if (chosenPlan.optionType == 'options1') {
|
||||
messageOption = chosenPlan.plan.packet1Options;
|
||||
} else if (chosenPlan.optionType == 'options2') {
|
||||
messageOption = chosenPlan.plan.packet2Options;
|
||||
} else if (chosenPlan.optionType == 'options3') {
|
||||
messageOption = chosenPlan.plan.packet3Options;
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_messageDate(state.chatMessages[index].updatedAt),
|
||||
_messageDate(message.updatedAt),
|
||||
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)
|
||||
PlanOrderMessageWidget(
|
||||
planName: chosenPlan.plan.name,
|
||||
planSlug: chosenPlan.plan.slug,
|
||||
price: messageOption.price,
|
||||
sendTime: message.updatedAt,
|
||||
isCurrentUser: isCurrentUserAuthor,
|
||||
),
|
||||
VisibilityDetector(
|
||||
key: Key(state.chatMessages[index].id.toString()),
|
||||
key: Key(message.id.toString()),
|
||||
onVisibilityChanged: (VisibilityInfo info) {
|
||||
if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
|
||||
unreadMessagesIdsCopy.add((id: state.chatMessages[index].id, index: index));
|
||||
if (!message.isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
|
||||
unreadMessagesIdsCopy.add((id: message.id, index: index));
|
||||
EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async {
|
||||
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy);
|
||||
if (!result) return;
|
||||
@@ -242,44 +265,74 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
},
|
||||
child: ChatBubbleWidget(
|
||||
isRead: state.chatMessages[index].isRead,
|
||||
sendTime: state.chatMessages[index].updatedAt,
|
||||
text: state.chatMessages[index].data.trim(),
|
||||
isRead: message.isRead,
|
||||
sendTime: message.updatedAt,
|
||||
text: message.data.trim(),
|
||||
isCurrentUser: isCurrentUserAuthor,
|
||||
mediaData: state.chatMessages[index].mediaData ?? [],
|
||||
messageState: state.chatMessages[index].messageState,
|
||||
messageUuid: state.chatMessages[index].uuid ?? '',
|
||||
mediaData: message.mediaData ?? [],
|
||||
messageState: message.messageState,
|
||||
messageUuid: message.uuid ?? '',
|
||||
userSlug: state.chatRoom.users.receiver.slug,
|
||||
messageType: message.type,
|
||||
isConsultant: amIConsultant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
|
||||
return VisibilityDetector(
|
||||
key: Key(state.chatMessages[index].id.toString()),
|
||||
onVisibilityChanged: (VisibilityInfo info) {
|
||||
if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
|
||||
unreadMessagesIds.add((id: state.chatMessages[index].id, index: index));
|
||||
EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async {
|
||||
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds);
|
||||
if (!result) return;
|
||||
if (!context.mounted) return;
|
||||
context.read<ChatsCubit>().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length);
|
||||
unreadMessagesIds.clear();
|
||||
});
|
||||
}
|
||||
},
|
||||
child: ChatBubbleWidget(
|
||||
isRead: state.chatMessages[index].isRead,
|
||||
sendTime: state.chatMessages[index].updatedAt,
|
||||
text: state.chatMessages[index].data.trim(),
|
||||
isCurrentUser: isCurrentUserAuthor,
|
||||
mediaData: state.chatMessages[index].mediaData ?? [],
|
||||
messageState: state.chatMessages[index].messageState,
|
||||
messageUuid: state.chatMessages[index].uuid ?? '',
|
||||
userSlug: state.chatRoom.users.receiver.slug,
|
||||
),
|
||||
final message = state.chatMessages[index];
|
||||
final isPlanOrderMessage = message.type == 'plan_order';
|
||||
|
||||
PacketOptions? messageOption;
|
||||
if (isPlanOrderMessage && chosenPlan != null) {
|
||||
if (chosenPlan.optionType == 'options1') {
|
||||
messageOption = chosenPlan.plan.packet1Options;
|
||||
} else if (chosenPlan.optionType == 'options2') {
|
||||
messageOption = chosenPlan.plan.packet2Options;
|
||||
} else if (chosenPlan.optionType == 'options3') {
|
||||
messageOption = chosenPlan.plan.packet3Options;
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)
|
||||
PlanOrderMessageWidget(
|
||||
planName: chosenPlan.plan.name,
|
||||
planSlug: chosenPlan.plan.slug,
|
||||
price: messageOption.price,
|
||||
sendTime: message.updatedAt,
|
||||
isCurrentUser: isCurrentUserAuthor,
|
||||
),
|
||||
VisibilityDetector(
|
||||
key: Key(message.id.toString()),
|
||||
onVisibilityChanged: (VisibilityInfo info) {
|
||||
if (!message.isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
|
||||
unreadMessagesIds.add((id: message.id, index: index));
|
||||
EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async {
|
||||
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds);
|
||||
if (!result) return;
|
||||
if (!context.mounted) return;
|
||||
context.read<ChatsCubit>().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length);
|
||||
unreadMessagesIds.clear();
|
||||
});
|
||||
}
|
||||
},
|
||||
child: ChatBubbleWidget(
|
||||
isRead: message.isRead,
|
||||
sendTime: message.updatedAt,
|
||||
text: message.data.trim(),
|
||||
isCurrentUser: isCurrentUserAuthor,
|
||||
mediaData: message.mediaData ?? [],
|
||||
messageState: message.messageState,
|
||||
messageUuid: message.uuid ?? '',
|
||||
userSlug: state.chatRoom.users.receiver.slug,
|
||||
messageType: message.type,
|
||||
isConsultant: amIConsultant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0),
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/message_action_bottom_sheet.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
@@ -24,7 +25,9 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
required this.isRead,
|
||||
required this.messageState,
|
||||
required this.messageUuid,
|
||||
required this.userSlug
|
||||
required this.userSlug,
|
||||
this.messageType,
|
||||
this.isConsultant,
|
||||
});
|
||||
|
||||
final DateTime sendTime;
|
||||
@@ -35,6 +38,8 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
final MessageState messageState;
|
||||
final String messageUuid;
|
||||
final String userSlug;
|
||||
final String? messageType;
|
||||
final bool? isConsultant;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
@@ -144,8 +149,8 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (mediaData.isNotEmpty && text.isNotEmpty) const SizedBox(height: 8),
|
||||
if (text.isNotEmpty) Text(text, style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16)),
|
||||
if (mediaData.isNotEmpty && _getDisplayText().isNotEmpty) const SizedBox(height: 8),
|
||||
if (_getDisplayText().isNotEmpty) _buildLinkableText(_getDisplayText()),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -163,4 +168,88 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getDisplayText() {
|
||||
if (messageType == 'plan_order') {
|
||||
if (isConsultant == true) {
|
||||
// Текст для консультанта
|
||||
return '1. Уточните у подопечного его цели и пожелания\n'
|
||||
'2. Согласуйте и выберите дату старта плана\n'
|
||||
'3. Ожидайте уведомления об успешной оплате\n'
|
||||
'4. После оплаты предоставьте необходимые данные, согласно вашему плану питания';
|
||||
} else {
|
||||
// Текст для пользователя
|
||||
return 'Укажите вашу цель в работе с консультантом\n'
|
||||
'Согласуйте удобную дату старта плана, после чего вам будет направлена ссылка на оплату.';
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
Widget _buildLinkableText(String text) {
|
||||
final urlRegex = RegExp(
|
||||
r'(?:(?:https?|ftp):\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
final textStyle = TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16);
|
||||
final linkStyle = textStyle.copyWith(
|
||||
color: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
|
||||
final matches = urlRegex.allMatches(text);
|
||||
if (matches.isEmpty) {
|
||||
return Text(text, style: textStyle);
|
||||
}
|
||||
|
||||
final spans = <TextSpan>[];
|
||||
int lastEnd = 0;
|
||||
|
||||
for (final match in matches) {
|
||||
// Добавляем текст до ссылки
|
||||
if (match.start > lastEnd) {
|
||||
spans.add(TextSpan(
|
||||
text: text.substring(lastEnd, match.start),
|
||||
style: textStyle,
|
||||
));
|
||||
}
|
||||
|
||||
// Добавляем кликабельную ссылку
|
||||
final url = match.group(0)!;
|
||||
final fullUrl = url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://')
|
||||
? url
|
||||
: 'https://$url';
|
||||
|
||||
spans.add(TextSpan(
|
||||
text: url,
|
||||
style: linkStyle,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
try {
|
||||
final uri = Uri.parse(fullUrl);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки открытия ссылок
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
lastEnd = match.end;
|
||||
}
|
||||
|
||||
// Добавляем оставшийся текст после последней ссылки
|
||||
if (lastEnd < text.length) {
|
||||
spans.add(TextSpan(
|
||||
text: text.substring(lastEnd),
|
||||
style: textStyle,
|
||||
));
|
||||
}
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(children: spans),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class PlanOrderMessageWidget extends StatelessWidget {
|
||||
const PlanOrderMessageWidget({
|
||||
super.key,
|
||||
required this.planName,
|
||||
required this.planSlug,
|
||||
required this.price,
|
||||
required this.sendTime,
|
||||
required this.isCurrentUser,
|
||||
});
|
||||
|
||||
final String planName;
|
||||
final String planSlug;
|
||||
final dynamic price;
|
||||
final DateTime sendTime;
|
||||
final bool isCurrentUser;
|
||||
|
||||
String _formatPrice(dynamic price) {
|
||||
if (price == null) return '0₽';
|
||||
if (price is num) {
|
||||
return '${price.toStringAsFixed(0)}₽';
|
||||
}
|
||||
if (price is String) {
|
||||
final numPrice = num.tryParse(price);
|
||||
if (numPrice != null) {
|
||||
return '${numPrice.toStringAsFixed(0)}₽';
|
||||
}
|
||||
return price.endsWith('₽') ? price : '$price₽';
|
||||
}
|
||||
return '0₽';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final formattedPrice = _formatPrice(price);
|
||||
final textStyle = TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16);
|
||||
final linkStyle = textStyle.copyWith(
|
||||
color: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
width: size.width,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(isCurrentUser ? 64 : 16, 10, isCurrentUser ? 16 : 64, 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
if (isCurrentUser)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4, bottom: 4),
|
||||
child: Text(
|
||||
sendTime.format('HH:mm', 'ru'),
|
||||
style: const TextStyle(color: Color(0xFF7B8794), height: 1, fontSize: 10),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: size.width * 0.7),
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrentUser ? appTheme.gray50 : appTheme.gray60004,
|
||||
borderRadius: BorderRadius.horizontal(
|
||||
left: isCurrentUser ? const Radius.circular(32) : const Radius.circular(16),
|
||||
right: isCurrentUser ? const Radius.circular(16) : const Radius.circular(24),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
left: isCurrentUser ? 16 : 8,
|
||||
right: isCurrentUser ? 8 : 16,
|
||||
),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: textStyle,
|
||||
children: [
|
||||
const TextSpan(text: 'Я хочу заказать '),
|
||||
TextSpan(
|
||||
text: planName,
|
||||
style: linkStyle,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'aboutPlanScreen',
|
||||
extra: {
|
||||
'slug': planSlug,
|
||||
'index': null,
|
||||
'navigateToComments': false,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
TextSpan(text: ' по цене $formattedPrice'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isCurrentUser)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 4),
|
||||
child: Text(
|
||||
sendTime.format('HH:mm', 'ru'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF7B8794),
|
||||
height: 1,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,5 +139,13 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -87,6 +87,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -62,6 +62,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -112,6 +112,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -66,6 +66,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -118,6 +118,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -137,6 +137,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -98,6 +98,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,6 +166,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -59,6 +59,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -98,6 +98,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -78,6 +78,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -68,5 +68,13 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -82,6 +82,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -60,6 +60,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -92,6 +92,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -110,6 +110,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -140,6 +140,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -60,6 +60,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1030,6 +1030,14 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -65,3 +65,11 @@ void main() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user