измененный вид контакта
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:flutter/material.dart';
|
||||||
import 'package:material_symbols_icons/symbols.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 '../../../../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';
|
import 'bubble_context.dart';
|
||||||
|
|
||||||
Widget buildContactCard(
|
Widget buildContactCard(
|
||||||
@@ -13,85 +21,313 @@ Widget buildContactCard(
|
|||||||
String? name,
|
String? name,
|
||||||
String? photoUrl,
|
String? photoUrl,
|
||||||
String? phoneNumber,
|
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 ?? '';
|
class _ContactCard extends StatefulWidget {
|
||||||
final last = lastName ?? '';
|
final BubbleContext ctx;
|
||||||
final hasFirstName = first.isNotEmpty;
|
final String? firstName;
|
||||||
final hasLastName = last.isNotEmpty;
|
final String? lastName;
|
||||||
|
final String? name;
|
||||||
|
final String? photoUrl;
|
||||||
|
final String? phoneNumber;
|
||||||
|
final int? contactId;
|
||||||
|
final String? userId;
|
||||||
|
|
||||||
final resolvedName = (hasFirstName || hasLastName)
|
const _ContactCard({
|
||||||
? '${hasFirstName ? first : ''}${hasLastName ? ' $last' : ''}'.trim()
|
required this.ctx,
|
||||||
: (name ?? 'Contact');
|
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(
|
String get resolvedName {
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
final first = firstName?.trim() ?? '';
|
||||||
child: Row(
|
final last = lastName?.trim() ?? '';
|
||||||
mainAxisSize: MainAxisSize.min,
|
final fullName = '$first $last'.trim();
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
if (fullName.isNotEmpty) return fullName;
|
||||||
children: [
|
final fallback = name?.trim() ?? '';
|
||||||
Container(
|
return fallback.isEmpty ? 'Contact' : fallback;
|
||||||
width: 48,
|
}
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
String get nameForAdd {
|
||||||
color: bgColor,
|
final first = firstName?.trim() ?? '';
|
||||||
borderRadius: BorderRadius.circular(24),
|
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,
|
||||||
),
|
),
|
||||||
child: photoUrl != null && photoUrl.isNotEmpty
|
);
|
||||||
? ClipRRect(
|
}
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
child: CachedNetworkImage(
|
@override
|
||||||
imageUrl: photoUrl,
|
Widget build(BuildContext context) {
|
||||||
fit: BoxFit.cover,
|
final ctx = widget.ctx;
|
||||||
memCacheWidth: kAvatarThumbSize,
|
final l10n = AppLocalizations.of(context)!;
|
||||||
memCacheHeight: kAvatarThumbSize,
|
final canAdd =
|
||||||
fadeInDuration: const Duration(milliseconds: 120),
|
widget.resolvedContactId != null &&
|
||||||
errorWidget: (_, _, _) => Icon(
|
widget.resolvedContactId != ctx.myId &&
|
||||||
Symbols.person,
|
_isContact != true;
|
||||||
color: isMe ? ctx.cs.onPrimaryContainer : ctx.cs.primary,
|
final buttonColor = ctx.isMe
|
||||||
size: 24,
|
? Colors.black.withValues(alpha: 0.16)
|
||||||
),
|
: ctx.cs.onSurface.withValues(alpha: 0.08);
|
||||||
),
|
|
||||||
)
|
return SizedBox(
|
||||||
: Icon(
|
key: const ValueKey('contact-card'),
|
||||||
Symbols.person,
|
width: 320,
|
||||||
color: isMe ? ctx.cs.onPrimaryContainer : ctx.cs.primary,
|
child: Padding(
|
||||||
size: 24,
|
padding: const EdgeInsets.fromLTRB(12, 10, 8, 3),
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Flexible(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
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: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
resolvedName.isNotEmpty ? resolvedName : 'Contact',
|
widget.resolvedName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: ctx.text,
|
color: ctx.text,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w600,
|
||||||
height: 1.2,
|
height: 1.15,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_isContact == true
|
||||||
|
? l10n.contactBubbleAlreadyAdded
|
||||||
|
: l10n.contactBubbleNew,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
style: TextStyle(
|
||||||
if (phoneNumber != null) ...[
|
color: ctx.dim,
|
||||||
const SizedBox(height: 2),
|
fontSize: 12,
|
||||||
Text(
|
height: 1.1,
|
||||||
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 {
|
class ContactBubble extends StatelessWidget {
|
||||||
@@ -109,6 +345,8 @@ class ContactBubble extends StatelessWidget {
|
|||||||
name: contact.name,
|
name: contact.name,
|
||||||
photoUrl: contact.photoUrl ?? contact.baseUrl,
|
photoUrl: contact.photoUrl ?? contact.baseUrl,
|
||||||
phoneNumber: contact.phoneNumber,
|
phoneNumber: contact.phoneNumber,
|
||||||
|
contactId: contact.contactId,
|
||||||
|
userId: contact.userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,6 +261,8 @@ class ForwardedContactBubble extends StatelessWidget {
|
|||||||
name: contact.name,
|
name: contact.name,
|
||||||
photoUrl: contact.photoUrl ?? contact.baseUrl,
|
photoUrl: contact.photoUrl ?? contact.baseUrl,
|
||||||
phoneNumber: contact.phoneNumber,
|
phoneNumber: contact.phoneNumber,
|
||||||
|
contactId: contact.contactId,
|
||||||
|
userId: contact.userId,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -970,6 +970,9 @@
|
|||||||
"addContactNotFoundSubtitle": "This number isn't on the app yet",
|
"addContactNotFoundSubtitle": "This number isn't on the app yet",
|
||||||
"addContactSearchOther": "Search for other number",
|
"addContactSearchOther": "Search for other number",
|
||||||
"addContactError": "Couldn't add contact",
|
"addContactError": "Couldn't add contact",
|
||||||
|
"contactBubbleNew": "New contact",
|
||||||
|
"contactBubbleAlreadyAdded": "Already in your contacts",
|
||||||
|
"contactBubbleOpenProfile": "Open profile",
|
||||||
"editContactMenu": "Edit contact",
|
"editContactMenu": "Edit contact",
|
||||||
"editContactTitle": "Edit contact",
|
"editContactTitle": "Edit contact",
|
||||||
"editContactFirstName": "First name",
|
"editContactFirstName": "First name",
|
||||||
|
|||||||
@@ -4232,6 +4232,24 @@ abstract class AppLocalizations {
|
|||||||
/// **'Couldn\'t add contact'**
|
/// **'Couldn\'t add contact'**
|
||||||
String get addContactError;
|
String get addContactError;
|
||||||
|
|
||||||
|
/// No description provided for @contactBubbleNew.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New contact'**
|
||||||
|
String get contactBubbleNew;
|
||||||
|
|
||||||
|
/// No description provided for @contactBubbleAlreadyAdded.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Already in your contacts'**
|
||||||
|
String get contactBubbleAlreadyAdded;
|
||||||
|
|
||||||
|
/// No description provided for @contactBubbleOpenProfile.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Open profile'**
|
||||||
|
String get contactBubbleOpenProfile;
|
||||||
|
|
||||||
/// No description provided for @editContactMenu.
|
/// No description provided for @editContactMenu.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -2207,6 +2207,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addContactError => 'Couldn\'t add contact';
|
String get addContactError => 'Couldn\'t add contact';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactBubbleNew => 'New contact';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactBubbleAlreadyAdded => 'Already in your contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactBubbleOpenProfile => 'Open profile';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get editContactMenu => 'Edit contact';
|
String get editContactMenu => 'Edit contact';
|
||||||
|
|
||||||
|
|||||||
@@ -2221,6 +2221,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addContactError => 'Не удалось добавить контакт';
|
String get addContactError => 'Не удалось добавить контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactBubbleNew => 'Новый контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactBubbleAlreadyAdded => 'Уже твой контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactBubbleOpenProfile => 'Открыть профиль';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get editContactMenu => 'Редактировать контакт';
|
String get editContactMenu => 'Редактировать контакт';
|
||||||
|
|
||||||
|
|||||||
@@ -728,6 +728,9 @@
|
|||||||
"addContactNotFoundSubtitle": "Этого номера пока нет в приложении",
|
"addContactNotFoundSubtitle": "Этого номера пока нет в приложении",
|
||||||
"addContactSearchOther": "Искать другой номер",
|
"addContactSearchOther": "Искать другой номер",
|
||||||
"addContactError": "Не удалось добавить контакт",
|
"addContactError": "Не удалось добавить контакт",
|
||||||
|
"contactBubbleNew": "Новый контакт",
|
||||||
|
"contactBubbleAlreadyAdded": "Уже твой контакт",
|
||||||
|
"contactBubbleOpenProfile": "Открыть профиль",
|
||||||
"editContactMenu": "Редактировать контакт",
|
"editContactMenu": "Редактировать контакт",
|
||||||
"editContactTitle": "Редактировать контакт",
|
"editContactTitle": "Редактировать контакт",
|
||||||
"editContactFirstName": "Имя",
|
"editContactFirstName": "Имя",
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:komet/backend/modules/contacts.dart';
|
||||||
|
import 'package:komet/backend/modules/messages.dart';
|
||||||
|
import 'package:komet/core/storage/app_database.dart';
|
||||||
|
import 'package:komet/frontend/widgets/message_bubble.dart';
|
||||||
|
import 'package:komet/l10n/app_localizations.dart';
|
||||||
|
import 'package:komet/models/attachment.dart';
|
||||||
|
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||||||
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||||
|
|
||||||
|
class _SyntheticPathProvider extends PathProviderPlatform
|
||||||
|
with MockPlatformInterfaceMixin {
|
||||||
|
final String directory;
|
||||||
|
|
||||||
|
_SyntheticPathProvider(this.directory);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getApplicationSupportPath() async => directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedMessage _contactMessage({
|
||||||
|
required String id,
|
||||||
|
required int contactId,
|
||||||
|
required String firstName,
|
||||||
|
required String lastName,
|
||||||
|
}) {
|
||||||
|
return CachedMessage(
|
||||||
|
id: id,
|
||||||
|
accountId: 1,
|
||||||
|
chatId: 2,
|
||||||
|
senderId: 3,
|
||||||
|
text: '',
|
||||||
|
time: DateTime(2026, 1, 2, 5, 46).millisecondsSinceEpoch,
|
||||||
|
attachments: [
|
||||||
|
ContactAttachment(
|
||||||
|
contactId: contactId,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
name: '$firstName $lastName',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('contact bubble shows profile and contextual contact action', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final directory = Directory.systemTemp.createTempSync(
|
||||||
|
'synthetic_contact_bubble_test',
|
||||||
|
);
|
||||||
|
addTearDown(() async {
|
||||||
|
await AppDatabase.close();
|
||||||
|
if (directory.existsSync()) directory.deleteSync(recursive: true);
|
||||||
|
});
|
||||||
|
PathProviderPlatform.instance = _SyntheticPathProvider(directory.path);
|
||||||
|
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
await AppDatabase.init();
|
||||||
|
await AppDatabase.saveProfile(
|
||||||
|
ProfileData(
|
||||||
|
id: 1,
|
||||||
|
firstName: 'Synthetic owner',
|
||||||
|
phone: 100000,
|
||||||
|
country: 'ZZ',
|
||||||
|
accountStatus: 0,
|
||||||
|
updateTime: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await AppDatabase.saveContacts([
|
||||||
|
{
|
||||||
|
'id': 77,
|
||||||
|
'account_id': 1,
|
||||||
|
'first_name': 'Existing',
|
||||||
|
'last_name': 'Contact',
|
||||||
|
'phone': 100001,
|
||||||
|
'photo_id': null,
|
||||||
|
'base_url': null,
|
||||||
|
'base_raw_url': null,
|
||||||
|
'update_time': 1,
|
||||||
|
'options': '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(await ContactsModule.getContact(1, 77), isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
locale: const Locale('ru'),
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
MessageBubble(
|
||||||
|
message: _contactMessage(
|
||||||
|
id: 'synthetic-existing-message',
|
||||||
|
contactId: 77,
|
||||||
|
firstName: 'Existing',
|
||||||
|
lastName: 'Contact',
|
||||||
|
),
|
||||||
|
isMe: false,
|
||||||
|
myId: 1,
|
||||||
|
chatType: 'DIALOG',
|
||||||
|
),
|
||||||
|
MessageBubble(
|
||||||
|
message: _contactMessage(
|
||||||
|
id: 'synthetic-new-message',
|
||||||
|
contactId: 88,
|
||||||
|
firstName: 'New',
|
||||||
|
lastName: 'Contact',
|
||||||
|
),
|
||||||
|
isMe: false,
|
||||||
|
myId: 1,
|
||||||
|
chatType: 'DIALOG',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.runAsync(
|
||||||
|
() => Future<void>.delayed(const Duration(milliseconds: 100)),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Existing Contact'), findsOneWidget);
|
||||||
|
expect(find.text('Уже твой контакт'), findsOneWidget);
|
||||||
|
expect(find.text('New Contact'), findsOneWidget);
|
||||||
|
expect(find.text('Новый контакт'), findsOneWidget);
|
||||||
|
expect(find.byKey(const ValueKey('contact-add-button')), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.byKey(const ValueKey('contact-profile-button')),
|
||||||
|
findsNWidgets(2),
|
||||||
|
);
|
||||||
|
expect(find.text('05:46'), findsNWidgets(2));
|
||||||
|
expect(
|
||||||
|
tester.getSize(find.byKey(const ValueKey('contact-card')).first).width,
|
||||||
|
320,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user