diff --git a/lib/main.dart b/lib/main.dart index f44ad61..193e7ef 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/providers/api_provider.dart'; import 'package:flutter/material.dart'; +import 'package:twonly/src/providers/connection_provider.dart'; import 'package:twonly/src/providers/hive.dart'; import 'package:twonly/src/providers/settings_change_provider.dart'; import 'package:twonly/src/services/fcm_service.dart'; @@ -34,6 +35,7 @@ void main() async { MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => settingsController), + ChangeNotifierProvider(create: (_) => ConnectionChangeProvider()), ], child: MyApp(), ), diff --git a/lib/src/app.dart b/lib/src/app.dart index 9ab0eef..cd372bb 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,6 +1,6 @@ import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/components/connection_state.dart'; +import 'package:twonly/src/providers/connection_provider.dart'; import 'package:twonly/src/providers/settings_change_provider.dart'; import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -31,7 +31,6 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State with WidgetsBindingObserver { - bool _isConnected = false; bool wasPaused = false; @override @@ -41,60 +40,18 @@ class _MyAppState extends State with WidgetsBindingObserver { WidgetsBinding.instance.addObserver(this); // register global callbacks to the widget tree - globalCallbackConnectionState = (isConnected) { - setState(() { - _isConnected = isConnected; - }); + globalCallbackConnectionState = (update) { + context.read().updateConnectionState(update); setupNotificationWithUsers(); }; - // WidgetsBinding.instance.addPostFrameCallback((_) { - // _requestPermissions(); - // _initService(); - // }); initAsync(); } Future initAsync() async { - // make sure the front end service will be killed - // FlutterForegroundTask.sendDataToTask(""); - // await FlutterForegroundTask.stopService(); - // connect async to the backend api apiProvider.connect(); } - // Future _requestPermissions() async { - // // Android 13+, you need to allow notification permission to display foreground service notification. - // // - // // iOS: If you need notification, ask for permission. - // final NotificationPermission notificationPermission = - // await FlutterForegroundTask.checkNotificationPermission(); - // if (notificationPermission != NotificationPermission.granted) { - // await FlutterForegroundTask.requestNotificationPermission(); - // } - - // if (Platform.isAndroid) { - // // Android 12+, there are restrictions on starting a foreground service. - // // - // // To restart the service on device reboot or unexpected problem, you need to allow below permission. - // if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) { - // // This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission. - // await FlutterForegroundTask.requestIgnoreBatteryOptimization(); - // } - - // // Use this utility only if you provide services that require long-term survival, - // // such as exact alarm service, healthcare service, or Bluetooth communication. - // // - // // This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission. - // // Using this permission may make app distribution difficult due to Google policy. - // // if (!await FlutterForegroundTask.canScheduleExactAlarms) { - // // When you call this function, will be gone to the settings page. - // // So you need to explain to the user why set it. - // // await FlutterForegroundTask.openAlarmsAndRemindersSettings(); - // // } - // } - // } - @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); @@ -108,11 +65,6 @@ class _MyAppState extends State with WidgetsBindingObserver { } else if (state == AppLifecycleState.paused) { wasPaused = true; globalIsAppInBackground = true; - - // apiProvider.close(() { - // use this only when uploading an image - // _startService(); - // }); } } @@ -165,11 +117,9 @@ class _MyAppState extends State with WidgetsBindingObserver { themeMode: context.watch().themeMode, initialRoute: '/', routes: { - "/": (context) => - MyAppMainWidget(isConnected: _isConnected, initialPage: 0), - "/chats": (context) => - MyAppMainWidget(isConnected: _isConnected, initialPage: 1) - // home: MyAppMainWidget(isConnected: _isConnected, initialPage: 0), + "/": (context) => MyAppMainWidget(initialPage: 0), + "/chats": (context) => MyAppMainWidget(initialPage: 1) + // home: MyAppMainWidget(isConnected: isConnected, initialPage: 0), }, ); }, @@ -178,10 +128,8 @@ class _MyAppState extends State with WidgetsBindingObserver { } class MyAppMainWidget extends StatefulWidget { - const MyAppMainWidget( - {super.key, required this.isConnected, required this.initialPage}); + const MyAppMainWidget({super.key, required this.initialPage}); - final bool isConnected; final int initialPage; @override @@ -201,7 +149,9 @@ class _MyAppMainWidgetState extends State { builder: (context, snapshot) { if (snapshot.hasData) { if (snapshot.data!) { - return HomeView(initialPage: widget.initialPage); + return HomeView( + initialPage: widget.initialPage, + ); } if (_showOnboarding) { @@ -226,7 +176,6 @@ class _MyAppMainWidgetState extends State { } }, ), - if (!widget.isConnected) ConnectionInfo() ], ); } diff --git a/lib/src/components/connection_state.dart b/lib/src/components/connection_state.dart index 7a2fcfc..1edf804 100644 --- a/lib/src/components/connection_state.dart +++ b/lib/src/components/connection_state.dart @@ -1,6 +1,5 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/misc.dart'; class ConnectionInfo extends StatefulWidget { const ConnectionInfo({super.key}); @@ -9,69 +8,87 @@ class ConnectionInfo extends StatefulWidget { State createState() => _ConnectionInfoWidgetState(); } -class _ConnectionInfoWidgetState extends State { - int redColorOpacity = 100; // Initial opacity value - bool redColorGoUp = true; // Direction of the opacity change - double screenWidth = 0; // To hold the screen width +class _ConnectionInfoWidgetState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _positionAnim; + late Animation _widthAnim; - Timer? _colorAnimationTimer; + bool showAnimation = false; + + final double minBarWidth = 40; + final double maxBarWidth = 150; @override void initState() { super.initState(); - _startColorAnimation(); - } - void _startColorAnimation() { - // Change the color every 200 milliseconds - _colorAnimationTimer = Timer.periodic(Duration(milliseconds: 200), (timer) { - setState(() { - if (redColorOpacity <= 100) { - redColorGoUp = true; - } - if (redColorOpacity >= 150) { - redColorGoUp = false; - } - if (redColorGoUp) { - redColorOpacity += 10; - } else { - redColorOpacity -= 10; - } - }); + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 4), + ); + + _positionAnim = Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + + _widthAnim = TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: minBarWidth, end: maxBarWidth), + weight: 50), + TweenSequenceItem( + tween: Tween(begin: maxBarWidth, end: minBarWidth), + weight: 50), + ]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + + // Delay start by 2 seconds + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + _controller.repeat(reverse: true); + setState(() { + showAnimation = true; + }); + } }); } @override void dispose() { - _colorAnimationTimer?.cancel(); + _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - screenWidth = MediaQuery.of(context).size.width; // Get the screen width + if (!showAnimation) return Container(); + double screenWidth = MediaQuery.of(context).size.width; - return Stack( - children: [ - Positioned( - top: 3, // Position it at the top - left: (screenWidth * 0.5) / 2, // Center it horizontally - child: AnimatedContainer( - duration: Duration(milliseconds: 100), - width: screenWidth * 0.5, // 50% of the screen width - decoration: BoxDecoration( - border: Border.all( - color: Colors.red[600]! - .withAlpha(redColorOpacity), // Use the animated opacity - width: 2.0, // Red border width + return SizedBox( + width: screenWidth, + height: 1, + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + double barWidth = _widthAnim.value; + double left = _positionAnim.value * (screenWidth - barWidth); + return Stack( + children: [ + Positioned( + left: left, + top: 0, + bottom: 0, + child: Container( + width: barWidth, + decoration: BoxDecoration( + color: context.color.primary, + borderRadius: BorderRadius.circular(4), + ), + ), ), - borderRadius: BorderRadius.all( - Radius.circular(10.0), - ), // Rounded corners - ), - ), - ), - ], + ], + ); + }, + ), ); } } diff --git a/lib/src/providers/api/media.dart b/lib/src/providers/api/media.dart index d378018..69c0137 100644 --- a/lib/src/providers/api/media.dart +++ b/lib/src/providers/api/media.dart @@ -238,8 +238,6 @@ class ImageUploader { ); if (wasSend.isError) { - // await box.put("retransmit-$messageId-offset", 0); - // await box.delete("retransmit-$messageId-uploadtoken"); Logger("api.dart").shout("error while uploading media"); return null; } diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index 8107c74..dc401ea 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -141,13 +141,13 @@ class ApiProvider { isAuthenticated = false; } - void _onData(dynamic msgBuffer) { + void _onData(dynamic msgBuffer) async { try { final msg = server.ServerToClient.fromBuffer(msgBuffer); if (msg.v0.hasResponse()) { messagesV0[msg.v0.seq] = msg; } else { - handleServerMessage(msg); + await handleServerMessage(msg); } } catch (e) { log.shout("Error parsing the servers message: $e"); diff --git a/lib/src/providers/connection_provider.dart b/lib/src/providers/connection_provider.dart new file mode 100644 index 0000000..c47795f --- /dev/null +++ b/lib/src/providers/connection_provider.dart @@ -0,0 +1,10 @@ +import 'package:flutter/foundation.dart'; + +class ConnectionChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { + bool _isConnected = false; + bool get isConnected => _isConnected; + Future updateConnectionState(bool update) async { + _isConnected = update; + notifyListeners(); + } +} diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 51c36db..28290c9 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -19,6 +19,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; extension ShortCutsExtension on BuildContext { AppLocalizations get lang => AppLocalizations.of(this)!; TwonlyDatabase get db => Provider.of(this); + ColorScheme get color => Theme.of(this).colorScheme; } Future writeLogToFile(LogRecord record) async { diff --git a/lib/src/views/chats/chat_list_view.dart b/lib/src/views/chats/chat_list_view.dart index f76c0ed..c5aef12 100644 --- a/lib/src/views/chats/chat_list_view.dart +++ b/lib/src/views/chats/chat_list_view.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:convert'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/components/connection_state.dart'; import 'package:twonly/src/components/flame.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/message_send_state_icon.dart'; @@ -12,6 +14,7 @@ import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/json_models/message.dart'; import 'package:twonly/src/providers/api/media.dart'; +import 'package:twonly/src/providers/connection_provider.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera_to_share/share_image_view.dart'; import 'package:twonly/src/views/chats/chat_item_details_view.dart'; @@ -31,6 +34,7 @@ class ChatListView extends StatefulWidget { class _ChatListViewState extends State { @override Widget build(BuildContext context) { + bool isConnected = context.watch().isConnected; return Scaffold( appBar: AppBar( title: Text("twonly"), @@ -71,60 +75,73 @@ class _ChatListViewState extends State { ) ], ), - body: StreamBuilder( - stream: twonlyDatabase.contactsDao.watchContactsForChatList(), - builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data == null) { - return Container(); - } + body: Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + child: isConnected ? Container() : ConnectionInfo(), + ), + Positioned.fill( + child: StreamBuilder( + stream: twonlyDatabase.contactsDao.watchContactsForChatList(), + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data == null) { + return Container(); + } - var contacts = snapshot.data!; - if (contacts.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: OutlinedButton.icon( - icon: Icon(Icons.person_add), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SearchUsernameView(), - ), + var contacts = snapshot.data!; + if (contacts.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: OutlinedButton.icon( + icon: Icon(Icons.person_add), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SearchUsernameView(), + ), + ); + }, + label: + Text(context.lang.chatListViewSearchUserNameBtn)), + ), + ); + } + + int maxTotalMediaCounter = 0; + if (contacts.isNotEmpty) { + maxTotalMediaCounter = contacts + .map((x) => x.totalMediaCounter) + .reduce((a, b) => a > b ? a : b); + } + + return RefreshIndicator( + onRefresh: () async { + await apiProvider.close(() {}); + await apiProvider.connect(); + await Future.delayed(Duration(seconds: 1)); + }, + child: ListView.builder( + restorationId: 'chat_list_view', + itemCount: contacts.length, + itemBuilder: (BuildContext context, int index) { + final user = contacts[index]; + return UserListItem( + key: ValueKey(user.userId), + user: user, + maxTotalMediaCounter: maxTotalMediaCounter, ); }, - label: Text(context.lang.chatListViewSearchUserNameBtn)), - ), - ); - } - - int maxTotalMediaCounter = 0; - if (contacts.isNotEmpty) { - maxTotalMediaCounter = contacts - .map((x) => x.totalMediaCounter) - .reduce((a, b) => a > b ? a : b); - } - - return RefreshIndicator( - onRefresh: () async { - await apiProvider.close(() {}); - await apiProvider.connect(); - await Future.delayed(Duration(seconds: 1)); - }, - child: ListView.builder( - restorationId: 'chat_list_view', - itemCount: contacts.length, - itemBuilder: (BuildContext context, int index) { - final user = contacts[index]; - return UserListItem( - key: ValueKey(user.userId), - user: user, - maxTotalMediaCounter: maxTotalMediaCounter, + ), ); }, ), - ); - }, + ), + ], ), floatingActionButton: Padding( padding: const EdgeInsets.only(bottom: 30.0), diff --git a/lib/src/views/chats/start_new_chat.dart b/lib/src/views/chats/start_new_chat.dart index 33a5674..2e4e20d 100644 --- a/lib/src/views/chats/start_new_chat.dart +++ b/lib/src/views/chats/start_new_chat.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; @@ -34,6 +35,8 @@ class _StartNewChat extends State { twonlyDatabase.contactsDao.watchContactsForShareView(); contactSub = stream.listen((update) { + update.sort((a, b) => + getContactDisplayName(a).compareTo(getContactDisplayName(b))); setState(() { allContacts = update; }); @@ -83,6 +86,7 @@ class _StartNewChat extends State { onChanged: (_) { filterUsers(); }, + controller: searchUserName, decoration: getInputDecoration( context, context.lang.shareImageSearchAllContacts, @@ -116,21 +120,18 @@ class UserList extends StatelessWidget { @override Widget build(BuildContext context) { - // Step 1: Sort the users alphabetically - users - .sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange)); - return ListView.builder( restorationId: 'new_message_users_list', itemCount: users.length + 2, itemBuilder: (BuildContext context, int i) { if (i == 0) { return ListTile( + key: Key("add_new_contact"), title: Text(context.lang.startNewChatNewContact), leading: CircleAvatar( child: FaIcon( FontAwesomeIcons.userPlus, - size: 15, + size: 13, ), ), onTap: () { @@ -144,17 +145,17 @@ class UserList extends StatelessWidget { ); } if (i == 1) { - return HeadLineComponent(context.lang.startNewChatYourContacts); + return Divider(); } Contact user = users[i - 2]; int flameCounter = getFlameCounterFromContact(user); return UserContextMenu( + key: Key(user.userId.toString()), contact: user, child: ListTile( title: Row( - mainAxisAlignment: MainAxisAlignment.start, // Center horizontally - crossAxisAlignment: - CrossAxisAlignment.center, // Center vertically + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(getContactDisplayName(user)), if (flameCounter >= 1) @@ -164,9 +165,25 @@ class UserList extends StatelessWidget { maxTotalMediaCounter, prefix: true, ), + Spacer(), + IconButton( + icon: FaIcon(FontAwesomeIcons.boxOpen, + size: 13, + color: user.archived ? null : Colors.transparent), + onPressed: user.archived + ? () async { + final update = + ContactsCompanion(archived: Value(false)); + await twonlyDatabase.contactsDao + .updateContact(user.userId, update); + } + : null) ], ), - leading: ContactAvatar(contact: user), + leading: ContactAvatar( + contact: user, + fontSize: 13, + ), onTap: () { Navigator.pushReplacement( context, diff --git a/lib/src/views/home_view.dart b/lib/src/views/home_view.dart index c259cb3..e5faa35 100644 --- a/lib/src/views/home_view.dart +++ b/lib/src/views/home_view.dart @@ -11,7 +11,10 @@ import 'package:flutter/material.dart'; Function(int) globalUpdateOfHomeViewPageIndex = (a) {}; class HomeView extends StatefulWidget { - const HomeView({super.key, required this.initialPage}); + const HomeView({ + super.key, + required this.initialPage, + }); final int initialPage; @override