изменена логика показа папок, добавлена кнопка создания папок, отображание от кого переслано сообщение, предпросмотр сообщений по зажатии на аватарке
This commit is contained in:
@@ -5,5 +5,11 @@
|
||||
## How to build?
|
||||
### This is app built on flutter, use flutter guide
|
||||
## How to countibute?
|
||||
### Join the dev team
|
||||
### Create a fork, do everything
|
||||
### And create pull requeste
|
||||
### Make sure your commits looks like:
|
||||
<code>fix: something went worng when user...</code>
|
||||
<code>add: search by id</code>
|
||||
<code>edit: refactored something</code>
|
||||
<code>Other actions should marked as "other:" and discribes what you did</code>
|
||||
|
||||
|
||||
BIN
assets/images/spermum.png
Normal file
BIN
assets/images/spermum.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/images/spermum_but_dark.webp
Normal file
BIN
assets/images/spermum_but_dark.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,6 @@ import 'package:video_player/video_player.dart';
|
||||
|
||||
bool _debugShowExactDate = false;
|
||||
|
||||
|
||||
void toggleDebugExactDate() {
|
||||
_debugShowExactDate = !_debugShowExactDate;
|
||||
print('Debug режим точной даты: $_debugShowExactDate');
|
||||
@@ -88,13 +87,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
ItemPositionsListener.create();
|
||||
final ValueNotifier<bool> _showScrollToBottomNotifier = ValueNotifier(false);
|
||||
|
||||
|
||||
late Contact _currentContact;
|
||||
|
||||
|
||||
Message? _replyingToMessage;
|
||||
|
||||
final Map<int, Contact> _contactDetailsCache = {};
|
||||
final Set<int> _loadingContactIds = {};
|
||||
|
||||
final Map<String, String> _lastReadMessageIdByParticipant = {};
|
||||
|
||||
@@ -143,6 +141,30 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadContactIfNeeded(int contactId) async {
|
||||
if (_contactDetailsCache.containsKey(contactId) ||
|
||||
_loadingContactIds.contains(contactId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_loadingContactIds.add(contactId);
|
||||
|
||||
try {
|
||||
final contacts = await ApiService.instance.fetchContactsByIds([
|
||||
contactId,
|
||||
]);
|
||||
if (contacts.isNotEmpty && mounted) {
|
||||
final contact = contacts.first;
|
||||
_contactDetailsCache[contact.id] = contact;
|
||||
setState(() {});
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка загрузки контакта $contactId: $e');
|
||||
} finally {
|
||||
_loadingContactIds.remove(contactId);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -375,7 +397,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
if (!mounted) return;
|
||||
print("✅ Получено ${allMessages.length} сообщений с сервера.");
|
||||
|
||||
|
||||
final Set<int> senderIds = {};
|
||||
for (final message in allMessages) {
|
||||
senderIds.add(message.senderId);
|
||||
@@ -389,7 +410,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
senderIds.remove(0); // Удаляем системный ID, если он есть
|
||||
|
||||
|
||||
final idsToFetch = senderIds
|
||||
.where((id) => !_contactDetailsCache.containsKey(id))
|
||||
.toList();
|
||||
@@ -475,8 +495,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_buildChatItems();
|
||||
_isLoadingMore = false;
|
||||
setState(() {});
|
||||
|
||||
|
||||
}
|
||||
|
||||
bool _isSameDay(DateTime date1, DateTime date2) {
|
||||
@@ -532,12 +550,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
print('DEBUG GROUPING: isGrouped=$isGrouped');
|
||||
}
|
||||
|
||||
|
||||
final isFirstInGroup =
|
||||
previousMessage == null ||
|
||||
!_isMessageGrouped(currentMessage, previousMessage);
|
||||
|
||||
|
||||
final isLastInGroup =
|
||||
i == source.length - 1 ||
|
||||
!_isMessageGrouped(source[i + 1], currentMessage);
|
||||
@@ -1155,7 +1171,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (mounted) {
|
||||
|
||||
Navigator.of(context).pop();
|
||||
|
||||
widget.onChatUpdated?.call();
|
||||
@@ -1213,11 +1228,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // Закрываем диалог подтверждения
|
||||
try {
|
||||
|
||||
ApiService.instance.leaveGroup(widget.chatId);
|
||||
|
||||
if (mounted) {
|
||||
|
||||
Navigator.of(context).pop();
|
||||
|
||||
widget.onChatUpdated?.call();
|
||||
@@ -1388,30 +1401,43 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
if (isMe) {
|
||||
final messageId = item.message.id;
|
||||
if (messageId.startsWith('local_')) {
|
||||
|
||||
|
||||
readStatus = MessageReadStatus.sending;
|
||||
} else {
|
||||
|
||||
|
||||
readStatus = MessageReadStatus.sent;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
String? forwardedFrom;
|
||||
String? forwardedFromAvatarUrl;
|
||||
if (message.isForwarded) {
|
||||
final link = message.link;
|
||||
if (link is Map<String, dynamic>) {
|
||||
final chatName = link['chatName'] as String?;
|
||||
final chatIconUrl = link['chatIconUrl'] as String?;
|
||||
|
||||
if (chatName != null) {
|
||||
forwardedFrom = chatName;
|
||||
forwardedFromAvatarUrl = chatIconUrl;
|
||||
} else {
|
||||
final forwardedMessage =
|
||||
link['message'] as Map<String, dynamic>?;
|
||||
final originalSenderId =
|
||||
message.link?['message']?['sender'] as int?;
|
||||
if (originalSenderId != null) {}
|
||||
forwardedMessage?['sender'] as int?;
|
||||
if (originalSenderId != null) {
|
||||
final originalSenderContact =
|
||||
_contactDetailsCache[originalSenderId];
|
||||
if (originalSenderContact == null) {
|
||||
_loadContactIfNeeded(originalSenderId);
|
||||
forwardedFrom = 'Участник $originalSenderId';
|
||||
forwardedFromAvatarUrl = null;
|
||||
} else {
|
||||
forwardedFrom = originalSenderContact.name;
|
||||
forwardedFromAvatarUrl =
|
||||
originalSenderContact.photoBaseUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String? senderName;
|
||||
if (widget.isGroupChat && !isMe) {
|
||||
@@ -1500,6 +1526,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
isGroupChat: widget.isGroupChat,
|
||||
isChannel: widget.isChannel,
|
||||
senderName: senderName,
|
||||
forwardedFrom: forwardedFrom,
|
||||
forwardedFromAvatarUrl: forwardedFromAvatarUrl,
|
||||
contactDetailsCache: _contactDetailsCache,
|
||||
onReplyTap: _scrollToMessage,
|
||||
useAutoReplyColor: context
|
||||
@@ -1659,7 +1687,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
leading: widget.isDesktopMode
|
||||
? null // В десктопном режиме нет кнопки "Назад"
|
||||
: IconButton(
|
||||
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
@@ -1908,7 +1935,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
)
|
||||
else
|
||||
|
||||
_ContactPresenceSubtitle(
|
||||
chatId: widget.chatId,
|
||||
userId: widget.contact.id,
|
||||
@@ -1998,7 +2024,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
],
|
||||
);
|
||||
case ChatWallpaperType.video:
|
||||
|
||||
if (Platform.isWindows) {
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
@@ -2195,16 +2220,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
|
||||
child: Focus(
|
||||
focusNode:
|
||||
_textFocusNode, // 2. focusNode теперь здесь
|
||||
onKeyEvent: (node, event) {
|
||||
|
||||
if (event is KeyDownEvent) {
|
||||
if (event.logicalKey ==
|
||||
LogicalKeyboardKey.enter) {
|
||||
|
||||
final bool isShiftPressed =
|
||||
HardwareKeyboard.instance.logicalKeysPressed
|
||||
.contains(
|
||||
@@ -2216,7 +2238,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
|
||||
if (!isShiftPressed) {
|
||||
|
||||
_sendMessage();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
@@ -3292,7 +3313,6 @@ class GroupProfileDraggableDialog extends StatelessWidget {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
width: 40,
|
||||
@@ -3303,7 +3323,6 @@ class GroupProfileDraggableDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Hero(
|
||||
@@ -3325,7 +3344,6 @@ class GroupProfileDraggableDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
@@ -3343,8 +3361,6 @@ class GroupProfileDraggableDialog extends StatelessWidget {
|
||||
IconButton(
|
||||
icon: Icon(Icons.settings, color: colors.primary),
|
||||
onPressed: () async {
|
||||
|
||||
|
||||
final myId = 0; // This should be passed or retrieved
|
||||
|
||||
Navigator.of(context).pop();
|
||||
@@ -3367,13 +3383,11 @@ class GroupProfileDraggableDialog extends StatelessWidget {
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
children: [
|
||||
|
||||
if (contact.description != null &&
|
||||
contact.description!.isNotEmpty)
|
||||
Text(
|
||||
@@ -3545,7 +3559,6 @@ class ContactProfileDialog extends StatelessWidget {
|
||||
},
|
||||
)
|
||||
else
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (!isChannel)
|
||||
@@ -3900,7 +3913,6 @@ class _RemoveMemberDialogState extends State<_RemoveMemberDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _PromoteAdminDialog extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> members;
|
||||
final Function(int) onPromoteToAdmin;
|
||||
@@ -3964,7 +3976,6 @@ class _ControlMessageChip extends StatelessWidget {
|
||||
});
|
||||
|
||||
String _formatControlMessage() {
|
||||
|
||||
final controlAttach = message.attaches.firstWhere(
|
||||
(a) => a['_type'] == 'CONTROL',
|
||||
);
|
||||
@@ -3974,7 +3985,6 @@ class _ControlMessageChip extends StatelessWidget {
|
||||
final isMe = message.senderId == myId;
|
||||
final senderDisplayName = isMe ? 'Вы' : senderName;
|
||||
|
||||
|
||||
String _formatUserList(List<int> userIds) {
|
||||
if (userIds.isEmpty) {
|
||||
return '';
|
||||
@@ -4120,7 +4130,6 @@ class _ControlMessageChip extends StatelessWidget {
|
||||
return '$senderName присоединился(ась) к группе';
|
||||
|
||||
default:
|
||||
|
||||
final eventTypeStr = eventType?.toString() ?? 'неизвестное';
|
||||
return 'Событие: $eventTypeStr';
|
||||
}
|
||||
@@ -4153,15 +4162,12 @@ class _ControlMessageChip extends StatelessWidget {
|
||||
}
|
||||
|
||||
void openUserProfileById(BuildContext context, int userId) {
|
||||
|
||||
final contact = ApiService.instance.getCachedContact(userId);
|
||||
|
||||
if (contact != null) {
|
||||
|
||||
final isGroup = contact.id < 0; // Groups have negative IDs
|
||||
|
||||
if (isGroup) {
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -4169,7 +4175,6 @@ void openUserProfileById(BuildContext context, int userId) {
|
||||
builder: (context) => GroupProfileDraggableDialog(contact: contact),
|
||||
);
|
||||
} else {
|
||||
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
opaque: false,
|
||||
@@ -4185,7 +4190,6 @@ void openUserProfileById(BuildContext context, int userId) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
412
lib/widgets/message_preview_dialog.dart
Normal file
412
lib/widgets/message_preview_dialog.dart
Normal file
@@ -0,0 +1,412 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:gwid/models/chat.dart';
|
||||
import 'package:gwid/models/message.dart';
|
||||
import 'package:gwid/models/contact.dart';
|
||||
import 'package:gwid/models/profile.dart';
|
||||
import 'package:gwid/api_service.dart';
|
||||
import 'package:gwid/widgets/chat_message_bubble.dart';
|
||||
import 'package:gwid/chat_screen.dart';
|
||||
|
||||
class MessagePreviewDialog {
|
||||
static String _formatTimestamp(int timestamp) {
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
final now = DateTime.now();
|
||||
if (now.day == dt.day && now.month == dt.month && now.year == dt.year) {
|
||||
return DateFormat('HH:mm', 'ru').format(dt);
|
||||
} else {
|
||||
final yesterday = now.subtract(const Duration(days: 1));
|
||||
if (dt.day == yesterday.day &&
|
||||
dt.month == yesterday.month &&
|
||||
dt.year == yesterday.year) {
|
||||
return 'Вчера';
|
||||
} else {
|
||||
return DateFormat('d MMM', 'ru').format(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool _isSavedMessages(Chat chat) {
|
||||
return chat.id == 0;
|
||||
}
|
||||
|
||||
static bool _isGroupChat(Chat chat) {
|
||||
return chat.type == 'CHAT' || chat.participantIds.length > 2;
|
||||
}
|
||||
|
||||
static bool _isSameDay(DateTime date1, DateTime date2) {
|
||||
return date1.year == date2.year &&
|
||||
date1.month == date2.month &&
|
||||
date1.day == date2.day;
|
||||
}
|
||||
|
||||
static bool _isMessageGrouped(
|
||||
Message currentMessage,
|
||||
Message? previousMessage,
|
||||
) {
|
||||
if (previousMessage == null) return false;
|
||||
|
||||
final currentTime = DateTime.fromMillisecondsSinceEpoch(
|
||||
currentMessage.time,
|
||||
);
|
||||
final previousTime = DateTime.fromMillisecondsSinceEpoch(
|
||||
previousMessage.time,
|
||||
);
|
||||
|
||||
final timeDifference = currentTime.difference(previousTime).inMinutes;
|
||||
|
||||
return currentMessage.senderId == previousMessage.senderId &&
|
||||
timeDifference <= 5;
|
||||
}
|
||||
|
||||
static String _formatDateSeparator(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
if (_isSameDay(date, now)) {
|
||||
return 'Сегодня';
|
||||
} else {
|
||||
final yesterday = now.subtract(const Duration(days: 1));
|
||||
if (_isSameDay(date, yesterday)) {
|
||||
return 'Вчера';
|
||||
} else {
|
||||
return DateFormat('d MMM yyyy', 'ru').format(date);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String _getChatTitle(Chat chat, Map<int, Contact> contacts) {
|
||||
final bool isSavedMessages = _isSavedMessages(chat);
|
||||
final bool isGroupChat = _isGroupChat(chat);
|
||||
final bool isChannel = chat.type == 'CHANNEL';
|
||||
|
||||
if (isSavedMessages) {
|
||||
return "Избранное";
|
||||
} else if (isChannel) {
|
||||
return chat.title ?? "Канал";
|
||||
} else if (isGroupChat) {
|
||||
return chat.title?.isNotEmpty == true ? chat.title! : "Группа";
|
||||
} else {
|
||||
final myId = chat.ownerId;
|
||||
final otherParticipantId = chat.participantIds.firstWhere(
|
||||
(id) => id != myId,
|
||||
orElse: () => myId,
|
||||
);
|
||||
final contact = contacts[otherParticipantId];
|
||||
return contact?.name ?? "Неизвестный чат";
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context,
|
||||
Chat chat,
|
||||
Map<int, Contact> contacts,
|
||||
Profile? myProfile,
|
||||
VoidCallback? onClose,
|
||||
Widget Function(BuildContext)? menuBuilder,
|
||||
) async {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
List<Message> messages = [];
|
||||
bool isLoading = true;
|
||||
|
||||
try {
|
||||
messages = await ApiService.instance.getMessageHistory(
|
||||
chat.id,
|
||||
force: false,
|
||||
);
|
||||
if (messages.length > 10) {
|
||||
messages = messages.sublist(messages.length - 10);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка загрузки сообщений для предпросмотра: $e');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
final Set<int> senderIds = messages.map((m) => m.senderId).toSet();
|
||||
senderIds.remove(0);
|
||||
|
||||
final Set<int> forwardedSenderIds = {};
|
||||
for (final message in messages) {
|
||||
if (message.isForwarded && message.link != null) {
|
||||
final link = message.link;
|
||||
if (link is Map<String, dynamic>) {
|
||||
final chatName = link['chatName'] as String?;
|
||||
if (chatName == null) {
|
||||
final forwardedMessage = link['message'] as Map<String, dynamic>?;
|
||||
final originalSenderId = forwardedMessage?['sender'] as int?;
|
||||
if (originalSenderId != null) {
|
||||
forwardedSenderIds.add(originalSenderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final allIdsToFetch = {
|
||||
...senderIds,
|
||||
...forwardedSenderIds,
|
||||
}.where((id) => !contacts.containsKey(id)).toList();
|
||||
|
||||
if (allIdsToFetch.isNotEmpty) {
|
||||
try {
|
||||
final newContacts = await ApiService.instance.fetchContactsByIds(
|
||||
allIdsToFetch,
|
||||
);
|
||||
for (final contact in newContacts) {
|
||||
contacts[contact.id] = contact;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка загрузки контактов для предпросмотра: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final chatTitle = _getChatTitle(chat, contacts);
|
||||
final bool isGroupChat = _isGroupChat(chat);
|
||||
final bool isChannel = chat.type == 'CHANNEL';
|
||||
final myId = myProfile?.id ?? chat.ownerId;
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
List<ChatItem> chatItems = [];
|
||||
for (int i = 0; i < messages.length; i++) {
|
||||
final currentMessage = messages[i];
|
||||
final previousMessage = (i > 0) ? messages[i - 1] : null;
|
||||
|
||||
final currentDate = DateTime.fromMillisecondsSinceEpoch(
|
||||
currentMessage.time,
|
||||
).toLocal();
|
||||
final previousDate = previousMessage != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(previousMessage.time).toLocal()
|
||||
: null;
|
||||
|
||||
if (previousMessage == null || !_isSameDay(currentDate, previousDate!)) {
|
||||
chatItems.add(DateSeparatorItem(currentDate));
|
||||
}
|
||||
|
||||
final isGrouped = _isMessageGrouped(currentMessage, previousMessage);
|
||||
final isFirstInGroup = previousMessage == null || !isGrouped;
|
||||
final isLastInGroup =
|
||||
i == messages.length - 1 ||
|
||||
!_isMessageGrouped(messages[i + 1], currentMessage);
|
||||
|
||||
chatItems.add(
|
||||
MessageItem(
|
||||
currentMessage,
|
||||
isFirstInGroup: isFirstInGroup,
|
||||
isLastInGroup: isLastInGroup,
|
||||
isGrouped: isGrouped,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: 0.75,
|
||||
minChildSize: 0.5,
|
||||
maxChildSize: 0.9,
|
||||
builder: (context, scrollController) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 8, bottom: 8),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.onSurfaceVariant.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: colors.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
chatTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
color: colors.onSurfaceVariant,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onClose?.call();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: messages.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'Нет сообщений',
|
||||
style: TextStyle(
|
||||
color: colors.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
controller: scrollController,
|
||||
reverse: true,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
),
|
||||
itemCount: chatItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final mappedIndex = chatItems.length - 1 - index;
|
||||
final item = chatItems[mappedIndex];
|
||||
|
||||
if (item is DateSeparatorItem) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_formatDateSeparator(item.date),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colors.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (item is MessageItem) {
|
||||
final message = item.message;
|
||||
final isMe = message.senderId == myId;
|
||||
final senderContact =
|
||||
contacts[message.senderId];
|
||||
final senderName = isMe
|
||||
? 'Вы'
|
||||
: (senderContact?.name ?? 'Неизвестный');
|
||||
|
||||
String? forwardedFrom;
|
||||
String? forwardedFromAvatarUrl;
|
||||
if (message.isForwarded) {
|
||||
final link = message.link;
|
||||
if (link is Map<String, dynamic>) {
|
||||
final chatName =
|
||||
link['chatName'] as String?;
|
||||
final chatIconUrl =
|
||||
link['chatIconUrl'] as String?;
|
||||
|
||||
if (chatName != null) {
|
||||
forwardedFrom = chatName;
|
||||
forwardedFromAvatarUrl = chatIconUrl;
|
||||
} else {
|
||||
final forwardedMessage =
|
||||
link['message']
|
||||
as Map<String, dynamic>?;
|
||||
final originalSenderId =
|
||||
forwardedMessage?['sender'] as int?;
|
||||
if (originalSenderId != null) {
|
||||
final originalSenderContact =
|
||||
contacts[originalSenderId];
|
||||
forwardedFrom =
|
||||
originalSenderContact?.name ??
|
||||
'Участник $originalSenderId';
|
||||
forwardedFromAvatarUrl =
|
||||
originalSenderContact?.photoBaseUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ChatMessageBubble(
|
||||
message: message,
|
||||
isMe: isMe,
|
||||
readStatus: null,
|
||||
deferImageLoading: true,
|
||||
myUserId: myId,
|
||||
chatId: chat.id,
|
||||
onReply: null,
|
||||
onEdit: null,
|
||||
canEditMessage: null,
|
||||
onDeleteForMe: null,
|
||||
onDeleteForAll: null,
|
||||
onReaction: null,
|
||||
onRemoveReaction: null,
|
||||
isGroupChat: isGroupChat,
|
||||
isChannel: isChannel,
|
||||
senderName: senderName,
|
||||
forwardedFrom: forwardedFrom,
|
||||
forwardedFromAvatarUrl:
|
||||
forwardedFromAvatarUrl,
|
||||
contactDetailsCache: contacts,
|
||||
onReplyTap: null,
|
||||
useAutoReplyColor: false,
|
||||
customReplyColor: null,
|
||||
isFirstInGroup: item.isFirstInGroup,
|
||||
isLastInGroup: item.isLastInGroup,
|
||||
isGrouped: item.isGrouped,
|
||||
avatarVerticalOffset: -8.0,
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (menuBuilder != null) ...[
|
||||
Divider(height: 1, color: colors.outline.withOpacity(0.2)),
|
||||
menuBuilder(context),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
|
||||
set(BINARY_NAME "Komet")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.gwid.com.gwid")
|
||||
set(APPLICATION_ID "com.gwid.app.gwid")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -801,10 +801,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1310,10 +1310,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
version: "0.7.6"
|
||||
timezone:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user