diff --git a/lib/account/account_key_value.dart b/lib/account/account_key_value.dart index 8c2c4b701e..088b0882f9 100644 --- a/lib/account/account_key_value.dart +++ b/lib/account/account_key_value.dart @@ -12,20 +12,36 @@ class AccountKeyValue extends HiveKeyValue { static const _refreshStickerLastTime = 'refreshStickerLastTime'; static const _primarySessionId = 'primarySessionId'; static const _hasNewAlbum = 'hasNewAlbum'; + static const _checkUpdateLastTime = 'checkUpdateLastTime'; + static const _ignoredVersion = '_ignoredVersion'; bool get hasSyncCircle => box.get(_hasSyncCircle, defaultValue: false) as bool; + set hasSyncCircle(bool value) => box.put(_hasSyncCircle, value); int get refreshStickerLastTime => box.get(_refreshStickerLastTime, defaultValue: 0) as int; + set refreshStickerLastTime(int value) => box.put(_refreshStickerLastTime, value); String? get primarySessionId => box.get(_primarySessionId, defaultValue: null) as String?; + set primarySessionId(String? value) => box.put(_primarySessionId, value); bool get hasNewAlbum => box.get(_hasNewAlbum, defaultValue: false) as bool; + set hasNewAlbum(bool value) => box.put(_hasNewAlbum, value); + + int get checkUpdateLastTime => + box.get(_checkUpdateLastTime, defaultValue: 0) as int; + + set checkUpdateLastTime(int value) => box.put(_checkUpdateLastTime, value); + + String? get ignoredVersion => + box.get(_ignoredVersion, defaultValue: '0.0.0') as String?; + + set ignoredVersion(String? value) => box.put(_ignoredVersion, value); } diff --git a/lib/app.dart b/lib/app.dart index b0eebc49e5..8e59f9b7c1 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -26,6 +26,8 @@ import 'ui/home/conversation/conversation_page.dart'; import 'ui/home/home.dart'; import 'ui/home/route/responsive_navigator_cubit.dart'; import 'ui/landing/landing.dart'; +import 'utils/app_lifecycle.dart'; +import 'utils/auto_update_checker.dart'; import 'utils/extension/extension.dart'; import 'utils/hook.dart'; import 'utils/logger.dart'; @@ -258,6 +260,20 @@ class _Home extends HookWidget { } }, [signed]); + useEffect(() { + void onAppStateChanged() { + if (isAppActive) { + checkUpdate(context: context); + } + } + + onAppStateChanged(); + appActiveListener.addListener(onAppStateChanged); + return () { + appActiveListener.removeListener(onAppStateChanged); + }; + }, []); + if (signed) { BlocProvider.of(context) ..limit = MediaQuery.of(context).size.height ~/ diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index b77f7999d4..cf1d1253ed 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -55,21 +55,24 @@ class MessageLookup extends MessageLookupByLibrary { static String m16(name) => "Remove ${name}"; - static String m17(name) => "Do you want to delete ${name} circle?"; + static String m17(newVersion, current) => + "Mixin Messenger ${newVersion} is now available, you have ${current}. Would you like to download it now?"; - static String m18(date) => "${date} join"; + static String m18(name) => "Do you want to delete ${name} circle?"; - static String m19(count) => "${count} Participants"; + static String m19(date) => "${date} join"; - static String m20(count) => "${count} Pinned Messages"; + static String m20(count) => "${count} Participants"; - static String m21(user, preview) => "${user} pinned ${preview}"; + static String m21(count) => "${count} Pinned Messages"; - static String m22(count) => "${count} related messages"; + static String m22(user, preview) => "${user} pinned ${preview}"; - static String m23(value) => "value now ${value}"; + static String m23(count) => "${count} related messages"; - static String m24(value) => "value then ${value}"; + static String m24(value) => "value now ${value}"; + + static String m25(value) => "value then ${value}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -138,6 +141,8 @@ class MessageLookup extends MessageLookupByLibrary { "chatWaiting": m5, "chatWaitingDesktop": MessageLookupByLibrary.simpleMessage("desktop"), "chats": MessageLookupByLibrary.simpleMessage("Chats"), + "checkUpdate": + MessageLookupByLibrary.simpleMessage("Check for updates"), "circleTitle": m6, "circles": MessageLookupByLibrary.simpleMessage("Circles"), "clear": MessageLookupByLibrary.simpleMessage("Clear"), @@ -181,6 +186,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteGroup": MessageLookupByLibrary.simpleMessage("Delete Group"), "developer": MessageLookupByLibrary.simpleMessage("Developer"), "done": MessageLookupByLibrary.simpleMessage("Done"), + "download": MessageLookupByLibrary.simpleMessage("Download"), "downloadLink": MessageLookupByLibrary.simpleMessage("Download Link: "), "editAnnouncement": MessageLookupByLibrary.simpleMessage("Edit group description"), @@ -232,6 +238,8 @@ class MessageLookup extends MessageLookupByLibrary { "groupsInCommon": MessageLookupByLibrary.simpleMessage("Groups in common"), "helpCenter": MessageLookupByLibrary.simpleMessage("Help center"), + "ignoreThisUpdate": + MessageLookupByLibrary.simpleMessage("Ignore this update"), "image": MessageLookupByLibrary.simpleMessage("Image"), "includeFiles": MessageLookupByLibrary.simpleMessage("Include Files"), "includeVideos": MessageLookupByLibrary.simpleMessage("Include Videos"), @@ -269,6 +277,9 @@ class MessageLookup extends MessageLookupByLibrary { "name": MessageLookupByLibrary.simpleMessage("Name"), "networkConnectionFailed": MessageLookupByLibrary.simpleMessage("Network connection failed"), + "newVersionAvailable": + MessageLookupByLibrary.simpleMessage("New version available"), + "newVersionDescription": m17, "next": MessageLookupByLibrary.simpleMessage("Next"), "noAudio": MessageLookupByLibrary.simpleMessage("NO AUDIO"), "noData": MessageLookupByLibrary.simpleMessage("NO DATA"), @@ -288,8 +299,8 @@ class MessageLookup extends MessageLookupByLibrary { "openLogDirectory": MessageLookupByLibrary.simpleMessage("open log directory"), "originalImage": MessageLookupByLibrary.simpleMessage("Original"), - "pageDeleteCircle": m17, - "pageEditProfileJoin": m18, + "pageDeleteCircle": m18, + "pageEditProfileJoin": m19, "pageLandingClickToReload": MessageLookupByLibrary.simpleMessage("CLICK TO RELOAD QR CODE"), "pageLandingLoginMessage": MessageLookupByLibrary.simpleMessage( @@ -298,12 +309,12 @@ class MessageLookup extends MessageLookupByLibrary { "Login to Mixin Messenger by QR Code"), "pageRightEmptyMessage": MessageLookupByLibrary.simpleMessage( "Select a conversation to start messaging"), - "participantsCount": m19, + "participantsCount": m20, "phoneNumber": MessageLookupByLibrary.simpleMessage("Phone number"), "photos": MessageLookupByLibrary.simpleMessage("Photos"), "pin": MessageLookupByLibrary.simpleMessage("Pin"), - "pinMessageCount": m20, - "pinned": m21, + "pinMessageCount": m21, + "pinned": m22, "pleaseWait": MessageLookupByLibrary.simpleMessage("Please wait a moment"), "post": MessageLookupByLibrary.simpleMessage("Post"), @@ -334,7 +345,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchEmpty": MessageLookupByLibrary.simpleMessage( "No chats, \ncontacts or messages found."), "searchMessageHistory": MessageLookupByLibrary.simpleMessage("Search"), - "searchRelatedMessage": m22, + "searchRelatedMessage": m23, "searchUser": MessageLookupByLibrary.simpleMessage("Search contact"), "searchUserHint": MessageLookupByLibrary.simpleMessage("Mixin ID or Phone number"), @@ -396,10 +407,10 @@ class MessageLookup extends MessageLookupByLibrary { "videos": MessageLookupByLibrary.simpleMessage("Videos"), "waitingForThisMessage": MessageLookupByLibrary.simpleMessage("Waiting for this message."), - "walletTransactionCurrentValue": m23, + "walletTransactionCurrentValue": m24, "walletTransactionThatTimeNoValue": MessageLookupByLibrary.simpleMessage("value then N/A"), - "walletTransactionThatTimeValue": m24, + "walletTransactionThatTimeValue": m25, "webView2RuntimeInstallDescription": MessageLookupByLibrary.simpleMessage( "The device has not installed the WebView2 Runtime component. Please download and install WebView2 Runtime first."), "webViewRuntimeNotAvailable": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index ba87a3de9b..3598e47743 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -52,21 +52,24 @@ class MessageLookup extends MessageLookupByLibrary { static String m16(name) => "移除 ${name}"; - static String m17(name) => "确定删除${name}圈子吗?"; + static String m17(newVersion, current) => + "发现新版本 Mixin Messenger ${newVersion},当前版本为 ${current}。是否要下载最新的版本?"; - static String m18(date) => "${date}加入"; + static String m18(name) => "确定删除${name}圈子吗?"; - static String m19(count) => "共 ${count} 人"; + static String m19(date) => "${date}加入"; - static String m20(count) => "${count}条置顶消息"; + static String m20(count) => "共 ${count} 人"; - static String m21(user, preview) => "${user}置顶了${preview}"; + static String m21(count) => "${count}条置顶消息"; - static String m22(count) => "${count} 条相关的消息"; + static String m22(user, preview) => "${user}置顶了${preview}"; - static String m23(value) => "价值 ${value}"; + static String m23(count) => "${count} 条相关的消息"; - static String m24(value) => "当时价值 ${value}"; + static String m24(value) => "价值 ${value}"; + + static String m25(value) => "当时价值 ${value}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -118,6 +121,7 @@ class MessageLookup extends MessageLookupByLibrary { "chatWaiting": m5, "chatWaitingDesktop": MessageLookupByLibrary.simpleMessage("桌面端"), "chats": MessageLookupByLibrary.simpleMessage("全部聊天"), + "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "circleTitle": m6, "circles": MessageLookupByLibrary.simpleMessage("圈子"), "clear": MessageLookupByLibrary.simpleMessage("清除"), @@ -156,6 +160,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteGroup": MessageLookupByLibrary.simpleMessage("删除群组"), "developer": MessageLookupByLibrary.simpleMessage("开发者"), "done": MessageLookupByLibrary.simpleMessage("完成"), + "download": MessageLookupByLibrary.simpleMessage("下载"), "downloadLink": MessageLookupByLibrary.simpleMessage("下载链接:"), "editAnnouncement": MessageLookupByLibrary.simpleMessage("编辑群公告"), "editCircle": MessageLookupByLibrary.simpleMessage("管理圈子"), @@ -201,6 +206,7 @@ class MessageLookup extends MessageLookupByLibrary { "groups": MessageLookupByLibrary.simpleMessage("群组"), "groupsInCommon": MessageLookupByLibrary.simpleMessage("共同群组"), "helpCenter": MessageLookupByLibrary.simpleMessage("帮助中心"), + "ignoreThisUpdate": MessageLookupByLibrary.simpleMessage("忽略这次更新"), "image": MessageLookupByLibrary.simpleMessage("照片"), "includeFiles": MessageLookupByLibrary.simpleMessage("包含文件"), "includeVideos": MessageLookupByLibrary.simpleMessage("包括视频"), @@ -233,6 +239,8 @@ class MessageLookup extends MessageLookupByLibrary { "name": MessageLookupByLibrary.simpleMessage("名字"), "networkConnectionFailed": MessageLookupByLibrary.simpleMessage("网络连接失败"), + "newVersionAvailable": MessageLookupByLibrary.simpleMessage("发现新版本"), + "newVersionDescription": m17, "next": MessageLookupByLibrary.simpleMessage("下一步"), "noAudio": MessageLookupByLibrary.simpleMessage("没有音频"), "noData": MessageLookupByLibrary.simpleMessage("没有数据"), @@ -250,8 +258,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("打开通知"), "openLogDirectory": MessageLookupByLibrary.simpleMessage("打开日志文件夹"), "originalImage": MessageLookupByLibrary.simpleMessage("原图"), - "pageDeleteCircle": m17, - "pageEditProfileJoin": m18, + "pageDeleteCircle": m18, + "pageEditProfileJoin": m19, "pageLandingClickToReload": MessageLookupByLibrary.simpleMessage("点击重新加载二维码"), "pageLandingLoginMessage": MessageLookupByLibrary.simpleMessage( @@ -260,12 +268,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("通过二维码登录 Mixin Messenger"), "pageRightEmptyMessage": MessageLookupByLibrary.simpleMessage("选择一个对话,开始发送信息"), - "participantsCount": m19, + "participantsCount": m20, "phoneNumber": MessageLookupByLibrary.simpleMessage("手机号"), "photos": MessageLookupByLibrary.simpleMessage("照片"), "pin": MessageLookupByLibrary.simpleMessage("置顶"), - "pinMessageCount": m20, - "pinned": m21, + "pinMessageCount": m21, + "pinned": m22, "pleaseWait": MessageLookupByLibrary.simpleMessage("请稍等一下"), "post": MessageLookupByLibrary.simpleMessage("文章"), "preview": MessageLookupByLibrary.simpleMessage("预览"), @@ -290,7 +298,7 @@ class MessageLookup extends MessageLookupByLibrary { "search": MessageLookupByLibrary.simpleMessage("搜索"), "searchEmpty": MessageLookupByLibrary.simpleMessage("找不到联系人或消息。"), "searchMessageHistory": MessageLookupByLibrary.simpleMessage("搜索聊天记录"), - "searchRelatedMessage": m22, + "searchRelatedMessage": m23, "searchUser": MessageLookupByLibrary.simpleMessage("搜索用户"), "searchUserHint": MessageLookupByLibrary.simpleMessage("Mixin ID 或手机号"), "send": MessageLookupByLibrary.simpleMessage("发送"), @@ -343,10 +351,10 @@ class MessageLookup extends MessageLookupByLibrary { "videos": MessageLookupByLibrary.simpleMessage("视频"), "waitingForThisMessage": MessageLookupByLibrary.simpleMessage("正在等待这个消息。"), - "walletTransactionCurrentValue": m23, + "walletTransactionCurrentValue": m24, "walletTransactionThatTimeNoValue": MessageLookupByLibrary.simpleMessage("当时价值 暂无"), - "walletTransactionThatTimeValue": m24, + "walletTransactionThatTimeValue": m25, "webView2RuntimeInstallDescription": MessageLookupByLibrary.simpleMessage( "该设备暂未安装 WebView2 组件,请先下载并安装 WebView2 Runtime。"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 715bd9c74a..86a4d2fb1c 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2589,6 +2589,56 @@ class Localization { args: [], ); } + + /// `Check for updates` + String get checkUpdate { + return Intl.message( + 'Check for updates', + name: 'checkUpdate', + desc: '', + args: [], + ); + } + + /// `New version available` + String get newVersionAvailable { + return Intl.message( + 'New version available', + name: 'newVersionAvailable', + desc: '', + args: [], + ); + } + + /// `Ignore this update` + String get ignoreThisUpdate { + return Intl.message( + 'Ignore this update', + name: 'ignoreThisUpdate', + desc: '', + args: [], + ); + } + + /// `Mixin Messenger {newVersion} is now available, you have {current}. Would you like to download it now?` + String newVersionDescription(Object newVersion, Object current) { + return Intl.message( + 'Mixin Messenger $newVersion is now available, you have $current. Would you like to download it now?', + name: 'newVersionDescription', + desc: '', + args: [newVersion, current], + ); + } + + /// `Download` + String get download { + return Intl.message( + 'Download', + name: 'download', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 5d3bf966e9..a1c67219ed 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -252,5 +252,10 @@ "groupsInCommon": "Groups in common", "failedToOpenFile": "Failed to open file {name}", "openLogDirectory": "open log directory", - "messageTooLong": "Message content is too long" + "messageTooLong": "Message content is too long", + "checkUpdate": "Check for updates", + "newVersionAvailable": "New version available", + "ignoreThisUpdate": "Ignore this update", + "newVersionDescription": "Mixin Messenger {newVersion} is now available, you have {current}. Would you like to download it now?", + "download": "Download" } diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 7f83d1f2a0..8dcfe61560 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -247,5 +247,10 @@ "groupsInCommon": "共同群组", "failedToOpenFile": "无法打开文件:{name}", "openLogDirectory": "打开日志文件夹", - "messageTooLong": "消息内容过长" + "messageTooLong": "消息内容过长", + "checkUpdate": "检查更新", + "newVersionAvailable": "发现新版本", + "ignoreThisUpdate": "忽略这次更新", + "newVersionDescription": "发现新版本 Mixin Messenger {newVersion},当前版本为 {current}。是否要下载最新的版本?", + "download": "下载" } diff --git a/lib/ui/setting/about_page.dart b/lib/ui/setting/about_page.dart index 8801efec83..238073bf2d 100644 --- a/lib/ui/setting/about_page.dart +++ b/lib/ui/setting/about_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import '../../constants/resources.dart'; +import '../../utils/auto_update_checker.dart'; import '../../utils/extension/extension.dart'; import '../../utils/hook.dart'; import '../../utils/system/package_info.dart'; @@ -82,6 +83,10 @@ class AboutPage extends HookWidget { onTap: () => openUri(context, 'https://mixin.one/pages/privacy'), ), + CellItem( + title: Text(context.l10n.checkUpdate), + onTap: () => checkUpdate(context: context, force: true), + ), ], ), ), diff --git a/lib/utils/auto_update_checker.dart b/lib/utils/auto_update_checker.dart new file mode 100644 index 0000000000..9935045952 --- /dev/null +++ b/lib/utils/auto_update_checker.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:github/github.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../account/account_key_value.dart'; +import '../constants/constants.dart'; +import '../widgets/dialog.dart'; +import 'extension/extension.dart'; +import 'logger.dart'; +import 'system/package_info.dart'; + +final github = GitHub(); + +Future checkUpdate({ + required BuildContext context, + bool force = false, +}) async { + final lastCheckUpdate = AccountKeyValue.instance.checkUpdateLastTime; + final now = DateTime.now().millisecondsSinceEpoch; + if (!force && now - lastCheckUpdate < hours24) { + d('checkUpdate: skip'); + return; + } + try { + final release = await github.repositories.getLatestRelease( + RepositorySlug('MixinNetwork', 'flutter-app'), + ); + final packageInfo = await getPackageInfo(); + final currentVersion = 'v${packageInfo.version}'; + + i('Latest release: ${release.tagName} ${release.publishedAt}, current version: $currentVersion'); + + AccountKeyValue.instance.checkUpdateLastTime = now; + + if (!force && release.tagName == AccountKeyValue.instance.ignoredVersion) { + // ignore this version + return; + } + if (release.tagName == currentVersion) { + // current version is the latest + return; + } + await showMixinDialog( + context: context, + child: _UpdateDialog( + release: release, + ignored: release.tagName == AccountKeyValue.instance.ignoredVersion, + currentVersion: currentVersion, + ), + ); + } catch (error, stackTrace) { + e('check update failed: $error, $stackTrace'); + } +} + +class _UpdateDialog extends HookWidget { + const _UpdateDialog({ + Key? key, + required this.release, + required this.ignored, + required this.currentVersion, + }) : super(key: key); + + final Release release; + + final bool ignored; + + final String currentVersion; + + @override + Widget build(BuildContext context) { + final ignoreUpdate = useState(ignored); + return SizedBox( + width: 400, + child: AlertDialogLayout( + title: Text(context.l10n.newVersionAvailable), + titleMarginBottom: 24, + content: DefaultTextStyle.merge( + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: context.theme.text, + ), + child: Column( + children: [ + Text(context.l10n.newVersionDescription( + release.tagName ?? '', currentVersion)), + if (!ignored) + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Checkbox( + value: ignoreUpdate.value, + onChanged: (checked) { + final ignore = checked ?? false; + ignoreUpdate.value = ignore; + AccountKeyValue.instance.ignoredVersion = + ignore ? release.tagName : null; + }, + ), + Text( + context.l10n.ignoreThisUpdate, + style: TextStyle( + color: context.theme.secondaryText, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + actions: [ + MixinButton( + backgroundTransparent: true, + onTap: () => Navigator.pop(context), + child: Text(context.l10n.cancel), + ), + MixinButton( + onTap: () => launch('https://mixin.one/messenger'), + child: Text(context.l10n.download), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart index 12e5067b90..762ca5f359 100644 --- a/lib/widgets/dialog.dart +++ b/lib/widgets/dialog.dart @@ -108,7 +108,7 @@ class AlertDialogLayout extends StatelessWidget { ), child: title!, ), - if (title != null) const SizedBox(height: 48), + if (title != null) SizedBox(height: titleMarginBottom), DefaultTextStyle.merge( style: TextStyle( fontSize: 18, diff --git a/pubspec.lock b/pubspec.lock index 7aa078d522..a64ce5900c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -627,6 +627,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.3.2" + github: + dependency: "direct main" + description: + name: github + url: "https://pub.dartlang.org" + source: hosted + version: "9.0.0" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4180010206..972caae14d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: flutter_portal: ^0.4.0 flutter_svg: ^1.0.3 gallery_saver: ^2.3.2 + github: ^9.0.0 hive: ^2.0.4 hive_flutter: ^1.0.0 http: ^0.13.3