изменена логика показа папок, добавлена кнопка создания папок, отображание от кого переслано сообщение, предпросмотр сообщений по зажатии на аватарке
This commit is contained in:
@@ -5,5 +5,11 @@
|
|||||||
## How to build?
|
## How to build?
|
||||||
### This is app built on flutter, use flutter guide
|
### This is app built on flutter, use flutter guide
|
||||||
## How to countibute?
|
## 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;
|
bool _debugShowExactDate = false;
|
||||||
|
|
||||||
|
|
||||||
void toggleDebugExactDate() {
|
void toggleDebugExactDate() {
|
||||||
_debugShowExactDate = !_debugShowExactDate;
|
_debugShowExactDate = !_debugShowExactDate;
|
||||||
print('Debug режим точной даты: $_debugShowExactDate');
|
print('Debug режим точной даты: $_debugShowExactDate');
|
||||||
@@ -88,13 +87,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
ItemPositionsListener.create();
|
ItemPositionsListener.create();
|
||||||
final ValueNotifier<bool> _showScrollToBottomNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _showScrollToBottomNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
|
|
||||||
late Contact _currentContact;
|
late Contact _currentContact;
|
||||||
|
|
||||||
|
|
||||||
Message? _replyingToMessage;
|
Message? _replyingToMessage;
|
||||||
|
|
||||||
final Map<int, Contact> _contactDetailsCache = {};
|
final Map<int, Contact> _contactDetailsCache = {};
|
||||||
|
final Set<int> _loadingContactIds = {};
|
||||||
|
|
||||||
final Map<String, String> _lastReadMessageIdByParticipant = {};
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -375,7 +397,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
print("✅ Получено ${allMessages.length} сообщений с сервера.");
|
print("✅ Получено ${allMessages.length} сообщений с сервера.");
|
||||||
|
|
||||||
|
|
||||||
final Set<int> senderIds = {};
|
final Set<int> senderIds = {};
|
||||||
for (final message in allMessages) {
|
for (final message in allMessages) {
|
||||||
senderIds.add(message.senderId);
|
senderIds.add(message.senderId);
|
||||||
@@ -389,7 +410,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
}
|
}
|
||||||
senderIds.remove(0); // Удаляем системный ID, если он есть
|
senderIds.remove(0); // Удаляем системный ID, если он есть
|
||||||
|
|
||||||
|
|
||||||
final idsToFetch = senderIds
|
final idsToFetch = senderIds
|
||||||
.where((id) => !_contactDetailsCache.containsKey(id))
|
.where((id) => !_contactDetailsCache.containsKey(id))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -475,8 +495,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
_buildChatItems();
|
_buildChatItems();
|
||||||
_isLoadingMore = false;
|
_isLoadingMore = false;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isSameDay(DateTime date1, DateTime date2) {
|
bool _isSameDay(DateTime date1, DateTime date2) {
|
||||||
@@ -532,12 +550,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
print('DEBUG GROUPING: isGrouped=$isGrouped');
|
print('DEBUG GROUPING: isGrouped=$isGrouped');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final isFirstInGroup =
|
final isFirstInGroup =
|
||||||
previousMessage == null ||
|
previousMessage == null ||
|
||||||
!_isMessageGrouped(currentMessage, previousMessage);
|
!_isMessageGrouped(currentMessage, previousMessage);
|
||||||
|
|
||||||
|
|
||||||
final isLastInGroup =
|
final isLastInGroup =
|
||||||
i == source.length - 1 ||
|
i == source.length - 1 ||
|
||||||
!_isMessageGrouped(source[i + 1], currentMessage);
|
!_isMessageGrouped(source[i + 1], currentMessage);
|
||||||
@@ -1155,7 +1171,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
widget.onChatUpdated?.call();
|
widget.onChatUpdated?.call();
|
||||||
@@ -1213,11 +1228,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(); // Закрываем диалог подтверждения
|
Navigator.of(context).pop(); // Закрываем диалог подтверждения
|
||||||
try {
|
try {
|
||||||
|
|
||||||
ApiService.instance.leaveGroup(widget.chatId);
|
ApiService.instance.leaveGroup(widget.chatId);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
widget.onChatUpdated?.call();
|
widget.onChatUpdated?.call();
|
||||||
@@ -1388,30 +1401,43 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
if (isMe) {
|
if (isMe) {
|
||||||
final messageId = item.message.id;
|
final messageId = item.message.id;
|
||||||
if (messageId.startsWith('local_')) {
|
if (messageId.startsWith('local_')) {
|
||||||
|
|
||||||
|
|
||||||
readStatus = MessageReadStatus.sending;
|
readStatus = MessageReadStatus.sending;
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
|
||||||
readStatus = MessageReadStatus.sent;
|
readStatus = MessageReadStatus.sent;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? forwardedFrom;
|
||||||
|
String? forwardedFromAvatarUrl;
|
||||||
if (message.isForwarded) {
|
if (message.isForwarded) {
|
||||||
final originalSenderId =
|
final link = message.link;
|
||||||
message.link?['message']?['sender'] as int?;
|
if (link is Map<String, dynamic>) {
|
||||||
if (originalSenderId != null) {}
|
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 =
|
||||||
|
_contactDetailsCache[originalSenderId];
|
||||||
|
if (originalSenderContact == null) {
|
||||||
|
_loadContactIfNeeded(originalSenderId);
|
||||||
|
forwardedFrom = 'Участник $originalSenderId';
|
||||||
|
forwardedFromAvatarUrl = null;
|
||||||
|
} else {
|
||||||
|
forwardedFrom = originalSenderContact.name;
|
||||||
|
forwardedFromAvatarUrl =
|
||||||
|
originalSenderContact.photoBaseUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
String? senderName;
|
String? senderName;
|
||||||
if (widget.isGroupChat && !isMe) {
|
if (widget.isGroupChat && !isMe) {
|
||||||
@@ -1500,6 +1526,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
isGroupChat: widget.isGroupChat,
|
isGroupChat: widget.isGroupChat,
|
||||||
isChannel: widget.isChannel,
|
isChannel: widget.isChannel,
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
|
forwardedFrom: forwardedFrom,
|
||||||
|
forwardedFromAvatarUrl: forwardedFromAvatarUrl,
|
||||||
contactDetailsCache: _contactDetailsCache,
|
contactDetailsCache: _contactDetailsCache,
|
||||||
onReplyTap: _scrollToMessage,
|
onReplyTap: _scrollToMessage,
|
||||||
useAutoReplyColor: context
|
useAutoReplyColor: context
|
||||||
@@ -1659,7 +1687,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
leading: widget.isDesktopMode
|
leading: widget.isDesktopMode
|
||||||
? null // В десктопном режиме нет кнопки "Назад"
|
? null // В десктопном режиме нет кнопки "Назад"
|
||||||
: IconButton(
|
: IconButton(
|
||||||
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
@@ -1908,7 +1935,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|
||||||
_ContactPresenceSubtitle(
|
_ContactPresenceSubtitle(
|
||||||
chatId: widget.chatId,
|
chatId: widget.chatId,
|
||||||
userId: widget.contact.id,
|
userId: widget.contact.id,
|
||||||
@@ -1998,7 +2024,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
case ChatWallpaperType.video:
|
case ChatWallpaperType.video:
|
||||||
|
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
@@ -2195,16 +2220,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
||||||
child: Focus(
|
child: Focus(
|
||||||
focusNode:
|
focusNode:
|
||||||
_textFocusNode, // 2. focusNode теперь здесь
|
_textFocusNode, // 2. focusNode теперь здесь
|
||||||
onKeyEvent: (node, event) {
|
onKeyEvent: (node, event) {
|
||||||
|
|
||||||
if (event is KeyDownEvent) {
|
if (event is KeyDownEvent) {
|
||||||
if (event.logicalKey ==
|
if (event.logicalKey ==
|
||||||
LogicalKeyboardKey.enter) {
|
LogicalKeyboardKey.enter) {
|
||||||
|
|
||||||
final bool isShiftPressed =
|
final bool isShiftPressed =
|
||||||
HardwareKeyboard.instance.logicalKeysPressed
|
HardwareKeyboard.instance.logicalKeysPressed
|
||||||
.contains(
|
.contains(
|
||||||
@@ -2216,7 +2238,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isShiftPressed) {
|
if (!isShiftPressed) {
|
||||||
|
|
||||||
_sendMessage();
|
_sendMessage();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
@@ -3292,7 +3313,6 @@ class GroupProfileDraggableDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(top: 8),
|
margin: const EdgeInsets.only(top: 8),
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -3303,7 +3323,6 @@ class GroupProfileDraggableDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Hero(
|
child: Hero(
|
||||||
@@ -3325,7 +3344,6 @@ class GroupProfileDraggableDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -3343,8 +3361,6 @@ class GroupProfileDraggableDialog extends StatelessWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.settings, color: colors.primary),
|
icon: Icon(Icons.settings, color: colors.primary),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|
||||||
|
|
||||||
final myId = 0; // This should be passed or retrieved
|
final myId = 0; // This should be passed or retrieved
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@@ -3367,13 +3383,11 @@ class GroupProfileDraggableDialog extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
if (contact.description != null &&
|
if (contact.description != null &&
|
||||||
contact.description!.isNotEmpty)
|
contact.description!.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
@@ -3545,7 +3559,6 @@ class ContactProfileDialog extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
if (!isChannel)
|
if (!isChannel)
|
||||||
@@ -3900,7 +3913,6 @@ class _RemoveMemberDialogState extends State<_RemoveMemberDialog> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _PromoteAdminDialog extends StatelessWidget {
|
class _PromoteAdminDialog extends StatelessWidget {
|
||||||
final List<Map<String, dynamic>> members;
|
final List<Map<String, dynamic>> members;
|
||||||
final Function(int) onPromoteToAdmin;
|
final Function(int) onPromoteToAdmin;
|
||||||
@@ -3964,7 +3976,6 @@ class _ControlMessageChip extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
String _formatControlMessage() {
|
String _formatControlMessage() {
|
||||||
|
|
||||||
final controlAttach = message.attaches.firstWhere(
|
final controlAttach = message.attaches.firstWhere(
|
||||||
(a) => a['_type'] == 'CONTROL',
|
(a) => a['_type'] == 'CONTROL',
|
||||||
);
|
);
|
||||||
@@ -3974,7 +3985,6 @@ class _ControlMessageChip extends StatelessWidget {
|
|||||||
final isMe = message.senderId == myId;
|
final isMe = message.senderId == myId;
|
||||||
final senderDisplayName = isMe ? 'Вы' : senderName;
|
final senderDisplayName = isMe ? 'Вы' : senderName;
|
||||||
|
|
||||||
|
|
||||||
String _formatUserList(List<int> userIds) {
|
String _formatUserList(List<int> userIds) {
|
||||||
if (userIds.isEmpty) {
|
if (userIds.isEmpty) {
|
||||||
return '';
|
return '';
|
||||||
@@ -4120,7 +4130,6 @@ class _ControlMessageChip extends StatelessWidget {
|
|||||||
return '$senderName присоединился(ась) к группе';
|
return '$senderName присоединился(ась) к группе';
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
||||||
final eventTypeStr = eventType?.toString() ?? 'неизвестное';
|
final eventTypeStr = eventType?.toString() ?? 'неизвестное';
|
||||||
return 'Событие: $eventTypeStr';
|
return 'Событие: $eventTypeStr';
|
||||||
}
|
}
|
||||||
@@ -4153,15 +4162,12 @@ class _ControlMessageChip extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void openUserProfileById(BuildContext context, int userId) {
|
void openUserProfileById(BuildContext context, int userId) {
|
||||||
|
|
||||||
final contact = ApiService.instance.getCachedContact(userId);
|
final contact = ApiService.instance.getCachedContact(userId);
|
||||||
|
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
|
|
||||||
final isGroup = contact.id < 0; // Groups have negative IDs
|
final isGroup = contact.id < 0; // Groups have negative IDs
|
||||||
|
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@@ -4169,7 +4175,6 @@ void openUserProfileById(BuildContext context, int userId) {
|
|||||||
builder: (context) => GroupProfileDraggableDialog(contact: contact),
|
builder: (context) => GroupProfileDraggableDialog(contact: contact),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
PageRouteBuilder(
|
PageRouteBuilder(
|
||||||
opaque: false,
|
opaque: false,
|
||||||
@@ -4185,7 +4190,6 @@ void openUserProfileById(BuildContext context, int userId) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
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")
|
set(BINARY_NAME "Komet")
|
||||||
# The unique GTK application identifier for this application. See:
|
# The unique GTK application identifier for this application. See:
|
||||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
# 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
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.16.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1310,10 +1310,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.6"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user