просто босс, просто начальник нахуй оптимизации
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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('Удалить пароль'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user