diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 75c7fd90e2..b0eac192a8 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -1,5 +1,5 @@ import 'dart:async'; - +import 'snackbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -16,6 +16,8 @@ import 'recent_dm_conversations.dart'; import 'store.dart'; import 'subscription_list.dart'; import 'text.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; class ZulipApp extends StatelessWidget { const ZulipApp({super.key, this.navigatorObservers}); @@ -113,49 +115,64 @@ class ZulipApp extends StatelessWidget { ); return GlobalStoreWidget( - child: Builder(builder: (context) { - final globalStore = GlobalStoreWidget.of(context); - // TODO(#524) choose initial account as last one used - final initialAccountId = globalStore.accounts.firstOrNull?.id; - return MaterialApp( - title: 'Zulip', - localizationsDelegates: ZulipLocalizations.localizationsDelegates, - supportedLocales: ZulipLocalizations.supportedLocales, - theme: theme, - - navigatorKey: navigatorKey, - navigatorObservers: navigatorObservers ?? const [], - builder: (BuildContext context, Widget? child) { - if (!ready.value) { - SchedulerBinding.instance.addPostFrameCallback( - (_) => _declareReady()); - } - GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context); - return child!; - }, - - // We use onGenerateInitialRoutes for the real work of specifying the - // initial nav state. To do that we need [MaterialApp] to decide to - // build a [Navigator]... which means specifying either `home`, `routes`, - // `onGenerateRoute`, or `onUnknownRoute`. Make it `onGenerateRoute`. - // It never actually gets called, though: `onGenerateInitialRoutes` - // handles startup, and then we always push whole routes with methods - // like [Navigator.push], never mere names as with [Navigator.pushNamed]. - onGenerateRoute: (_) => null, - - onGenerateInitialRoutes: (_) { - return [ - MaterialWidgetRoute(page: const ChooseAccountPage()), - if (initialAccountId != null) ...[ - HomePage.buildRoute(accountId: initialAccountId), - InboxPage.buildRoute(accountId: initialAccountId), - ], - ]; - }); + child: Builder(builder: (context) { + final globalStore = GlobalStoreWidget.of(context); + // TODO(#524) choose initial account as last one used + final initialAccountId = globalStore.accounts.firstOrNull?.id; + return MaterialApp( + title: 'Zulip', + localizationsDelegates: ZulipLocalizations.localizationsDelegates, + supportedLocales: ZulipLocalizations.supportedLocales, + theme: theme, + + navigatorKey: navigatorKey, + navigatorObservers: navigatorObservers ?? const [], + builder: (BuildContext context, Widget? child) { + if (!ready.value) { + SchedulerBinding.instance.addPostFrameCallback( + (_) => _declareReady()); + } + GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context); + return child!; + }, + + // We use onGenerateInitialRoutes for the real work of specifying the + // initial nav state. To do that we need [MaterialApp] to decide to + // build a [Navigator]... which means specifying either `home`, `routes`, + // `onGenerateRoute`, or `onUnknownRoute`. Make it `onGenerateRoute`. + // It never actually gets called, though: `onGenerateInitialRoutes` + // handles startup, and then we always push whole routes with methods + // like [Navigator.push], never mere names as with [Navigator.pushNamed]. + onGenerateRoute: (_) => null, + + onGenerateInitialRoutes: (_) { + return [ + MaterialWidgetRoute(page: const ChooseAccountPage()), + if (initialAccountId != null) ...[ + HomePage.buildRoute(accountId: initialAccountId), + InboxPage.buildRoute(accountId: initialAccountId), + ], + ]; + }); })); } } + + + + + + + + + + + + + + + /// The Zulip "brand color", a purplish blue. /// /// This is chosen as the sRGB midpoint of the Zulip logo's gradient. @@ -166,18 +183,18 @@ class ChooseAccountPage extends StatelessWidget { const ChooseAccountPage({super.key}); Widget _buildAccountItem( - BuildContext context, { - required int accountId, - required Widget title, - Widget? subtitle, - }) { + BuildContext context, { + required int accountId, + required Widget title, + Widget? subtitle, + }) { return Card( - clipBehavior: Clip.hardEdge, - child: ListTile( - title: title, - subtitle: subtitle, - onTap: () => Navigator.push(context, - HomePage.buildRoute(accountId: accountId)))); + clipBehavior: Clip.hardEdge, + child: ListTile( + title: title, + subtitle: subtitle, + onTap: () => Navigator.push(context, + HomePage.buildRoute(accountId: accountId)))); } @override @@ -186,27 +203,31 @@ class ChooseAccountPage extends StatelessWidget { assert(!PerAccountStoreWidget.debugExistsOf(context)); final globalStore = GlobalStoreWidget.of(context); return Scaffold( - appBar: AppBar( - title: Text(zulipLocalizations.chooseAccountPageTitle), - actions: const [ChooseAccountPageOverflowButton()]), - body: SafeArea( - minimum: const EdgeInsets.all(8), - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - for (final (:accountId, :account) in globalStore.accountEntries) - _buildAccountItem(context, - accountId: accountId, - title: Text(account.realmUrl.toString()), - subtitle: Text(account.email)), - const SizedBox(height: 12), - ElevatedButton( - onPressed: () => Navigator.push(context, - AddAccountPage.buildRoute()), - child: Text(zulipLocalizations.chooseAccountButtonAddAnAccount)), - ]))), - )); + appBar: AppBar( + title: Text(zulipLocalizations.chooseAccountPageTitle), + actions: const [ChooseAccountPageOverflowButton()]), + body: SafeArea( + minimum: const EdgeInsets.all(8), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + for (final (:accountId, :account) in globalStore.accountEntries) + _buildAccountItem(context, + accountId: accountId, + title: Text(account.realmUrl.toString()), + subtitle: Text(account.email)), + const SizedBox(height: 12), + ElevatedButton( + onPressed: () => Navigator.push(context, + AddAccountPage.buildRoute()), + child: Text(zulipLocalizations.chooseAccountButtonAddAnAccount)), + SizedBox( + height: 40, // Adjust the height as needed + child: SnackBarPage(), + ), + ]))), + )); } } @@ -218,26 +239,34 @@ class ChooseAccountPageOverflowButton extends StatelessWidget { @override Widget build(BuildContext context) { return PopupMenuButton( - itemBuilder: (BuildContext context) => const [ - PopupMenuItem( - value: ChooseAccountPageOverflowMenuItem.aboutZulip, - child: Text('About Zulip')), - ], - onSelected: (item) { - switch (item) { - case ChooseAccountPageOverflowMenuItem.aboutZulip: - Navigator.push(context, AboutZulipPage.buildRoute(context)); - } - }); + itemBuilder: (BuildContext context) => const [ + PopupMenuItem( + value: ChooseAccountPageOverflowMenuItem.aboutZulip, + child: Text('About Zulip')), + ], + onSelected: (item) { + switch (item) { + case ChooseAccountPageOverflowMenuItem.aboutZulip: + Navigator.push(context, AboutZulipPage.buildRoute(context)); + } + }); } } + + + + + + + + class HomePage extends StatelessWidget { const HomePage({super.key}); static Route buildRoute({required int accountId}) { return MaterialAccountWidgetRoute(accountId: accountId, - page: const HomePage()); + page: const HomePage()); } @override @@ -245,8 +274,9 @@ class HomePage extends StatelessWidget { final store = PerAccountStoreWidget.of(context); final zulipLocalizations = ZulipLocalizations.of(context); - InlineSpan bold(String text) => TextSpan( - text: text, style: const TextStyle(fontWeight: FontWeight.bold)); + InlineSpan bold(String text) => + TextSpan( + text: text, style: const TextStyle(fontWeight: FontWeight.bold)); int? testStreamId; if (store.connection.realmUrl.origin == 'https://chat.zulip.org') { @@ -254,52 +284,76 @@ class HomePage extends StatelessWidget { } return Scaffold( - appBar: AppBar(title: const Text("Home")), - body: Center( - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - DefaultTextStyle.merge( - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 18), - child: Column(children: [ - const Text('🚧 Under construction 🚧'), - const SizedBox(height: 12), - Text.rich(TextSpan( - text: 'Connected to: ', - children: [bold(store.realmUrl.toString())])), - Text.rich(TextSpan( - text: 'Zulip server version: ', - children: [bold(store.zulipVersion)])), - Text(zulipLocalizations.subscribedToNStreams(store.subscriptions.length)), - ])), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => Navigator.push(context, - MessageListPage.buildRoute(context: context, - narrow: const AllMessagesNarrow())), - child: Text(zulipLocalizations.allMessagesPageTitle)), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => Navigator.push(context, - InboxPage.buildRoute(context: context)), - child: const Text("Inbox")), // TODO(i18n) - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => Navigator.push(context, - SubscriptionListPage.buildRoute(context: context)), - child: const Text("Subscribed streams")), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => Navigator.push(context, - RecentDmConversationsPage.buildRoute(context: context)), - child: Text(zulipLocalizations.recentDmConversationsPageTitle)), - if (testStreamId != null) ...[ - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => Navigator.push(context, - MessageListPage.buildRoute(context: context, - narrow: StreamNarrow(testStreamId!))), - child: const Text("#test here")), // scaffolding hack, see above - ], - ]))); - } -} + appBar: AppBar(title: const Text("Home")), + body: SingleChildScrollView( + child: Center( + child: Column( + + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DefaultTextStyle.merge( + + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 18), + child: Column( + children: [ + const Text('🚧 Under construction 🚧'), + const SizedBox(height: 12), + Text.rich(TextSpan( + text: 'Connected to: ', + children: [bold(store.realmUrl.toString())])), + Text.rich(TextSpan( + text: 'Zulip server version: ', + children: [bold(store.zulipVersion)])), + Text(zulipLocalizations.subscribedToNStreams(store.subscriptions.length)), + ] + ) + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.push(context, + MessageListPage.buildRoute(context: context, + narrow: const AllMessagesNarrow())), + child: Text(zulipLocalizations.allMessagesPageTitle) + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.push(context, + InboxPage.buildRoute(context: context)), + child: const Text("Inbox") + ), // TODO(i18n) + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.push(context, + SubscriptionListPage.buildRoute(context: context)), + child: const Text("Subscribed streams") + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.push(context, + RecentDmConversationsPage.buildRoute(context: context)), + child: Text(zulipLocalizations.recentDmConversationsPageTitle) + ), + if (testStreamId != null) ...[ + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.push(context, + MessageListPage.buildRoute(context: context, + narrow: StreamNarrow(testStreamId!))), + child: const Text("#test here") + ), + + // Wrap SnackBarPage in a SizedBox to constrain its size + SizedBox( + height: 40, // Adjust the height as needed + child: SnackBarPage(), // class which is imported from snackbar.dart file + ), + ], + ] + ) + ) + ) + ); + + + }} \ No newline at end of file diff --git a/lib/widgets/inbox.dart b/lib/widgets/inbox.dart index 8aa5172efc..bd44bb3673 100644 --- a/lib/widgets/inbox.dart +++ b/lib/widgets/inbox.dart @@ -12,12 +12,14 @@ import 'store.dart'; import 'text.dart'; import 'unread_count_badge.dart'; + + class InboxPage extends StatefulWidget { const InboxPage({super.key}); static Route buildRoute({int? accountId, BuildContext? context}) { return MaterialAccountWidgetRoute(accountId: accountId, context: context, - page: const InboxPage()); + page: const InboxPage()); } @override @@ -76,7 +78,7 @@ class _InboxPageState extends State with PerAccountStoreAwareStateMix // row's collapsed state when it's cleared of unreads. // TODO(perf) handle those updates efficiently collapsedStreamIds.removeWhere((streamId) => - !unreadsModel!.streams.containsKey(streamId)); + !unreadsModel!.streams.containsKey(streamId)); if (unreadsModel!.dms.isEmpty) { allDmsCollapsed = false; } @@ -103,7 +105,7 @@ class _InboxPageState extends State with PerAccountStoreAwareStateMix continue; } final hasMention = unreadsModel!.dms[dmNarrow]!.any( - (messageId) => unreadsModel!.mentions.contains(messageId)); + (messageId) => unreadsModel!.mentions.contains(messageId)); if (hasMention) allDmsHasMention = true; dmItems.add((dmNarrow, countInNarrow, hasMention)); allDmsCount += countInNarrow; @@ -113,15 +115,15 @@ class _InboxPageState extends State with PerAccountStoreAwareStateMix } final sortedUnreadStreams = unreadsModel!.streams.entries - // Filter out any straggling unreads in unsubscribed streams. - // There won't normally be any, but it happens with certain infrequent - // state changes, typically for less than a few hundred milliseconds. - // See [Unreads]. - // - // Also, we want to depend on the subscription data for things like - // choosing the stream icon. - .where((entry) => subscriptions.containsKey(entry.key)) - .toList() + // Filter out any straggling unreads in unsubscribed streams. + // There won't normally be any, but it happens with certain infrequent + // state changes, typically for less than a few hundred milliseconds. + // See [Unreads]. + // + // Also, we want to depend on the subscription data for things like + // choosing the stream icon. + .where((entry) => subscriptions.containsKey(entry.key)) + .toList() ..sort((a, b) { final subA = subscriptions[a.key]!; final subB = subscriptions[b.key]!; @@ -159,26 +161,35 @@ class _InboxPageState extends State with PerAccountStoreAwareStateMix } return Scaffold( - appBar: AppBar(title: const Text('Inbox')), - body: SafeArea( - // Don't pad the bottom here; we want the list content to do that. - bottom: false, - child: StickyHeaderListView.builder( - itemCount: sections.length, - itemBuilder: (context, index) { - final section = sections[index]; - switch (section) { - case _AllDmsSectionData(): - return _AllDmsSection( - data: section, - collapsed: allDmsCollapsed, - pageState: this, - ); - case _StreamSectionData(:var streamId): - final collapsed = collapsedStreamIds.contains(streamId); - return _StreamSection(data: section, collapsed: collapsed, pageState: this); - } - }))); + appBar: AppBar(title: const Text('Inbox')), + + body: SafeArea( + // Don't pad the bottom here; we want the list content to do that. + bottom: false, + child: StickyHeaderListView.builder( + itemCount: sections.length, + + itemBuilder: (context, index) { + final section = sections[index]; + switch (section) { + case _AllDmsSectionData(): + return _AllDmsSection( + data: section, + collapsed: allDmsCollapsed, + pageState: this, + ); + + case _StreamSectionData(:var streamId): + final collapsed = collapsedStreamIds.contains(streamId); + return _StreamSection(data: section, collapsed: collapsed, pageState: this); + + + } + + } + )) + + ); } } @@ -229,39 +240,39 @@ abstract class _HeaderItem extends StatelessWidget { @override Widget build(BuildContext context) { return Material( - color: collapsed ? Colors.white : uncollapsedBackgroundColor, - child: InkWell( - // TODO use onRowTap to handle taps that are not on the collapse button. - // Probably we should give the collapse button a 44px or 48px square - // touch target: - // - // But that's in tension with the Figma, which gives these header rows - // 40px min height. - onTap: onCollapseButtonTap, - child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ - Padding(padding: const EdgeInsets.all(10), - child: Icon(size: 20, color: const Color(0x7F1D2E48), - collapsed ? ZulipIcons.arrow_right : ZulipIcons.arrow_down)), - Icon(size: 18, color: collapsed ? collapsedIconColor : uncollapsedIconColor, - icon), - const SizedBox(width: 5), - Expanded(child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - style: const TextStyle( - fontSize: 17, - height: (20 / 17), - color: Color(0xFF222222), - ).merge(weightVariableTextStyle(context, wght: 600)), - maxLines: 1, - overflow: TextOverflow.ellipsis, - title))), - const SizedBox(width: 12), - if (hasMention) const _AtMentionMarker(), - Padding(padding: const EdgeInsetsDirectional.only(end: 16), - child: UnreadCountBadge(backgroundColor: unreadCountBadgeBackgroundColor, bold: true, - count: count)), - ]))); + color: collapsed ? Colors.white : uncollapsedBackgroundColor, + child: InkWell( + // TODO use onRowTap to handle taps that are not on the collapse button. + // Probably we should give the collapse button a 44px or 48px square + // touch target: + // + // But that's in tension with the Figma, which gives these header rows + // 40px min height. + onTap: onCollapseButtonTap, + child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Padding(padding: const EdgeInsets.all(10), + child: Icon(size: 20, color: const Color(0x7F1D2E48), + collapsed ? ZulipIcons.arrow_right : ZulipIcons.arrow_down)), + Icon(size: 18, color: collapsed ? collapsedIconColor : uncollapsedIconColor, + icon), + const SizedBox(width: 5), + Expanded(child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + style: const TextStyle( + fontSize: 17, + height: (20 / 17), + color: Color(0xFF222222), + ).merge(weightVariableTextStyle(context, wght: 600)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + title))), + const SizedBox(width: 12), + if (hasMention) const _AtMentionMarker(), + Padding(padding: const EdgeInsetsDirectional.only(end: 16), + child: UnreadCountBadge(backgroundColor: unreadCountBadgeBackgroundColor, bold: true, + count: count)), + ]))); } } @@ -306,18 +317,18 @@ class _AllDmsSection extends StatelessWidget { pageState: pageState, ); return StickyHeaderItem( - header: header, - child: Column(children: [ - header, - if (!collapsed) ...data.items.map((item) { - final (narrow, count, hasMention) = item; - return _DmItem( - narrow: narrow, - count: count, - hasMention: hasMention, - ); - }), - ])); + header: header, + child: Column(children: [ + header, + if (!collapsed) ...data.items.map((item) { + final (narrow, count, hasMention) = item; + return _DmItem( + narrow: narrow, + count: count, + hasMention: hasMention, + ); + }), + ])); } } @@ -341,39 +352,39 @@ class _DmItem extends StatelessWidget { [] => selfUser.fullName, [var otherUserId] => store.users[otherUserId]?.fullName ?? '(unknown user)', - // TODO(i18n): List formatting, like you can do in JavaScript: - // new Intl.ListFormat('ja').format(['Chris', 'Greg', 'Alya', 'Shu']) - // // 'Chris、Greg、Alya、Shu' + // TODO(i18n): List formatting, like you can do in JavaScript: + // new Intl.ListFormat('ja').format(['Chris', 'Greg', 'Alya', 'Shu']) + // // 'Chris、Greg、Alya、Shu' _ => narrow.otherRecipientIds.map((id) => store.users[id]?.fullName ?? '(unknown user)').join(', '), }; return Material( - color: Colors.white, - child: InkWell( - onTap: () { - Navigator.push(context, - MessageListPage.buildRoute(context: context, narrow: narrow)); - }, - child: ConstrainedBox(constraints: const BoxConstraints(minHeight: 34), - child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox(width: 63), - Expanded(child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - style: const TextStyle( - fontSize: 17, - height: (20 / 17), - color: Color(0xFF222222), - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - title))), - const SizedBox(width: 12), - if (hasMention) const _AtMentionMarker(), - Padding(padding: const EdgeInsetsDirectional.only(end: 16), - child: UnreadCountBadge(backgroundColor: null, - count: count)), - ])))); + color: Colors.white, + child: InkWell( + onTap: () { + Navigator.push(context, + MessageListPage.buildRoute(context: context, narrow: narrow)); + }, + child: ConstrainedBox(constraints: const BoxConstraints(minHeight: 34), + child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + const SizedBox(width: 63), + Expanded(child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + style: const TextStyle( + fontSize: 17, + height: (20 / 17), + color: Color(0xFF222222), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + title))), + const SizedBox(width: 12), + if (hasMention) const _AtMentionMarker(), + Padding(padding: const EdgeInsetsDirectional.only(end: 16), + child: UnreadCountBadge(backgroundColor: null, + count: count)), + ])))); } } @@ -393,9 +404,9 @@ class _StreamHeaderItem extends _HeaderItem { @override get collapsedIconColor => subscription.colorSwatch().iconOnPlainBackground; @override get uncollapsedIconColor => subscription.colorSwatch().iconOnBarBackground; @override get uncollapsedBackgroundColor => - subscription.colorSwatch().barBackground; + subscription.colorSwatch().barBackground; @override get unreadCountBadgeBackgroundColor => - subscription.colorSwatch().unreadCountBadgeBackground; + subscription.colorSwatch().unreadCountBadgeBackground; @override get onCollapseButtonTap => () { if (collapsed) { @@ -429,19 +440,19 @@ class _StreamSection extends StatelessWidget { pageState: pageState, ); return StickyHeaderItem( - header: header, - child: Column(children: [ - header, - if (!collapsed) ...data.items.map((item) { - final (topic, count, hasMention, _) = item; - return _TopicItem( - streamId: data.streamId, - topic: topic, - count: count, - hasMention: hasMention, - ); - }), - ])); + header: header, + child: Column(children: [ + header, + if (!collapsed) ...data.items.map((item) { + final (topic, count, hasMention, _) = item; + return _TopicItem( + streamId: data.streamId, + topic: topic, + count: count, + hasMention: hasMention, + ); + }), + ])); } } @@ -464,33 +475,33 @@ class _TopicItem extends StatelessWidget { final subscription = store.subscriptions[streamId]!; return Material( - color: Colors.white, - child: InkWell( - onTap: () { - final narrow = TopicNarrow(streamId, topic); - Navigator.push(context, - MessageListPage.buildRoute(context: context, narrow: narrow)); - }, - child: ConstrainedBox(constraints: const BoxConstraints(minHeight: 34), - child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox(width: 63), - Expanded(child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - style: const TextStyle( - fontSize: 17, - height: (20 / 17), - color: Color(0xFF222222), - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - topic))), - const SizedBox(width: 12), - if (hasMention) const _AtMentionMarker(), - Padding(padding: const EdgeInsetsDirectional.only(end: 16), - child: UnreadCountBadge(backgroundColor: subscription.colorSwatch(), - count: count)), - ])))); + color: Colors.white, + child: InkWell( + onTap: () { + final narrow = TopicNarrow(streamId, topic); + Navigator.push(context, + MessageListPage.buildRoute(context: context, narrow: narrow)); + }, + child: ConstrainedBox(constraints: const BoxConstraints(minHeight: 34), + child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + const SizedBox(width: 63), + Expanded(child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + style: const TextStyle( + fontSize: 17, + height: (20 / 17), + color: Color(0xFF222222), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + topic))), + const SizedBox(width: 12), + if (hasMention) const _AtMentionMarker(), + Padding(padding: const EdgeInsetsDirectional.only(end: 16), + child: UnreadCountBadge(backgroundColor: subscription.colorSwatch(), + count: count)), + ])))); } } @@ -504,7 +515,7 @@ class _AtMentionMarker extends StatelessWidget { // Design for at-mention marker based on Figma screen: // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?type=design&node-id=224-16386&mode=design&t=JsNndFQ8fKFH0SjS-0 return Padding( - padding: const EdgeInsetsDirectional.only(end: 4), - child: Icon(ZulipIcons.at_sign, size: 14, color: markerColor)); + padding: const EdgeInsetsDirectional.only(end: 4), + child: Icon(ZulipIcons.at_sign, size: 14, color: markerColor)); } } diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index d80f31bea4..0e98c894ad 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -22,14 +22,14 @@ import 'profile.dart'; import 'sticky_header.dart'; import 'store.dart'; import 'text.dart'; - +import 'snackbar.dart'; class MessageListPage extends StatefulWidget { const MessageListPage({super.key, required this.narrow}); static Route buildRoute({int? accountId, BuildContext? context, - required Narrow narrow}) { + required Narrow narrow}) { return MaterialAccountWidgetRoute(accountId: accountId, context: context, - page: MessageListPage(narrow: narrow)); + page: MessageListPage(narrow: narrow)); } /// A [ComposeBoxController], if this [MessageListPage] offers a compose box. @@ -67,7 +67,7 @@ class _MessageListPageState extends State { case StreamNarrow(:final streamId): case TopicNarrow(:final streamId): backgroundColor = store.subscriptions[streamId]?.colorSwatch().barBackground - ?? _kUnsubscribedStreamRecipientHeaderColor; + ?? _kUnsubscribedStreamRecipientHeaderColor; // All recipient headers will match this color; remove distracting line // (but are recipient headers even needed for topic narrows?) removeAppBarBottomBorder = true; @@ -80,38 +80,43 @@ class _MessageListPageState extends State { } return Scaffold( - appBar: AppBar(title: MessageListAppBarTitle(narrow: widget.narrow), - backgroundColor: backgroundColor, - shape: removeAppBarBottomBorder - ? const Border() - : null, // i.e., inherit - ), - // TODO question for Vlad: for a stream view, should we set - // [backgroundColor] based on stream color, as in this frame: - // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=132%3A9684&mode=dev - // That's not obviously preferred over the default background that - // we matched to the Figma in 21dbae120. See another frame, which uses that: - // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=147%3A9088&mode=dev - body: Builder( - builder: (BuildContext context) => Center( - child: Column(children: [ - MediaQuery.removePadding( - // Scaffold knows about the app bar, and so has run this - // BuildContext, which is under `body`, through - // MediaQuery.removePadding with `removeTop: true`. - context: context, - - // The compose box, when present, pads the bottom inset. - // TODO this copies the details of when the compose box is shown; - // if those details get complicated, refactor to avoid copying. - // TODO(#311) If we have a bottom nav, it will pad the bottom - // inset, and this should always be true. - removeBottom: widget.narrow is! AllMessagesNarrow, - - child: Expanded( - child: MessageList(narrow: widget.narrow))), - ComposeBox(controllerKey: _composeBoxKey, narrow: widget.narrow), - ])))); + appBar: AppBar(title: MessageListAppBarTitle(narrow: widget.narrow), + backgroundColor: backgroundColor, + shape: removeAppBarBottomBorder + ? const Border() + : null, // i.e., inherit + ), + // TODO question for Vlad: for a stream view, should we set + // [backgroundColor] based on stream color, as in this frame: + // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=132%3A9684&mode=dev + // That's not obviously preferred over the default background that + // we matched to the Figma in 21dbae120. See another frame, which uses that: + // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=147%3A9088&mode=dev + body: Builder( + builder: (BuildContext context) => Center( + child: Column(children: [ + MediaQuery.removePadding( + // Scaffold knows about the app bar, and so has run this + // BuildContext, which is under `body`, through + // MediaQuery.removePadding with `removeTop: true`. + context: context, + + // The compose box, when present, pads the bottom inset. + // TODO this copies the details of when the compose box is shown; + // if those details get complicated, refactor to avoid copying. + // TODO(#311) If we have a bottom nav, it will pad the bottom + // inset, and this should always be true. + removeBottom: widget.narrow is! AllMessagesNarrow, + + child: Expanded( + child: MessageList(narrow: widget.narrow))), + ComposeBox(controllerKey: _composeBoxKey, narrow: widget.narrow), + SizedBox( + height: 40, // Adjust the height as needed + child: SnackBarPage(), // class which is imported from snackbar.dart file + // for showing snackbar + ), + ])))); } } @@ -124,17 +129,17 @@ class MessageListAppBarTitle extends StatelessWidget { // A null [Icon.icon] makes a blank space. final icon = (stream != null) ? iconDataForStream(stream) : null; return Row( - mainAxisSize: MainAxisSize.min, - // TODO(design): The vertical alignment of the stream privacy icon is a bit ad hoc. - // For screenshots of some experiments, see: - // https://github.com/zulip/zulip-flutter/pull/219#discussion_r1281024746 - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Icon(size: 16, icon), - const SizedBox(width: 8), - Flexible(child: Text(text)), - ]); + mainAxisSize: MainAxisSize.min, + // TODO(design): The vertical alignment of the stream privacy icon is a bit ad hoc. + // For screenshots of some experiments, see: + // https://github.com/zulip/zulip-flutter/pull/219#discussion_r1281024746 + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Icon(size: 16, icon), + const SizedBox(width: 8), + Flexible(child: Text(text)), + ]); } @override @@ -269,34 +274,34 @@ class _MessageListState extends State with PerAccountStoreAwareStat return DefaultTextStyle.merge( // TODO figure out text color -- web is supposedly hsl(0deg 0% 20%), // but seems much darker than that - style: const TextStyle(color: Color.fromRGBO(0, 0, 0, 1)), - // Pad the left and right insets, for small devices in landscape. - child: SafeArea( - // Don't let this be the place we pad the bottom inset. When there's - // no compose box, we want to let the message-list content pad it. - // TODO(#311) Remove as unnecessary if we do a bottom nav. - // The nav will pad the bottom inset, and an ancestor of this widget - // will have a `MediaQuery.removePadding` with `removeBottom: true`. - bottom: false, - - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 760), - child: NotificationListener( - onNotification: _handleScrollMetricsNotification, - child: Stack( - children: [ - _buildListView(context), - Positioned( - bottom: 0, - right: 0, - // TODO(#311) SafeArea shouldn't be needed if we have a - // bottom nav. That will pad the bottom inset. - child: SafeArea( - child: ScrollToBottomButton( - scrollController: scrollController, - visibleValue: _scrollToBottomVisibleValue))), - ])))))); + style: const TextStyle(color: Color.fromRGBO(0, 0, 0, 1)), + // Pad the left and right insets, for small devices in landscape. + child: SafeArea( + // Don't let this be the place we pad the bottom inset. When there's + // no compose box, we want to let the message-list content pad it. + // TODO(#311) Remove as unnecessary if we do a bottom nav. + // The nav will pad the bottom inset, and an ancestor of this widget + // will have a `MediaQuery.removePadding` with `removeBottom: true`. + bottom: false, + + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 760), + child: NotificationListener( + onNotification: _handleScrollMetricsNotification, + child: Stack( + children: [ + _buildListView(context), + Positioned( + bottom: 0, + right: 0, + // TODO(#311) SafeArea shouldn't be needed if we have a + // bottom nav. That will pad the bottom inset. + child: SafeArea( + child: ScrollToBottomButton( + scrollController: scrollController, + visibleValue: _scrollToBottomVisibleValue))), + ])))))); } Widget _buildListView(context) { @@ -306,91 +311,91 @@ class _MessageListState extends State with PerAccountStoreAwareStat // TODO: Offer `ScrollViewKeyboardDismissBehavior.interactive` (or // similar) if that is ever offered: // https://github.com/flutter/flutter/issues/57609#issuecomment-1355340849 - keyboardDismissBehavior: switch (Theme.of(context).platform) { + keyboardDismissBehavior: switch (Theme.of(context).platform) { // This seems to offer the only built-in way to close the keyboard // on iOS. It's not ideal; see TODO above. - TargetPlatform.iOS => ScrollViewKeyboardDismissBehavior.onDrag, + TargetPlatform.iOS => ScrollViewKeyboardDismissBehavior.onDrag, // The Android keyboard seems to have a built-in close button. - _ => ScrollViewKeyboardDismissBehavior.manual, - }, - - controller: scrollController, - semanticChildCount: length + 2, - anchor: 1.0, - center: centerSliverKey, - - slivers: [ - SliverStickyHeaderList( - headerPlacement: HeaderPlacement.scrollingStart, - delegate: SliverChildBuilderDelegate( - // To preserve state across rebuilds for individual [MessageItem] - // widgets as the size of [MessageListView.items] changes we need - // to match old widgets by their key to their new position in - // the list. - // - // The keys are of type [ValueKey] with a value of [Message.id] - // and here we use a O(log n) binary search method. This could - // be improved but for now it only triggers for materialized - // widgets. As a simple test, flinging through All Messages in - // CZO on a Pixel 5, this only runs about 10 times per rebuild - // and the timing for each call is <100 microseconds. - // - // Non-message items (e.g., start and end markers) that do not - // have state that needs to be preserved have not been given keys - // and will not trigger this callback. - findChildIndexCallback: (Key key) { - final valueKey = key as ValueKey; - final index = model!.findItemWithMessageId(valueKey.value); - if (index == -1) return null; - return length - 1 - (index - 2); - }, - childCount: length + 2, - (context, i) { - // To reinforce that the end of the feed has been reached: - // https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20Mark-as-read/near/1680603 - if (i == 0) return const SizedBox(height: 36); - - if (i == 1) return MarkAsReadWidget(narrow: widget.narrow); - - final data = model!.items[length - 1 - (i - 2)]; - return _buildItem(data, i); - })), - - // This is a trivial placeholder that occupies no space. Its purpose is - // to have the key that's passed to [ScrollView.center], and so to cause - // the above [SliverStickyHeaderList] to run from bottom to top. - const SliverToBoxAdapter(key: centerSliverKey), - ]); + _ => ScrollViewKeyboardDismissBehavior.manual, + }, + + controller: scrollController, + semanticChildCount: length + 2, + anchor: 1.0, + center: centerSliverKey, + + slivers: [ + SliverStickyHeaderList( + headerPlacement: HeaderPlacement.scrollingStart, + delegate: SliverChildBuilderDelegate( + // To preserve state across rebuilds for individual [MessageItem] + // widgets as the size of [MessageListView.items] changes we need + // to match old widgets by their key to their new position in + // the list. + // + // The keys are of type [ValueKey] with a value of [Message.id] + // and here we use a O(log n) binary search method. This could + // be improved but for now it only triggers for materialized + // widgets. As a simple test, flinging through All Messages in + // CZO on a Pixel 5, this only runs about 10 times per rebuild + // and the timing for each call is <100 microseconds. + // + // Non-message items (e.g., start and end markers) that do not + // have state that needs to be preserved have not been given keys + // and will not trigger this callback. + findChildIndexCallback: (Key key) { + final valueKey = key as ValueKey; + final index = model!.findItemWithMessageId(valueKey.value); + if (index == -1) return null; + return length - 1 - (index - 2); + }, + childCount: length + 2, + (context, i) { + // To reinforce that the end of the feed has been reached: + // https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20Mark-as-read/near/1680603 + if (i == 0) return const SizedBox(height: 36); + + if (i == 1) return MarkAsReadWidget(narrow: widget.narrow); + + final data = model!.items[length - 1 - (i - 2)]; + return _buildItem(data, i); + })), + + // This is a trivial placeholder that occupies no space. Its purpose is + // to have the key that's passed to [ScrollView.center], and so to cause + // the above [SliverStickyHeaderList] to run from bottom to top. + const SliverToBoxAdapter(key: centerSliverKey), + ]); } Widget _buildItem(MessageListItem data, int i) { switch (data) { case MessageListHistoryStartItem(): return const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: Text("No earlier messages."))); // TODO use an icon + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text("No earlier messages."))); // TODO use an icon case MessageListLoadingItem(): return const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: CircularProgressIndicator())); // TODO perhaps a different indicator + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: CircularProgressIndicator())); // TODO perhaps a different indicator case MessageListRecipientHeaderItem(): final header = RecipientHeader(message: data.message, narrow: widget.narrow); return StickyHeaderItem(allowOverflow: true, - header: header, child: header); + header: header, child: header); case MessageListDateSeparatorItem(): final header = RecipientHeader(message: data.message, narrow: widget.narrow); return StickyHeaderItem(allowOverflow: true, - header: header, - child: DateSeparator(message: data.message)); + header: header, + child: DateSeparator(message: data.message)); case MessageListMessageItem(): final header = RecipientHeader(message: data.message, narrow: widget.narrow); return MessageItem( - key: ValueKey(data.message.id), - header: header, - trailingWhitespace: i == 1 ? 8 : 11, - item: data); + key: ValueKey(data.message.id), + header: header, + trailingWhitespace: i == 1 ? 8 : 11, + item: data); } } } @@ -406,25 +411,25 @@ class ScrollToBottomButton extends StatelessWidget { final durationMsAtSpeedLimit = (1000 * distance / 8000).ceil(); final durationMs = max(300, durationMsAtSpeedLimit); scrollController.animateTo( - 0, - duration: Duration(milliseconds: durationMs), - curve: Curves.ease); + 0, + duration: Duration(milliseconds: durationMs), + curve: Curves.ease); } @override Widget build(BuildContext context) { return ValueListenableBuilder( - valueListenable: visibleValue, - builder: (BuildContext context, bool value, Widget? child) { - return (value && child != null) ? child : const SizedBox.shrink(); - }, - // TODO: fix hardcoded values for size and style here - child: IconButton( - tooltip: "Scroll to bottom", - icon: const Icon(Icons.expand_circle_down_rounded), - iconSize: 40, - color: const HSLColor.fromAHSL(0.5,240,0.96,0.68).toColor(), - onPressed: _navigateToBottom)); + valueListenable: visibleValue, + builder: (BuildContext context, bool value, Widget? child) { + return (value && child != null) ? child : const SizedBox.shrink(); + }, + // TODO: fix hardcoded values for size and style here + child: IconButton( + tooltip: "Scroll to bottom", + icon: const Icon(Icons.expand_circle_down_rounded), + iconSize: 40, + color: const HSLColor.fromAHSL(0.5,240,0.96,0.68).toColor(), + onPressed: _navigateToBottom)); } } @@ -446,8 +451,8 @@ class MarkAsReadWidget extends StatelessWidget { if (!context.mounted) return; final zulipLocalizations = ZulipLocalizations.of(context); await showErrorDialog(context: context, - title: zulipLocalizations.errorMarkAsReadFailedTitle, - message: e.toString()); + title: zulipLocalizations.errorMarkAsReadFailedTitle, + message: e.toString()); return; } if (!context.mounted) return; @@ -462,33 +467,33 @@ class MarkAsReadWidget extends StatelessWidget { final store = PerAccountStoreWidget.of(context); final unreadCount = store.unreads.countInNarrow(narrow); return AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: (unreadCount > 0) ? CrossFadeState.showSecond : CrossFadeState.showFirst, - firstChild: const SizedBox.shrink(), - secondChild: SizedBox(width: double.infinity, - // Design referenced from: - // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?type=design&node-id=132-9684&mode=design&t=jJwHzloKJ0TMOG4M-0 - child: Padding( - // vertical padding adjusted for tap target height (48px) of button - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10 - ((48 - 38) / 2)), - child: FilledButton.icon( - style: FilledButton.styleFrom( - backgroundColor: _UnreadMarker.color, - minimumSize: const Size.fromHeight(38), - textStyle: - // Restate [FilledButton]'s default, which inherits from - // [zulipTypography]… - Theme.of(context).textTheme.labelLarge! - // …then clobber some attributes to follow Figma: - .merge(const TextStyle( - fontSize: 18, - height: (23 / 18)) - .merge(weightVariableTextStyle(context, wght: 400))), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)), - ), - onPressed: () => _handlePress(context), - icon: const Icon(Icons.playlist_add_check), - label: Text(zulipLocalizations.markAllAsReadLabel))))); + duration: const Duration(milliseconds: 300), + crossFadeState: (unreadCount > 0) ? CrossFadeState.showSecond : CrossFadeState.showFirst, + firstChild: const SizedBox.shrink(), + secondChild: SizedBox(width: double.infinity, + // Design referenced from: + // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?type=design&node-id=132-9684&mode=design&t=jJwHzloKJ0TMOG4M-0 + child: Padding( + // vertical padding adjusted for tap target height (48px) of button + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10 - ((48 - 38) / 2)), + child: FilledButton.icon( + style: FilledButton.styleFrom( + backgroundColor: _UnreadMarker.color, + minimumSize: const Size.fromHeight(38), + textStyle: + // Restate [FilledButton]'s default, which inherits from + // [zulipTypography]… + Theme.of(context).textTheme.labelLarge! + // …then clobber some attributes to follow Figma: + .merge(const TextStyle( + fontSize: 18, + height: (23 / 18)) + .merge(weightVariableTextStyle(context, wght: 400))), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)), + ), + onPressed: () => _handlePress(context), + icon: const Icon(Icons.playlist_add_check), + label: Text(zulipLocalizations.markAllAsReadLabel))))); } } @@ -503,7 +508,7 @@ class RecipientHeader extends StatelessWidget { final message = this.message; return switch (message) { StreamMessage() => StreamMessageRecipientHeader(message: message, - showStream: narrow is AllMessagesNarrow), + showStream: narrow is AllMessagesNarrow), DmMessage() => DmRecipientHeader(message: message), }; } @@ -525,30 +530,30 @@ class DateSeparator extends StatelessWidget { return ColoredBox(color: Colors.white, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2), - child: Row(children: [ - const Expanded( - child: SizedBox(height: 0, - child: DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 0, - color: Colors.black)))))), - Padding(padding: const EdgeInsets.fromLTRB(2, 0, 2, textBottomPadding), - child: DateText( - color: _textColor, - fontSize: 16, - height: (16 / 16), - timestamp: message.timestamp)), - const SizedBox(height: 0, width: 12, - child: DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 0, - color: Colors.black))))) - ])), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2), + child: Row(children: [ + const Expanded( + child: SizedBox(height: 0, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 0, + color: Colors.black)))))), + Padding(padding: const EdgeInsets.fromLTRB(2, 0, 2, textBottomPadding), + child: DateText( + color: _textColor, + fontSize: 16, + height: (16 / 16), + timestamp: message.timestamp)), + const SizedBox(height: 0, width: 12, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 0, + color: Colors.black))))) + ])), ); } } @@ -569,16 +574,16 @@ class MessageItem extends StatelessWidget { Widget build(BuildContext context) { final message = item.message; return StickyHeaderItem( - allowOverflow: !item.isLastInBlock, - header: header, - child: _UnreadMarker( - isRead: message.flags.contains(MessageFlag.read), - child: ColoredBox( - color: Colors.white, - child: Column(children: [ - MessageWithPossibleSender(item: item), - if (trailingWhitespace != null && item.isLastInBlock) SizedBox(height: trailingWhitespace!), - ])))); + allowOverflow: !item.isLastInBlock, + header: header, + child: _UnreadMarker( + isRead: message.flags.contains(MessageFlag.read), + child: ColoredBox( + color: Colors.white, + child: Column(children: [ + MessageWithPossibleSender(item: item), + if (trailingWhitespace != null && item.isLastInBlock) SizedBox(height: trailingWhitespace!), + ])))); } } @@ -598,28 +603,28 @@ class _UnreadMarker extends StatelessWidget { @override Widget build(BuildContext context) { return Stack( - children: [ - child, - Positioned( - top: 0, - left: 0, - bottom: 0, - width: 4, - child: AnimatedOpacity( - opacity: isRead ? 0 : 1, - // Web uses 2s and 0.3s durations, and a CSS ease-out curve. - // See zulip:web/styles/message_row.css . - duration: Duration(milliseconds: isRead ? 2000 : 300), - curve: Curves.easeOut, - child: DecoratedBox( - decoration: BoxDecoration( - color: color, - // TODO(#95): Don't show this extra border in dark mode, see: - // https://github.com/zulip/zulip-flutter/pull/317#issuecomment-1784311663 - border: Border(left: BorderSide( - width: 1, - color: Colors.white.withOpacity(0.6))))))), - ]); + children: [ + child, + Positioned( + top: 0, + left: 0, + bottom: 0, + width: 4, + child: AnimatedOpacity( + opacity: isRead ? 0 : 1, + // Web uses 2s and 0.3s durations, and a CSS ease-out curve. + // See zulip:web/styles/message_row.css . + duration: Duration(milliseconds: isRead ? 2000 : 300), + curve: Curves.easeOut, + child: DecoratedBox( + decoration: BoxDecoration( + color: color, + // TODO(#95): Don't show this extra border in dark mode, see: + // https://github.com/zulip/zulip-flutter/pull/317#issuecomment-1784311663 + border: Border(left: BorderSide( + width: 1, + color: Colors.white.withOpacity(0.6))))))), + ]); } } @@ -650,7 +655,7 @@ class StreamMessageRecipientHeader extends StatelessWidget { final swatch = subscription.colorSwatch(); backgroundColor = swatch.barBackground; contrastingColor = - (ThemeData.estimateBrightnessForColor(swatch.barBackground) == Brightness.dark) + (ThemeData.estimateBrightnessForColor(swatch.barBackground) == Brightness.dark) ? Colors.white : Colors.black; iconColor = swatch.iconOnBarBackground; @@ -674,62 +679,62 @@ class StreamMessageRecipientHeader extends StatelessWidget { final streamName = stream?.name ?? message.displayRecipient; // TODO(log) if missing streamWidget = GestureDetector( - onTap: () => Navigator.push(context, - MessageListPage.buildRoute(context: context, - narrow: StreamNarrow(message.streamId))), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - // Figma specifies 5px horizontal spacing around an icon that's - // 18x18 and includes 1px padding. The icon SVG is flush with - // the edges, so make it 16x16 with 6px horizontal padding. - // Bottom padding added here to shift icon up to - // match alignment with text visually. - padding: const EdgeInsets.only(left: 6, right: 6, bottom: 3), - child: Icon(size: 16, color: iconColor, - // A null [Icon.icon] makes a blank space. - (stream != null) ? iconDataForStream(stream) : null)), - Padding( - padding: const EdgeInsets.symmetric(vertical: 11), - child: Text(streamName, - style: textStyle, - overflow: TextOverflow.ellipsis), - ), - Padding( - // Figma has 5px horizontal padding around an 8px wide icon. - // Icon is 16px wide here so horizontal padding is 1px. - padding: const EdgeInsets.symmetric(horizontal: 1), - child: Icon(size: 16, - color: contrastingColor.withOpacity(0.6), - ZulipIcons.chevron_right)), - ])); + onTap: () => Navigator.push(context, + MessageListPage.buildRoute(context: context, + narrow: StreamNarrow(message.streamId))), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + // Figma specifies 5px horizontal spacing around an icon that's + // 18x18 and includes 1px padding. The icon SVG is flush with + // the edges, so make it 16x16 with 6px horizontal padding. + // Bottom padding added here to shift icon up to + // match alignment with text visually. + padding: const EdgeInsets.only(left: 6, right: 6, bottom: 3), + child: Icon(size: 16, color: iconColor, + // A null [Icon.icon] makes a blank space. + (stream != null) ? iconDataForStream(stream) : null)), + Padding( + padding: const EdgeInsets.symmetric(vertical: 11), + child: Text(streamName, + style: textStyle, + overflow: TextOverflow.ellipsis), + ), + Padding( + // Figma has 5px horizontal padding around an 8px wide icon. + // Icon is 16px wide here so horizontal padding is 1px. + padding: const EdgeInsets.symmetric(horizontal: 1), + child: Icon(size: 16, + color: contrastingColor.withOpacity(0.6), + ZulipIcons.chevron_right)), + ])); } return GestureDetector( - onTap: () => Navigator.push(context, - MessageListPage.buildRoute(context: context, - narrow: TopicNarrow.ofMessage(message))), - child: ColoredBox( - color: backgroundColor, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // TODO(#282): Long stream name will break layout; find a fix. - streamWidget, - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11), - child: Text(topic, - // TODO: Give a way to see the whole topic (maybe a - // long-press interaction?) - overflow: TextOverflow.ellipsis, - style: textStyle))), - // TODO topic links? - // Then web also has edit/resolve/mute buttons. Skip those for mobile. - RecipientHeaderDate(message: message, - color: contrastingColor.withOpacity(0.4)), - ]))); + onTap: () => Navigator.push(context, + MessageListPage.buildRoute(context: context, + narrow: TopicNarrow.ofMessage(message))), + child: ColoredBox( + color: backgroundColor, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // TODO(#282): Long stream name will break layout; find a fix. + streamWidget, + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11), + child: Text(topic, + // TODO: Give a way to see the whole topic (maybe a + // long-press interaction?) + overflow: TextOverflow.ellipsis, + style: textStyle))), + // TODO topic links? + // Then web also has edit/resolve/mute buttons. Skip those for mobile. + RecipientHeaderDate(message: message, + color: contrastingColor.withOpacity(0.4)), + ]))); } } @@ -745,40 +750,40 @@ class DmRecipientHeader extends StatelessWidget { final String title; if (message.allRecipientIds.length > 1) { title = zulipLocalizations.messageListGroupYouAndOthers(message.allRecipientIds - .where((id) => id != store.selfUserId) - .map((id) => store.users[id]?.fullName ?? zulipLocalizations.unknownUserName) - .sorted() - .join(", ")); + .where((id) => id != store.selfUserId) + .map((id) => store.users[id]?.fullName ?? zulipLocalizations.unknownUserName) + .sorted() + .join(", ")); } else { // TODO pick string; web has glitchy "You and $yourname" title = zulipLocalizations.messageListGroupYouWithYourself; } return GestureDetector( - onTap: () => Navigator.push(context, - MessageListPage.buildRoute(context: context, - narrow: DmNarrow.ofMessage(message, selfUserId: store.selfUserId))), - child: ColoredBox( - color: _kDmRecipientHeaderColor, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 6), - child: Icon(size: 16, ZulipIcons.user)), - Expanded( - child: Text(title, - style: const TextStyle( - fontSize: 16, - letterSpacing: 0.02 * 16, - height: (18 / 16), - ).merge(weightVariableTextStyle(context, wght: 600)), - overflow: TextOverflow.ellipsis)), - RecipientHeaderDate(message: message, - color: _kDmRecipientHeaderDateColor), - ])))); + onTap: () => Navigator.push(context, + MessageListPage.buildRoute(context: context, + narrow: DmNarrow.ofMessage(message, selfUserId: store.selfUserId))), + child: ColoredBox( + color: _kDmRecipientHeaderColor, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: Icon(size: 16, ZulipIcons.user)), + Expanded( + child: Text(title, + style: const TextStyle( + fontSize: 16, + letterSpacing: 0.02 * 16, + height: (18 / 16), + ).merge(weightVariableTextStyle(context, wght: 600)), + overflow: TextOverflow.ellipsis)), + RecipientHeaderDate(message: message, + color: _kDmRecipientHeaderDateColor), + ])))); } } @@ -797,15 +802,15 @@ class RecipientHeaderDate extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.fromLTRB(10, 0, 16, 0), - child: DateText( - color: color, - fontSize: 16, - // In Figma this has a line-height of 19, but using 18 - // here to match the stream/topic text widgets helps - // to align all the text to the same baseline. - height: (18 / 16), - timestamp: message.timestamp)); + padding: const EdgeInsets.fromLTRB(10, 0, 16, 0), + child: DateText( + color: color, + fontSize: 16, + // In Figma this has a line-height of 19, but using 18 + // here to match the stream/topic text widgets helps + // to align all the text to the same baseline. + height: (18 / 16), + timestamp: message.timestamp)); } } @@ -827,29 +832,29 @@ class DateText extends StatelessWidget { Widget build(BuildContext context) { final zulipLocalizations = ZulipLocalizations.of(context); return Text( - style: TextStyle( - color: color, - fontSize: fontSize, - height: height, - // This is equivalent to css `all-small-caps`, see: - // https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-caps#all-small-caps - fontFeatures: const [FontFeature.enable('c2sc'), FontFeature.enable('smcp')], - ), - formatHeaderDate( - zulipLocalizations, - DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), - now: DateTime.now())); + style: TextStyle( + color: color, + fontSize: fontSize, + height: height, + // This is equivalent to css `all-small-caps`, see: + // https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-caps#all-small-caps + fontFeatures: const [FontFeature.enable('c2sc'), FontFeature.enable('smcp')], + ), + formatHeaderDate( + zulipLocalizations, + DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), + now: DateTime.now())); } } @visibleForTesting String formatHeaderDate( - ZulipLocalizations zulipLocalizations, - DateTime dateTime, { - required DateTime now, -}) { + ZulipLocalizations zulipLocalizations, + DateTime dateTime, { + required DateTime now, + }) { assert(!dateTime.isUtc && !now.isUtc, - '`dateTime` and `now` need to be in local time.'); + '`dateTime` and `now` need to be in local time.'); if (dateTime.year == now.year && dateTime.month == now.month && @@ -858,8 +863,8 @@ String formatHeaderDate( } final yesterday = now - .copyWith(hour: 12, minute: 0, second: 0, millisecond: 0, microsecond: 0) - .add(const Duration(days: -1)); + .copyWith(hour: 12, minute: 0, second: 0, millisecond: 0, microsecond: 0) + .add(const Duration(days: -1)); if (dateTime.year == yesterday.year && dateTime.month == yesterday.month && dateTime.day == yesterday.day) { @@ -896,73 +901,73 @@ class MessageWithPossibleSender extends StatelessWidget { Widget? senderRow; if (item.showSender) { final time = _kMessageTimestampFormat - .format(DateTime.fromMillisecondsSinceEpoch(1000 * message.timestamp)); + .format(DateTime.fromMillisecondsSinceEpoch(1000 * message.timestamp)); senderRow = Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Flexible( - child: GestureDetector( - onTap: () => Navigator.push(context, - ProfilePage.buildRoute(context: context, - userId: message.senderId)), - child: Row( - children: [ - Avatar(size: 32, borderRadius: 3, - userId: message.senderId), - const SizedBox(width: 8), - Flexible( - child: Text(message.senderFullName, // TODO get from user data - style: const TextStyle( - fontFamily: 'Source Sans 3', - fontSize: 18, - height: (22 / 18), - ).merge(weightVariableTextStyle(context, wght: 600, - wghtIfPlatformRequestsBold: 900)), - overflow: TextOverflow.ellipsis)), - ]))), - const SizedBox(width: 4), - Text(time, - style: TextStyle( - color: _kMessageTimestampColor, - fontFamily: 'Source Sans 3', - fontSize: 16, - height: (18 / 16), - fontFeatures: const [FontFeature.enable('c2sc'), FontFeature.enable('smcp')], - ).merge(weightVariableTextStyle(context))), - ]); + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Flexible( + child: GestureDetector( + onTap: () => Navigator.push(context, + ProfilePage.buildRoute(context: context, + userId: message.senderId)), + child: Row( + children: [ + Avatar(size: 32, borderRadius: 3, + userId: message.senderId), + const SizedBox(width: 8), + Flexible( + child: Text(message.senderFullName, // TODO get from user data + style: const TextStyle( + fontFamily: 'Source Sans 3', + fontSize: 18, + height: (22 / 18), + ).merge(weightVariableTextStyle(context, wght: 600, + wghtIfPlatformRequestsBold: 900)), + overflow: TextOverflow.ellipsis)), + ]))), + const SizedBox(width: 4), + Text(time, + style: TextStyle( + color: _kMessageTimestampColor, + fontFamily: 'Source Sans 3', + fontSize: 16, + height: (18 / 16), + fontFeatures: const [FontFeature.enable('c2sc'), FontFeature.enable('smcp')], + ).merge(weightVariableTextStyle(context))), + ]); } return GestureDetector( - behavior: HitTestBehavior.translucent, - onLongPress: () => showMessageActionSheet(context: context, message: message), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Column(children: [ - if (senderRow != null) - Padding(padding: const EdgeInsets.fromLTRB(16, 2, 16, 0), - child: senderRow), - Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - MessageContent(message: message, content: item.content), - if ((message.reactions?.total ?? 0) > 0) - ReactionChipsList(messageId: message.id, reactions: message.reactions!) - ])), - SizedBox(width: 16, - child: message.flags.contains(MessageFlag.starred) - // TODO(#157): fix how star marker aligns with message content - // Design from Figma at: - // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=813%3A28817&mode=dev . - ? Padding(padding: const EdgeInsets.only(top: 4), - child: Icon(ZulipIcons.star_filled, size: 16, color: _starColor)) - : null), - ]), - ]))); + behavior: HitTestBehavior.translucent, + onLongPress: () => showMessageActionSheet(context: context, message: message), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column(children: [ + if (senderRow != null) + Padding(padding: const EdgeInsets.fromLTRB(16, 2, 16, 0), + child: senderRow), + Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + MessageContent(message: message, content: item.content), + if ((message.reactions?.total ?? 0) > 0) + ReactionChipsList(messageId: message.id, reactions: message.reactions!) + ])), + SizedBox(width: 16, + child: message.flags.contains(MessageFlag.starred) + // TODO(#157): fix how star marker aligns with message content + // Design from Figma at: + // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=813%3A28817&mode=dev . + ? Padding(padding: const EdgeInsets.only(top: 4), + child: Icon(ZulipIcons.star_filled, size: 16, color: _starColor)) + : null), + ]), + ]))); } } @@ -972,10 +977,10 @@ final _kMessageTimestampFormat = DateFormat('h:mm aa', 'en_US'); final _kMessageTimestampColor = const HSLColor.fromAHSL(1, 0, 0, 0.5).toColor(); Future markNarrowAsRead( - BuildContext context, - Narrow narrow, - bool useLegacy, // TODO(server-6) -) async { + BuildContext context, + Narrow narrow, + bool useLegacy, // TODO(server-6) + ) async { final store = PerAccountStoreWidget.of(context); final connection = store.connection; if (useLegacy) { @@ -1003,22 +1008,22 @@ Future markNarrowAsRead( while (true) { final result = await updateMessageFlagsForNarrow(connection, - anchor: anchor, - // [AnchorCode.oldest] is an anchor ID lower than any valid - // message ID; and follow-up requests will have already - // processed the anchor ID, so we just want this to be - // unconditionally false. - includeAnchor: false, - // There is an upper limit of 5000 messages per batch - // (numBefore + numAfter <= 5000) enforced on the server. - // See `update_message_flags_in_narrow` in zerver/views/message_flags.py . - // zulip-mobile uses `numAfter` of 5000, but web uses 1000 - // for more responsive feedback. See zulip@f0d87fcf6. - numBefore: 0, - numAfter: 1000, - narrow: apiNarrow, - op: UpdateMessageFlagsOp.add, - flag: MessageFlag.read); + anchor: anchor, + // [AnchorCode.oldest] is an anchor ID lower than any valid + // message ID; and follow-up requests will have already + // processed the anchor ID, so we just want this to be + // unconditionally false. + includeAnchor: false, + // There is an upper limit of 5000 messages per batch + // (numBefore + numAfter <= 5000) enforced on the server. + // See `update_message_flags_in_narrow` in zerver/views/message_flags.py . + // zulip-mobile uses `numAfter` of 5000, but web uses 1000 + // for more responsive feedback. See zulip@f0d87fcf6. + numBefore: 0, + numAfter: 1000, + narrow: apiNarrow, + op: UpdateMessageFlagsOp.add, + flag: MessageFlag.read); if (!context.mounted) { scaffoldMessenger.clearSnackBars(); return; @@ -1044,8 +1049,8 @@ Future markNarrowAsRead( // This should be impossible given that `foundNewest` was false // (and that our `numAfter` was positive.) await showErrorDialog(context: context, - title: zulipLocalizations.errorMarkAsReadFailedTitle, - message: zulipLocalizations.errorInvalidResponse); + title: zulipLocalizations.errorMarkAsReadFailedTitle, + message: zulipLocalizations.errorInvalidResponse); return; } anchor = NumericAnchor(result.lastProcessedId!); @@ -1065,7 +1070,7 @@ Future markNarrowAsRead( // is better for now if we allow them to run their timer through // and clear the backlog later. scaffoldMessenger.showSnackBar(SnackBar(behavior: SnackBarBehavior.floating, - content: Text(zulipLocalizations.markAsReadInProgress))); + content: Text(zulipLocalizations.markAsReadInProgress))); } } @@ -1086,8 +1091,8 @@ Future _legacyMarkNarrowAsRead(BuildContext context, Narrow narrow) async // of pushing the button. if (unreadDms == null) return; await updateMessageFlags(connection, - messages: unreadDms, - op: UpdateMessageFlagsOp.add, - flag: MessageFlag.read); + messages: unreadDms, + op: UpdateMessageFlagsOp.add, + flag: MessageFlag.read); } } diff --git a/lib/widgets/snackbar.dart b/lib/widgets/snackbar.dart new file mode 100644 index 0000000000..e0824e4574 --- /dev/null +++ b/lib/widgets/snackbar.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'dart:async'; + + +class SnackBarPage extends StatefulWidget { + const SnackBarPage({Key? key}) : super(key: key); + + @override + _SnackBarPageState createState() => _SnackBarPageState(); +} + +class _SnackBarPageState extends State { + late ConnectivityResult _connectivityResult; + late StreamSubscription _connectivitySubscription; + + @override + void initState() { + super.initState(); + // Initialize connectivity status + _initConnectivity(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Subscribe to connectivity changes + _connectivitySubscription = Connectivity().onConnectivityChanged.listen((ConnectivityResult result) { + setState(() { + _connectivityResult = result; + }); + // Show or dismiss snackbar based on connectivity changes + _showSnackBar(result); + }); + } + + @override + void dispose() { + _connectivitySubscription.cancel(); // Cancel subscription to avoid memory leaks + super.dispose(); + } + + Future _initConnectivity() async { + final ConnectivityResult connectivityResult = await Connectivity().checkConnectivity(); + setState(() { + _connectivityResult = connectivityResult; + }); + // Show initial snackbar based on connectivity status + _showSnackBar(connectivityResult); + } + + void _showSnackBar(ConnectivityResult connectivityResult) { + final bool isConnected = connectivityResult != ConnectivityResult.none; + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + if (isConnected) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon( + Icons.sync, + color: Colors.white, + ), + SizedBox(width: 8), + Text( + 'Connecting', + style: TextStyle(color: Colors.white), + ), + ], + ), + duration: Duration(seconds: 2), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon( + Icons.error_outline, + color: Colors.white, + ), + SizedBox(width: 2), + Text( + 'No Internet Connection', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Text(''), + ), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d9af90d6c9..c8f7172f4c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import connectivity_plus import device_info_plus import file_selector_macos import firebase_core @@ -17,6 +18,7 @@ import sqlite3_flutter_libs import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) diff --git a/pubspec.lock b/pubspec.lock index 2f069a73a3..c6e7a5db49 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "1a52f1afae8ab7ac4741425114713bdbba802f1ce1e0648e167ffcc6e05e96cf" + sha256: "4eec93681221723a686ad580c2e7d960e1017cf1a4e0a263c2573c2c6b0bf5cd" url: "https://pub.dev" source: hosted - version: "1.3.21" + version: "1.3.25" analyzer: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.9.1" characters: dependency: transitive description: @@ -201,6 +201,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.dev" + source: hosted + version: "1.2.4" convert: dependency: "direct main" description: @@ -245,10 +261,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" dbus: dependency: transitive description: @@ -317,10 +333,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6" + sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4" url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.2.1" file_selector_linux: dependency: transitive description: @@ -357,10 +373,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "7e049e32a9d347616edb39542cf92cd53fdb4a99fb6af0a0bff327c14cd76445" + sha256: "53316975310c8af75a96e365f9fccb67d1c544ef0acdbf0d88bbe30eedd1c4f9" url: "https://pub.dev" source: hosted - version: "2.25.4" + version: "2.27.0" firebase_core_platform_interface: dependency: transitive description: @@ -373,34 +389,34 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "57e61d6010e253b36d38191cefd6199d7849152cdcd234b61ca290cdb278a0ba" + sha256: c8e1d59385eee98de63c92f961d2a7062c5d9a65e7f45bdc7f1b0b205aab2492 url: "https://pub.dev" source: hosted - version: "2.11.4" + version: "2.11.5" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "9c97b20c012542252a8853f11334efd833ddae83551fe37d27f87d885c655038" + sha256: e41586e0fd04fe9a40424f8b0053d0832e6d04f49e020cdaf9919209a28497e9 url: "https://pub.dev" source: hosted - version: "14.7.15" + version: "14.7.19" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: d464b255e922c7915dc4b0ebc305ebad4e1f130519bee3d6e568ef2ea1613a4b + sha256: f7a9d74ff7fc588a924f6b2eaeaa148b0db521b13a9db55f6ad45864fa98c06e url: "https://pub.dev" source: hosted - version: "4.5.23" + version: "4.5.27" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: f3f71aeec719ec1fe2c99f75cd74d00d33f1c240cf1e402cc9d43883e84f935a + sha256: fc21e771166860c55b103701c5ac7cdb2eec28897b97c42e6e5703cbedf9e02e url: "https://pub.dev" source: hosted - version: "3.6.4" + version: "3.6.8" fixnum: dependency: transitive description: @@ -439,10 +455,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3 + sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00" url: "https://pub.dev" source: hosted - version: "16.3.2" + version: "16.3.3" flutter_local_notifications_linux: dependency: transitive description: @@ -482,6 +498,14 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 + url: "https://pub.dev" + source: hosted + version: "8.2.4" frontend_server_client: dependency: transitive description: @@ -571,10 +595,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + sha256: "917a5cadd67d052554cfb258595e54217de53fac5b52939426e26319a02e6297" url: "https://pub.dev" source: hosted - version: "0.8.9+1" + version: "0.8.9+2" image_picker_linux: dependency: transitive description: @@ -595,10 +619,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b + sha256: "3d2c323daea9d60608f1caf30be32a938916f4975434b8352e6f73dae496da38" url: "https://pub.dev" source: hosted - version: "2.9.3" + version: "2.9.4" image_picker_windows: dependency: transitive description: @@ -724,6 +748,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" node_preamble: dependency: transitive description: @@ -904,10 +936,10 @@ packages: dependency: "direct main" description: name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" shelf: dependency: transitive description: @@ -1013,10 +1045,10 @@ packages: dependency: transitive description: name: sqlparser - sha256: dc384bb1f56d1384ce078edb5ff8247976abdab79d0c83e437210c85f06ecb61 + sha256: "7b20045d1ccfb7bc1df7e8f9fee5ae58673fce6ff62cefbb0e0fd7214e90e5a0" url: "https://pub.dev" source: hosted - version: "0.34.0" + version: "0.34.1" stack_trace: dependency: "direct dev" description: @@ -1117,10 +1149,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.2.5" url_launcher_android: dependency: "direct main" description: @@ -1133,10 +1165,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.2.5" url_launcher_linux: dependency: transitive description: @@ -1197,10 +1229,10 @@ packages: dependency: transitive description: name: vm_service - sha256: a75f83f14ad81d5fe4b3319710b90dec37da0e22612326b696c9e1b8f34bbf48 + sha256: e7d5ecd604e499358c5fe35ee828c0298a320d54455e791e9dcf73486bc8d9f0 url: "https://pub.dev" source: hosted - version: "14.2.0" + version: "14.1.0" watcher: dependency: transitive description: @@ -1245,10 +1277,10 @@ packages: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" win32_registry: dependency: transitive description: @@ -1282,5 +1314,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0-256.0.dev <4.0.0" - flutter: ">=3.21.0-12.0.pre.26" + dart: ">=3.4.0-140.0.dev <4.0.0" + flutter: ">=3.20.0-7.0.pre.63" diff --git a/pubspec.yaml b/pubspec.yaml index 3c85f33b44..537d14f129 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,9 +24,14 @@ environment: # that by the time we want to release, these will have become stable. # TODO: Before general release, switch to stable Flutter and Dart versions, # or pin exact versions: https://github.com/zulip/zulip-flutter/issues/15 +# sdk: '>=3.4.0-140.0.dev <4.0.0' +# flutter: '>=3.20.0-7.0.pre.63' + sdk: '>=3.4.0-256.0.dev <4.0.0' flutter: '>=3.21.0-12.0.pre.26' + + # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, @@ -39,6 +44,7 @@ dependencies: flutter_localizations: sdk: flutter + app_settings: ^5.0.0 collection: ^1.17.2 convert: ^3.1.1 @@ -65,6 +71,7 @@ dependencies: url_launcher: ^6.1.11 url_launcher_android: ">=6.1.0" sqlite3: ^2.4.0 + connectivity_plus: ^5.0.2 dev_dependencies: flutter_driver: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0d4b4d65c2..a44504aafd 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseCorePluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4a4d9be3e7..c2ced0f6aa 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus file_selector_windows firebase_core share_plus