просто босс, просто начальник нахуй оптимизации

This commit is contained in:
Jganenok
2026-05-29 11:21:41 +07:00
parent b3edf7249e
commit e7f401b01d
6 changed files with 334 additions and 248 deletions
@@ -81,7 +81,7 @@ class _ChatListScreenState extends State<ChatListScreen>
double _navPageAnimStart = 0; double _navPageAnimStart = 0;
double _navPageAnimEnd = 0; double _navPageAnimEnd = 0;
double _navDragDx = 0; final ValueNotifier<double> _navDragDx = ValueNotifier(0);
double _navDragBaseLeft = 0; double _navDragBaseLeft = 0;
double _revealAnimBegin = 0.0; double _revealAnimBegin = 0.0;
double _closeAnimBegin = 0.0; double _closeAnimBegin = 0.0;
@@ -144,7 +144,6 @@ class _ChatListScreenState extends State<ChatListScreen>
_isSelectionMode, _isSelectionMode,
_shouldCollapseSearch, _shouldCollapseSearch,
_selectedChats.length, _selectedChats.length,
_pullRatio,
_storiesDockedOpen, _storiesDockedOpen,
_storiesAnimClosing, _storiesAnimClosing,
_storiesOverscrollRevealArmed, _storiesOverscrollRevealArmed,
@@ -619,7 +618,19 @@ class _ChatListScreenState extends State<ChatListScreen>
}); });
} }
int? _pageChatsBaseKey;
final Map<int, List<CachedChat>> _pageChatsCache = {};
List<CachedChat> _chatsForPageIndex(int pageIndex) { List<CachedChat> _chatsForPageIndex(int pageIndex) {
final baseKey =
Object.hash(identityHashCode(_chats), identityHashCode(_folders));
if (_pageChatsBaseKey != baseKey) {
_pageChatsBaseKey = baseKey;
_pageChatsCache.clear();
}
final cached = _pageChatsCache[pageIndex];
if (cached != null) return cached;
List<CachedChat> base; List<CachedChat> base;
if (_folders.isEmpty) { if (_folders.isEmpty) {
base = _chats; base = _chats;
@@ -634,7 +645,9 @@ class _ChatListScreenState extends State<ChatListScreen>
final pinned = base.where((c) => (c.favIndex ?? 0) > 0).toList() final pinned = base.where((c) => (c.favIndex ?? 0) > 0).toList()
..sort((a, b) => a.favIndex!.compareTo(b.favIndex!)); ..sort((a, b) => a.favIndex!.compareTo(b.favIndex!));
final regular = base.where((c) => (c.favIndex ?? 0) <= 0).toList(); final regular = base.where((c) => (c.favIndex ?? 0) <= 0).toList();
return [...pinned, ...regular]; final result = [...pinned, ...regular];
_pageChatsCache[pageIndex] = result;
return result;
} }
void _syncFolderChatScrollControllers() { void _syncFolderChatScrollControllers() {
@@ -947,6 +960,7 @@ class _ChatListScreenState extends State<ChatListScreen>
} }
_contactRebuildTimer?.cancel(); _contactRebuildTimer?.cancel();
_storiesUi.dispose(); _storiesUi.dispose();
_navDragDx.dispose();
super.dispose(); super.dispose();
} }
@@ -955,7 +969,7 @@ class _ChatListScreenState extends State<ChatListScreen>
required double Function(int index) bubbleLeftForIndex, required double Function(int index) bubbleLeftForIndex,
}) { }) {
if (_navDragging) { if (_navDragging) {
final left = (_navDragBaseLeft + _navDragDx).clamp( final left = (_navDragBaseLeft + _navDragDx.value).clamp(
bubbleLeftForIndex(0), bubbleLeftForIndex(0),
bubbleLeftForIndex(3), bubbleLeftForIndex(3),
); );
@@ -1343,6 +1357,7 @@ class _ChatListScreenState extends State<ChatListScreen>
if (hasSeparator && index == pinnedCount) { if (hasSeparator && index == pinnedCount) {
return Padding( return Padding(
key: const ValueKey('pinned_divider'),
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Divider( child: Divider(
height: 1, height: 1,
@@ -1357,10 +1372,13 @@ class _ChatListScreenState extends State<ChatListScreen>
final isPinned = (chat.favIndex ?? 0) > 0; final isPinned = (chat.favIndex ?? 0) > 0;
if (chat.type.isNotEmpty && chat.type == "DIALOG" && chat.id != 0) { if (chat.type.isNotEmpty && chat.type == "DIALOG" && chat.id != 0) {
final secondId = chat.participants.entries int secondId = _profile?.id ?? 0;
.where((entry) => entry.key != _profile?.id) for (final entry in chat.participants.entries) {
.first if (entry.key != _profile?.id) {
.key; secondId = entry.key;
break;
}
}
final name = ContactCache.get(secondId); final name = ContactCache.get(secondId);
final avatar = ContactCache.getAvatar(secondId); final avatar = ContactCache.getAvatar(secondId);
// ContactCache.isOfficial covers contacts loaded via opcode 32; // ContactCache.isOfficial covers contacts loaded via opcode 32;
@@ -1510,12 +1528,6 @@ class _ChatListScreenState extends State<ChatListScreen>
final minBubbleLeft = bubbleLeftForIndex(0); final minBubbleLeft = bubbleLeftForIndex(0);
final maxBubbleLeft = bubbleLeftForIndex(3); final maxBubbleLeft = bubbleLeftForIndex(3);
final bubbleLeft = _navDragging
? (_navDragBaseLeft + _navDragDx).clamp(minBubbleLeft, maxBubbleLeft)
: leftOffset;
final navRowT = ((bubbleLeft - 4) / inactiveWidth).clamp(0.0, 3.0);
double navInterpolatedWidth(int tabIndex, double rowT) { double navInterpolatedWidth(int tabIndex, double rowT) {
final rt = rowT.clamp(0.0, 3.0); final rt = rowT.clamp(0.0, 3.0);
final i0 = rt.floor().clamp(0, 3); final i0 = rt.floor().clamp(0, 3);
@@ -1568,39 +1580,46 @@ class _ChatListScreenState extends State<ChatListScreen>
if (_isSelectionMode) return; if (_isSelectionMode) return;
_navPageAnimController.stop(); _navPageAnimController.stop();
_navPageAnimController.value = 1.0; _navPageAnimController.value = 1.0;
_navDragDx.value = 0;
setState(() { setState(() {
_navDragging = true; _navDragging = true;
_navDragDx = 0;
_navDragBaseLeft = bubbleLeftForIndex(_currentNavIndex); _navDragBaseLeft = bubbleLeftForIndex(_currentNavIndex);
}); });
}, },
onHorizontalDragUpdate: (details) { onHorizontalDragUpdate: (details) {
if (!_navDragging) return; if (!_navDragging) return;
setState(() { _navDragDx.value += details.delta.dx;
_navDragDx += details.delta.dx;
});
}, },
onHorizontalDragEnd: (_) { onHorizontalDragEnd: (_) {
if (!_navDragging) return; if (!_navDragging) return;
final left = (_navDragBaseLeft + _navDragDx).clamp( final left = (_navDragBaseLeft + _navDragDx.value).clamp(
minBubbleLeft, minBubbleLeft,
maxBubbleLeft, maxBubbleLeft,
); );
final next = indexForBubbleLeft(left); final next = indexForBubbleLeft(left);
_navDragDx.value = 0;
setState(() { setState(() {
_currentNavIndex = next; _currentNavIndex = next;
_navDragging = false; _navDragging = false;
_navDragDx = 0;
}); });
}, },
onHorizontalDragCancel: () { onHorizontalDragCancel: () {
if (!_navDragging) return; if (!_navDragging) return;
_navDragDx.value = 0;
setState(() { setState(() {
_navDragging = false; _navDragging = false;
_navDragDx = 0;
}); });
}, },
child: Stack( child: ValueListenableBuilder<double>(
valueListenable: _navDragDx,
builder: (context, navDragDx, _) {
final bubbleLeft = _navDragging
? (_navDragBaseLeft + navDragDx)
.clamp(minBubbleLeft, maxBubbleLeft)
: leftOffset;
final navRowT =
((bubbleLeft - 4) / inactiveWidth).clamp(0.0, 3.0);
return Stack(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
children: [ children: [
AnimatedPositioned( AnimatedPositioned(
@@ -1672,6 +1691,8 @@ class _ChatListScreenState extends State<ChatListScreen>
), ),
), ),
], ],
);
},
), ),
), ),
), ),
@@ -1717,7 +1738,8 @@ class _ChatListScreenState extends State<ChatListScreen>
width: pageW * 4, width: pageW * 4,
height: pageH, height: pageH,
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _navPageAnimController, animation: Listenable.merge(
[_navPageAnimController, _navDragDx]),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@@ -2066,6 +2088,7 @@ class _ChatListScreenState extends State<ChatListScreen>
final isSelected = _selectedChats.contains(id); final isSelected = _selectedChats.contains(id);
return InkWell( return InkWell(
key: ValueKey('chat_$id'),
onTap: () { onTap: () {
if (_isSelectionMode) { if (_isSelectionMode) {
_toggleSelection(id); _toggleSelection(id);
+33 -8
View File
@@ -115,6 +115,7 @@ class _ChatScreenState extends State<ChatScreen>
int _otherStatus = 0; int _otherStatus = 0;
int? _otherSeenTime; int? _otherSeenTime;
final ValueNotifier<String> _headerStatusNotifier = ValueNotifier(''); final ValueNotifier<String> _headerStatusNotifier = ValueNotifier('');
final ValueNotifier<int> _otherReadTime = ValueNotifier(0);
int _tempIdCounter = 0; int _tempIdCounter = 0;
late final AnimationController _attachAnim; late final AnimationController _attachAnim;
@@ -123,6 +124,8 @@ class _ChatScreenState extends State<ChatScreen>
Timer? _shimmerStartTimer; Timer? _shimmerStartTimer;
bool _historyKickedOff = false; bool _historyKickedOff = false;
List<CachedMessage> _messages = []; List<CachedMessage> _messages = [];
List<Object>? _combinedItemsCache;
int? _combinedItemsKey;
int _myId = 0; int _myId = 0;
CachedChat? chat; CachedChat? chat;
@@ -183,6 +186,7 @@ class _ChatScreenState extends State<ChatScreen>
chat = value.first; chat = value.first;
}); });
_recomputeHeaderStatus(); _recomputeHeaderStatus();
_syncOtherReadTime();
} }
}).catchError((_) {}); }).catchError((_) {});
@@ -395,6 +399,7 @@ class _ChatScreenState extends State<ChatScreen>
} }
_typingTimers.clear(); _typingTimers.clear();
_headerStatusNotifier.dispose(); _headerStatusNotifier.dispose();
_otherReadTime.dispose();
_uploadStatus.dispose(); _uploadStatus.dispose();
_attachAnim.dispose(); _attachAnim.dispose();
_messageController.dispose(); _messageController.dispose();
@@ -419,17 +424,28 @@ class _ChatScreenState extends State<ChatScreen>
} }
} }
String? _effectiveStatus(CachedMessage msg) { int _computeOtherReadTime() {
if (msg.senderId != _myId) return null;
if (msg.status == 'sending' || msg.status == 'error') return msg.status;
final c = chat; final c = chat;
if (c == null) return 'sent'; if (c == null) return 0;
int otherReadTime = 0; int otherReadTime = 0;
for (final entry in c.participants.entries) { for (final entry in c.participants.entries) {
if (entry.key != _myId && entry.value > otherReadTime) { if (entry.key != _myId && entry.value > otherReadTime) {
otherReadTime = entry.value; otherReadTime = entry.value;
} }
} }
return otherReadTime;
}
void _syncOtherReadTime() {
final t = _computeOtherReadTime();
if (_otherReadTime.value != t) _otherReadTime.value = t;
}
String? _effectiveStatus(CachedMessage msg) {
if (msg.senderId != _myId) return null;
if (msg.status == 'sending' || msg.status == 'error') return msg.status;
if (chat == null) return 'sent';
final otherReadTime = _otherReadTime.value;
if (otherReadTime > 0 && otherReadTime >= msg.time) return 'read'; if (otherReadTime > 0 && otherReadTime >= msg.time) return 'read';
return 'sent'; return 'sent';
} }
@@ -555,9 +571,8 @@ class _ChatScreenState extends State<ChatScreen>
final c = chat; final c = chat;
if (c == null) return; if (c == null) return;
if (c.participants[userId] == mark) return; if (c.participants[userId] == mark) return;
setState(() { c.participants[userId] = mark;
c.participants[userId] = mark; _syncOtherReadTime();
});
} }
Future<void> _sendMessage() async { Future<void> _sendMessage() async {
@@ -614,6 +629,7 @@ class _ChatScreenState extends State<ChatScreen>
ChatsModule.refreshChats(api, [widget.chatId]).then((list) { ChatsModule.refreshChats(api, [widget.chatId]).then((list) {
if (!mounted || list.isEmpty) return; if (!mounted || list.isEmpty) return;
setState(() => chat = list.first); setState(() => chat = list.first);
_syncOtherReadTime();
}), }),
); );
} }
@@ -708,6 +724,10 @@ class _ChatScreenState extends State<ChatScreen>
} }
List<Object> _buildCombinedItems() { List<Object> _buildCombinedItems() {
final key = Object.hashAll(_messages.map(identityHashCode));
final cached = _combinedItemsCache;
if (cached != null && _combinedItemsKey == key) return cached;
final List<Object> items = []; final List<Object> items = [];
final Set<int> usedDates = {}; final Set<int> usedDates = {};
@@ -740,6 +760,8 @@ class _ChatScreenState extends State<ChatScreen>
} }
_separatorKeys.removeWhere((k, _) => !usedDates.contains(k)); _separatorKeys.removeWhere((k, _) => !usedDates.contains(k));
_combinedItemsCache = items;
_combinedItemsKey = key;
return items; return items;
} }
@@ -1022,7 +1044,9 @@ class _ChatScreenState extends State<ChatScreen>
return Stack( return Stack(
key: _listKey, key: _listKey,
children: [ children: [
ValueListenableBuilder<double>( ValueListenableBuilder<int>(
valueListenable: _otherReadTime,
builder: (context, _, _) => ValueListenableBuilder<double>(
valueListenable: AppCacheExtent.current, valueListenable: AppCacheExtent.current,
builder: (context, cacheExtent, _) => ListView.builder( builder: (context, cacheExtent, _) => ListView.builder(
controller: _scrollController, controller: _scrollController,
@@ -1082,6 +1106,7 @@ class _ChatScreenState extends State<ChatScreen>
}, },
), ),
), ),
),
Positioned( Positioned(
top: 8, top: 8,
left: 0, left: 0,
@@ -43,7 +43,7 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
int? _accountId; int? _accountId;
List<CloudFile> _files = []; List<CloudFile> _files = [];
bool _isUploading = false; bool _isUploading = false;
double _uploadProgress = 0; final ValueNotifier<double> _uploadProgress = ValueNotifier(0);
bool _animateNewCard = false; bool _animateNewCard = false;
@override @override
@@ -68,16 +68,19 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
} }
mgr.onProgress = (progress, _) { mgr.onProgress = (progress, _) {
if (!mounted) return; if (!mounted) return;
setState(() { _isUploading = true; _uploadProgress = progress; }); if (!_isUploading) setState(() => _isUploading = true);
_uploadProgress.value = progress;
}; };
mgr.onDone = (file) { mgr.onDone = (file) {
if (!mounted) return; if (!mounted) return;
setState(() { _isUploading = false; _uploadProgress = 0; }); _uploadProgress.value = 0;
setState(() => _isUploading = false);
_prependFile(file); _prependFile(file);
}; };
mgr.onError = (msg) { mgr.onError = (msg) {
if (!mounted) return; if (!mounted) return;
setState(() { _isUploading = false; _uploadProgress = 0; }); _uploadProgress.value = 0;
setState(() => _isUploading = false);
showCustomNotification(context, 'Ошибка: $msg'); showCustomNotification(context, 'Ошибка: $msg');
}; };
} }
@@ -91,6 +94,7 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
_mode.dispose(); _mode.dispose();
_pageController.dispose(); _pageController.dispose();
_currentFilePage.dispose(); _currentFilePage.dispose();
_uploadProgress.dispose();
super.dispose(); super.dispose();
} }
@@ -218,7 +222,8 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
final picked = result.files.first; final picked = result.files.first;
if (picked.path == null) return; if (picked.path == null) return;
setState(() { _isUploading = true; _uploadProgress = 0; }); _uploadProgress.value = 0;
setState(() => _isUploading = true);
await UploadManager.instance.start( await UploadManager.instance.start(
chatId: chatId, chatId: chatId,
@@ -427,17 +432,27 @@ class _CloudStorageScreenState extends State<CloudStorageScreen>
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
if (_isUploading) ...[ if (_isUploading) ...[
LinearProgressIndicator( ValueListenableBuilder<double>(
value: _uploadProgress, valueListenable: _uploadProgress,
borderRadius: BorderRadius.circular(4), builder: (context, progress, _) => Column(
minHeight: 5, mainAxisSize: MainAxisSize.min,
color: cs.primary, crossAxisAlignment: CrossAxisAlignment.stretch,
backgroundColor: cs.surfaceContainerHighest, children: [
), LinearProgressIndicator(
const SizedBox(height: 8), value: progress,
Text( borderRadius: BorderRadius.circular(4),
'Загрузка ${(_uploadProgress * 100).toStringAsFixed(0)}%', minHeight: 5,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), color: cs.primary,
backgroundColor: cs.surfaceContainerHighest,
),
const SizedBox(height: 8),
Text(
'Загрузка ${(progress * 100).toStringAsFixed(0)}%',
style:
TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
],
),
), ),
] else if (_files.isEmpty) ...[ ] else if (_files.isEmpty) ...[
Text( Text(
@@ -307,33 +307,40 @@ class TwoFactorSetupScreen extends StatefulWidget {
class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> { class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> {
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
final _confirmController = TextEditingController();
final _hintController = TextEditingController(); final _hintController = TextEditingController();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _codeController = TextEditingController(); final _codeController = TextEditingController();
int _step = 0; int _step = 0;
bool _isLoading = false; final ValueNotifier<bool> _isLoading = ValueNotifier(false);
String? _trackId; String? _trackId;
String? _errorMessage; String? _errorMessage;
@override @override
void dispose() { void dispose() {
_passwordController.dispose(); _passwordController.dispose();
_confirmController.dispose();
_hintController.dispose(); _hintController.dispose();
_emailController.dispose(); _emailController.dispose();
_codeController.dispose(); _codeController.dispose();
_isLoading.dispose();
super.dispose(); super.dispose();
} }
Future<void> _nextStep() async { Future<void> _nextStep() async {
setState(() { _isLoading.value = true;
_isLoading = true; setState(() => _errorMessage = null);
_errorMessage = null;
});
try { try {
switch (_step) { switch (_step) {
case 0: case 0:
if (_passwordController.text.length < 6) {
setState(
() => _errorMessage = 'Пароль должен быть минимум 6 символов',
);
break;
}
final trackId = await accountModule.create2faTrack(); final trackId = await accountModule.create2faTrack();
setState(() { setState(() {
_trackId = trackId; _trackId = trackId;
@@ -341,10 +348,8 @@ class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> {
}); });
break; break;
case 1: case 1:
if (_passwordController.text.length < 6) { if (_confirmController.text != _passwordController.text) {
setState( setState(() => _errorMessage = 'Пароли не совпадают');
() => _errorMessage = 'Пароль должен быть минимум 6 символов',
);
break; break;
} }
await accountModule.set2faPassword( await accountModule.set2faPassword(
@@ -392,7 +397,7 @@ class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> {
setState(() => _errorMessage = e.toString()); setState(() => _errorMessage = e.toString());
} finally { } finally {
if (mounted) { if (mounted) {
setState(() => _isLoading = false); _isLoading.value = false;
} }
} }
} }
@@ -458,26 +463,29 @@ class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: ValueListenableBuilder<bool>(
onPressed: _isLoading ? null : _nextStep, valueListenable: _isLoading,
style: FilledButton.styleFrom( builder: (context, loading, _) => FilledButton(
backgroundColor: cs.primary, onPressed: loading ? null : _nextStep,
foregroundColor: cs.onPrimary, style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: cs.primary,
shape: RoundedRectangleBorder( foregroundColor: cs.onPrimary,
borderRadius: BorderRadius.circular(12), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
), ),
child: loading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimary,
),
)
: Text(_step == 4 ? 'Установить пароль' : 'Продолжить'),
), ),
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimary,
),
)
: Text(_step == 4 ? 'Установить пароль' : 'Продолжить'),
), ),
), ),
], ],
@@ -591,7 +599,7 @@ class _TwoFactorSetupScreenState extends State<TwoFactorSetupScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_PasswordField( _PasswordField(
controller: _passwordController, controller: _confirmController,
hintText: 'Повторите пароль', hintText: 'Повторите пароль',
), ),
], ],
@@ -714,7 +722,7 @@ class TwoFactorManageScreen extends StatefulWidget {
class _TwoFactorManageScreenState extends State<TwoFactorManageScreen> { class _TwoFactorManageScreenState extends State<TwoFactorManageScreen> {
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
bool _isLoading = false; final ValueNotifier<bool> _isLoading = ValueNotifier(false);
bool _isAuthenticated = false; bool _isAuthenticated = false;
String? _trackId; String? _trackId;
TwoFactorDetails? _details; TwoFactorDetails? _details;
@@ -723,14 +731,13 @@ class _TwoFactorManageScreenState extends State<TwoFactorManageScreen> {
@override @override
void dispose() { void dispose() {
_passwordController.dispose(); _passwordController.dispose();
_isLoading.dispose();
super.dispose(); super.dispose();
} }
Future<void> _authenticate() async { Future<void> _authenticate() async {
setState(() { _isLoading.value = true;
_isLoading = true; setState(() => _errorMessage = null);
_errorMessage = null;
});
try { try {
_trackId = await accountModule.enter2faPanel(); _trackId = await accountModule.enter2faPanel();
@@ -743,7 +750,7 @@ class _TwoFactorManageScreenState extends State<TwoFactorManageScreen> {
} catch (e) { } catch (e) {
setState(() => _errorMessage = 'Неверный пароль'); setState(() => _errorMessage = 'Неверный пароль');
} finally { } finally {
if (mounted) setState(() => _isLoading = false); if (mounted) _isLoading.value = false;
} }
} }
@@ -807,26 +814,29 @@ class _TwoFactorManageScreenState extends State<TwoFactorManageScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: ValueListenableBuilder<bool>(
onPressed: _isLoading ? null : _authenticate, valueListenable: _isLoading,
style: FilledButton.styleFrom( builder: (context, loading, _) => FilledButton(
backgroundColor: cs.primary, onPressed: loading ? null : _authenticate,
foregroundColor: cs.onPrimary, style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: cs.primary,
shape: RoundedRectangleBorder( foregroundColor: cs.onPrimary,
borderRadius: BorderRadius.circular(12), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
), ),
child: loading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimary,
),
)
: const Text('Продолжить'),
), ),
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimary,
),
)
: const Text('Продолжить'),
), ),
), ),
], ],
@@ -917,13 +927,14 @@ class _TwoFactorPasswordChangeScreenState
extends State<TwoFactorPasswordChangeScreen> { extends State<TwoFactorPasswordChangeScreen> {
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
final _hintController = TextEditingController(); final _hintController = TextEditingController();
bool _isLoading = false; final ValueNotifier<bool> _isLoading = ValueNotifier(false);
String? _errorMessage; String? _errorMessage;
@override @override
void dispose() { void dispose() {
_passwordController.dispose(); _passwordController.dispose();
_hintController.dispose(); _hintController.dispose();
_isLoading.dispose();
super.dispose(); super.dispose();
} }
@@ -933,10 +944,8 @@ class _TwoFactorPasswordChangeScreenState
return; return;
} }
setState(() { _isLoading.value = true;
_isLoading = true; setState(() => _errorMessage = null);
_errorMessage = null;
});
try { try {
final trackId = await accountModule.enter2faPanel(); final trackId = await accountModule.enter2faPanel();
@@ -956,7 +965,7 @@ class _TwoFactorPasswordChangeScreenState
} catch (e) { } catch (e) {
setState(() => _errorMessage = e.toString()); setState(() => _errorMessage = e.toString());
} finally { } finally {
if (mounted) setState(() => _isLoading = false); if (mounted) _isLoading.value = false;
} }
} }
@@ -1037,26 +1046,29 @@ class _TwoFactorPasswordChangeScreenState
const SizedBox(height: 24), const SizedBox(height: 24),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: ValueListenableBuilder<bool>(
onPressed: _isLoading ? null : _changePassword, valueListenable: _isLoading,
style: FilledButton.styleFrom( builder: (context, loading, _) => FilledButton(
backgroundColor: cs.primary, onPressed: loading ? null : _changePassword,
foregroundColor: cs.onPrimary, style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: cs.primary,
shape: RoundedRectangleBorder( foregroundColor: cs.onPrimary,
borderRadius: BorderRadius.circular(12), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
), ),
child: loading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimary,
),
)
: const Text('Сохранить'),
), ),
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimary,
),
)
: const Text('Сохранить'),
), ),
), ),
], ],
@@ -1080,7 +1092,7 @@ class _TwoFactorEmailChangeScreenState
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _codeController = TextEditingController(); final _codeController = TextEditingController();
int _step = 0; int _step = 0;
bool _isLoading = false; final ValueNotifier<bool> _isLoading = ValueNotifier(false);
String? _trackId; String? _trackId;
String? _errorMessage; String? _errorMessage;
@@ -1089,14 +1101,13 @@ class _TwoFactorEmailChangeScreenState
_passwordController.dispose(); _passwordController.dispose();
_emailController.dispose(); _emailController.dispose();
_codeController.dispose(); _codeController.dispose();
_isLoading.dispose();
super.dispose(); super.dispose();
} }
Future<void> _nextStep() async { Future<void> _nextStep() async {
setState(() { _isLoading.value = true;
_isLoading = true; setState(() => _errorMessage = null);
_errorMessage = null;
});
try { try {
switch (_step) { switch (_step) {
@@ -1140,7 +1151,7 @@ class _TwoFactorEmailChangeScreenState
} catch (e) { } catch (e) {
setState(() => _errorMessage = e.toString()); setState(() => _errorMessage = e.toString());
} finally { } finally {
if (mounted) setState(() => _isLoading = false); if (mounted) _isLoading.value = false;
} }
} }
@@ -1256,26 +1267,29 @@ class _TwoFactorEmailChangeScreenState
const SizedBox(height: 24), const SizedBox(height: 24),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: ValueListenableBuilder<bool>(
onPressed: _isLoading ? null : _nextStep, valueListenable: _isLoading,
style: FilledButton.styleFrom( builder: (context, loading, _) => FilledButton(
backgroundColor: cs.primary, onPressed: loading ? null : _nextStep,
foregroundColor: cs.onPrimary, style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: cs.primary,
shape: RoundedRectangleBorder( foregroundColor: cs.onPrimary,
borderRadius: BorderRadius.circular(12), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
), ),
child: loading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimary,
),
)
: Text(_step == 2 ? 'Сохранить' : 'Продолжить'),
), ),
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onPrimary,
),
)
: Text(_step == 2 ? 'Сохранить' : 'Продолжить'),
), ),
), ),
], ],
@@ -1294,20 +1308,19 @@ class TwoFactorRemoveScreen extends StatefulWidget {
class _TwoFactorRemoveScreenState extends State<TwoFactorRemoveScreen> { class _TwoFactorRemoveScreenState extends State<TwoFactorRemoveScreen> {
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
bool _isLoading = false; final ValueNotifier<bool> _isLoading = ValueNotifier(false);
String? _errorMessage; String? _errorMessage;
@override @override
void dispose() { void dispose() {
_passwordController.dispose(); _passwordController.dispose();
_isLoading.dispose();
super.dispose(); super.dispose();
} }
Future<void> _remove2fa() async { Future<void> _remove2fa() async {
setState(() { _isLoading.value = true;
_isLoading = true; setState(() => _errorMessage = null);
_errorMessage = null;
});
try { try {
final trackId = await accountModule.enter2faPanel(); final trackId = await accountModule.enter2faPanel();
@@ -1323,7 +1336,7 @@ class _TwoFactorRemoveScreenState extends State<TwoFactorRemoveScreen> {
} catch (e) { } catch (e) {
setState(() => _errorMessage = e.toString()); setState(() => _errorMessage = e.toString());
} finally { } finally {
if (mounted) setState(() => _isLoading = false); if (mounted) _isLoading.value = false;
} }
} }
@@ -1402,26 +1415,29 @@ class _TwoFactorRemoveScreenState extends State<TwoFactorRemoveScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: ValueListenableBuilder<bool>(
onPressed: _isLoading ? null : _remove2fa, valueListenable: _isLoading,
style: FilledButton.styleFrom( builder: (context, loading, _) => FilledButton(
backgroundColor: cs.error, onPressed: loading ? null : _remove2fa,
foregroundColor: cs.onError, style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: cs.error,
shape: RoundedRectangleBorder( foregroundColor: cs.onError,
borderRadius: BorderRadius.circular(12), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
), ),
child: loading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onError,
),
)
: const Text('Удалить пароль'),
), ),
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onError,
),
)
: const Text('Удалить пароль'),
), ),
), ),
], ],
+55 -63
View File
@@ -50,6 +50,10 @@ class MessageBubble extends StatelessWidget {
static const Radius _smallRadius = Radius.circular(4); static const Radius _smallRadius = Radius.circular(4);
static const Radius _photoRadius = Radius.circular(photoBorderRadius); static const Radius _photoRadius = Radius.circular(photoBorderRadius);
static final Color _reactionChipBg = Colors.black.withValues(alpha: 0.18);
static const BorderRadius _reactionChipRadius =
BorderRadius.all(Radius.circular(10));
static Color bubbleTextColor(BuildContext context) => static Color bubbleTextColor(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark Theme.of(context).brightness == Brightness.dark
? Colors.white ? Colors.white
@@ -271,14 +275,30 @@ class MessageBubble extends StatelessWidget {
final showAvatarSlot = !isMe; final showAvatarSlot = !isMe;
final showAvatar = showAvatarSlot && final showAvatar = showAvatarSlot &&
chatType == "CHAT" && chatType == "CHAT" &&
nextMessage?.senderId != message.senderId && nextMessage?.senderId != message.senderId;
prevMessage?.senderId == message.senderId;
final bubbleListenable = Listenable.merge([ final maxBubbleWidth = MediaQuery.sizeOf(context).width * 0.75;
AppBubbleShape.current, final bubbleColor =
AppBubbleBehavior.current, isMe ? cs.primaryContainer : cs.surfaceContainerHighest;
?reactionsListenable,
]); _BubbleCtx makeCtx() => _BubbleCtx(
context: context,
cs: cs,
text: textColor,
shape: shape,
contentType: contentType,
hasPhotoWithCaption: hasPhotoCap,
hasMultiplePhotosNoCaption: hasMultiPhotos,
reactionInfo: _resolveReactionInfo(),
);
final Widget bubbleContent =
reactionsListenable != null && contentType == MessageType.text
? ValueListenableBuilder<Map<String, dynamic>?>(
valueListenable: reactionsListenable!,
builder: (context, _, _) => _buildContent(makeCtx()),
)
: _buildContent(makeCtx());
return GestureDetector( return GestureDetector(
onTap: Haptics.tap, onTap: Haptics.tap,
@@ -310,63 +330,35 @@ class MessageBubble extends StatelessWidget {
isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [ children: [
ListenableBuilder( ListenableBuilder(
listenable: bubbleListenable, listenable: Listenable.merge([
builder: (context, _) { AppBubbleShape.current,
final reactionInfo = _resolveReactionInfo(); AppBubbleBehavior.current,
final ctx = _BubbleCtx( ]),
context: context, builder: (context, child) => Container(
cs: cs, constraints: BoxConstraints(maxWidth: maxBubbleWidth),
text: textColor, decoration: BoxDecoration(
shape: shape, color: bubbleColor,
contentType: contentType, borderRadius: _borderRadiusFor(
hasPhotoWithCaption: hasPhotoCap, AppBubbleShape.current.value,
hasMultiplePhotosNoCaption: hasMultiPhotos, AppBubbleBehavior.current.value,
reactionInfo: reactionInfo, shape,
); hasPhotoCap,
return AnimatedSize( hasMultiPhotos,
duration: const Duration(milliseconds: 150),
curve: Curves.easeOutCubic,
alignment: isMe
? Alignment.bottomRight
: Alignment.bottomLeft,
child: Container(
constraints: BoxConstraints(
maxWidth:
MediaQuery.sizeOf(context).width * 0.75,
),
decoration: BoxDecoration(
color: isMe
? cs.primaryContainer
: cs.surfaceContainerHighest,
borderRadius: _borderRadiusFor(
AppBubbleShape.current.value,
AppBubbleBehavior.current.value,
shape,
hasPhotoCap,
hasMultiPhotos,
),
),
padding: padding,
child: _buildContent(ctx),
), ),
); ),
}, padding: padding,
child: child,
),
child: bubbleContent,
), ),
if (contentType != MessageType.text) if (contentType != MessageType.text)
AnimatedSize( reactionsListenable != null
duration: const Duration(milliseconds: 150), ? ValueListenableBuilder<Map<String, dynamic>?>(
curve: Curves.easeOutCubic, valueListenable: reactionsListenable!,
alignment: isMe builder: (context, info, _) =>
? Alignment.centerRight _buildReactionsBarFor(cs, info),
: Alignment.centerLeft, )
child: reactionsListenable != null : _buildReactionsBar(cs),
? ValueListenableBuilder<Map<String, dynamic>?>(
valueListenable: reactionsListenable!,
builder: (context, info, _) =>
_buildReactionsBarFor(cs, info),
)
: _buildReactionsBar(cs),
),
], ],
), ),
], ],
@@ -432,8 +424,8 @@ class MessageBubble extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: isYours color: isYours
? cs.primary.withValues(alpha: 0.22) ? cs.primary.withValues(alpha: 0.22)
: Colors.black.withValues(alpha: 0.18), : _reactionChipBg,
borderRadius: BorderRadius.circular(10), borderRadius: _reactionChipRadius,
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
+28 -13
View File
@@ -66,18 +66,33 @@ void main() async {
} }
attachInfoCacheApi(api); attachInfoCacheApi(api);
ChatsModule.attachGlobalPushHandlers(api); ChatsModule.attachGlobalPushHandlers(api);
final packageInfoFuture = PackageInfo.fromPlatform();
final localeFuture = _loadInitialLocale();
final hapticsFuture = Haptics.load();
final prefsFuture = SharedPreferences.getInstance();
final accentFuture = AppAccent.load();
final bubbleShapeFuture = AppBubbleShape.load();
final bubbleBehaviorFuture = AppBubbleBehavior.load();
final cacheExtentFuture = AppCacheExtent.load();
final themeModeFuture = AppThemeModeConfig.load();
final amoledFuture = AppAmoled.load();
final themeScheduleFuture = AppThemeSchedule.load();
final messageActionsFuture = AppMessageActionsStyle.load();
final swipeBackFuture = AppSwipeBackDesktop.load();
await api.connect(); await api.connect();
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await packageInfoFuture;
if (packageInfo.packageName == 'ru.oneme.app') { if (packageInfo.packageName == 'ru.oneme.app') {
await PushService.instance.init(api: api, account: accountModule); await PushService.instance.init(api: api, account: accountModule);
} }
final initialLocale = await _loadInitialLocale(); final initialLocale = await localeFuture;
await Haptics.load(); await hapticsFuture;
final prefs = await SharedPreferences.getInstance(); final prefs = await prefsFuture;
await FileHistoryCache.load(prefs); await FileHistoryCache.load(prefs);
final initialFpsOverlay = prefs.getBool('dev_fps_overlay') ?? false; final initialFpsOverlay = prefs.getBool('dev_fps_overlay') ?? false;
final initialVpnBypass = prefs.getBool(VpnBypassService.prefKey) ?? false; final initialVpnBypass = prefs.getBool(VpnBypassService.prefKey) ?? false;
@@ -87,15 +102,15 @@ void main() async {
final initialFontScale = AppFonts.clampScale( final initialFontScale = AppFonts.clampScale(
prefs.getDouble(AppFonts.scalePrefKey) ?? AppFonts.defaultScale, prefs.getDouble(AppFonts.scalePrefKey) ?? AppFonts.defaultScale,
); );
final initialAccentSeed = await AppAccent.load(); final initialAccentSeed = await accentFuture;
AppBubbleShape.current.value = await AppBubbleShape.load(); AppBubbleShape.current.value = await bubbleShapeFuture;
AppBubbleBehavior.current.value = await AppBubbleBehavior.load(); AppBubbleBehavior.current.value = await bubbleBehaviorFuture;
AppCacheExtent.current.value = await AppCacheExtent.load(); AppCacheExtent.current.value = await cacheExtentFuture;
AppThemeModeConfig.current.value = await AppThemeModeConfig.load(); AppThemeModeConfig.current.value = await themeModeFuture;
AppAmoled.current.value = await AppAmoled.load(); AppAmoled.current.value = await amoledFuture;
AppThemeSchedule.current.value = await AppThemeSchedule.load(); AppThemeSchedule.current.value = await themeScheduleFuture;
AppMessageActionsStyle.current.value = await AppMessageActionsStyle.load(); AppMessageActionsStyle.current.value = await messageActionsFuture;
AppSwipeBackDesktop.current.value = await AppSwipeBackDesktop.load(); AppSwipeBackDesktop.current.value = await swipeBackFuture;
runApp( runApp(
KometApp( KometApp(
initialLocale: initialLocale, initialLocale: initialLocale,