добавлены анимации на экране сообщений, добавлено редактирование профиля (локально), изменена панель сообщений
добавлен баг с незагрузкой аватарок в чатах
This commit is contained in:
139
lib/widgets/contact_avatar_widget.dart
Normal file
139
lib/widgets/contact_avatar_widget.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/services/contact_local_names_service.dart';
|
||||
|
||||
class ContactAvatarWidget extends StatefulWidget {
|
||||
final int contactId;
|
||||
final String? originalAvatarUrl;
|
||||
final double radius;
|
||||
final String? fallbackText;
|
||||
final Color? backgroundColor;
|
||||
final Color? textColor;
|
||||
|
||||
const ContactAvatarWidget({
|
||||
super.key,
|
||||
required this.contactId,
|
||||
this.originalAvatarUrl,
|
||||
this.radius = 24,
|
||||
this.fallbackText,
|
||||
this.backgroundColor,
|
||||
this.textColor,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ContactAvatarWidget> createState() => _ContactAvatarWidgetState();
|
||||
}
|
||||
|
||||
class _ContactAvatarWidgetState extends State<ContactAvatarWidget> {
|
||||
String? _localAvatarPath;
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadLocalAvatar();
|
||||
|
||||
_subscription = ContactLocalNamesService().changes.listen((contactId) {
|
||||
if (contactId == widget.contactId && mounted) {
|
||||
_loadLocalAvatar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ContactAvatarWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.contactId != widget.contactId ||
|
||||
oldWidget.originalAvatarUrl != widget.originalAvatarUrl) {
|
||||
_loadLocalAvatar();
|
||||
}
|
||||
}
|
||||
|
||||
void _loadLocalAvatar() {
|
||||
final localPath = ContactLocalNamesService().getContactAvatarPath(
|
||||
widget.contactId,
|
||||
);
|
||||
if (localPath != null) {
|
||||
final file = File(localPath);
|
||||
if (file.existsSync()) {
|
||||
setState(() {
|
||||
_localAvatarPath = localPath;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_localAvatarPath = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ImageProvider? _getAvatarImage() {
|
||||
if (_localAvatarPath != null) {
|
||||
return FileImage(File(_localAvatarPath!));
|
||||
} else if (widget.originalAvatarUrl != null) {
|
||||
if (widget.originalAvatarUrl!.startsWith('file://')) {
|
||||
final path = widget.originalAvatarUrl!.replaceFirst('file://', '');
|
||||
return FileImage(File(path));
|
||||
}
|
||||
return NetworkImage(widget.originalAvatarUrl!);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final avatarImage = _getAvatarImage();
|
||||
|
||||
return CircleAvatar(
|
||||
radius: widget.radius,
|
||||
backgroundColor:
|
||||
widget.backgroundColor ?? theme.colorScheme.secondaryContainer,
|
||||
backgroundImage: avatarImage,
|
||||
child: avatarImage == null
|
||||
? Text(
|
||||
widget.fallbackText ?? '?',
|
||||
style: TextStyle(
|
||||
color:
|
||||
widget.textColor ?? theme.colorScheme.onSecondaryContainer,
|
||||
fontSize: widget.radius * 0.8,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
ImageProvider? getContactAvatarImage({
|
||||
required int contactId,
|
||||
String? originalAvatarUrl,
|
||||
}) {
|
||||
final localPath = ContactLocalNamesService().getContactAvatarPath(contactId);
|
||||
|
||||
if (localPath != null) {
|
||||
final file = File(localPath);
|
||||
if (file.existsSync()) {
|
||||
return FileImage(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (originalAvatarUrl != null) {
|
||||
if (originalAvatarUrl.startsWith('file://')) {
|
||||
final path = originalAvatarUrl.replaceFirst('file://', '');
|
||||
return FileImage(File(path));
|
||||
}
|
||||
return NetworkImage(originalAvatarUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
95
lib/widgets/contact_display_name.dart
Normal file
95
lib/widgets/contact_display_name.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/screens/edit_contact_screen.dart';
|
||||
|
||||
class ContactDisplayName extends StatefulWidget {
|
||||
final int contactId;
|
||||
final String? originalFirstName;
|
||||
final String? originalLastName;
|
||||
final String? fallbackName;
|
||||
final TextStyle? style;
|
||||
final int? maxLines;
|
||||
final TextOverflow? overflow;
|
||||
|
||||
const ContactDisplayName({
|
||||
super.key,
|
||||
required this.contactId,
|
||||
this.originalFirstName,
|
||||
this.originalLastName,
|
||||
this.fallbackName,
|
||||
this.style,
|
||||
this.maxLines,
|
||||
this.overflow,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ContactDisplayName> createState() => _ContactDisplayNameState();
|
||||
}
|
||||
|
||||
class _ContactDisplayNameState extends State<ContactDisplayName> {
|
||||
String? _localFirstName;
|
||||
String? _localLastName;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadLocalData();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ContactDisplayName oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.contactId != widget.contactId) {
|
||||
_loadLocalData();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadLocalData() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
final localData = await ContactLocalDataHelper.getContactData(
|
||||
widget.contactId,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_localFirstName = localData?['firstName'] as String?;
|
||||
_localLastName = localData?['lastName'] as String?;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String get _displayName {
|
||||
final firstName = _localFirstName ?? widget.originalFirstName ?? '';
|
||||
final lastName = _localLastName ?? widget.originalLastName ?? '';
|
||||
final fullName = '$firstName $lastName'.trim();
|
||||
|
||||
if (fullName.isNotEmpty) {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
return widget.fallbackName ?? 'ID ${widget.contactId}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return Text(
|
||||
widget.fallbackName ?? '...',
|
||||
style: widget.style,
|
||||
maxLines: widget.maxLines,
|
||||
overflow: widget.overflow,
|
||||
);
|
||||
}
|
||||
|
||||
return Text(
|
||||
_displayName,
|
||||
style: widget.style,
|
||||
maxLines: widget.maxLines,
|
||||
overflow: widget.overflow,
|
||||
);
|
||||
}
|
||||
}
|
||||
96
lib/widgets/contact_name_widget.dart
Normal file
96
lib/widgets/contact_name_widget.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/services/contact_local_names_service.dart';
|
||||
|
||||
class ContactNameWidget extends StatefulWidget {
|
||||
final int contactId;
|
||||
final String? originalName;
|
||||
final String? originalFirstName;
|
||||
final String? originalLastName;
|
||||
final TextStyle? style;
|
||||
final int? maxLines;
|
||||
final TextOverflow? overflow;
|
||||
|
||||
const ContactNameWidget({
|
||||
super.key,
|
||||
required this.contactId,
|
||||
this.originalName,
|
||||
this.originalFirstName,
|
||||
this.originalLastName,
|
||||
this.style,
|
||||
this.maxLines,
|
||||
this.overflow,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ContactNameWidget> createState() => _ContactNameWidgetState();
|
||||
}
|
||||
|
||||
class _ContactNameWidgetState extends State<ContactNameWidget> {
|
||||
late String _displayName;
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateDisplayName();
|
||||
|
||||
_subscription = ContactLocalNamesService().changes.listen((contactId) {
|
||||
if (contactId == widget.contactId && mounted) {
|
||||
_updateDisplayName();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ContactNameWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.contactId != widget.contactId ||
|
||||
oldWidget.originalName != widget.originalName ||
|
||||
oldWidget.originalFirstName != widget.originalFirstName ||
|
||||
oldWidget.originalLastName != widget.originalLastName) {
|
||||
_updateDisplayName();
|
||||
}
|
||||
}
|
||||
|
||||
void _updateDisplayName() {
|
||||
setState(() {
|
||||
_displayName = ContactLocalNamesService().getDisplayName(
|
||||
contactId: widget.contactId,
|
||||
originalName: widget.originalName,
|
||||
originalFirstName: widget.originalFirstName,
|
||||
originalLastName: widget.originalLastName,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
_displayName,
|
||||
style: widget.style,
|
||||
maxLines: widget.maxLines,
|
||||
overflow: widget.overflow,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
String getContactDisplayName({
|
||||
required int contactId,
|
||||
String? originalName,
|
||||
String? originalFirstName,
|
||||
String? originalLastName,
|
||||
}) {
|
||||
return ContactLocalNamesService().getDisplayName(
|
||||
contactId: contactId,
|
||||
originalName: originalName,
|
||||
originalFirstName: originalFirstName,
|
||||
originalLastName: originalLastName,
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:gwid/models/contact.dart';
|
||||
import 'package:gwid/models/profile.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/widgets/chat_message_bubble.dart';
|
||||
import 'package:gwid/widgets/contact_name_widget.dart';
|
||||
import 'package:gwid/chat_screen.dart';
|
||||
|
||||
class ControlMessageChip extends StatelessWidget {
|
||||
@@ -26,8 +27,15 @@ class ControlMessageChip extends StatelessWidget {
|
||||
);
|
||||
|
||||
final eventType = controlAttach['event'];
|
||||
final senderName =
|
||||
contacts[message.senderId]?.name ?? 'ID ${message.senderId}';
|
||||
final senderContact = contacts[message.senderId];
|
||||
final senderName = senderContact != null
|
||||
? getContactDisplayName(
|
||||
contactId: senderContact.id,
|
||||
originalName: senderContact.name,
|
||||
originalFirstName: senderContact.firstName,
|
||||
originalLastName: senderContact.lastName,
|
||||
)
|
||||
: 'ID ${message.senderId}';
|
||||
final isMe = message.senderId == myId;
|
||||
final senderDisplayName = isMe ? 'Вы' : senderName;
|
||||
|
||||
@@ -40,7 +48,16 @@ class ControlMessageChip extends StatelessWidget {
|
||||
if (id == myId) {
|
||||
return 'Вы';
|
||||
}
|
||||
return contacts[id]?.name ?? 'участник с ID $id';
|
||||
final contact = contacts[id];
|
||||
if (contact != null) {
|
||||
return getContactDisplayName(
|
||||
contactId: contact.id,
|
||||
originalName: contact.name,
|
||||
originalFirstName: contact.firstName,
|
||||
originalLastName: contact.lastName,
|
||||
);
|
||||
}
|
||||
return 'участник с ID $id';
|
||||
})
|
||||
.where((name) => name.isNotEmpty)
|
||||
.join(', ');
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/services/avatar_cache_service.dart';
|
||||
import 'package:gwid/widgets/contact_name_widget.dart';
|
||||
import 'package:gwid/widgets/contact_avatar_widget.dart';
|
||||
import 'package:gwid/services/contact_local_names_service.dart';
|
||||
|
||||
class UserProfilePanel extends StatefulWidget {
|
||||
final int userId;
|
||||
@@ -33,27 +37,62 @@ class UserProfilePanel extends StatefulWidget {
|
||||
|
||||
class _UserProfilePanelState extends State<UserProfilePanel> {
|
||||
final ScrollController _nameScrollController = ScrollController();
|
||||
String? _localDescription;
|
||||
StreamSubscription? _changesSubscription;
|
||||
|
||||
String get _displayName {
|
||||
if (widget.firstName != null || widget.lastName != null) {
|
||||
final firstName = widget.firstName ?? '';
|
||||
final lastName = widget.lastName ?? '';
|
||||
final fullName = '$firstName $lastName'.trim();
|
||||
return fullName.isNotEmpty
|
||||
? fullName
|
||||
: (widget.name ?? 'ID ${widget.userId}');
|
||||
final displayName = getContactDisplayName(
|
||||
contactId: widget.userId,
|
||||
originalName: widget.name,
|
||||
originalFirstName: widget.firstName,
|
||||
originalLastName: widget.lastName,
|
||||
);
|
||||
return displayName;
|
||||
}
|
||||
|
||||
String? get _displayDescription {
|
||||
if (_localDescription != null && _localDescription!.isNotEmpty) {
|
||||
return _localDescription;
|
||||
}
|
||||
return widget.name ?? 'ID ${widget.userId}';
|
||||
return widget.description;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadLocalDescription();
|
||||
|
||||
_changesSubscription = ContactLocalNamesService().changes.listen((
|
||||
contactId,
|
||||
) {
|
||||
if (contactId == widget.userId && mounted) {
|
||||
_loadLocalDescription();
|
||||
}
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkNameLength();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadLocalDescription() async {
|
||||
final localData = await ContactLocalNamesService().getContactData(
|
||||
widget.userId,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_localDescription = localData?['notes'] as String?;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_changesSubscription?.cancel();
|
||||
_nameScrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _checkNameLength() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_nameScrollController.hasClients) {
|
||||
@@ -99,12 +138,6 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameScrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
@@ -130,11 +163,13 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
AvatarCacheService().getAvatarWidget(
|
||||
widget.avatarUrl,
|
||||
userId: widget.userId,
|
||||
size: 80,
|
||||
fallbackText: _displayName,
|
||||
ContactAvatarWidget(
|
||||
contactId: widget.userId,
|
||||
originalAvatarUrl: widget.avatarUrl,
|
||||
radius: 40,
|
||||
fallbackText: _displayName.isNotEmpty
|
||||
? _displayName[0].toUpperCase()
|
||||
: '?',
|
||||
backgroundColor: colors.primaryContainer,
|
||||
textColor: colors.onPrimaryContainer,
|
||||
),
|
||||
@@ -213,11 +248,11 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.description != null &&
|
||||
widget.description!.isNotEmpty) ...[
|
||||
if (_displayDescription != null &&
|
||||
_displayDescription!.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
widget.description!,
|
||||
_displayDescription!,
|
||||
style: TextStyle(
|
||||
color: colors.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
|
||||
Reference in New Issue
Block a user