набурмалдил
This commit is contained in:
@@ -937,6 +937,20 @@ class AccountModule {
|
|||||||
_checkPacketError(packet, 'authorizeWebQrLogin');
|
_checkPacketError(packet, 'authorizeWebQrLogin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> beginAddAccount() async {
|
||||||
|
try {
|
||||||
|
await _api.disconnect();
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
await TokenStorage.clearActiveAccount();
|
||||||
|
|
||||||
|
ContactCache.clear();
|
||||||
|
TranscriptionCache.clear();
|
||||||
|
ChatsModule.resetForAccountSwitch();
|
||||||
|
|
||||||
|
logger.i('Добавление аккаунта: сессия сброшена, активный аккаунт очищен');
|
||||||
|
}
|
||||||
|
|
||||||
Future<ProfileData> switchAccount(int accountId) async {
|
Future<ProfileData> switchAccount(int accountId) async {
|
||||||
final profile = await AppDatabase.loadProfile(accountId);
|
final profile = await AppDatabase.loadProfile(accountId);
|
||||||
if (profile == null) {
|
if (profile == null) {
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class TokenStorage {
|
|||||||
await prefs.setString(_activeAccountKey, accountId.toString());
|
await prefs.setString(_activeAccountKey, accountId.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> clearActiveAccount() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(_activeAccountKey);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<int?> getActiveAccountId() async {
|
static Future<int?> getActiveAccountId() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final val = prefs.getString(_activeAccountKey);
|
final val = prefs.getString(_activeAccountKey);
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ import 'proxy_settings_sheet.dart';
|
|||||||
import 'server_settings_sheet.dart';
|
import 'server_settings_sheet.dart';
|
||||||
import '../profile/spoof_screen.dart';
|
import '../profile/spoof_screen.dart';
|
||||||
import '../../widgets/custom_notification.dart';
|
import '../../widgets/custom_notification.dart';
|
||||||
|
import '../../widgets/adaptive_shell.dart';
|
||||||
|
import '../../../backend/api.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({super.key});
|
final int? returnToAccountId;
|
||||||
|
|
||||||
|
const LoginScreen({super.key, this.returnToAccountId});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LoginScreen> createState() => _LoginScreenState();
|
State<LoginScreen> createState() => _LoginScreenState();
|
||||||
@@ -34,11 +38,30 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
if (api.state == SessionState.disconnected) {
|
||||||
|
unawaited(api.connect());
|
||||||
|
}
|
||||||
_selectedCountry = countriesByCode['RU'] ?? allCountries.first;
|
_selectedCountry = countriesByCode['RU'] ?? allCountries.first;
|
||||||
_clampCountryToAllowed();
|
_clampCountryToAllowed();
|
||||||
_checkTOS();
|
_checkTOS();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onBackPressed() async {
|
||||||
|
final returnId = widget.returnToAccountId;
|
||||||
|
if (returnId != null) {
|
||||||
|
try {
|
||||||
|
await accountModule.switchAccount(returnId);
|
||||||
|
} catch (_) {}
|
||||||
|
if (!mounted) return;
|
||||||
|
await Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(builder: (_) => const AdaptiveShell()),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Navigator.canPop(context)) Navigator.pop(context);
|
||||||
|
}
|
||||||
|
|
||||||
void _clampCountryToAllowed() {
|
void _clampCountryToAllowed() {
|
||||||
final allowed = api.registrationCountries;
|
final allowed = api.registrationCountries;
|
||||||
if (allowed.any((c) => c.code == _selectedCountry.code)) return;
|
if (allowed.any((c) => c.code == _selectedCountry.code)) return;
|
||||||
@@ -666,9 +689,10 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
if (Navigator.canPop(context))
|
if (Navigator.canPop(context) ||
|
||||||
|
widget.returnToAccountId != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: _onBackPressed,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Symbols.arrow_back,
|
Symbols.arrow_back,
|
||||||
color: cs.onSurfaceVariant,
|
color: cs.onSurfaceVariant,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import '../../../backend/modules/chats.dart';
|
|||||||
import '../../../backend/modules/cloud_storage.dart';
|
import '../../../backend/modules/cloud_storage.dart';
|
||||||
import '../../../backend/modules/folders.dart';
|
import '../../../backend/modules/folders.dart';
|
||||||
import '../../../core/storage/app_database.dart';
|
import '../../../core/storage/app_database.dart';
|
||||||
|
import '../../../core/storage/token_storage.dart';
|
||||||
import '../../../main.dart' show accountModule, api, messagesModule;
|
import '../../../main.dart' show accountModule, api, messagesModule;
|
||||||
|
|
||||||
class _StoriesScrollPhysics extends BouncingScrollPhysics {
|
class _StoriesScrollPhysics extends BouncingScrollPhysics {
|
||||||
@@ -100,7 +101,6 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
|
|
||||||
bool _navDragging = false;
|
bool _navDragging = false;
|
||||||
bool _isFabOpen = false;
|
bool _isFabOpen = false;
|
||||||
bool _showCacheWarning = false;
|
|
||||||
bool _storiesAnimClosing = false;
|
bool _storiesAnimClosing = false;
|
||||||
Timer? _contactRebuildTimer;
|
Timer? _contactRebuildTimer;
|
||||||
bool get _isSelectionMode => _selectedChats.isNotEmpty;
|
bool get _isSelectionMode => _selectedChats.isNotEmpty;
|
||||||
@@ -140,7 +140,6 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
_selectedFolderId,
|
_selectedFolderId,
|
||||||
_isInitialLoading,
|
_isInitialLoading,
|
||||||
_foldersListKnown,
|
_foldersListKnown,
|
||||||
_showCacheWarning,
|
|
||||||
_isSelectionMode,
|
_isSelectionMode,
|
||||||
_shouldCollapseSearch,
|
_shouldCollapseSearch,
|
||||||
_selectedChats.length,
|
_selectedChats.length,
|
||||||
@@ -446,12 +445,6 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_sessionState = state;
|
_sessionState = state;
|
||||||
if (state == SessionState.disconnected && _chats.isNotEmpty) {
|
|
||||||
_showCacheWarning = true;
|
|
||||||
}
|
|
||||||
if (state == SessionState.online) {
|
|
||||||
_showCacheWarning = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (state == SessionState.online) {
|
if (state == SessionState.online) {
|
||||||
_reloadChatsAndFolders();
|
_reloadChatsAndFolders();
|
||||||
@@ -1145,42 +1138,6 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_showCacheWarning)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 8),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: cs.errorContainer.withValues(alpha: 0.3),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(
|
|
||||||
color: cs.error.withValues(alpha: 0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Symbols.cloud_off,
|
|
||||||
size: 18,
|
|
||||||
color: cs.error,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
const Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Ошибка соединения, сейчас вы смотрите КЕШ',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 3, 20, 4),
|
padding: const EdgeInsets.fromLTRB(20, 3, 20, 4),
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -2393,9 +2350,16 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
controller.dispose();
|
controller.dispose();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (accountId == null) {
|
if (accountId == null) {
|
||||||
await Navigator.push(
|
final previousId = await TokenStorage.getActiveAccountId();
|
||||||
context,
|
try {
|
||||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
await accountModule.beginAddAccount();
|
||||||
|
} catch (_) {}
|
||||||
|
if (!mounted) return;
|
||||||
|
await Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => LoginScreen(returnToAccountId: previousId),
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
final Map<int, Timer> _typingTimers = {};
|
final Map<int, Timer> _typingTimers = {};
|
||||||
int _otherStatus = 0;
|
int _otherStatus = 0;
|
||||||
int? _otherSeenTime;
|
int? _otherSeenTime;
|
||||||
|
int? _participantsCount;
|
||||||
final ValueNotifier<String> _headerStatusNotifier = ValueNotifier('');
|
final ValueNotifier<String> _headerStatusNotifier = ValueNotifier('');
|
||||||
final ValueNotifier<int> _otherReadTime = ValueNotifier(0);
|
final ValueNotifier<int> _otherReadTime = ValueNotifier(0);
|
||||||
int _tempIdCounter = 0;
|
int _tempIdCounter = 0;
|
||||||
@@ -172,9 +173,21 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
);
|
);
|
||||||
|
|
||||||
unawaited(_fastPreloadCache());
|
unawaited(_fastPreloadCache());
|
||||||
|
unawaited(_loadParticipantsCount());
|
||||||
WidgetsBinding.instance.addPostFrameCallback(_onFirstFrameRendered);
|
WidgetsBinding.instance.addPostFrameCallback(_onFirstFrameRendered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadParticipantsCount() async {
|
||||||
|
if (widget.chatType != 'CHAT' && widget.chatType != 'CHANNEL') return;
|
||||||
|
final info = await ChatsModule.getChatInfo(api, widget.chatId);
|
||||||
|
if (!mounted) return;
|
||||||
|
final count = info?['participantsCount'] as int?;
|
||||||
|
if (count != null && count != _participantsCount) {
|
||||||
|
_participantsCount = count;
|
||||||
|
_recomputeHeaderStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _fastPreloadCache() async {
|
Future<void> _fastPreloadCache() async {
|
||||||
final p = await AppDatabase.loadActiveProfile();
|
final p = await AppDatabase.loadActiveProfile();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -521,10 +534,12 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
String _headerStatus() {
|
String _headerStatus() {
|
||||||
if (_typingUserIds.isNotEmpty) return 'Печатает...';
|
if (_typingUserIds.isNotEmpty) return 'Печатает...';
|
||||||
if (widget.chatType == 'CHAT') {
|
if (widget.chatType == 'CHAT') {
|
||||||
return '${chat?.participants.length ?? 0} участников';
|
final count = _participantsCount ?? chat?.participants.length ?? 0;
|
||||||
|
return '$count участников';
|
||||||
}
|
}
|
||||||
if (widget.chatType == 'CHANNEL') {
|
if (widget.chatType == 'CHANNEL') {
|
||||||
return '${chat?.participants.length ?? 0} подписчиков';
|
final count = _participantsCount ?? chat?.participants.length ?? 0;
|
||||||
|
return '$count подписчиков';
|
||||||
}
|
}
|
||||||
if (_otherStatus == 1) return 'В сети';
|
if (_otherStatus == 1) return 'В сети';
|
||||||
if (_otherStatus == 3) return 'Был(-а) недавно';
|
if (_otherStatus == 3) return 'Был(-а) недавно';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import '../../../core/protocol/packet.dart';
|
|||||||
import '../../../core/utils/logger.dart';
|
import '../../../core/utils/logger.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../widgets/custom_notification.dart';
|
import '../../widgets/custom_notification.dart';
|
||||||
|
import '../../widgets/login_success_screen.dart';
|
||||||
|
|
||||||
class DebugMenuScreen extends StatefulWidget {
|
class DebugMenuScreen extends StatefulWidget {
|
||||||
const DebugMenuScreen({super.key});
|
const DebugMenuScreen({super.key});
|
||||||
@@ -397,6 +398,73 @@ 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 LoginSuccessScreen(preview: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 17,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.celebration,
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
size: 22,
|
||||||
|
weight: 400,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'test hello',
|
||||||
|
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(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ Future<ImageProvider?> precacheLoginAvatar(
|
|||||||
class LoginSuccessScreen extends StatefulWidget {
|
class LoginSuccessScreen extends StatefulWidget {
|
||||||
final ImageProvider? avatar;
|
final ImageProvider? avatar;
|
||||||
|
|
||||||
const LoginSuccessScreen({super.key, this.avatar});
|
final bool preview;
|
||||||
|
|
||||||
|
const LoginSuccessScreen({super.key, this.avatar, this.preview = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LoginSuccessScreen> createState() => _LoginSuccessScreenState();
|
State<LoginSuccessScreen> createState() => _LoginSuccessScreenState();
|
||||||
@@ -54,6 +56,8 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
|
|||||||
late final Animation<double> _fadeOut;
|
late final Animation<double> _fadeOut;
|
||||||
|
|
||||||
bool _navigated = false;
|
bool _navigated = false;
|
||||||
|
bool _handedOff = false;
|
||||||
|
final GlobalKey _subtitleKey = GlobalKey();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -114,26 +118,66 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
|
|||||||
void _onStatus(AnimationStatus status) {
|
void _onStatus(AnimationStatus status) {
|
||||||
if (status == AnimationStatus.completed && !_navigated && mounted) {
|
if (status == AnimationStatus.completed && !_navigated && mounted) {
|
||||||
_navigated = true;
|
_navigated = true;
|
||||||
Navigator.of(context).pushAndRemoveUntil(
|
|
||||||
PageRouteBuilder(
|
final navigator = Navigator.of(context);
|
||||||
transitionDuration: const Duration(milliseconds: 360),
|
final overlay = navigator.overlay;
|
||||||
reverseTransitionDuration: const Duration(milliseconds: 200),
|
final entry = _buildGreetingEntry();
|
||||||
pageBuilder: (_, __, ___) => const AdaptiveShell(),
|
if (entry != null) {
|
||||||
transitionsBuilder: (_, animation, __, child) {
|
setState(() => _handedOff = true);
|
||||||
return FadeTransition(
|
}
|
||||||
opacity: CurvedAnimation(
|
|
||||||
parent: animation,
|
if (widget.preview) {
|
||||||
curve: Curves.easeOutCubic,
|
navigator.pop();
|
||||||
),
|
} else {
|
||||||
child: child,
|
navigator.pushAndRemoveUntil(
|
||||||
);
|
PageRouteBuilder(
|
||||||
},
|
transitionDuration: const Duration(milliseconds: 360),
|
||||||
),
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
||||||
(route) => false,
|
pageBuilder: (_, _, _) => const AdaptiveShell(),
|
||||||
);
|
transitionsBuilder: (_, animation, _, child) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry != null && overlay != null) {
|
||||||
|
overlay.insert(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OverlayEntry? _buildGreetingEntry() {
|
||||||
|
final box = _subtitleKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
if (box == null || !box.attached) return null;
|
||||||
|
|
||||||
|
final topLeft = box.localToGlobal(Offset.zero);
|
||||||
|
final size = box.size;
|
||||||
|
final color = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||||
|
|
||||||
|
late final OverlayEntry entry;
|
||||||
|
entry = OverlayEntry(
|
||||||
|
builder: (_) => Positioned(
|
||||||
|
left: topLeft.dx,
|
||||||
|
top: topLeft.dy,
|
||||||
|
width: size.width,
|
||||||
|
child: _GreetingLinger(
|
||||||
|
greeting: _greeting,
|
||||||
|
color: color,
|
||||||
|
onDone: () => entry.remove(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.removeStatusListener(_onStatus);
|
_controller.removeStatusListener(_onStatus);
|
||||||
@@ -149,11 +193,12 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
|
|||||||
body: AnimatedBuilder(
|
body: AnimatedBuilder(
|
||||||
animation: _controller,
|
animation: _controller,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return Opacity(
|
final celebrationOpacity = 1.0 - _fadeOut.value;
|
||||||
opacity: 1.0 - _fadeOut.value,
|
return Stack(
|
||||||
child: Stack(
|
children: [
|
||||||
children: [
|
Positioned.fill(
|
||||||
Positioned.fill(
|
child: Opacity(
|
||||||
|
opacity: celebrationOpacity,
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: RadialGradient(
|
gradient: RadialGradient(
|
||||||
@@ -169,11 +214,14 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Center(
|
),
|
||||||
child: Column(
|
Center(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
SizedBox(
|
children: [
|
||||||
|
Opacity(
|
||||||
|
opacity: celebrationOpacity,
|
||||||
|
child: SizedBox(
|
||||||
width: 220,
|
width: 220,
|
||||||
height: 220,
|
height: 220,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@@ -186,15 +234,21 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 28),
|
),
|
||||||
_buildTitle(cs),
|
const SizedBox(height: 28),
|
||||||
const SizedBox(height: 8),
|
Opacity(
|
||||||
_buildSubtitle(cs),
|
opacity: celebrationOpacity,
|
||||||
],
|
child: _buildTitle(cs),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Opacity(
|
||||||
|
opacity: _handedOff ? 0.0 : 1.0,
|
||||||
|
child: _buildSubtitle(cs),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -315,6 +369,7 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
child: Text(
|
child: Text(
|
||||||
_greeting,
|
_greeting,
|
||||||
|
key: _subtitleKey,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.onSurfaceVariant,
|
color: cs.onSurfaceVariant,
|
||||||
@@ -329,6 +384,78 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _GreetingLinger extends StatefulWidget {
|
||||||
|
final String greeting;
|
||||||
|
final Color color;
|
||||||
|
final VoidCallback onDone;
|
||||||
|
|
||||||
|
const _GreetingLinger({
|
||||||
|
required this.greeting,
|
||||||
|
required this.color,
|
||||||
|
required this.onDone,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_GreetingLinger> createState() => _GreetingLingerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GreetingLingerState extends State<_GreetingLinger>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
late final Animation<double> _opacity;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1200),
|
||||||
|
);
|
||||||
|
_opacity = TweenSequence<double>([
|
||||||
|
TweenSequenceItem(tween: ConstantTween<double>(1.0), weight: 55),
|
||||||
|
TweenSequenceItem(
|
||||||
|
tween: Tween<double>(begin: 1.0, end: 0.0)
|
||||||
|
.chain(CurveTween(curve: Curves.easeInCubic)),
|
||||||
|
weight: 45,
|
||||||
|
),
|
||||||
|
]).animate(_controller);
|
||||||
|
_controller.addStatusListener((status) {
|
||||||
|
if (status == AnimationStatus.completed) widget.onDone();
|
||||||
|
});
|
||||||
|
_controller.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IgnorePointer(
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _opacity,
|
||||||
|
builder: (context, _) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: _opacity.value,
|
||||||
|
child: Text(
|
||||||
|
widget.greeting,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.color,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _CheckPainter extends CustomPainter {
|
class _CheckPainter extends CustomPainter {
|
||||||
final double progress;
|
final double progress;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|||||||
@@ -300,6 +300,10 @@ class MessageBubble extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: _buildContent(makeCtx());
|
: _buildContent(makeCtx());
|
||||||
|
|
||||||
|
final reactionsUnder = _reactionsUnderBubble(contentType);
|
||||||
|
final reactionsInside =
|
||||||
|
contentType != MessageType.text && !reactionsUnder;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: Haptics.tap,
|
onTap: Haptics.tap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -349,16 +353,15 @@ class MessageBubble extends StatelessWidget {
|
|||||||
padding: padding,
|
padding: padding,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
child: bubbleContent,
|
child: reactionsInside
|
||||||
),
|
? Column(
|
||||||
if (contentType != MessageType.text)
|
mainAxisSize: MainAxisSize.min,
|
||||||
reactionsListenable != null
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
? ValueListenableBuilder<Map<String, dynamic>?>(
|
children: [bubbleContent, _reactionsBar(cs)],
|
||||||
valueListenable: reactionsListenable!,
|
|
||||||
builder: (context, info, _) =>
|
|
||||||
_buildReactionsBarFor(cs, info),
|
|
||||||
)
|
)
|
||||||
: _buildReactionsBar(cs),
|
: bubbleContent,
|
||||||
|
),
|
||||||
|
if (reactionsUnder) _reactionsBar(cs),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -378,6 +381,27 @@ class MessageBubble extends StatelessWidget {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _reactionsUnderBubble(MessageType contentType) {
|
||||||
|
if (contentType != MessageType.attachment) return false;
|
||||||
|
final attachments = message.attachments;
|
||||||
|
if (attachments == null || attachments.isEmpty) return false;
|
||||||
|
if (attachments.first is ForwardedMessageAttachment) return false;
|
||||||
|
if (attachments.any((a) => a is ContactAttachment)) return false;
|
||||||
|
if (attachments.whereType<PhotoAttachment>().length >= 2) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _reactionsBar(ColorScheme cs) {
|
||||||
|
final listenable = reactionsListenable;
|
||||||
|
if (listenable != null) {
|
||||||
|
return ValueListenableBuilder<Map<String, dynamic>?>(
|
||||||
|
valueListenable: listenable,
|
||||||
|
builder: (context, info, _) => _buildReactionsBarFor(cs, info),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _buildReactionsBar(cs);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildContent(_BubbleCtx ctx) {
|
Widget _buildContent(_BubbleCtx ctx) {
|
||||||
switch (ctx.contentType) {
|
switch (ctx.contentType) {
|
||||||
case MessageType.control:
|
case MessageType.control:
|
||||||
@@ -1752,10 +1776,7 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.preloadedText != null) {
|
_transcriptionText = widget.preloadedText;
|
||||||
_transcriptionText = widget.preloadedText;
|
|
||||||
_transcriptionVisible = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDuration(int seconds) {
|
String _formatDuration(int seconds) {
|
||||||
@@ -1917,14 +1938,19 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
|||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
SizedBox(
|
||||||
_formatDuration(widget.duration),
|
width: 32,
|
||||||
style: TextStyle(
|
child: Center(
|
||||||
color: widget.textColor.withValues(alpha: 0.7),
|
child: Text(
|
||||||
fontSize: 11,
|
_formatDuration(widget.duration),
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.textColor.withValues(alpha: 0.7),
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AnimatedSize(
|
child: AnimatedSize(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
|
|||||||
+19
-1
@@ -700,8 +700,13 @@ class _StartupScreenState extends State<_StartupScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _tryAutoLogin() async {
|
Future<void> _tryAutoLogin() async {
|
||||||
final accountId = await TokenStorage.getActiveAccountId();
|
int? accountId = await TokenStorage.getActiveAccountId();
|
||||||
|
|
||||||
if (accountId == null || await TokenStorage.readToken(accountId) == null) {
|
if (accountId == null || await TokenStorage.readToken(accountId) == null) {
|
||||||
|
accountId = await _recoverActiveAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountId == null) {
|
||||||
_goToLogin();
|
_goToLogin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -718,6 +723,19 @@ class _StartupScreenState extends State<_StartupScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int?> _recoverActiveAccount() async {
|
||||||
|
final profiles = await AppDatabase.loadAllProfiles();
|
||||||
|
for (final profile in profiles) {
|
||||||
|
if (await TokenStorage.readToken(profile.id) != null) {
|
||||||
|
await TokenStorage.setActiveAccount(profile.id);
|
||||||
|
await AppDatabase.setActiveAccount(profile.id);
|
||||||
|
await ContactsModule.primeCacheFromDb(profile.id);
|
||||||
|
return profile.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
void _goToLogin() {
|
void _goToLogin() {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
|
|||||||
Reference in New Issue
Block a user