измененный вид контакта
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../../core/config/app_colors.dart';
|
||||
import '../../../../backend/modules/contacts.dart';
|
||||
import '../../../../core/utils/haptics.dart';
|
||||
import '../../../../l10n/app_localizations.dart';
|
||||
import '../../../../main.dart' show api;
|
||||
import '../../../../models/attachment.dart';
|
||||
import '../../../screens/contacts/open_contact_profile.dart';
|
||||
import '../../custom_notification.dart';
|
||||
import '../../komet_avatar.dart';
|
||||
import '../../small_spinner.dart';
|
||||
import 'bubble_context.dart';
|
||||
|
||||
Widget buildContactCard(
|
||||
@@ -13,85 +21,313 @@ Widget buildContactCard(
|
||||
String? name,
|
||||
String? photoUrl,
|
||||
String? phoneNumber,
|
||||
int? contactId,
|
||||
String? userId,
|
||||
}) {
|
||||
final isMe = ctx.isMe;
|
||||
return _ContactCard(
|
||||
ctx: ctx,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
name: name,
|
||||
photoUrl: photoUrl,
|
||||
phoneNumber: phoneNumber,
|
||||
contactId: contactId,
|
||||
userId: userId,
|
||||
);
|
||||
}
|
||||
|
||||
final first = firstName ?? '';
|
||||
final last = lastName ?? '';
|
||||
final hasFirstName = first.isNotEmpty;
|
||||
final hasLastName = last.isNotEmpty;
|
||||
class _ContactCard extends StatefulWidget {
|
||||
final BubbleContext ctx;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? name;
|
||||
final String? photoUrl;
|
||||
final String? phoneNumber;
|
||||
final int? contactId;
|
||||
final String? userId;
|
||||
|
||||
final resolvedName = (hasFirstName || hasLastName)
|
||||
? '${hasFirstName ? first : ''}${hasLastName ? ' $last' : ''}'.trim()
|
||||
: (name ?? 'Contact');
|
||||
const _ContactCard({
|
||||
required this.ctx,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.name,
|
||||
this.photoUrl,
|
||||
this.phoneNumber,
|
||||
this.contactId,
|
||||
this.userId,
|
||||
});
|
||||
|
||||
final bgColor = isMe ? ctx.systemTint : ctx.cs.surfaceContainerHighest;
|
||||
int? get resolvedContactId => contactId ?? int.tryParse(userId?.trim() ?? '');
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: photoUrl != null && photoUrl.isNotEmpty
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: photoUrl,
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: kAvatarThumbSize,
|
||||
memCacheHeight: kAvatarThumbSize,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, _, _) => Icon(
|
||||
Symbols.person,
|
||||
color: isMe ? ctx.cs.onPrimaryContainer : ctx.cs.primary,
|
||||
size: 24,
|
||||
),
|
||||
String get resolvedName {
|
||||
final first = firstName?.trim() ?? '';
|
||||
final last = lastName?.trim() ?? '';
|
||||
final fullName = '$first $last'.trim();
|
||||
if (fullName.isNotEmpty) return fullName;
|
||||
final fallback = name?.trim() ?? '';
|
||||
return fallback.isEmpty ? 'Contact' : fallback;
|
||||
}
|
||||
|
||||
String get nameForAdd {
|
||||
final first = firstName?.trim() ?? '';
|
||||
return first.isEmpty ? resolvedName : first;
|
||||
}
|
||||
|
||||
int get resolvedPhone {
|
||||
final digits = phoneNumber?.replaceAll(RegExp(r'\D'), '') ?? '';
|
||||
return int.tryParse(digits) ?? 0;
|
||||
}
|
||||
|
||||
@override
|
||||
State<_ContactCard> createState() => _ContactCardState();
|
||||
}
|
||||
|
||||
class _ContactCardState extends State<_ContactCard> {
|
||||
bool? _isContact;
|
||||
bool _adding = false;
|
||||
int _statusGeneration = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ContactsModule.revision.addListener(_onContactsChanged);
|
||||
unawaited(_refreshContactStatus());
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_ContactCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.resolvedContactId != widget.resolvedContactId ||
|
||||
oldWidget.ctx.message.accountId != widget.ctx.message.accountId ||
|
||||
oldWidget.ctx.myId != widget.ctx.myId) {
|
||||
_isContact = null;
|
||||
unawaited(_refreshContactStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ContactsModule.revision.removeListener(_onContactsChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onContactsChanged() {
|
||||
unawaited(_refreshContactStatus());
|
||||
}
|
||||
|
||||
Future<void> _refreshContactStatus() async {
|
||||
final generation = ++_statusGeneration;
|
||||
final contactId = widget.resolvedContactId;
|
||||
if (contactId == null) {
|
||||
if (mounted && generation == _statusGeneration) {
|
||||
setState(() => _isContact = false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (contactId == widget.ctx.myId) {
|
||||
if (mounted && generation == _statusGeneration) {
|
||||
setState(() => _isContact = true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
CachedContact? contact;
|
||||
try {
|
||||
contact = await ContactsModule.getContact(
|
||||
widget.ctx.message.accountId,
|
||||
contactId,
|
||||
);
|
||||
} catch (_) {}
|
||||
if (!mounted || generation != _statusGeneration) return;
|
||||
setState(() => _isContact = contact != null);
|
||||
}
|
||||
|
||||
Future<void> _addContact() async {
|
||||
final contactId = widget.resolvedContactId;
|
||||
if (contactId == null || _adding || _isContact != false) return;
|
||||
Haptics.tap();
|
||||
setState(() => _adding = true);
|
||||
try {
|
||||
final contact = await ContactsModule.addContact(
|
||||
api,
|
||||
contactId,
|
||||
widget.nameForAdd,
|
||||
phone: widget.resolvedPhone,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (contact == null) {
|
||||
showCustomNotification(
|
||||
context,
|
||||
AppLocalizations.of(context)!.addContactError,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => _isContact = true);
|
||||
showCustomNotification(
|
||||
context,
|
||||
AppLocalizations.of(context)!.nfcContactAdded,
|
||||
);
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
showCustomNotification(
|
||||
context,
|
||||
AppLocalizations.of(context)!.addContactError,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _adding = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _openProfile() {
|
||||
final contactId = widget.resolvedContactId;
|
||||
if (contactId == null) return;
|
||||
Haptics.tap();
|
||||
unawaited(
|
||||
openContactDialogProfile(
|
||||
context,
|
||||
contactId: contactId,
|
||||
name: widget.resolvedName,
|
||||
avatarUrl: widget.photoUrl,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ctx = widget.ctx;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final canAdd =
|
||||
widget.resolvedContactId != null &&
|
||||
widget.resolvedContactId != ctx.myId &&
|
||||
_isContact != true;
|
||||
final buttonColor = ctx.isMe
|
||||
? Colors.black.withValues(alpha: 0.16)
|
||||
: ctx.cs.onSurface.withValues(alpha: 0.08);
|
||||
|
||||
return SizedBox(
|
||||
key: const ValueKey('contact-card'),
|
||||
width: 320,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 8, 3),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
KometAvatar(
|
||||
name: widget.resolvedName,
|
||||
imageUrl: widget.photoUrl,
|
||||
size: 48,
|
||||
fadeIn: false,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.resolvedName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: ctx.text,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.15,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_isContact == true
|
||||
? l10n.contactBubbleAlreadyAdded
|
||||
: l10n.contactBubbleNew,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: ctx.dim,
|
||||
fontSize: 12,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Symbols.person,
|
||||
color: isMe ? ctx.cs.onPrimaryContainer : ctx.cs.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
resolvedName.isNotEmpty ? resolvedName : 'Contact',
|
||||
style: TextStyle(
|
||||
color: ctx.text,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.2,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (phoneNumber != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
phoneNumber,
|
||||
style: TextStyle(color: ctx.dim, fontSize: 12, height: 1.2),
|
||||
if (canAdd) ...[
|
||||
const SizedBox(width: 6),
|
||||
_ContactActionButton(
|
||||
key: const ValueKey('contact-add-button'),
|
||||
tooltip: l10n.nfcAddContact,
|
||||
icon: Symbols.person_add,
|
||||
color: buttonColor,
|
||||
foreground: ctx.text,
|
||||
loading: _adding || _isContact == null,
|
||||
onPressed: _isContact == false && !_adding
|
||||
? _addContact
|
||||
: null,
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 6),
|
||||
_ContactActionButton(
|
||||
key: const ValueKey('contact-profile-button'),
|
||||
tooltip: l10n.contactBubbleOpenProfile,
|
||||
icon: Symbols.chat_bubble,
|
||||
color: buttonColor,
|
||||
foreground: ctx.text,
|
||||
onPressed: widget.resolvedContactId == null
|
||||
? null
|
||||
: _openProfile,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Align(alignment: Alignment.centerRight, child: ctx.meta()),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ContactActionButton extends StatelessWidget {
|
||||
final String tooltip;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final Color foreground;
|
||||
final bool loading;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const _ContactActionButton({
|
||||
super.key,
|
||||
required this.tooltip,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.foreground,
|
||||
this.loading = false,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: color,
|
||||
shape: const CircleBorder(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: SizedBox(
|
||||
width: 38,
|
||||
height: 38,
|
||||
child: IconButton(
|
||||
tooltip: tooltip,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: onPressed,
|
||||
icon: loading
|
||||
? SmallSpinner(size: 17, color: foreground)
|
||||
: Icon(icon, color: foreground, size: 20, fill: 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContactBubble extends StatelessWidget {
|
||||
@@ -109,6 +345,8 @@ class ContactBubble extends StatelessWidget {
|
||||
name: contact.name,
|
||||
photoUrl: contact.photoUrl ?? contact.baseUrl,
|
||||
phoneNumber: contact.phoneNumber,
|
||||
contactId: contact.contactId,
|
||||
userId: contact.userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +261,8 @@ class ForwardedContactBubble extends StatelessWidget {
|
||||
name: contact.name,
|
||||
photoUrl: contact.photoUrl ?? contact.baseUrl,
|
||||
phoneNumber: contact.phoneNumber,
|
||||
contactId: contact.contactId,
|
||||
userId: contact.userId,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user