нахуевёртил
This commit is contained in:
@@ -30,6 +30,8 @@ class Api {
|
||||
|
||||
SessionState _sessionState = SessionState.disconnected;
|
||||
final _stateController = StreamController<SessionState>.broadcast();
|
||||
final _sessionExpiredController =
|
||||
StreamController<SessionExpiredException>.broadcast();
|
||||
Map<dynamic, dynamic>? _userAgent;
|
||||
|
||||
Map<dynamic, dynamic>? get userAgent => _userAgent;
|
||||
@@ -40,6 +42,8 @@ class Api {
|
||||
_registrationCountries ?? allCountries;
|
||||
|
||||
Stream<SessionState> get stateStream => _stateController.stream;
|
||||
Stream<SessionExpiredException> get sessionExpiredStream =>
|
||||
_sessionExpiredController.stream;
|
||||
SessionState get state => _sessionState;
|
||||
|
||||
StreamSubscription<Uint8List>? _dataSubscription;
|
||||
@@ -195,6 +199,7 @@ class Api {
|
||||
_dispatcher.dispose();
|
||||
_connection.dispose();
|
||||
_stateController.close();
|
||||
_sessionExpiredController.close();
|
||||
}
|
||||
|
||||
// Внутрянка
|
||||
@@ -208,6 +213,13 @@ class Api {
|
||||
|
||||
Future<void> _onDataReceived(Uint8List data) async {
|
||||
await for (final packet in _receiver.feed(data)) {
|
||||
if (packet.isError &&
|
||||
packet.payload is Map &&
|
||||
packet.payload['message'] == 'FAIL_LOGIN_TOKEN') {
|
||||
_sessionExpiredController.add(
|
||||
SessionExpiredException(messageFromErrorPayload(packet.payload)),
|
||||
);
|
||||
}
|
||||
_dispatcher.dispatch(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import '../api.dart';
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
import '../../core/protocol/packet.dart';
|
||||
@@ -17,6 +18,8 @@ enum AuthRequestType {
|
||||
final String value;
|
||||
}
|
||||
|
||||
enum LoginStatus { idle, loading, success, error }
|
||||
|
||||
class RequestCodeResult {
|
||||
final String token;
|
||||
|
||||
@@ -39,10 +42,8 @@ class VerifyCodeResult {
|
||||
return c is Map ? c.cast<dynamic, dynamic>() : null;
|
||||
}
|
||||
|
||||
/// trackId из passwordChallenge — передаётся в [AccountModule.checkPassword].
|
||||
String? get challengeTrackId => passwordChallenge?['trackId'] as String?;
|
||||
|
||||
/// Подсказка к паролю из passwordChallenge.
|
||||
String? get challengeHint => passwordChallenge?['hint'] as String?;
|
||||
|
||||
int? get accountId {
|
||||
@@ -68,8 +69,6 @@ class TwoFactorResult {
|
||||
const TwoFactorResult({required this.loginToken});
|
||||
}
|
||||
|
||||
/// При отсутствии [LoginSyncParams] в [AccountModule.login] сервер вернёт
|
||||
/// полный снимок данных (cold start), иначе только дельту (warm start).
|
||||
class LoginSyncParams {
|
||||
final int chatsSync;
|
||||
final int contactsSync;
|
||||
@@ -131,7 +130,9 @@ class SessionInfo {
|
||||
|
||||
factory SessionInfo.fromMap(Map<dynamic, dynamic> map) {
|
||||
return SessionInfo(
|
||||
id: map['id'],
|
||||
id: map['id'] is int
|
||||
? map['id']
|
||||
: (int.tryParse(map['id']?.toString() ?? '')),
|
||||
client: map['client'] ?? '',
|
||||
location: map['location'] ?? '',
|
||||
current: map['current'] ?? false,
|
||||
@@ -139,6 +140,23 @@ class SessionInfo {
|
||||
info: map['info'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
int get uniqueId => Object.hash(id, client, time, info);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SessionInfo &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
client == other.client &&
|
||||
location == other.location &&
|
||||
current == other.current &&
|
||||
time == other.time &&
|
||||
info == other.info;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, client, location, current, time, info);
|
||||
}
|
||||
|
||||
class LoginResult {
|
||||
@@ -157,9 +175,12 @@ class LoginResult {
|
||||
|
||||
class AccountModule {
|
||||
final Api _api;
|
||||
final _loginStatusController = StreamController<LoginStatus>.broadcast();
|
||||
|
||||
AccountModule(this._api);
|
||||
|
||||
Stream<LoginStatus> get loginStatusStream => _loginStatusController.stream;
|
||||
|
||||
Future<RequestCodeResult> requestCode(
|
||||
String phone, {
|
||||
String language = 'ru',
|
||||
@@ -200,7 +221,6 @@ class AccountModule {
|
||||
if (sessionToken != null && accountId != null) {
|
||||
await TokenStorage.saveToken(sessionToken, accountId);
|
||||
await TokenStorage.setActiveAccount(accountId);
|
||||
logger.i('Токен аккаунта $accountId сохранён, установлен активным');
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -226,19 +246,27 @@ class AccountModule {
|
||||
|
||||
final requestPayload = _buildLoginPayload(authToken, syncParams);
|
||||
|
||||
final packet = await _api.sendRequest(Opcode.login, requestPayload);
|
||||
_loginStatusController.add(LoginStatus.loading);
|
||||
try {
|
||||
final packet = await _api.sendRequest(Opcode.login, requestPayload);
|
||||
|
||||
_checkPacketError(packet, 'login');
|
||||
_checkPacketError(packet, 'login');
|
||||
|
||||
final data = packet.payload;
|
||||
if (data is! Map) {
|
||||
throw Exception('login: неожиданный тип payload: ${data.runtimeType}');
|
||||
final data = packet.payload;
|
||||
if (data is! Map) {
|
||||
throw Exception('login: неожиданный тип payload: ${data.runtimeType}');
|
||||
}
|
||||
|
||||
final result = await _processLoginResponse(
|
||||
data.cast<dynamic, dynamic>(),
|
||||
resolvedAccountId,
|
||||
);
|
||||
_loginStatusController.add(LoginStatus.success);
|
||||
return result;
|
||||
} catch (e) {
|
||||
_loginStatusController.add(LoginStatus.error);
|
||||
rethrow;
|
||||
}
|
||||
|
||||
return _processLoginResponse(
|
||||
data.cast<dynamic, dynamic>(),
|
||||
resolvedAccountId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<SessionInfo>> getSessions() async {
|
||||
@@ -278,13 +306,6 @@ class AccountModule {
|
||||
logger.i('Аккаунт $accountId удалён локально');
|
||||
}
|
||||
|
||||
/// Проверяет 2FA-пароль (opcode 115).
|
||||
///
|
||||
/// [trackId] — из [VerifyCodeResult.challengeTrackId].
|
||||
/// [accountId] — из [VerifyCodeResult.accountId].
|
||||
///
|
||||
/// При неверном пароле бросает [Exception].
|
||||
/// При успехе сохраняет токен и устанавливает аккаунт активным.
|
||||
Future<TwoFactorResult> checkPassword({
|
||||
required String password,
|
||||
required String trackId,
|
||||
@@ -487,7 +508,11 @@ class AccountModule {
|
||||
|
||||
void _checkPacketError(Packet packet, String method) {
|
||||
if (packet.isError) {
|
||||
throw PacketError(messageFromErrorPayload(packet.payload));
|
||||
final payload = packet.payload;
|
||||
if (payload is Map && payload['message'] == 'FAIL_LOGIN_TOKEN') {
|
||||
throw SessionExpiredException(messageFromErrorPayload(payload));
|
||||
}
|
||||
throw PacketError(messageFromErrorPayload(payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,8 +137,6 @@ class ChatsModule {
|
||||
static Future<void> clearCache(int accountId) =>
|
||||
AppDatabase.clearChatsCache(accountId);
|
||||
|
||||
// internal
|
||||
|
||||
static Map<int, Map<dynamic, dynamic>> _buildContactsMap(dynamic contacts) {
|
||||
if (contacts is! List) return {};
|
||||
final result = <int, Map<dynamic, dynamic>>{};
|
||||
|
||||
@@ -59,6 +59,10 @@ class PacketError implements Exception {
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class SessionExpiredException extends PacketError {
|
||||
const SessionExpiredException(super.message);
|
||||
}
|
||||
|
||||
String messageFromErrorPayload(dynamic payload) {
|
||||
if (payload is Map) {
|
||||
for (final key in ['localizedMessage', 'message', 'title']) {
|
||||
|
||||
@@ -68,9 +68,13 @@ class PacketDispatcher {
|
||||
}
|
||||
|
||||
if (packet.isError) {
|
||||
completer.completeError(
|
||||
PacketError(messageFromErrorPayload(packet.payload)),
|
||||
);
|
||||
final message = messageFromErrorPayload(packet.payload);
|
||||
if (packet.payload is Map &&
|
||||
packet.payload['message'] == 'FAIL_LOGIN_TOKEN') {
|
||||
completer.completeError(SessionExpiredException(message));
|
||||
} else {
|
||||
completer.completeError(PacketError(message));
|
||||
}
|
||||
} else {
|
||||
completer.complete(packet);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,31 @@ import '../../../backend/modules/chats.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../main.dart' show api;
|
||||
|
||||
class _StoriesScrollPhysics extends BouncingScrollPhysics {
|
||||
final bool Function() blockPositive;
|
||||
|
||||
const _StoriesScrollPhysics({
|
||||
required this.blockPositive,
|
||||
ScrollPhysics? parent,
|
||||
}) : super(parent: parent);
|
||||
|
||||
@override
|
||||
_StoriesScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return _StoriesScrollPhysics(
|
||||
blockPositive: blockPositive,
|
||||
parent: buildParent(ancestor),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double applyBoundaryConditions(ScrollMetrics position, double value) {
|
||||
if (blockPositive() && value > 0.0) {
|
||||
return value - max(0.0, position.pixels);
|
||||
}
|
||||
return super.applyBoundaryConditions(position, value);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatListScreen extends StatefulWidget {
|
||||
const ChatListScreen({super.key});
|
||||
|
||||
@@ -62,13 +87,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
_selectedChats.add(chatId);
|
||||
}
|
||||
|
||||
if (_isSelectionMode) {
|
||||
if (_scrollController.hasClients && _scrollController.offset < 132) {
|
||||
_shouldCollapseSearch = true;
|
||||
}
|
||||
} else {
|
||||
_shouldCollapseSearch = false;
|
||||
}
|
||||
_shouldCollapseSearch = _isSelectionMode;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,6 +99,20 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
}
|
||||
|
||||
bool _isInitialLoading = true;
|
||||
DateTime _storiesLockdownUntil = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
bool _shouldBlockPositiveScroll() {
|
||||
if (_pullRatio > 0 ||
|
||||
_storiesDockedOpen ||
|
||||
_storiesRevealController.isAnimating) {
|
||||
return true;
|
||||
}
|
||||
if (DateTime.now().isBefore(_storiesLockdownUntil)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
late AnimationController _shimmerController;
|
||||
|
||||
@override
|
||||
@@ -89,13 +122,14 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
);
|
||||
_navPageAnimController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
value: 1.0,
|
||||
)..addListener(() {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
_navPageAnimController =
|
||||
AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
value: 1.0,
|
||||
)..addListener(() {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
_shimmerController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
@@ -272,6 +306,9 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
return;
|
||||
}
|
||||
if (_storiesAnimClosing && _storiesRevealController.isAnimating) return;
|
||||
_storiesLockdownUntil = DateTime.now().add(
|
||||
const Duration(milliseconds: 800),
|
||||
);
|
||||
_storiesRevealController.stop();
|
||||
_storiesAnimClosing = true;
|
||||
final from = _pullRatio.clamp(0.0, 1.0);
|
||||
@@ -293,8 +330,20 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
|
||||
bool _onStoriesScrollNotification(ScrollNotification n) {
|
||||
if (_currentNavIndex != 0) return false;
|
||||
if (n is! ScrollUpdateNotification) return false;
|
||||
if (!_scrollController.hasClients) return false;
|
||||
|
||||
if (n is OverscrollNotification && n.overscroll > 0) {
|
||||
if ((_storiesDockedOpen ||
|
||||
_storiesRevealController.isAnimating ||
|
||||
_pullRatio > 0) &&
|
||||
!_storiesAnimClosing &&
|
||||
DateTime.now().isAfter(_storiesRevealLayoutSettleUntil)) {
|
||||
_startStoriesAutoClose();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (n is! ScrollUpdateNotification) return false;
|
||||
if (!_storiesDockedOpen || _storiesRevealController.isAnimating) {
|
||||
return false;
|
||||
}
|
||||
@@ -372,13 +421,14 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
required double Function(int index) bubbleLeftForIndex,
|
||||
}) {
|
||||
if (_navDragging) {
|
||||
final left = (_navDragBaseLeft + _navDragDx)
|
||||
.clamp(bubbleLeftForIndex(0), bubbleLeftForIndex(3));
|
||||
final left = (_navDragBaseLeft + _navDragDx).clamp(
|
||||
bubbleLeftForIndex(0),
|
||||
bubbleLeftForIndex(3),
|
||||
);
|
||||
return ((left - 4) / inactiveWidth).clamp(0.0, 3.0);
|
||||
}
|
||||
if (_navPageAnimController.isAnimating) {
|
||||
final t =
|
||||
Curves.easeOutCubic.transform(_navPageAnimController.value);
|
||||
final t = Curves.easeOutCubic.transform(_navPageAnimController.value);
|
||||
return ui.lerpDouble(_navPageAnimStart, _navPageAnimEnd, t)!;
|
||||
}
|
||||
return _currentNavIndex.toDouble();
|
||||
@@ -390,8 +440,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
}
|
||||
double fromT;
|
||||
if (_navPageAnimController.isAnimating) {
|
||||
final t =
|
||||
Curves.easeOutCubic.transform(_navPageAnimController.value);
|
||||
final t = Curves.easeOutCubic.transform(_navPageAnimController.value);
|
||||
fromT = ui.lerpDouble(_navPageAnimStart, _navPageAnimEnd, t)!;
|
||||
} else {
|
||||
fromT = _currentNavIndex.toDouble();
|
||||
@@ -428,7 +477,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: Alignment.topCenter,
|
||||
child: _shouldCollapseSearch
|
||||
? const SizedBox(width: double.infinity, height: 0)
|
||||
? const SizedBox(width: double.infinity, height: 52)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -667,8 +716,17 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
|
||||
Widget _buildChatsTabBody() {
|
||||
return Listener(
|
||||
onPointerDown: (_) {
|
||||
_storiesLockdownUntil = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
},
|
||||
onPointerSignal: (pointerSignal) {
|
||||
if (pointerSignal is PointerScrollEvent) {
|
||||
if (_shouldBlockPositiveScroll() &&
|
||||
pointerSignal.scrollDelta.dy > 0) {
|
||||
_storiesLockdownUntil = DateTime.now().add(
|
||||
const Duration(milliseconds: 300),
|
||||
);
|
||||
}
|
||||
if (_scrollController.hasClients && _scrollController.offset <= 0) {
|
||||
if (pointerSignal.scrollDelta.dy < 0) {
|
||||
_startStoriesAutoReveal(max(_pullRatio, 0.18));
|
||||
@@ -687,37 +745,32 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
physics: _StoriesScrollPhysics(
|
||||
blockPositive: _shouldBlockPositiveScroll,
|
||||
parent: const AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (_isInitialLoading) {
|
||||
return _buildChatShimmer();
|
||||
}
|
||||
final chat = _chats[index];
|
||||
return _buildChatItem(
|
||||
chat.id.toString(),
|
||||
chat.title ?? 'Чат',
|
||||
chat.lastMsgText ?? '',
|
||||
_formatTime(chat.lastMsgTime),
|
||||
(chat.iconUrl != null && chat.iconUrl!.isNotEmpty)
|
||||
? chat.iconUrl!
|
||||
: '',
|
||||
isOnline: chat.isOnline,
|
||||
unreadCount: chat.unreadCount,
|
||||
isMuted: chat.dontDisturbUntil > 0,
|
||||
);
|
||||
},
|
||||
childCount:
|
||||
_isInitialLoading ? 10 : _chats.length,
|
||||
),
|
||||
),
|
||||
const SliverPadding(
|
||||
padding: EdgeInsets.only(bottom: 100),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
if (_isInitialLoading) {
|
||||
return _buildChatShimmer();
|
||||
}
|
||||
final chat = _chats[index];
|
||||
return _buildChatItem(
|
||||
chat.id.toString(),
|
||||
chat.title ?? 'Чат',
|
||||
chat.lastMsgText ?? '',
|
||||
_formatTime(chat.lastMsgTime),
|
||||
(chat.iconUrl != null && chat.iconUrl!.isNotEmpty)
|
||||
? chat.iconUrl!
|
||||
: '',
|
||||
isOnline: chat.isOnline,
|
||||
unreadCount: chat.unreadCount,
|
||||
isMuted: chat.dontDisturbUntil > 0,
|
||||
);
|
||||
}, childCount: _isInitialLoading ? 10 : _chats.length),
|
||||
),
|
||||
const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -747,12 +800,10 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
final maxBubbleLeft = bubbleLeftForIndex(3);
|
||||
|
||||
final bubbleLeft = _navDragging
|
||||
? (_navDragBaseLeft + _navDragDx)
|
||||
.clamp(minBubbleLeft, maxBubbleLeft)
|
||||
? (_navDragBaseLeft + _navDragDx).clamp(minBubbleLeft, maxBubbleLeft)
|
||||
: leftOffset;
|
||||
|
||||
final navRowT =
|
||||
((bubbleLeft - 4) / inactiveWidth).clamp(0.0, 3.0);
|
||||
final navRowT = ((bubbleLeft - 4) / inactiveWidth).clamp(0.0, 3.0);
|
||||
|
||||
double navInterpolatedWidth(int tabIndex, double rowT) {
|
||||
final rt = rowT.clamp(0.0, 3.0);
|
||||
@@ -820,8 +871,10 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
},
|
||||
onHorizontalDragEnd: (_) {
|
||||
if (!_navDragging) return;
|
||||
final left = (_navDragBaseLeft + _navDragDx)
|
||||
.clamp(minBubbleLeft, maxBubbleLeft);
|
||||
final left = (_navDragBaseLeft + _navDragDx).clamp(
|
||||
minBubbleLeft,
|
||||
maxBubbleLeft,
|
||||
);
|
||||
final next = indexForBubbleLeft(left);
|
||||
setState(() {
|
||||
_currentNavIndex = next;
|
||||
@@ -886,8 +939,8 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
width: _navDragging
|
||||
? navInterpolatedWidth(index, navRowT)
|
||||
: (isSelected
|
||||
? (activeWidth - 0.5)
|
||||
: (inactiveWidth - 0.5)),
|
||||
? (activeWidth - 0.5)
|
||||
: (inactiveWidth - 0.5)),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
child: _buildNavItem(
|
||||
@@ -939,7 +992,8 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
bubbleLeftForIndex: bubbleLeftForPageT,
|
||||
);
|
||||
|
||||
final showChatsFab = !_isSelectionMode &&
|
||||
final showChatsFab =
|
||||
!_isSelectionMode &&
|
||||
(_navDragging || _navPageAnimController.isAnimating
|
||||
? pageDisplayT < 1.0
|
||||
: _currentNavIndex == 0);
|
||||
@@ -950,10 +1004,10 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
child: SizedBox(
|
||||
width: pageW,
|
||||
height: pageH,
|
||||
child: UnconstrainedBox(
|
||||
constrainedAxis: Axis.vertical,
|
||||
child: OverflowBox(
|
||||
alignment: Alignment.topLeft,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
maxWidth: pageW * 4,
|
||||
maxHeight: pageH,
|
||||
child: SizedBox(
|
||||
width: pageW * 4,
|
||||
height: pageH,
|
||||
@@ -1013,7 +1067,9 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
onTap: _toggleFab,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: val * 0.2),
|
||||
color: Colors.black.withValues(
|
||||
alpha: val * 0.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1066,7 +1122,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 64,
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
@@ -1360,12 +1416,13 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
bool instant = false,
|
||||
}) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final bool isSelected =
|
||||
selectedOverride ?? (_currentNavIndex == index);
|
||||
final Duration animDur =
|
||||
instant ? Duration.zero : const Duration(milliseconds: 350);
|
||||
final Duration opacityDur =
|
||||
instant ? Duration.zero : const Duration(milliseconds: 200);
|
||||
final bool isSelected = selectedOverride ?? (_currentNavIndex == index);
|
||||
final Duration animDur = instant
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 350);
|
||||
final Duration opacityDur = instant
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 200);
|
||||
return GestureDetector(
|
||||
onTap: () => _onNavTabSelected(index),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
|
||||
@@ -252,7 +252,7 @@ class _DevicesScreenState extends State<DevicesScreen>
|
||||
(session) => _buildDeviceItem(
|
||||
context,
|
||||
cs,
|
||||
id: session.id ?? 0,
|
||||
id: session.uniqueId,
|
||||
title: session.client + (session.current ? ' (текущая)' : ''),
|
||||
platform: session.info,
|
||||
location: session.location,
|
||||
|
||||
+29
-5
@@ -8,8 +8,10 @@ import 'backend/modules/account.dart';
|
||||
import 'backend/modules/messages.dart';
|
||||
import 'core/storage/app_database.dart';
|
||||
import 'core/storage/token_storage.dart';
|
||||
import 'core/protocol/packet.dart';
|
||||
import 'frontend/screens/auth/login_screen.dart';
|
||||
import 'frontend/screens/chats/chat_list_screen.dart';
|
||||
import 'frontend/widgets/custom_notification.dart';
|
||||
|
||||
final api = Api();
|
||||
final accountModule = AccountModule(api);
|
||||
@@ -40,6 +42,7 @@ class KometApp extends StatefulWidget {
|
||||
const KometApp({super.key, required this.initialLocale});
|
||||
|
||||
final Locale initialLocale;
|
||||
static final navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
static KometAppState? stateOf(BuildContext context) {
|
||||
return context.findAncestorStateOfType<KometAppState>();
|
||||
@@ -53,11 +56,35 @@ class KometAppState extends State<KometApp> {
|
||||
static const _fallbackSeed = Color(0xFFC1C4FF);
|
||||
|
||||
late Locale _locale;
|
||||
bool _isLoggingOut = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_locale = widget.initialLocale;
|
||||
api.sessionExpiredStream.listen((SessionExpiredException e) async {
|
||||
if (_isLoggingOut) return;
|
||||
_isLoggingOut = true;
|
||||
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId != null) {
|
||||
await accountModule.removeAccount(accountId);
|
||||
}
|
||||
|
||||
final navState = KometApp.navigatorKey.currentState;
|
||||
if (navState != null) {
|
||||
final overlayContext = navState.overlay?.context;
|
||||
if (overlayContext != null) {
|
||||
showCustomNotification(overlayContext, e.message);
|
||||
}
|
||||
|
||||
await navState.pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
_isLoggingOut = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> applyLocale(Locale locale) async {
|
||||
@@ -114,6 +141,7 @@ class KometAppState extends State<KometApp> {
|
||||
colorScheme: darkScheme,
|
||||
textTheme: GoogleFonts.interTextTheme(ThemeData.dark().textTheme),
|
||||
),
|
||||
navigatorKey: KometApp.navigatorKey,
|
||||
home: const _StartupScreen(),
|
||||
);
|
||||
},
|
||||
@@ -151,11 +179,7 @@ class _StartupScreenState extends State<_StartupScreen> {
|
||||
|
||||
try {
|
||||
await accountModule.login(accountId: accountId);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Background auto-login failed (safe to ignore if offline): $e',
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _goToLogin() {
|
||||
|
||||
+8
-8
@@ -21,10 +21,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.4.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -313,18 +313,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.13.0"
|
||||
material_symbols_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -614,10 +614,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
version: "0.7.10"
|
||||
timezone:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user