diff --git a/commet/lib/client/alert.dart b/commet/lib/client/alert.dart index b12ea2a1e..50ab51659 100644 --- a/commet/lib/client/alert.dart +++ b/commet/lib/client/alert.dart @@ -1,11 +1,12 @@ import 'package:commet/utils/notifying_list.dart'; +import 'package:flutter/widgets.dart'; enum AlertType { info, warning, critical } class Alert { late String Function() _messageGetter; late String Function() _titleGetter; - late Function()? action; + late Function(BuildContext context)? action; AlertType type; String get title => _titleGetter(); diff --git a/commet/lib/client/components/push_notification/notification_manager.dart b/commet/lib/client/components/push_notification/notification_manager.dart index e11c9436b..1f25f57e5 100644 --- a/commet/lib/client/components/push_notification/notification_manager.dart +++ b/commet/lib/client/components/push_notification/notification_manager.dart @@ -46,7 +46,7 @@ class NotificationManager { messageGetter: () => "The last attempt to start the notification updating service failed. Push notifications will not be updated in the background until this is resolved. Tap for more info", titleGetter: () => "Couldn't update notifications in background", - action: () => LinkUtils.open(Uri.parse( + action: (_) => LinkUtils.open(Uri.parse( "https://commet.chat/troubleshoot/android-background-service-failed/")), ); diff --git a/commet/lib/config/build_config.dart b/commet/lib/config/build_config.dart index e342733a5..353d9a27b 100644 --- a/commet/lib/config/build_config.dart +++ b/commet/lib/config/build_config.dart @@ -55,6 +55,12 @@ class BuildConfig { static Uri donationRewardsApiHost = Uri.https("stripe-rewards.commet.chat"); + static const String _BUILD_DATE = + String.fromEnvironment('BUILD_DATE', defaultValue: "0"); + + static DateTime get BUILD_DATE => + DateTime.fromMillisecondsSinceEpoch(int.parse(_BUILD_DATE)); + // IM SO SORRY static const String appName = MOBILE ? (ANDROID diff --git a/commet/lib/config/preferences.dart b/commet/lib/config/preferences.dart index 30811b3ef..cae1b9524 100644 --- a/commet/lib/config/preferences.dart +++ b/commet/lib/config/preferences.dart @@ -74,6 +74,7 @@ class Preferences { static const String _hideRoomSidePanel = "hide_room_side_panel"; + static const String _checkForUpdates = "check_for_updates"; static const String _runningDonationCheckFlow = "running_donation_check_flow"; final StreamController _onSettingChanged = StreamController.broadcast(); @@ -474,6 +475,12 @@ class Preferences { _onSettingChanged.add(null); } + bool? get checkForUpdates => _preferences!.getBool(_checkForUpdates); + + Future setCheckForUpdates(bool value) async { + await _preferences!.setBool(_checkForUpdates, value); + } + (String, DateTime)? get runningDonationCheckFlow { var result = _preferences!.getString(_runningDonationCheckFlow); diff --git a/commet/lib/main.dart b/commet/lib/main.dart index ad898ad65..6e735b9f1 100644 --- a/commet/lib/main.dart +++ b/commet/lib/main.dart @@ -17,14 +17,17 @@ import 'package:commet/ui/pages/bubble/bubble_page.dart'; import 'package:commet/ui/pages/fatal_error/fatal_error_page.dart'; import 'package:commet/ui/pages/login/login_page.dart'; import 'package:commet/ui/pages/main/main_page.dart'; +import 'package:commet/ui/pages/setup/menus/check_for_updates.dart'; import 'package:commet/utils/android_intent_helper.dart'; import 'package:commet/utils/custom_uri.dart'; import 'package:commet/utils/background_tasks/background_task_manager.dart'; import 'package:commet/utils/database/database_server.dart'; import 'package:commet/utils/emoji/unicode_emoji.dart'; import 'package:commet/utils/event_bus.dart'; +import 'package:commet/utils/first_time_setup.dart'; import 'package:commet/utils/scaled_app.dart'; import 'package:commet/utils/shortcuts_manager.dart'; +import 'package:commet/utils/update_checker.dart'; import 'package:commet/utils/window_management.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; @@ -251,6 +254,11 @@ Future startGui() async { var initialTheme = await preferences.resolveTheme(); + if (preferences.checkForUpdates == null && + UpdateChecker.shouldCheckForUpdates) { + FirstTimeSetup.registerPostLoginSetup(UpdateCheckerSetup()); + } + runApp(App( clientManager: clientManager!, initialTheme: initialTheme, diff --git a/commet/lib/ui/molecules/alert_view.dart b/commet/lib/ui/molecules/alert_view.dart index aab107bbc..568f160ef 100644 --- a/commet/lib/ui/molecules/alert_view.dart +++ b/commet/lib/ui/molecules/alert_view.dart @@ -11,7 +11,7 @@ class AlertView extends StatelessWidget { return Material( color: Colors.transparent, child: InkWell( - onTap: alert.action, + onTap: alert.action == null ? null : () => alert.action?.call(context), child: Row(children: [ Padding( padding: const EdgeInsets.all(8.0), diff --git a/commet/lib/ui/organisms/home_screen/home_screen.dart b/commet/lib/ui/organisms/home_screen/home_screen.dart index e401302a0..608fe5ae1 100644 --- a/commet/lib/ui/organisms/home_screen/home_screen.dart +++ b/commet/lib/ui/organisms/home_screen/home_screen.dart @@ -2,9 +2,11 @@ import 'dart:async'; import 'package:commet/client/client.dart'; import 'package:commet/client/client_manager.dart'; +import 'package:commet/main.dart'; import 'package:commet/ui/organisms/invitation_view/incoming_invitations_view.dart'; import 'package:commet/utils/event_bus.dart'; import 'package:commet/ui/organisms/home_screen/home_screen_view.dart'; +import 'package:commet/utils/update_checker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -28,6 +30,11 @@ class _HomeScreenState extends State { @override void initState() { syncSub = widget.clientManager.onSync.stream.listen(onSync); + + if (preferences.checkForUpdates == true) { + UpdateChecker.checkForUpdates(); + } + updateRecent(); super.initState(); } diff --git a/commet/lib/ui/pages/settings/categories/about/settings_category_about.dart b/commet/lib/ui/pages/settings/categories/about/settings_category_about.dart index fc7d265d5..4ead0ae3f 100644 --- a/commet/lib/ui/pages/settings/categories/about/settings_category_about.dart +++ b/commet/lib/ui/pages/settings/categories/about/settings_category_about.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart' as m; import 'package:tiamat/tiamat.dart' as tiamat; import 'package:vodozemac/vodozemac.dart' as vod; +import 'package:intl/intl.dart' as intl; class SettingsCategoryAbout implements SettingsCategory { String get labelSettingsAppLogs => Intl.message("Logs", @@ -142,6 +143,9 @@ class _AppInfoState extends State<_AppInfo> { BuildConfig.VERSION_TAG), tiamat.Text.labelLow( "${BuildConfig.GIT_HASH.substring(0, 7)} ${BuildConfig.BUILD_DETAIL}"), + tiamat.Text.labelLow("Built: " + + intl.DateFormat(intl.DateFormat.YEAR_MONTH_DAY) + .format(BuildConfig.BUILD_DATE)), if (deviceInfo != null) Row( children: [ @@ -214,7 +218,7 @@ Platform: `${BuildConfig.PLATFORM}` Version: `${BuildConfig.VERSION_TAG}` Git Hash: `${BuildConfig.GIT_HASH}` Detail: `${BuildConfig.BUILD_DETAIL}` - +Build Timestamp: `${BuildConfig.BUILD_DATE.millisecondsSinceEpoch} (${intl.DateFormat(intl.DateFormat.YEAR_MONTH_DAY).format(BuildConfig.BUILD_DATE)})` **System Info** ${deviceInfo?.data["name"] is String ? "Name: `${deviceInfo!.data["name"]}`" : ""} diff --git a/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart b/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart index 2de7528c9..dad9240fd 100644 --- a/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart +++ b/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart @@ -1,4 +1,6 @@ import 'package:commet/main.dart'; +import 'package:commet/ui/pages/setup/menus/check_for_updates.dart'; +import 'package:commet/utils/update_checker.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; @@ -127,6 +129,8 @@ class GeneralSettingsPageState extends State { }); }, ), + if (UpdateChecker.shouldCheckForUpdates) + CheckForUpdatesSettingWidget(), ]), ), const SizedBox( diff --git a/commet/lib/ui/pages/setup/menus/check_for_updates.dart b/commet/lib/ui/pages/setup/menus/check_for_updates.dart new file mode 100644 index 000000000..fe61f5d2a --- /dev/null +++ b/commet/lib/ui/pages/setup/menus/check_for_updates.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:commet/main.dart'; +import 'package:commet/ui/pages/settings/categories/app/general_settings_page.dart'; +import 'package:commet/ui/pages/setup/setup_menu.dart'; +import 'package:flutter/material.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; + +class UpdateCheckerSetup implements SetupMenu { + StreamController controller = StreamController(); + + GlobalKey key = GlobalKey(); + + @override + Widget builder(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + tiamat.Text.largeTitle("Check for updates"), + tiamat.Text.label( + "Would you like Commet to automatically check for new updates?"), + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + decoration: BoxDecoration( + color: ColorScheme.of(context).surfaceContainerLowest, + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: CheckForUpdatesSettingWidget(), + )), + ), + ], + ); + } + + @override + Stream get onStateChanged => controller.stream; + + @override + SetupMenuState state = SetupMenuState.canProgress; + + @override + Future submit() async { + if (preferences.checkForUpdates == null) { + preferences.setCheckForUpdates(false); + } + } +} + +class CheckForUpdatesSettingWidget extends StatefulWidget { + const CheckForUpdatesSettingWidget({super.key}); + + @override + State createState() => + _CheckForUpdatesSettingWidgetState(); +} + +class _CheckForUpdatesSettingWidgetState + extends State { + @override + Widget build(BuildContext context) { + return GeneralSettingsPageState.settingToggle( + preferences.checkForUpdates ?? false, + title: "Check for updates", + description: + "Automatically check if there is a newer version of Commet available", + onChanged: (v) { + preferences.setCheckForUpdates(v); + setState(() {}); + }, + ); + } +} diff --git a/commet/lib/ui/pages/setup/setup_page.dart b/commet/lib/ui/pages/setup/setup_page.dart index 763c16d51..849ab6824 100644 --- a/commet/lib/ui/pages/setup/setup_page.dart +++ b/commet/lib/ui/pages/setup/setup_page.dart @@ -2,8 +2,6 @@ import 'package:commet/config/build_config.dart'; import 'package:commet/ui/atoms/scaled_safe_area.dart'; import 'package:commet/ui/pages/setup/setup_menu.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - import 'package:tiamat/tiamat.dart'; import 'package:tiamat/tiamat.dart' as tiamat; @@ -19,9 +17,6 @@ class _SetupPageState extends State { int currentMenuIndex = 0; late SetupMenu currentMenu; - String get setupPageBeforeYouBegin => Intl.message("Before you begin...", - name: "setupPageBeforeYouBegin", desc: "Title for first time setup page"); - @override void initState() { super.initState(); @@ -40,7 +35,6 @@ class _SetupPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - tiamat.Text.largeTitle(setupPageBeforeYouBegin), Flexible( child: ClipRRect( borderRadius: BorderRadius.circular(10), diff --git a/commet/lib/utils/update_checker.dart b/commet/lib/utils/update_checker.dart new file mode 100644 index 000000000..255771199 --- /dev/null +++ b/commet/lib/utils/update_checker.dart @@ -0,0 +1,138 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:commet/client/alert.dart'; +import 'package:commet/config/build_config.dart'; +import 'package:commet/config/platform_utils.dart'; +import 'package:commet/debug/log.dart'; +import 'package:commet/main.dart'; +import 'package:commet/ui/navigation/adaptive_dialog.dart'; +import 'package:commet/utils/error_utils.dart'; +import 'package:commet/utils/link_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as path; + +import 'package:http/http.dart' as http; + +class UpdateChecker { + static bool foundUpdate = false; + + static Future checkForUpdates() async { + if (foundUpdate) return; + + if (!shouldCheckForUpdates) { + return; + } + + if (preferences.checkForUpdates != true) { + return; + } + + const key = "chat.commet.published_version"; + + var url = Uri.parse( + "https://data.commet.chat/_matrix/federation/v1/query/profile?user_id=@updates:data.commet.chat"); + + var response = await http.get(url); + + if (response.statusCode == 200) { + foundUpdate = true; + } + + Log.i("Got update data: ${response.body}"); + + var fields = jsonDecode(response.body) as Map; + + if (fields.containsKey(key)) { + var date = fields["chat.commet.build_date_ms"]; + + var val = int.parse(date); + var time = DateTime.fromMillisecondsSinceEpoch(val); + + var canAutoUpdate = fields["chat.commet.auto_update"] == "true"; + Log.i("Supports auto update: $canAutoUpdate"); + if (time.isAfter(BuildConfig.BUILD_DATE)) { + var tag = fields[key]; + clientManager!.alertManager.addAlert(Alert(AlertType.info, + messageGetter: () => + "There is a newer version of Commet available: ${tag}", + titleGetter: () => "Update Available", + action: (context) => doUpdateAction(context, canAutoUpdate))); + } else { + Log.i( + "Found an update, but it's build date is not after the current build, current: ${BuildConfig.BUILD_DATE.toString()} remote: ${time.toString()}"); + } + + return; + } + } + + static bool get shouldCheckForUpdates { + if (PlatformUtils.isWeb) { + return false; + } + + if (BuildConfig.VERSION_TAG == "v0.0.0-artifact") { + return false; + } + + return true; + } + + static doUpdateAction(BuildContext context, bool canAutoUpdate) async { + if (PlatformUtils.isWindows) { + windowsUpdateAction(context, canAutoUpdate); + } + + if (PlatformUtils.isAndroid) { + LinkUtils.open(Uri.parse("https://commet.chat/install/android/")); + } + + if (PlatformUtils.isLinux) { + LinkUtils.open(Uri.parse("https://commet.chat/install/linux/")); + } + } + + static windowsUpdateAction(BuildContext context, bool canAutoUpdate) async { + var exe = Platform.resolvedExecutable; + + var installPath = path.dirname(exe); + + var installerPath = + path.join(installPath, "installer", "commet-installer.exe"); + + Log.i("Installed at: $installerPath"); + + if (await File(installerPath).exists() && canAutoUpdate) { + var confirmation = await AdaptiveDialog.confirmation(context, + prompt: "Would you like to run the update installer?"); + + if (confirmation == true) { + await ErrorUtils.tryRun(context, () async { + Log.i("Found installer, doing automatic update"); + + for (var client in clientManager!.clients) { + await client.close(); + } + + Process.run( + installerPath, + [ + "--command", + "update", + ], + runInShell: true); + + // TODO: not this + await Future.delayed(Duration(seconds: 1)); + }); + + exit(0); + } + + if (confirmation == null) return; + } + + LinkUtils.open(Uri.parse("https://commet.chat/install/windows/")); + } +} diff --git a/commet/scripts/build_release.dart b/commet/scripts/build_release.dart index 2ffbaa169..dd42a1938 100644 --- a/commet/scripts/build_release.dart +++ b/commet/scripts/build_release.dart @@ -79,6 +79,8 @@ Future main(List args) async { "VERSION_TAG=$version", "--dart-define", "ENABLE_GOOGLE_SERVICES=$enableGoogleServices", + "--dart-define", + "BUILD_DATE=${DateTime.now().millisecondsSinceEpoch}", if (buildDetail != null) "--dart-define", if (buildDetail != null) "BUILD_DETAIL=$buildDetail", if (platform == "web") "--dart-define",