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

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