fix: щавель виноград, ебырь омайгад
This commit is contained in:
@@ -68,6 +68,9 @@ class Api {
|
||||
Timer? _reconnectTimer;
|
||||
int _reconnectAttempts = 0;
|
||||
bool _autoReconnect = false;
|
||||
int _sessionEpoch = 0;
|
||||
|
||||
int get sessionEpoch => _sessionEpoch;
|
||||
|
||||
/// Залипает на время сессии: VPN-путь не сработал — идём мимо туннеля.
|
||||
bool _bypassActive = false;
|
||||
@@ -125,6 +128,7 @@ class Api {
|
||||
_callsSeed = response.payload['callsSeed'] as int?;
|
||||
_registrationCountries = _parseRegistrationCountries(response.payload);
|
||||
_sessionState = SessionState.online;
|
||||
_sessionEpoch++;
|
||||
_startPinging();
|
||||
logger.i('Сессия онлайн, хэндшейк ок');
|
||||
if (_onReconnectCallback != null) {
|
||||
|
||||
@@ -328,6 +328,36 @@ class ChatsModule {
|
||||
_bump();
|
||||
}
|
||||
|
||||
static Future<int?> markUnread(
|
||||
Api api,
|
||||
int accountId,
|
||||
int chatId,
|
||||
int mark,
|
||||
) async {
|
||||
int? unread;
|
||||
try {
|
||||
final resp = await api.sendRequest(Opcode.chatMark, {
|
||||
'type': 'SET_AS_UNREAD',
|
||||
'chatId': chatId,
|
||||
'mark': mark,
|
||||
});
|
||||
final payload = resp.payload;
|
||||
if (payload is Map) unread = payload['unread'] as int?;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
if (unread == null) return null;
|
||||
|
||||
final rows = await AppDatabase.loadChat(accountId, chatId);
|
||||
if (rows.isNotEmpty) {
|
||||
final row = Map<String, dynamic>.from(rows.first);
|
||||
row['unread_count'] = unread;
|
||||
await AppDatabase.saveChats([row]);
|
||||
_bump();
|
||||
}
|
||||
return unread;
|
||||
}
|
||||
|
||||
static Future<void> applyOutgoing(
|
||||
int accountId,
|
||||
int chatId, {
|
||||
|
||||
@@ -5,7 +5,7 @@ abstract class ServerConfig {
|
||||
static const int defaultPort = 443;
|
||||
static const String prefHostKey = 'server_host_override';
|
||||
static const String prefPortKey = 'server_port_override';
|
||||
static const Duration pingInterval = Duration(seconds: 30);
|
||||
static const Duration pingInterval = Duration(seconds: 10);
|
||||
static const Duration requestTimeout = Duration(seconds: 30);
|
||||
static const int maxReconnectAttempts = 50;
|
||||
|
||||
|
||||
@@ -83,6 +83,15 @@ String messageFromErrorPayload(dynamic payload) {
|
||||
return s.isNotEmpty ? s : 'Неизвестная ошибка';
|
||||
}
|
||||
|
||||
bool isSessionStateError(Object error) {
|
||||
if (error is SessionExpiredException) return true;
|
||||
final text = error.toString().toLowerCase();
|
||||
return text.contains('состояние сессии') ||
|
||||
text.contains('сессия не найдена') ||
|
||||
text.contains('авторизационная сессия') ||
|
||||
text.contains('сессия не онлайн');
|
||||
}
|
||||
|
||||
/// Payload меньше этого размера отправляется без сжатия (как в оригинале).
|
||||
const int _compressionThreshold = 32;
|
||||
|
||||
|
||||
@@ -4,17 +4,21 @@ import 'package:komet/l10n/app_localizations.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'password_2fa_screen.dart';
|
||||
import 'registration_screen.dart';
|
||||
import '../../../backend/api.dart';
|
||||
import '../../../core/protocol/packet.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/custom_notification.dart';
|
||||
import '../../widgets/login_success_screen.dart';
|
||||
|
||||
class CodeConfirmationScreen extends StatefulWidget {
|
||||
final String phoneNumber;
|
||||
final String rawPhone;
|
||||
final String token;
|
||||
|
||||
const CodeConfirmationScreen({
|
||||
super.key,
|
||||
required this.phoneNumber,
|
||||
required this.rawPhone,
|
||||
required this.token,
|
||||
});
|
||||
|
||||
@@ -38,9 +42,22 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
|
||||
Animation<double>? _routeAnimation;
|
||||
AnimationStatusListener? _routeAnimationListener;
|
||||
|
||||
late String _token;
|
||||
late int _epoch;
|
||||
StreamSubscription<SessionState>? _stateSub;
|
||||
bool _recovering = false;
|
||||
bool _verifying = false;
|
||||
bool _dropNotified = false;
|
||||
|
||||
bool get _sessionStale =>
|
||||
api.sessionEpoch != _epoch || api.state != SessionState.online;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_token = widget.token;
|
||||
_epoch = api.sessionEpoch;
|
||||
_stateSub = api.stateStream.listen(_onSessionState);
|
||||
_startTimer();
|
||||
|
||||
_shakeController = AnimationController(
|
||||
@@ -68,6 +85,7 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
|
||||
if (_routeAnimationListener != null) {
|
||||
_routeAnimation?.removeStatusListener(_routeAnimationListener!);
|
||||
}
|
||||
_stateSub?.cancel();
|
||||
_timer?.cancel();
|
||||
_errorTimer?.cancel();
|
||||
_shakeController.dispose();
|
||||
@@ -76,6 +94,62 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSessionState(SessionState state) {
|
||||
if (!mounted) return;
|
||||
if (state != SessionState.online) {
|
||||
if (!_dropNotified) {
|
||||
_dropNotified = true;
|
||||
showCustomNotification(
|
||||
context,
|
||||
'Соединение прервалось, восстанавливаем…',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (api.sessionEpoch != _epoch) _recoverStaleSession();
|
||||
}
|
||||
|
||||
Future<void> _recoverStaleSession() async {
|
||||
if (_recovering) return;
|
||||
setState(() => _recovering = true);
|
||||
try {
|
||||
if (api.state != SessionState.online) {
|
||||
final back = await api.stateStream
|
||||
.firstWhere((s) => s == SessionState.online)
|
||||
.timeout(
|
||||
const Duration(seconds: 12),
|
||||
onTimeout: () => SessionState.disconnected,
|
||||
);
|
||||
if (back != SessionState.online) {
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Нет соединения с сервером');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
final fresh = await accountModule.requestCode(widget.rawPhone);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_token = fresh.token;
|
||||
_epoch = api.sessionEpoch;
|
||||
_dropNotified = false;
|
||||
_codeController.clear();
|
||||
_errorMessage = null;
|
||||
});
|
||||
_startTimer();
|
||||
showCustomNotification(
|
||||
context,
|
||||
'Соединение восстановлено — выслали новый код',
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Не удалось обновить код: $e');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _recovering = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleKeyboardOpen() {
|
||||
if (_keyboardScheduled) return;
|
||||
_keyboardScheduled = true;
|
||||
@@ -136,13 +210,21 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
|
||||
}
|
||||
|
||||
Future<void> _verifyCode() async {
|
||||
if (_codeController.text.length != 6) return;
|
||||
if (_codeController.text.length != 6 || _recovering || _verifying) return;
|
||||
|
||||
if (_sessionStale) {
|
||||
_recoverStaleSession();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _verifying = true);
|
||||
var verified = false;
|
||||
try {
|
||||
final result = await accountModule.verifyCode(
|
||||
_codeController.text,
|
||||
widget.token,
|
||||
_token,
|
||||
);
|
||||
verified = true;
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -196,15 +278,21 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
transitionDuration: const Duration(milliseconds: 240),
|
||||
pageBuilder: (_, __, ___) => LoginSuccessScreen(avatar: avatar),
|
||||
transitionsBuilder: (_, animation, __, child) =>
|
||||
pageBuilder: (_, _, _) => LoginSuccessScreen(avatar: avatar),
|
||||
transitionsBuilder: (_, animation, _, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_showError(e.toString());
|
||||
if (!verified && (isSessionStateError(e) || _sessionStale)) {
|
||||
_recoverStaleSession();
|
||||
} else {
|
||||
_showError(e.toString());
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _verifying = false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,9 +507,11 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
FloatingActionButton(
|
||||
onPressed: () {
|
||||
if (_codeController.text.length == 6) _verifyCode();
|
||||
},
|
||||
onPressed: (_recovering || _verifying)
|
||||
? null
|
||||
: () {
|
||||
if (_codeController.text.length == 6) _verifyCode();
|
||||
},
|
||||
backgroundColor: _codeController.text.length == 6
|
||||
? cs.primaryContainer
|
||||
: cs.surfaceContainerHighest,
|
||||
@@ -429,12 +519,21 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_forward,
|
||||
color: _codeController.text.length == 6
|
||||
? cs.onPrimaryContainer
|
||||
: cs.onSurfaceVariant,
|
||||
),
|
||||
child: _recovering
|
||||
? SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: cs.onPrimaryContainer,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.arrow_forward,
|
||||
color: _codeController.text.length == 6
|
||||
? cs.onPrimaryContainer
|
||||
: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -19,6 +19,7 @@ import '../../widgets/custom_notification.dart';
|
||||
import '../../widgets/adaptive_shell.dart';
|
||||
import '../../widgets/sheet_helpers.dart';
|
||||
import '../../../backend/api.dart';
|
||||
import '../../../core/protocol/packet.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
@@ -39,13 +40,21 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
Timer? _phoneErrorTimer;
|
||||
int _logoTapCount = 0;
|
||||
Timer? _logoTapTimer;
|
||||
late SessionState _sessionState;
|
||||
StreamSubscription<SessionState>? _stateSub;
|
||||
|
||||
bool get _isOnline => _sessionState == SessionState.online;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sessionState = api.state;
|
||||
if (api.state == SessionState.disconnected) {
|
||||
unawaited(api.connect());
|
||||
}
|
||||
_stateSub = api.stateStream.listen((state) {
|
||||
if (mounted) setState(() => _sessionState = state);
|
||||
});
|
||||
_selectedCountry = countriesByCode['RU'] ?? allCountries.first;
|
||||
_clampCountryToAllowed();
|
||||
_checkTOS();
|
||||
@@ -79,6 +88,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stateSub?.cancel();
|
||||
_phoneErrorTimer?.cancel();
|
||||
_logoTapTimer?.cancel();
|
||||
_phoneController.dispose();
|
||||
@@ -458,6 +468,13 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
final fullPhone =
|
||||
'${_selectedCountry.phoneCode}${_phoneController.text}';
|
||||
|
||||
if (!_isOnline) {
|
||||
_showPhoneError(
|
||||
'Нет соединения с сервером. Подождите подключения.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await accountModule.requestCode(
|
||||
fullPhone,
|
||||
@@ -470,13 +487,18 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
builder: (context) => CodeConfirmationScreen(
|
||||
phoneNumber:
|
||||
'${_selectedCountry.phoneCode} $formattedPhone',
|
||||
rawPhone: fullPhone,
|
||||
token: result.token,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!screenContext.mounted) return;
|
||||
_showPhoneError(e.toString());
|
||||
_showPhoneError(
|
||||
isSessionStateError(e)
|
||||
? 'Нет соединения с сервером. Попробуйте ещё раз.'
|
||||
: e.toString(),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
@@ -510,6 +532,10 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
_showPhoneConfirmationDialog(_phoneController.text);
|
||||
}
|
||||
|
||||
void _notifyConnecting() {
|
||||
showCustomNotification(context, 'Подключаемся к серверу, секунду…');
|
||||
}
|
||||
|
||||
void _showServerSettingsSheet(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
showModalBottomSheet<void>(
|
||||
@@ -956,21 +982,32 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: _isPhoneValid
|
||||
? _validateAndSubmit
|
||||
: null,
|
||||
onPressed: !_isPhoneValid
|
||||
? null
|
||||
: (_isOnline
|
||||
? _validateAndSubmit
|
||||
: _notifyConnecting),
|
||||
backgroundColor: _isPhoneValid
|
||||
? cs.primaryContainer
|
||||
: cs.surfaceContainerHighest,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_forward,
|
||||
color: _isPhoneValid
|
||||
? cs.onPrimaryContainer
|
||||
: cs.onSurfaceVariant,
|
||||
),
|
||||
child: _isPhoneValid && !_isOnline
|
||||
? SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: cs.onPrimaryContainer,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.arrow_forward,
|
||||
color: _isPhoneValid
|
||||
? cs.onPrimaryContainer
|
||||
: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../backend/api.dart';
|
||||
import '../../../core/protocol/packet.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/custom_notification.dart';
|
||||
import '../../widgets/login_success_screen.dart';
|
||||
@@ -18,24 +21,69 @@ class _Password2FAScreenState extends State<Password2FAScreen> {
|
||||
bool _isPasswordVisible = false;
|
||||
bool _isLoading = false;
|
||||
|
||||
late int _epoch;
|
||||
StreamSubscription<SessionState>? _stateSub;
|
||||
bool _recovering = false;
|
||||
bool _dropNotified = false;
|
||||
|
||||
bool get _sessionStale =>
|
||||
api.sessionEpoch != _epoch || api.state != SessionState.online;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_epoch = api.sessionEpoch;
|
||||
_stateSub = api.stateStream.listen(_onSessionState);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stateSub?.cancel();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSessionState(SessionState state) {
|
||||
if (!mounted) return;
|
||||
if (state != SessionState.online) {
|
||||
if (!_dropNotified) {
|
||||
_dropNotified = true;
|
||||
showCustomNotification(context, 'Соединение прервалось…');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (api.sessionEpoch != _epoch) _recoverStaleSession();
|
||||
}
|
||||
|
||||
void _recoverStaleSession() {
|
||||
if (_recovering || !mounted) return;
|
||||
_recovering = true;
|
||||
showCustomNotification(
|
||||
context,
|
||||
'Соединение прервалось — войдите заново',
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Future<void> _checkPassword() async {
|
||||
if (_passwordController.text.isEmpty) return;
|
||||
if (_passwordController.text.isEmpty || _isLoading || _recovering) return;
|
||||
|
||||
if (_sessionStale) {
|
||||
_recoverStaleSession();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
var passed = false;
|
||||
try {
|
||||
final result = await accountModule.checkPassword(
|
||||
password: _passwordController.text,
|
||||
trackId: widget.trackId,
|
||||
);
|
||||
passed = true;
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -54,8 +102,8 @@ class _Password2FAScreenState extends State<Password2FAScreen> {
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
transitionDuration: const Duration(milliseconds: 240),
|
||||
pageBuilder: (_, __, ___) => LoginSuccessScreen(avatar: avatar),
|
||||
transitionsBuilder: (_, animation, __, child) =>
|
||||
pageBuilder: (_, _, _) => LoginSuccessScreen(avatar: avatar),
|
||||
transitionsBuilder: (_, animation, _, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
),
|
||||
(route) => false,
|
||||
@@ -67,7 +115,11 @@ class _Password2FAScreenState extends State<Password2FAScreen> {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
showCustomNotification(context, 'Неверный пароль: $e');
|
||||
if (!passed && (isSessionStateError(e) || _sessionStale)) {
|
||||
_recoverStaleSession();
|
||||
} else {
|
||||
showCustomNotification(context, 'Неверный пароль: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -503,7 +503,6 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
void _markRead() {
|
||||
if (_myId == 0 || _messages.isEmpty) return;
|
||||
final newest = _messages.last;
|
||||
if (newest.senderId == _myId) return;
|
||||
if (newest.id == _lastMarkedId) return;
|
||||
_lastMarkedId = newest.id;
|
||||
unawaited(
|
||||
@@ -511,6 +510,21 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _markMessageUnread(CachedMessage message) async {
|
||||
final unread = await ChatsModule.markUnread(
|
||||
api,
|
||||
_myId,
|
||||
widget.chatId,
|
||||
message.time,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (unread == null) {
|
||||
showCustomNotification(context, 'Не удалось пометить непрочитанным');
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
bool _badgeRefreshing = false;
|
||||
bool _badgeRefreshQueued = false;
|
||||
|
||||
@@ -3449,6 +3463,9 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
onForward: message.isControl
|
||||
? null
|
||||
: () => _forwardMessages([message]),
|
||||
onMarkUnread: message.isControl
|
||||
? null
|
||||
: () => _markMessageUnread(message),
|
||||
loadReportReasons: canReport
|
||||
? () => _loadReportReasons(reportTypeId)
|
||||
: null,
|
||||
@@ -5890,6 +5907,7 @@ class _SelectableMessageRow extends StatefulWidget {
|
||||
final VoidCallback? onEdit;
|
||||
final VoidCallback? onReply;
|
||||
final VoidCallback? onForward;
|
||||
final VoidCallback? onMarkUnread;
|
||||
final Future<List<({int id, String title})>> Function()? loadReportReasons;
|
||||
final Future<bool> Function(int reasonId)? onReport;
|
||||
|
||||
@@ -5906,6 +5924,7 @@ class _SelectableMessageRow extends StatefulWidget {
|
||||
this.onEdit,
|
||||
this.onReply,
|
||||
this.onForward,
|
||||
this.onMarkUnread,
|
||||
this.loadReportReasons,
|
||||
this.onReport,
|
||||
});
|
||||
@@ -5958,6 +5977,7 @@ class _SelectableMessageRowState extends State<_SelectableMessageRow> {
|
||||
onEdit: widget.onEdit,
|
||||
onReply: widget.onReply,
|
||||
onForward: widget.onForward,
|
||||
onMarkUnread: widget.onMarkUnread,
|
||||
onDispose: controller.dispose,
|
||||
);
|
||||
}
|
||||
@@ -5988,6 +6008,7 @@ class _SelectableMessageRowState extends State<_SelectableMessageRow> {
|
||||
onEdit: widget.onEdit,
|
||||
onReply: widget.onReply,
|
||||
onForward: widget.onForward,
|
||||
onMarkUnread: widget.onMarkUnread,
|
||||
onDispose: controller.dispose,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import '../../widgets/custom_notification.dart';
|
||||
import '../../widgets/glossy_pill.dart';
|
||||
import '../../widgets/sheet_helpers.dart';
|
||||
import '../../widgets/login_success_screen.dart';
|
||||
import '../auth/login_screen.dart';
|
||||
import '../calls/call_screen.dart';
|
||||
import '../../../core/calls/call_controller.dart';
|
||||
import '../../widgets/connection_status.dart';
|
||||
@@ -351,6 +352,68 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Material(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 17,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.dialpad,
|
||||
color: cs.onSurfaceVariant,
|
||||
size: 22,
|
||||
weight: 400,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Экран ввода номера',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Открыть без выхода из аккаунта и обрыва сессии',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Symbols.chevron_right,
|
||||
color: cs.onSurfaceVariant,
|
||||
size: 22,
|
||||
weight: 400,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
|
||||
@@ -83,6 +83,7 @@ void showMessageActions({
|
||||
VoidCallback? onEdit,
|
||||
VoidCallback? onReply,
|
||||
VoidCallback? onForward,
|
||||
VoidCallback? onMarkUnread,
|
||||
MessageActionsInteraction interaction = MessageActionsInteraction.dragAndRelease,
|
||||
}) {
|
||||
final overlay = Overlay.of(context, rootOverlay: true);
|
||||
@@ -104,6 +105,7 @@ void showMessageActions({
|
||||
onEdit: onEdit,
|
||||
onReply: onReply,
|
||||
onForward: onForward,
|
||||
onMarkUnread: onMarkUnread,
|
||||
onDismiss: () {
|
||||
if (entry.mounted) entry.remove();
|
||||
onDispose();
|
||||
@@ -130,6 +132,7 @@ class _MessageActionsLayer extends StatefulWidget {
|
||||
final VoidCallback? onEdit;
|
||||
final VoidCallback? onReply;
|
||||
final VoidCallback? onForward;
|
||||
final VoidCallback? onMarkUnread;
|
||||
|
||||
const _MessageActionsLayer({
|
||||
required this.snapshot,
|
||||
@@ -148,6 +151,7 @@ class _MessageActionsLayer extends StatefulWidget {
|
||||
this.onEdit,
|
||||
this.onReply,
|
||||
this.onForward,
|
||||
this.onMarkUnread,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -380,6 +384,8 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
|
||||
_Action(Symbols.reply, 'Ответить', _reply),
|
||||
if (widget.onForward != null)
|
||||
_Action(Symbols.forward, 'Переслать', _forward),
|
||||
if (widget.onMarkUnread != null)
|
||||
_Action(Symbols.mark_chat_unread, 'Непрочитанное', _markUnread),
|
||||
if (widget.editHistory != null && widget.editHistory!.isNotEmpty)
|
||||
_Action(Symbols.history, 'История изменений', _showHistoryView),
|
||||
if (widget.onReport != null && widget.loadReportReasons != null)
|
||||
@@ -517,6 +523,12 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
|
||||
onForward?.call();
|
||||
}
|
||||
|
||||
Future<void> _markUnread() async {
|
||||
final onMarkUnread = widget.onMarkUnread;
|
||||
await _close();
|
||||
onMarkUnread?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
|
||||
Reference in New Issue
Block a user