feat: users in chats list, avatars and nicknames, users count in chats
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:komet/backend/modules/messages.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
@@ -15,7 +16,7 @@ import '../../../backend/modules/account.dart';
|
||||
import '../../../backend/modules/chats.dart';
|
||||
import '../../../backend/modules/folders.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../main.dart' show accountModule, api;
|
||||
import '../../../main.dart' show accountModule, api, messagesModule;
|
||||
|
||||
class _StoriesScrollPhysics extends BouncingScrollPhysics {
|
||||
final bool Function() blockPositive;
|
||||
@@ -60,40 +61,49 @@ class ChatListScreen extends StatefulWidget {
|
||||
class _ChatListScreenState extends State<ChatListScreen>
|
||||
with TickerProviderStateMixin {
|
||||
String? _selectedFolderId;
|
||||
|
||||
List<ChatFolder> _folders = [];
|
||||
|
||||
int _currentNavIndex = 0;
|
||||
bool _navDragging = false;
|
||||
double _navDragDx = 0;
|
||||
double _navDragBaseLeft = 0;
|
||||
late AnimationController _navPageAnimController;
|
||||
|
||||
double _navPageAnimStart = 0;
|
||||
double _navPageAnimEnd = 0;
|
||||
bool _isFabOpen = false;
|
||||
bool _showCacheWarning = false;
|
||||
late AnimationController _fabController;
|
||||
final Set<String> _selectedChats = {};
|
||||
late PageController _folderPageController;
|
||||
final List<ScrollController> _folderChatScrollControllers = [];
|
||||
final List<VoidCallback> _folderChatScrollListenerFns = [];
|
||||
double _pullRatio = 0.0;
|
||||
static const double _kStoriesPullTriggerPx = 16.0;
|
||||
late AnimationController _storiesRevealController;
|
||||
double _navDragDx = 0;
|
||||
double _navDragBaseLeft = 0;
|
||||
double _revealAnimBegin = 0.0;
|
||||
double _closeAnimBegin = 0.0;
|
||||
double _pullRatio = 0.0;
|
||||
static const double _kStoriesPullTriggerPx = 16.0;
|
||||
|
||||
bool _navDragging = false;
|
||||
bool _isFabOpen = false;
|
||||
bool _showCacheWarning = false;
|
||||
bool _storiesAnimClosing = false;
|
||||
bool _storiesDockedOpen = false;
|
||||
bool _storiesOverscrollRevealArmed = true;
|
||||
bool _shouldCollapseSearch = false;
|
||||
bool get _isSelectionMode => _selectedChats.isNotEmpty;
|
||||
bool? _foldersListKnown;
|
||||
|
||||
late AnimationController _navPageAnimController;
|
||||
late AnimationController _fabController;
|
||||
late PageController _folderPageController;
|
||||
late AnimationController _storiesRevealController;
|
||||
|
||||
final List<ScrollController> _folderChatScrollControllers = [];
|
||||
final List<VoidCallback> _folderChatScrollListenerFns = [];
|
||||
final Set<String> _selectedChats = {};
|
||||
|
||||
DateTime _storiesRevealLayoutSettleUntil =
|
||||
DateTime.fromMillisecondsSinceEpoch(0);
|
||||
ProfileData? _profile;
|
||||
|
||||
List<CachedChat> _chats = [];
|
||||
|
||||
SessionState _sessionState = SessionState.disconnected;
|
||||
|
||||
StreamSubscription? _stateSub;
|
||||
StreamSubscription<LoginStatus>? _loginSub;
|
||||
bool? _foldersListKnown;
|
||||
bool _shouldCollapseSearch = false;
|
||||
|
||||
bool get _isSelectionMode => _selectedChats.isNotEmpty;
|
||||
|
||||
void _toggleSelection(String chatId) {
|
||||
setState(() {
|
||||
@@ -1015,18 +1025,60 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
return _buildChatShimmer();
|
||||
}
|
||||
final chat = chats[index];
|
||||
return _buildChatItem(
|
||||
chat.id.toString(),
|
||||
chat.title ?? 'Чат',
|
||||
chat.lastMsgText ?? '',
|
||||
_formatTime(chat.lastMsgTime),
|
||||
(chat.iconUrl != null && chat.iconUrl!.isNotEmpty)
|
||||
? chat.iconUrl!
|
||||
: '',
|
||||
isOnline: chat.isOnline,
|
||||
unreadCount: chat.unreadCount,
|
||||
isMuted: chat.dontDisturbUntil > 0,
|
||||
);
|
||||
|
||||
if (chat.type.isNotEmpty && chat.type == "DIALOG" && chat.id != 0) {
|
||||
final secondId = chat.participants.entries.where((entry) => entry.key != _profile?.id).first.key;
|
||||
// TODO: Нормальное кеширование контактов
|
||||
final ss = messagesModule.searchContactById(secondId);
|
||||
final name = ContactCache.get(secondId);
|
||||
final avatar = ContactCache.getAvatar(secondId);
|
||||
|
||||
return _buildChatItem(
|
||||
chat.id.toString(),
|
||||
name ?? "Пользователь",
|
||||
chat.lastMsgText?.replaceAll('\n', ' ') ?? '',
|
||||
_formatTime(chat.lastMsgTime),
|
||||
avatar ?? "",
|
||||
isOnline: chat.isOnline,
|
||||
unreadCount: chat.unreadCount,
|
||||
isMuted: chat.dontDisturbUntil > 0,
|
||||
);
|
||||
} else {
|
||||
if (chat.lastMsgSenderId != null ) {
|
||||
final ss = messagesModule.searchContactById(chat.lastMsgSenderId!);
|
||||
}
|
||||
|
||||
final name = chat.lastMsgSenderId != null
|
||||
? ContactCache.get(chat.lastMsgSenderId!)
|
||||
: null;
|
||||
|
||||
final avatar = chat.lastMsgSenderId != null
|
||||
? ContactCache.getAvatar(chat.lastMsgSenderId!)
|
||||
: null;
|
||||
|
||||
String fullMsg = "";
|
||||
|
||||
if (name?.isNotEmpty == true && chat.id != 0) {
|
||||
fullMsg += "$name: ";
|
||||
}
|
||||
|
||||
if (chat.lastMsgText?.isNotEmpty == true) {
|
||||
fullMsg += chat.lastMsgText ?? "";
|
||||
}
|
||||
|
||||
return _buildChatItem(
|
||||
chat.id.toString(),
|
||||
chat.id == 0 ? "Избранное" : chat.title ?? "Чат",
|
||||
fullMsg,
|
||||
_formatTime(chat.lastMsgTime),
|
||||
(chat.iconUrl != null && chat.iconUrl!.isNotEmpty)
|
||||
? chat.iconUrl!
|
||||
: '',
|
||||
isOnline: chat.isOnline,
|
||||
unreadCount: chat.unreadCount,
|
||||
isMuted: chat.dontDisturbUntil > 0,
|
||||
);
|
||||
}
|
||||
}, childCount: _isInitialLoading ? 10 : chats.length),
|
||||
),
|
||||
SliverPadding(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:komet/backend/modules/chats.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../backend/api.dart';
|
||||
@@ -35,7 +36,8 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
late AnimationController _shimmerController;
|
||||
List<CachedMessage> _messages = [];
|
||||
int _myId = 0;
|
||||
|
||||
CachedChat? chat;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -51,6 +53,11 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
Future<void> _loadHistory() async {
|
||||
final activeProfile = await AppDatabase.loadActiveProfile();
|
||||
_myId = activeProfile?.id ?? 0;
|
||||
ChatsModule.getChat(_myId, widget.chatId).then((value) {
|
||||
chat = value[0];
|
||||
}).catchError((error) {
|
||||
|
||||
});
|
||||
|
||||
final cachedRows = await AppDatabase.loadMessages(
|
||||
_myId,
|
||||
@@ -240,6 +247,10 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
// TODO: Локализация
|
||||
// TODO: Cклонения
|
||||
String? status = chat?.type == "CHAT" ? "${chat?.participants.length.toString()} участников" : "last seen recently";
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
@@ -284,7 +295,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'last seen recently',
|
||||
status ?? "",
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
@@ -353,6 +364,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
myId: _myId,
|
||||
prevMessage: prevMessage,
|
||||
nextMessage: nextMessage,
|
||||
chatType: chat!.type,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:komet/backend/modules/chats.dart';
|
||||
import 'package:komet/backend/modules/contacts.dart';
|
||||
import 'package:komet/main.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../backend/modules/messages.dart';
|
||||
import '../../models/attachment.dart';
|
||||
@@ -37,6 +40,7 @@ class MessageBubble extends StatelessWidget {
|
||||
final int myId;
|
||||
final CachedMessage? prevMessage;
|
||||
final CachedMessage? nextMessage;
|
||||
final String chatType;
|
||||
|
||||
const MessageBubble({
|
||||
super.key,
|
||||
@@ -45,6 +49,7 @@ class MessageBubble extends StatelessWidget {
|
||||
required this.myId,
|
||||
this.prevMessage,
|
||||
this.nextMessage,
|
||||
required this.chatType
|
||||
});
|
||||
|
||||
bool get isGroupedWithNext {
|
||||
@@ -277,6 +282,11 @@ class MessageBubble extends StatelessWidget {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final isDark = cs.brightness == Brightness.dark;
|
||||
|
||||
// TODO: Нормальное кеширование контактов
|
||||
final ss = messagesModule.searchContactById(message.senderId);
|
||||
String? senderAvatar = ContactCache.getAvatar(message.senderId);
|
||||
String? displaySender = ContactCache.get(message.senderId);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: isMe ? 60 : 12,
|
||||
@@ -285,22 +295,52 @@ class MessageBubble extends StatelessWidget {
|
||||
bottom: bottomMargin,
|
||||
),
|
||||
child: Align(
|
||||
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.75,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isMe
|
||||
? (isDark ? const Color(0xFF2C5F8D) : const Color(0xFF007AFF))
|
||||
: (isDark
|
||||
? cs.surfaceContainerHighest
|
||||
: const Color(0xFFE9E9EB)),
|
||||
borderRadius: _borderRadius,
|
||||
),
|
||||
padding: padding,
|
||||
child: _buildContent(context),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
if (senderAvatar != null && senderAvatar.isNotEmpty && !isMe && chatType != "DIALOG"
|
||||
&& nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId)
|
||||
CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundImage: NetworkImage(senderAvatar),
|
||||
backgroundColor: cs.primaryContainer,
|
||||
)
|
||||
else if (displaySender != null && !isMe && chatType != "DIALOG"
|
||||
&& nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId)
|
||||
CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: cs.primaryContainer,
|
||||
child: Text(
|
||||
displaySender!.isNotEmpty
|
||||
? displaySender[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer),
|
||||
),
|
||||
)
|
||||
// Заглушка для паддинга
|
||||
else
|
||||
CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: Color(0x00000000)
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.75,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isMe
|
||||
? (isDark ? const Color(0xFF2C5F8D) : const Color(0xFF007AFF))
|
||||
: (isDark
|
||||
? cs.surfaceContainerHighest
|
||||
: const Color(0xFFE9E9EB)),
|
||||
borderRadius: _borderRadius,
|
||||
),
|
||||
padding: padding,
|
||||
child: _buildContent(context),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -327,30 +367,46 @@ class MessageBubble extends StatelessWidget {
|
||||
final forwarded = _getForwardedAttachment();
|
||||
final isForwarded = forwarded != null;
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
// TODO: Нормальное кеширование контактов
|
||||
final ss = messagesModule.searchContactById(message.senderId);
|
||||
String? displaySender = ContactCache.get(message.senderId);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: isForwarded
|
||||
? _buildForwardedInlineText(context, forwarded, textColor)
|
||||
: Text(
|
||||
message.text ?? '',
|
||||
style: TextStyle(color: textColor, fontSize: 16, height: 1.3),
|
||||
),
|
||||
if (message.senderId != message.accountId && prevMessage?.senderId != message.senderId)
|
||||
Text(
|
||||
displaySender ?? "",
|
||||
textAlign: TextAlign.left,
|
||||
// TODO: Получение цветов по хешу ника
|
||||
style: TextStyle(color: cs.onPrimaryContainer)
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text(
|
||||
_formatTime(message.time),
|
||||
style: TextStyle(
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
fontSize: 10,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: isForwarded
|
||||
? _buildForwardedInlineText(context, forwarded, textColor)
|
||||
: Text(
|
||||
message.text ?? '',
|
||||
style: TextStyle(color: textColor, fontSize: 16, height: 1.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text(
|
||||
_formatTime(message.time),
|
||||
style: TextStyle(
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isMe) ...[const SizedBox(width: 4), _buildStatusIcon(context)],
|
||||
],
|
||||
),
|
||||
if (isMe) ...[const SizedBox(width: 4), _buildStatusIcon(context)],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user