feat: изменение контакта
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:komet/main.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../contacts/edit_contact_sheet.dart';
|
||||
import '../../../backend/modules/messages.dart' show ContactCache;
|
||||
import '../../../core/cache/info_cache.dart';
|
||||
import '../../../core/config/app_show_extra_info.dart';
|
||||
@@ -77,6 +78,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
String _selectedTab = '';
|
||||
bool _descExpanded = false;
|
||||
bool _showRealName = false;
|
||||
String? _nameOverride;
|
||||
|
||||
int? _otherId;
|
||||
ContactInfo? _contactData;
|
||||
@@ -275,12 +277,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
icon: Icon(Icons.arrow_back, color: cs.onSurface),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.more_vert, color: cs.onSurface),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
actions: [_buildMoreButton(cs)],
|
||||
),
|
||||
SliverToBoxAdapter(child: _buildBody(cs)),
|
||||
],
|
||||
@@ -316,6 +313,96 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
ContactName? _nameEntry(String type) {
|
||||
final data = _contactData;
|
||||
if (data == null) return null;
|
||||
for (final n in data.names) {
|
||||
if (n.type == type) return n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool get _isContact => _nameEntry('CUSTOM') != null;
|
||||
|
||||
String _joinName(String first, String last) => last.trim().isEmpty
|
||||
? first.trim()
|
||||
: '${first.trim()} ${last.trim()}';
|
||||
|
||||
Widget _buildMoreButton(ColorScheme cs) {
|
||||
final canEdit = widget.chatType == 'DIALOG' && _isContact;
|
||||
if (!canEdit) {
|
||||
return IconButton(
|
||||
icon: Icon(Icons.more_vert, color: cs.onSurface),
|
||||
onPressed: () {},
|
||||
);
|
||||
}
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return PopupMenuButton<String>(
|
||||
icon: Icon(Icons.more_vert, color: cs.onSurface),
|
||||
onSelected: (v) {
|
||||
if (v == 'edit') _openEdit();
|
||||
},
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.edit, size: 20, color: cs.onSurface),
|
||||
const SizedBox(width: 12),
|
||||
Text(l10n.editContactMenu),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openEdit() async {
|
||||
final custom = _nameEntry('CUSTOM');
|
||||
final oneme = _nameEntry('ONEME');
|
||||
final peerId = _otherId ?? widget.dialogPeerId ?? 0;
|
||||
if (peerId == 0) return;
|
||||
|
||||
final result = await showEditContactSheet(
|
||||
context,
|
||||
contactId: peerId,
|
||||
avatarUrl: _contactData?.avatarUrl ?? widget.imageUrl,
|
||||
customFirst: custom?.firstName ?? '',
|
||||
customLast: custom?.lastName ?? '',
|
||||
onemeFirst: oneme?.firstName ?? '',
|
||||
onemeLast: oneme?.lastName ?? '',
|
||||
);
|
||||
if (!mounted || result == null) return;
|
||||
|
||||
switch (result.action) {
|
||||
case EditContactAction.updated:
|
||||
_applyUpdatedNames(result.firstName, result.lastName);
|
||||
case EditContactAction.removed:
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
void _applyUpdatedNames(String first, String last) {
|
||||
final data = _contactData;
|
||||
final newCustom = ContactName(
|
||||
type: 'CUSTOM',
|
||||
name: first,
|
||||
firstName: first,
|
||||
lastName: last,
|
||||
);
|
||||
final others = data == null
|
||||
? <ContactName>[]
|
||||
: data.names.where((n) => n.type != 'CUSTOM').toList();
|
||||
setState(() {
|
||||
_contactData = ContactInfo(
|
||||
raw: data?.raw ?? const <String, dynamic>{},
|
||||
names: [newCustom, ...others],
|
||||
);
|
||||
_nameOverride = _joinName(first, last);
|
||||
_showRealName = false;
|
||||
});
|
||||
}
|
||||
|
||||
String? get _realName {
|
||||
final data = _contactData;
|
||||
if (data == null) return null;
|
||||
@@ -339,19 +426,20 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
);
|
||||
final custom = _nameOverride ?? widget.name;
|
||||
final real = _realName;
|
||||
final hasToggle =
|
||||
widget.chatType == 'DIALOG' && real != null && real != widget.name;
|
||||
widget.chatType == 'DIALOG' && real != null && real != custom;
|
||||
|
||||
final nameSwap = AnimatedTextSwap(
|
||||
showAlternate: _showRealName,
|
||||
alignment: Alignment.center,
|
||||
alternate: Text(
|
||||
real ?? widget.name,
|
||||
real ?? custom,
|
||||
style: nameStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
child: Text(widget.name, style: nameStyle, textAlign: TextAlign.center),
|
||||
child: Text(custom, style: nameStyle, textAlign: TextAlign.center),
|
||||
);
|
||||
|
||||
if (!hasToggle) return nameSwap;
|
||||
|
||||
@@ -583,6 +583,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
storiesModule.storiesChanged.addListener(_onStoriesDataChanged);
|
||||
KometSettings.hideAllChatsFolder.addListener(_requestReload);
|
||||
KometSettings.showHiddenChats.addListener(_requestReload);
|
||||
ContactsModule.revision.addListener(_requestReload);
|
||||
_maybeLoadStories();
|
||||
_typingSub = api.pushStream
|
||||
.where((p) => p.opcode == Opcode.notifTyping)
|
||||
@@ -1228,6 +1229,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
storiesModule.storiesChanged.removeListener(_onStoriesDataChanged);
|
||||
KometSettings.hideAllChatsFolder.removeListener(_requestReload);
|
||||
KometSettings.showHiddenChats.removeListener(_requestReload);
|
||||
ContactsModule.revision.removeListener(_requestReload);
|
||||
_loginSub?.cancel();
|
||||
_stateSub?.cancel();
|
||||
_typingSub?.cancel();
|
||||
|
||||
@@ -27,6 +27,7 @@ import '../../../main.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../../backend/api.dart';
|
||||
import '../../../backend/modules/messages.dart';
|
||||
import '../../../backend/modules/contacts.dart';
|
||||
import '../../../backend/modules/animoji.dart';
|
||||
import '../../../models/animoji.dart';
|
||||
import '../../../backend/modules/complaints.dart';
|
||||
@@ -632,6 +633,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
});
|
||||
debugForceOffline.addListener(_recomputeHeaderStatus);
|
||||
PresenceFetch.revision.addListener(_onPresenceChanged);
|
||||
ContactsModule.revision.addListener(_onContactsChanged);
|
||||
_floatingDateAnimController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 220),
|
||||
@@ -987,7 +989,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ChatInfoScreen(
|
||||
chatId: widget.chatId,
|
||||
name: widget.name,
|
||||
name: _headerName(),
|
||||
imageUrl: widget.imageUrl,
|
||||
chatType: widget.chatType,
|
||||
initialTab: initialTab,
|
||||
@@ -1823,6 +1825,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
.listenable(widget.chatId)
|
||||
.removeListener(_recomputeHeaderStatus);
|
||||
PresenceFetch.revision.removeListener(_onPresenceChanged);
|
||||
ContactsModule.revision.removeListener(_onContactsChanged);
|
||||
if (_wallpaperListening) {
|
||||
ChatWallpaperStore.instance.revision.removeListener(
|
||||
_applyEffectiveWallpaper,
|
||||
@@ -2794,6 +2797,22 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
}
|
||||
|
||||
void _onContactsChanged() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
String _headerName() {
|
||||
if (_commentsMode) return AppLocalizations.of(context)!.commentsTitle;
|
||||
if (widget.chatType == 'DIALOG') {
|
||||
final otherId = _resolveOtherId();
|
||||
if (otherId != null) {
|
||||
final cached = ContactCache.get(otherId);
|
||||
if (cached != null && cached.isNotEmpty) return cached;
|
||||
}
|
||||
}
|
||||
return widget.name;
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar(ColorScheme cs) {
|
||||
final glossy = AppVisualStyle.current.value.glossyChrome;
|
||||
final searchT = Curves.easeOut.transform(_searchAnim.value.clamp(0.0, 1.0));
|
||||
@@ -2879,9 +2898,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
cs: cs,
|
||||
embedded: widget.embedded,
|
||||
chatId: widget.chatId,
|
||||
name: _commentsMode
|
||||
? AppLocalizations.of(context)!.commentsTitle
|
||||
: widget.name,
|
||||
name: _headerName(),
|
||||
imageUrl: widget.imageUrl,
|
||||
chatType: widget.chatType,
|
||||
isOfficial: chat?.isOfficial ?? false,
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:komet/backend/modules/contacts.dart';
|
||||
import 'package:komet/frontend/widgets/custom_notification.dart';
|
||||
import 'package:komet/frontend/widgets/komet_avatar.dart';
|
||||
import 'package:komet/l10n/app_localizations.dart';
|
||||
import 'package:komet/main.dart';
|
||||
|
||||
enum EditContactAction { updated, removed }
|
||||
|
||||
class EditContactResult {
|
||||
final EditContactAction action;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
|
||||
const EditContactResult(
|
||||
this.action, {
|
||||
this.firstName = '',
|
||||
this.lastName = '',
|
||||
});
|
||||
}
|
||||
|
||||
Future<EditContactResult?> showEditContactSheet(
|
||||
BuildContext context, {
|
||||
required int contactId,
|
||||
required String avatarUrl,
|
||||
required String customFirst,
|
||||
required String customLast,
|
||||
required String onemeFirst,
|
||||
required String onemeLast,
|
||||
}) {
|
||||
return showGeneralDialog<EditContactResult>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
barrierColor: Colors.black.withValues(alpha: 0.28),
|
||||
transitionDuration: const Duration(milliseconds: 260),
|
||||
pageBuilder: (_, _, _) => const SizedBox.shrink(),
|
||||
transitionBuilder: (_, anim, _, _) {
|
||||
final t = Curves.easeOutCubic.transform(anim.value);
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 14 * t, sigmaY: 14 * t),
|
||||
child: Opacity(
|
||||
opacity: anim.value,
|
||||
child: Transform.scale(
|
||||
scale: 0.94 + 0.06 * t,
|
||||
child: _EditContactCard(
|
||||
contactId: contactId,
|
||||
avatarUrl: avatarUrl,
|
||||
customFirst: customFirst,
|
||||
customLast: customLast,
|
||||
onemeFirst: onemeFirst,
|
||||
onemeLast: onemeLast,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _EditContactCard extends StatefulWidget {
|
||||
final int contactId;
|
||||
final String avatarUrl;
|
||||
final String customFirst;
|
||||
final String customLast;
|
||||
final String onemeFirst;
|
||||
final String onemeLast;
|
||||
|
||||
const _EditContactCard({
|
||||
required this.contactId,
|
||||
required this.avatarUrl,
|
||||
required this.customFirst,
|
||||
required this.customLast,
|
||||
required this.onemeFirst,
|
||||
required this.onemeLast,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_EditContactCard> createState() => _EditContactCardState();
|
||||
}
|
||||
|
||||
class _EditContactCardState extends State<_EditContactCard> {
|
||||
late final TextEditingController _firstCtrl;
|
||||
late final TextEditingController _lastCtrl;
|
||||
|
||||
bool _saving = false;
|
||||
bool _deleting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_firstCtrl = TextEditingController(text: widget.customFirst);
|
||||
_lastCtrl = TextEditingController(text: widget.customLast);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_firstCtrl.dispose();
|
||||
_lastCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get _busy => _saving || _deleting;
|
||||
|
||||
bool get _dirty =>
|
||||
_firstCtrl.text.trim() != widget.customFirst.trim() ||
|
||||
_lastCtrl.text.trim() != widget.customLast.trim();
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_dirty || _busy) return;
|
||||
setState(() => _saving = true);
|
||||
|
||||
final first = _firstCtrl.text.trim();
|
||||
final last = _lastCtrl.text.trim();
|
||||
final sendFirst = first.isEmpty ? widget.onemeFirst : first;
|
||||
final sendLast = last.isEmpty ? widget.onemeLast : last;
|
||||
|
||||
final updated = await ContactsModule.updateContact(
|
||||
api,
|
||||
contactId: widget.contactId,
|
||||
firstName: sendFirst,
|
||||
lastName: sendLast,
|
||||
);
|
||||
if (!mounted) return;
|
||||
|
||||
if (updated == null) {
|
||||
setState(() => _saving = false);
|
||||
showCustomNotification(
|
||||
context,
|
||||
AppLocalizations.of(context)!.editContactError,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop(
|
||||
EditContactResult(
|
||||
EditContactAction.updated,
|
||||
firstName: updated.firstName,
|
||||
lastName: updated.lastName ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _delete() async {
|
||||
if (_busy) return;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(l10n.editContactDeleteConfirmTitle),
|
||||
content: Text(l10n.editContactDeleteConfirmBody),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: Text(l10n.editContactDeleteCancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: Text(l10n.editContactDelete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
setState(() => _deleting = true);
|
||||
final ok = await ContactsModule.removeContact(api, widget.contactId);
|
||||
if (!mounted) return;
|
||||
|
||||
if (!ok) {
|
||||
setState(() => _deleting = false);
|
||||
showCustomNotification(context, l10n.editContactError);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop(
|
||||
const EditContactResult(EditContactAction.removed),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final width = MediaQuery.sizeOf(context).width;
|
||||
final avatarName = widget.customFirst.isNotEmpty
|
||||
? widget.customFirst
|
||||
: widget.onemeFirst;
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
width: width > 420 ? 380 : double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: KometAvatar(
|
||||
name: avatarName,
|
||||
size: 88,
|
||||
imageUrl: widget.avatarUrl.isEmpty
|
||||
? null
|
||||
: widget.avatarUrl,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_inputRow(
|
||||
cs,
|
||||
controller: _firstCtrl,
|
||||
hint: l10n.editContactFirstName,
|
||||
),
|
||||
_divider(cs),
|
||||
_inputRow(
|
||||
cs,
|
||||
controller: _lastCtrl,
|
||||
hint: l10n.editContactLastName,
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: _dirty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: _saveButton(cs, l10n),
|
||||
)
|
||||
: const SizedBox(width: double.infinity),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_deleteButton(cs, l10n),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _inputRow(
|
||||
ColorScheme cs, {
|
||||
required TextEditingController controller,
|
||||
required String hint,
|
||||
}) {
|
||||
final hasText = controller.text.isNotEmpty;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
maxLength: 60,
|
||||
enabled: !_busy,
|
||||
onChanged: (_) => setState(() {}),
|
||||
style: TextStyle(color: cs.onSurface, fontSize: 16),
|
||||
decoration: InputDecoration(
|
||||
isCollapsed: true,
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasText)
|
||||
InkWell(
|
||||
onTap: _busy
|
||||
? null
|
||||
: () => setState(() => controller.clear()),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _saveButton(ColorScheme cs, AppLocalizations l10n) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: _busy ? null : _save,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
child: _saving
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(
|
||||
l10n.editContactSave,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _deleteButton(ColorScheme cs, AppLocalizations l10n) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: _busy ? null : _delete,
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
foregroundColor: cs.error,
|
||||
),
|
||||
child: _deleting
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: cs.error),
|
||||
)
|
||||
: Text(
|
||||
l10n.editContactDelete,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _divider(ColorScheme cs) =>
|
||||
Divider(height: 1, thickness: 0.5, color: cs.outlineVariant);
|
||||
}
|
||||
Reference in New Issue
Block a user