From 705797aaadc3852e414417194178bce73171636e Mon Sep 17 00:00:00 2001 From: Dhanesh Sawant Date: Thu, 2 Jan 2025 16:31:49 +0530 Subject: [PATCH 1/4] dm page : Implement "No DMs yet" UI design Implemented the UI for the "No DMs yet" state on the RecentDmConversationsPage according to the design provided. Fixes part of #127 --- assets/icons/ZulipIcons.ttf | Bin 11668 -> 13692 bytes assets/icons/no_dm.svg | 10 +++ assets/icons/no_dm_down_arrow.svg | 3 + lib/widgets/icons.dart | 26 ++++--- lib/widgets/recent_dm_conversations.dart | 82 ++++++++++++++++++++++- lib/widgets/theme.dart | 7 ++ 6 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 assets/icons/no_dm.svg create mode 100644 assets/icons/no_dm_down_arrow.svg diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf index 57a301f2e8125621e6f781304f2eed0793d16c48..b0450ebd8127106259127e111d119562c04b4508 100644 GIT binary patch delta 3614 zcmcguTZ|i58UD|lGiS!*_1L@X%O=^2?TyFYwbx#I?Q!roG%2U*)ae-DsNFW3i>H{=LRHyMJOO1po%~gfdXpYf|@3LXFORY z6*TP&GycziF8}5G{xd!^FMa;jGafM_3ey?lG`@MqlN%qou=Okv+d#y3Zrf3*?Yy$% zFGS)ztoH6-S~&U|ed>=y`gP2`bNI;VL#gMt{SpBlCCa_GcyQssiN9WY9k$mX?jj~k zCg#w86S97B>G-MheRn>9?|T^EI9{Zy2S3(I5*p5=w_b;j13hJi_92_e8fg1?a!15Fft4J&1x{hLK zM`6+QTLq$op0k=_E$OQk3iMhZP#oz>fYtB;k5f0Snpi1grU!kmL@wM~)C(9TU@!Oy z`%o1VE(Mwej857J%3hNQ@)Kv28X6R z`ZRr(osNR&H=VHSpc2;Tsp zD%{k*sR}i47byj6#dwz1pf~HMR9h3L^=L}~)S1g+wF=8T;;XXfvDP7;Q%q|JSjWC9 zbNuhLE$lo%kaks5|F{>ltL-DA!gB3zi*;Iz+DxBo-xMW{o@oCHSMf9biF!2k=8WUztY~ThuSX~ z(z~gk;c7wl1TqS}5Qqc4FAx`ce;_zh2rwj z_5`#N9Nly>kP+xpfv5_d4rKb`+vX2g^2T~1C3vLh25PT_a_wbn!P3&=Yn0=l7guR6)!X4D4A@hcG=9trTn~u#=$tY{Osbq_lnNfEqsv@g$hL%$)$dDC@K~E3&KuBX^qB+@>+C{JeN_n%7)lFKcR9byx-T zqbvvSR5Arj6SZU_9%Y!5*$Q*=PNSa6JIFG`43rDQjwz_clYU*?+K@62jKB)8vY7A< zs;Y8sEyaGG+3rmm=@It14@dhj zl$>F67BYpVuS>@haA!=ITnHgWSJ;f)#mBB9kgR~yth9JM778u;oU~|3hyida=YcbFudD%VJp!@x6y zT@qOC38yt&!V<}@bQk|>N=|Oy7z)MW+>*1|Tt3O2xd9+|I&J1i6jkO|a} z`sCzi9$NKUUs&~GnytJ_W{9z_p{_jVjpAFH)S*cka-3fv;-3 zNo~B~*W!d!UlBE&gZ@B)VOnu{9pUtMs0x({s*Bi5)_R`k*+J}Jo zEp0!XTv$8tXYR)%Kx7*j`e=TxeSGDYn;Z1KLsaK!=+TT8pKlP!`Ofm{`J4az&DiT) z|FN(*+iE|x_{;$&Sfb(mPJ4ApJERS=fdQ_EPq#aB{nrnafDHHb+S20L<&W!MGU3=& z`fOjgk+52F0#31U<!NM>{yNoRjT1lJKuIgdEtRL2o>7T9Md%bD#mmMh(&RB@kwcyyQQq@NqQG?;MedZ~v0&ss{uvqPP{3XEO~aM(4m6($ z_Ob@*%JggE5ym)d!)D82 z3?Pmk*>-~&)2vZpHIw;l`g`z&(-3xZiX_X43C83!C2|3K(7R*b>6rzdWD1)Jg-@6z zY-%)xmoY;vg&s+kysw2&BZK|Sl#_N(*05P^5T?Il9Fp^N#oNac9;e9Q2|jhxnTRp4 zj%V=-m9V4|RXLeM`-21N<-9gyyGJ544D(8IZlPR~yWQyvsPTw=L(VZF#TsUb7}qH& ztT?AS8eHcY8f6l33_`e3C2c>GC0QyEEJN_A^MHbYk=83HsmUA zD&*53$2lW!WOzD@$L#KKgUor)&ZG zhJK4L$|qgcenm;^p%rF`*5G?%aDT$!6K0H=`>%;us-G47W-NSW4hHL{@7|RTz!nGN z+r*JV)DYTTc6E*G2nW@99mOfE{hkQ&;Y2abF3apK!>)^r$nj58W1kgHCH_Kn&fs$1 z2C5XciHYD`_#o4VgO_%-_c6L$$auwUj!~tGV~^KUZoW|+ZBB6iTyWVm{g`M~za#Ga zHHo7NX>wa3Nj|1fCeJDq$j22j37NE41=_ct@c@UQwu$-&GhTuPO+I))Xc- zIfz8(Bk GUjG-Z`OHZG diff --git a/assets/icons/no_dm.svg b/assets/icons/no_dm.svg new file mode 100644 index 0000000000..0bbef6272b --- /dev/null +++ b/assets/icons/no_dm.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/no_dm_down_arrow.svg b/assets/icons/no_dm_down_arrow.svg new file mode 100644 index 0000000000..614b367c62 --- /dev/null +++ b/assets/icons/no_dm_down_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart index ba21bec42f..1726b47482 100644 --- a/lib/widgets/icons.dart +++ b/lib/widgets/icons.dart @@ -99,35 +99,41 @@ abstract final class ZulipIcons { /// The Zulip custom icon "mute". static const IconData mute = IconData(0xf119, fontFamily: "Zulip Icons"); + /// The Zulip custom icon "no_dm". + static const IconData no_dm = IconData(0xf11a, fontFamily: "Zulip Icons"); + + /// The Zulip custom icon "no_dm_down_arrow". + static const IconData no_dm_down_arrow = IconData(0xf11b, fontFamily: "Zulip Icons"); + /// The Zulip custom icon "read_receipts". - static const IconData read_receipts = IconData(0xf11a, fontFamily: "Zulip Icons"); + static const IconData read_receipts = IconData(0xf11c, fontFamily: "Zulip Icons"); /// The Zulip custom icon "send". - static const IconData send = IconData(0xf11b, fontFamily: "Zulip Icons"); + static const IconData send = IconData(0xf11d, fontFamily: "Zulip Icons"); /// The Zulip custom icon "share". - static const IconData share = IconData(0xf11c, fontFamily: "Zulip Icons"); + static const IconData share = IconData(0xf11e, fontFamily: "Zulip Icons"); /// The Zulip custom icon "share_ios". - static const IconData share_ios = IconData(0xf11d, fontFamily: "Zulip Icons"); + static const IconData share_ios = IconData(0xf11f, fontFamily: "Zulip Icons"); /// The Zulip custom icon "smile". - static const IconData smile = IconData(0xf11e, fontFamily: "Zulip Icons"); + static const IconData smile = IconData(0xf120, fontFamily: "Zulip Icons"); /// The Zulip custom icon "star". - static const IconData star = IconData(0xf11f, fontFamily: "Zulip Icons"); + static const IconData star = IconData(0xf121, fontFamily: "Zulip Icons"); /// The Zulip custom icon "star_filled". - static const IconData star_filled = IconData(0xf120, fontFamily: "Zulip Icons"); + static const IconData star_filled = IconData(0xf122, fontFamily: "Zulip Icons"); /// The Zulip custom icon "topic". - static const IconData topic = IconData(0xf121, fontFamily: "Zulip Icons"); + static const IconData topic = IconData(0xf123, fontFamily: "Zulip Icons"); /// The Zulip custom icon "unmute". - static const IconData unmute = IconData(0xf122, fontFamily: "Zulip Icons"); + static const IconData unmute = IconData(0xf124, fontFamily: "Zulip Icons"); /// The Zulip custom icon "user". - static const IconData user = IconData(0xf123, fontFamily: "Zulip Icons"); + static const IconData user = IconData(0xf125, fontFamily: "Zulip Icons"); // END GENERATED ICON DATA } diff --git a/lib/widgets/recent_dm_conversations.dart b/lib/widgets/recent_dm_conversations.dart index c9d3131591..9f27e80bb0 100644 --- a/lib/widgets/recent_dm_conversations.dart +++ b/lib/widgets/recent_dm_conversations.dart @@ -49,7 +49,86 @@ class _RecentDmConversationsPageBodyState extends State { modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.3), mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.8).toColor(), navigationButtonBg: Colors.black.withValues(alpha: 0.05), + newDmButtonBg: const Color(0xff4F42C9), sectionCollapseIcon: const Color(0x7f1e2e48), star: const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(), subscriptionListHeaderLine: const HSLColor.fromAHSL(0.2, 240, 0.1, 0.5).toColor(), @@ -214,6 +215,7 @@ class DesignVariables extends ThemeExtension { // TODO(design-dark) need proper dark-theme color (this is ad hoc) mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.6).toColor(), navigationButtonBg: Colors.white.withValues(alpha: 0.05), + newDmButtonBg: const Color(0xff4F42C9), // TODO(design-dark) need proper dark-theme color (this is ad hoc) sectionCollapseIcon: const Color(0x7fb6c8e2), // TODO(design-dark) unchanged in dark theme? @@ -267,6 +269,7 @@ class DesignVariables extends ThemeExtension { required this.modalBarrierColor, required this.mutedUnreadBadge, required this.navigationButtonBg, + required this.newDmButtonBg, required this.sectionCollapseIcon, required this.star, required this.subscriptionListHeaderLine, @@ -329,6 +332,7 @@ class DesignVariables extends ThemeExtension { final Color modalBarrierColor; final Color mutedUnreadBadge; final Color navigationButtonBg; + final Color newDmButtonBg; final Color sectionCollapseIcon; final Color star; final Color subscriptionListHeaderLine; @@ -378,6 +382,7 @@ class DesignVariables extends ThemeExtension { Color? modalBarrierColor, Color? mutedUnreadBadge, Color? navigationButtonBg, + Color? newDmButtonBg, Color? sectionCollapseIcon, Color? star, Color? subscriptionListHeaderLine, @@ -426,6 +431,7 @@ class DesignVariables extends ThemeExtension { modalBarrierColor: modalBarrierColor ?? this.modalBarrierColor, mutedUnreadBadge: mutedUnreadBadge ?? this.mutedUnreadBadge, navigationButtonBg: navigationButtonBg ?? this.navigationButtonBg, + newDmButtonBg: newDmButtonBg ?? this.newDmButtonBg, sectionCollapseIcon: sectionCollapseIcon ?? this.sectionCollapseIcon, star: star ?? this.star, subscriptionListHeaderLine: subscriptionListHeaderLine ?? this.subscriptionListHeaderLine, @@ -481,6 +487,7 @@ class DesignVariables extends ThemeExtension { modalBarrierColor: Color.lerp(modalBarrierColor, other.modalBarrierColor, t)!, mutedUnreadBadge: Color.lerp(mutedUnreadBadge, other.mutedUnreadBadge, t)!, navigationButtonBg: Color.lerp(navigationButtonBg, other.navigationButtonBg, t)!, + newDmButtonBg: Color.lerp(newDmButtonBg, other.newDmButtonBg, t)!, sectionCollapseIcon: Color.lerp(sectionCollapseIcon, other.sectionCollapseIcon, t)!, star: Color.lerp(star, other.star, t)!, subscriptionListHeaderLine: Color.lerp(subscriptionListHeaderLine, other.subscriptionListHeaderLine, t)!, From 67f583c04990b96b4f2976d1f14178fde6e300ba Mon Sep 17 00:00:00 2001 From: Dhanesh Sawant Date: Fri, 3 Jan 2025 22:35:53 +0530 Subject: [PATCH 2/4] dm page: added search within recent dm & to start a new dm or group conversation Implemented the search functionality in the dm page which searches from within the recent conversations. Implemented starting a new dm with other user 1-to-1 or in a group both. the UI is similar to that in the figma design --- lib/widgets/new_dm.dart | 211 +++++++++++++++++++++++ lib/widgets/recent_dm_conversations.dart | 198 +++++++++++++++++---- 2 files changed, 374 insertions(+), 35 deletions(-) create mode 100644 lib/widgets/new_dm.dart diff --git a/lib/widgets/new_dm.dart b/lib/widgets/new_dm.dart new file mode 100644 index 0000000000..562712c9b3 --- /dev/null +++ b/lib/widgets/new_dm.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; + +import '../api/model/model.dart'; +import '../model/narrow.dart'; +import 'content.dart'; +import 'message_list.dart'; +import 'page.dart'; +import 'store.dart'; +import 'theme.dart'; + +class NewDmScreen extends StatefulWidget { + const NewDmScreen({super.key}); + + static Route buildRoute({int? accountId, BuildContext? context}) { + return MaterialAccountWidgetRoute( + accountId: accountId, + context: context, + page: const NewDmScreen() + ); + } + + @override + State createState() => _NewDmScreenState(); +} + +class _NewDmScreenState extends State { + final List _selectedUsers = []; + final TextEditingController _searchController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + + + List _allUsers = []; + bool _isLoading = true; + bool _isDataFetched = false; // To ensure `_fetchUsers` is called only once + + @override + void initState() { + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + if (!_isDataFetched) { + _isDataFetched = true; // Avoid calling `_fetchUsers` multiple times + _fetchUsers(); + } + } + + Future _fetchUsers() async { + setState(() { + _isLoading = true; + }); + + try { + final store = PerAccountStoreWidget.of(context); + final usersMap = store.users; + setState(() { + _allUsers = usersMap.values.toList(); + _isLoading = false; + }); + print('Fetched ${_allUsers.length} users'); + } catch (error) { + setState(() { + _isLoading = false; + }); + // Handle error appropriately + print('Error fetching users: $error'); + } + } + + List get _filteredUsers { + final query = _searchController.text.toLowerCase(); + return _allUsers.where((user) => + !_selectedUsers.contains(user) && + user.fullName.toLowerCase().contains(query) + ).toList(); + } + + void _handleUserSelect(User user) { + setState(() { + _selectedUsers.add(user); + _searchController.clear(); + }); + Future.delayed(Duration(milliseconds: 10), () { + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + }); + } + + void _handleUserRemove(User user) { + setState(() { + _selectedUsers.remove(user); + }); + } + + void _handleDone() { + if (_selectedUsers.isNotEmpty) { + final store = PerAccountStoreWidget.of(context); + final narrow = DmNarrow.withOtherUsers( + _selectedUsers.map((u) => u.userId), + selfUserId: store.selfUserId, + ); + Navigator.pushReplacement(context, + MessageListPage.buildRoute(context: context, narrow: narrow)); + } + } + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + DesignVariables designVariables = DesignVariables.of(context); + + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + title: const Text('New DM'), + actions: [ + TextButton( + onPressed: _selectedUsers.isEmpty ? null : _handleDone, + child: Text( + 'Next', + style: TextStyle( + color: _selectedUsers.isEmpty + ? Colors.grey + : designVariables.icon, + ), + ), + ), + ], + ), + body: _isLoading? const Center(child: CircularProgressIndicator()) : Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: const Color(0xff313131), + constraints: BoxConstraints( + minWidth: double.infinity, + maxHeight: screenHeight * 0.2, // Limit height to 20% of screen + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.vertical, + child: Wrap( + spacing: 5, + children: [ + ..._selectedUsers.map((user) => Chip( + avatar: Avatar(userId: user.userId, size: 32, borderRadius: 3), + label: Text(user.fullName), + onDeleted: () => _handleUserRemove(user), + backgroundColor: Color(0xFF40000000), + )), + SizedBox( + width: 150, + child: TextField( + controller: _searchController, + decoration: const InputDecoration( + hintText: 'Add person', + border: InputBorder.none, + ), + onChanged: (_) => setState(() {}), + ), + ), + ], + ), + ), + ), + ), + ], + ), + const Divider(height: 1), + Expanded( + child: ListView.builder( + itemCount: _filteredUsers.length, + itemBuilder: (context, index) { + final user = _filteredUsers[index]; + return ListTile( + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _selectedUsers.contains(user) + ? Icons.check_circle + : Icons.circle_outlined, + color: _selectedUsers.contains(user) + ? Theme.of(context).primaryColor + : Colors.grey, + ), + const SizedBox(width: 8), // Add spacing between the icon and avatar + Avatar(userId: user.userId, size: 32, borderRadius: 3), + ], + ), + title: Text(user.fullName), + onTap: () => _handleUserSelect(user), + ); + }, + ), + ), + + ], + ), + ); + } +} diff --git a/lib/widgets/recent_dm_conversations.dart b/lib/widgets/recent_dm_conversations.dart index 9f27e80bb0..c510a13e91 100644 --- a/lib/widgets/recent_dm_conversations.dart +++ b/lib/widgets/recent_dm_conversations.dart @@ -6,6 +6,7 @@ import '../model/unreads.dart'; import 'content.dart'; import 'icons.dart'; import 'message_list.dart'; +import 'new_dm.dart'; import 'store.dart'; import 'theme.dart'; import 'unread_count_badge.dart'; @@ -21,6 +22,9 @@ class _RecentDmConversationsPageBodyState extends State _filteredConversations = []; + @override void onNewStore() { model?.removeListener(_modelChanged); @@ -30,12 +34,21 @@ class _RecentDmConversationsPageBodyState extends State createState() => _SearchRowState(); +} + +class _SearchRowState extends State { + bool _showCancelButton = false; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_onTextChanged); + } + + @override + void dispose() { + widget.controller.removeListener(_onTextChanged); + super.dispose(); + } + + void _onTextChanged() { + setState(() { + _showCancelButton = widget.controller.text.isNotEmpty; + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), // Add padding around the row + child: Row( + children: [ + const Icon( + Icons.search, + size: 24.0, + color: Colors.grey, + ), + const SizedBox(width: 8.0), // Add space between the icon and the text field + // Text Field + Expanded( + child: TextField( + controller: widget.controller, + decoration: const InputDecoration( + hintText: 'Search...', // Placeholder text + border: InputBorder.none, // Remove the border + ), + style: TextStyle(fontSize: 16.0), // Customize the text style + ), + ), + if (_showCancelButton) ...[ + SizedBox(width: 8.0), + GestureDetector( + onTap: () { + widget.controller.clear(); + }, + child: Icon( + Icons.cancel, + size: 20.0, + color: Colors.grey, + ), + ), + ], + ], + ), + ); + } +} + class RecentDmConversationsItem extends StatelessWidget { const RecentDmConversationsItem({ super.key, From 1a5cf9fff483658a60b25abf72f180dcd9b3a29f Mon Sep 17 00:00:00 2001 From: Dhanesh Sawant Date: Fri, 3 Jan 2025 23:54:15 +0530 Subject: [PATCH 3/4] updating Android NDK version that integration_test requires --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index f360f667e5..227e9e4f99 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -21,7 +21,7 @@ try { android { namespace "com.zulip.flutter" - + ndkVersion = "26.3.11579264" compileSdkVersion flutter.compileSdkVersion compileOptions { From 8f9b37707fabbb38411533f35875ed05879bab4f Mon Sep 17 00:00:00 2001 From: Dhanesh Sawant Date: Wed, 19 Feb 2025 11:47:41 +0530 Subject: [PATCH 4/4] dm page: Implement FocusNode for search & add NewDirectMessageButton Introduced FocusNode in SearchRow to manage search input focus. Separated searching and non-searching modes using _isSearching state. Added NewDirectMessageButton which will only be displayed in search mode. Modified _allUsers list to remove the current user (selfUserId) from available DM options. also solved merge conflicts within ZulipIcons.ttf and theme.dart file --- assets/icons/ZulipIcons.ttf | Bin 13692 -> 14488 bytes lib/widgets/new_dm.dart | 136 +++++--- lib/widgets/recent_dm_conversations.dart | 387 ++++++++++++++++------- lib/widgets/theme.dart | 49 ++- 4 files changed, 403 insertions(+), 169 deletions(-) diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf index b0450ebd8127106259127e111d119562c04b4508..1ee5f37bb595b832d0bb15c57bbe164215b1162e 100644 GIT binary patch delta 2485 zcmai0TWnKh9RGhkr?;(_^{jN=g?6;iZtHqK>*+4-x=HY1I>&}a6q#id#${}bZL+A8 z%)}T?NU+2Z$Kfh(2hd?!mnf;$`~#&N&+pP3Xz*`+t}J z<@f)8m-Mgwrw^Bj5m7ZACxH@=k38K!7~J|b5z7(@XP+90r=Gbna*s&739T2$CW=$3 z6K@{~2)F>XFooG7ikSV$4c2_l0sHF;#_ zt3SW*<-ReFp1d5i_p2Eoi(S`?znxr4vi^#X?I{Z>a!`O4=smi{B$i?0>=gUMuwb}g z_{MNYaFa+?Snr}CG9dppd=YA=C}e_?l%g~=s?nyYl{zqvQU)suWf8CH0sWY27f1cf z(2bI;+Lz-{6Uj$*7_?GMb)1ExES7Unlo8fMEwr9$;2;W(F16((;^?)mg=iwj(=oWS!#GPlSmcx*++~Z(oNrCHI-do-2sE=GXHm7a6Tr93gs&CrJh@hc z;gmGXD)e~`xfdIVlzGN^y*Lg`MPS1BhaZt%6hDGq7PL(0DXM9^zCm>e+X|9NXleWD zg+t9XKT}2=HEu0HFMu=~;5`d+vaZc*nSBmZF{+0;FHV6P(JQDS`3}1&j5ZEJ+FT1( zWoWkH&^q9z4Qs2nKt@g(5bRX7MZ=EDDY2KeRBnn{tMDD32|%nYi<>Gbqugn_&4;mfN(G7V}hXOgFqYDzRt%^A0q>c#Wl#U$aVI6oFao#nwK_1b8*Oq2< zL@_Y>A;gtFX?E9JgS3NXkJHt(P90Jm6t7!bJn|6iK>sQ3-+syyUw%D zt2IM4@4IBzY1b|3oU~MX#+_RGBKNzG=n*YGQT@447G4nic-4q`+-7D9#92trr2LF! zQo@kUwvx96Y}akxfb9bNz!nf{Fb!bl3_K&@E#VAaP#hHN!)`oo`0_I) zlk&Kotj^=+LCmH=NbY6f(DSlT3x@&QHCr7IavqLXesP2x^YivlqaAHy2(3fw3)>zF z?hUNG=76T7F|6sRebp8vr$vj?SitOHYcm-ZFu6Ucj2tq{dFV2?g9T#D7-O0imSC4r@~l>!a(n7Lc@_%C?oXCg zcaKnHKi1^}%Nuf0Z+)=dZu9t;{dLv$`i=EoW(X_FprYJP3>QZdiIL)PV&w~?&($*k z%@)5)+AjK}?ty`B$!C>Z&EasfOWH5`UEQneg%P=?x4M)!5(yLqhKF9#RCp1I}=*KWODenOENl(IZdNEZ4v$KBbOvPi!T?Qq7?8M zEvdeg#pqi(`X5El|7$LKW3L+Vhvc2~F%#=!{p_)dTUKg~W8+#*pP-^@-Uu6Xs$zdQ3k zzWyViO~xAgz&yaxag+`NFvm)3Xx}GY&@Hv*dG6x$h!frsp(du{VBcp z6Op=$xrdkLSLcp?F!mJ^yh;?`YtA-iR(`pE7rr|{sfmd$<{icRJ>Yb+wcOc={qq-M z_wfB>esQ|qxE1_(hA8j=6KkzTr_HXh??E7j@BW2GYxeLLXY)j6jYuiC7q2hJ|9td2 zQDGV1zT5W(LZhMsbn)xodh7PC$wr+%HfJll)I+D}UAjX*uqZ3BChM?=N=><<+)_4` zZ^=smtXJtZQqX@Ak4}S>f*qm^Whn=bAjTXeD39+H6|hny3-tp3@lT651N!;kn<7)h z+Xmdcbb>+%NKjfNwvfoeautpy%4!HbP7#bLc$CG+Nf>xb9F#al>VXF*eM}HKgmMOG zS%Pqmdf`>ZN(M7gQf(}j5H>>3fQA9?x+oZfDqRTCsSX-$*$_gDm?O!Ze*ks;ro|L=he3 z`jF=7T)*hY75q80V4;aLJ&!jxn@ZiFT%~jL29yX1B`TsZu3pjx7k3x^=vPOuyN_IW z4Nn4Map@9Rb-T~g8Hno@Nr#0F{V2hUB!#eiZ);FZM?beUHFh&wm60h1rm5-Oxoz96(7LYqa@GSFldS|!0C z2ci`m=@A-szSGVlz2JZtjQ>lb-NlccW9${r~;2{jCSVZ#-KIk>JU#kok^^%lZA%O{b?^ Yb|%ZeIb)S_WQAP?vsJdlZZO`-zl9U_ga7~l diff --git a/lib/widgets/new_dm.dart b/lib/widgets/new_dm.dart index 562712c9b3..fe5f548539 100644 --- a/lib/widgets/new_dm.dart +++ b/lib/widgets/new_dm.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import '../api/model/model.dart'; import '../model/narrow.dart'; import 'content.dart'; @@ -58,41 +57,37 @@ class _NewDmScreenState extends State { final usersMap = store.users; setState(() { _allUsers = usersMap.values.toList(); + _allUsers.removeWhere((user) => user.userId == store.selfUserId); _isLoading = false; }); - print('Fetched ${_allUsers.length} users'); } catch (error) { setState(() { _isLoading = false; }); - // Handle error appropriately - print('Error fetching users: $error'); } } List get _filteredUsers { final query = _searchController.text.toLowerCase(); return _allUsers.where((user) => - !_selectedUsers.contains(user) && user.fullName.toLowerCase().contains(query) ).toList(); } void _handleUserSelect(User user) { setState(() { - _selectedUsers.add(user); + if (_selectedUsers.contains(user)) { + _selectedUsers.remove(user); + } else { + _selectedUsers.add(user); + } _searchController.clear(); }); - Future.delayed(Duration(milliseconds: 10), () { + Future.delayed(const Duration(milliseconds: 10), () { _scrollController.jumpTo(_scrollController.position.maxScrollExtent); }); } - void _handleUserRemove(User user) { - setState(() { - _selectedUsers.remove(user); - }); - } void _handleDone() { if (_selectedUsers.isNotEmpty) { @@ -112,50 +107,73 @@ class _NewDmScreenState extends State { DesignVariables designVariables = DesignVariables.of(context); return Scaffold( + backgroundColor: designVariables.bgContextMenu, appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.pop(context), + automaticallyImplyLeading: false, + title: Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: Row( + children: [ + Icon(Icons.chevron_left, color: designVariables.icon, size: 24), + Text('Back', style: TextStyle(color: designVariables.icon, fontSize: 20, fontWeight: FontWeight.w600)), + ], + ), + ), + const Spacer(), // Pushes the title to the center + const Text('New DM', textAlign: TextAlign.center), + const Spacer(), // Ensures title stays centered + ], ), - title: const Text('New DM'), + centerTitle: false, // Prevents default centering when using custom layout actions: [ TextButton( onPressed: _selectedUsers.isEmpty ? null : _handleDone, - child: Text( - 'Next', - style: TextStyle( - color: _selectedUsers.isEmpty - ? Colors.grey - : designVariables.icon, - ), + child: Row( + children: [ + Text( + 'Next', + style: TextStyle( + color: _selectedUsers.isEmpty ? designVariables.icon.withValues(alpha: 0.5) : designVariables.icon, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + Icon(Icons.chevron_right, color: _selectedUsers.isEmpty ? designVariables.icon.withValues(alpha: 0.5) : designVariables.icon, size: 24), + ], ), ), ], ), + body: _isLoading? const Center(child: CircularProgressIndicator()) : Column( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - color: const Color(0xff313131), + color: designVariables.bgSearchInput, constraints: BoxConstraints( minWidth: double.infinity, maxHeight: screenHeight * 0.2, // Limit height to 20% of screen ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.fromLTRB(14,11,14,0), child: SingleChildScrollView( controller: _scrollController, scrollDirection: Axis.vertical, child: Wrap( - spacing: 5, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 16, children: [ ..._selectedUsers.map((user) => Chip( - avatar: Avatar(userId: user.userId, size: 32, borderRadius: 3), - label: Text(user.fullName), - onDeleted: () => _handleUserRemove(user), - backgroundColor: Color(0xFF40000000), + avatar: Avatar(userId: user.userId, size: 22, borderRadius: 3), + label: Text(user.fullName, style: TextStyle(fontSize: 16, color: designVariables.labelMenuButton)), + deleteIcon: null, + backgroundColor: designVariables.bgMenuButtonSelected, + padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 1), // Adjust padding to control height + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap )), SizedBox( width: 150, @@ -181,24 +199,48 @@ class _NewDmScreenState extends State { itemCount: _filteredUsers.length, itemBuilder: (context, index) { final user = _filteredUsers[index]; - return ListTile( - leading: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - _selectedUsers.contains(user) - ? Icons.check_circle - : Icons.circle_outlined, - color: _selectedUsers.contains(user) - ? Theme.of(context).primaryColor - : Colors.grey, - ), - const SizedBox(width: 8), // Add spacing between the icon and avatar - Avatar(userId: user.userId, size: 32, borderRadius: 3), - ], + final isSelected = _selectedUsers.contains(user); // Check if user is selected + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: isSelected ? designVariables.bgMenuButtonSelected : designVariables.bgContextMenu, + borderRadius: BorderRadius.circular(10) + ), + child: ListTile( + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isSelected ? Icons.check_circle : Icons.circle_outlined, + color: isSelected ? designVariables.radioFillSelected : Colors.grey, + ), + const SizedBox(width: 8), + Stack( + clipBehavior: Clip.none, + children: [ + Avatar(userId: user.userId, size: 32, borderRadius: 3), + if (user.isActive) + Positioned( + bottom: -2, + right: -2, + child: Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: designVariables.statusOnline, + shape: BoxShape.circle, + border: Border.all(color: isSelected ? designVariables.bgMenuButtonSelected: designVariables.bgContextMenu, width: 1.5), + ), + ), + ), + ], + ), + ], + ), + title: Text(user.fullName, style: TextStyle(color: designVariables.textMessage, fontSize: 17, fontWeight: FontWeight.w500)), + onTap: () => _handleUserSelect(user), ), - title: Text(user.fullName), - onTap: () => _handleUserSelect(user), ); }, ), diff --git a/lib/widgets/recent_dm_conversations.dart b/lib/widgets/recent_dm_conversations.dart index 49df65f124..fa166ab039 100644 --- a/lib/widgets/recent_dm_conversations.dart +++ b/lib/widgets/recent_dm_conversations.dart @@ -19,12 +19,14 @@ class RecentDmConversationsPageBody extends StatefulWidget { State createState() => _RecentDmConversationsPageBodyState(); } -class _RecentDmConversationsPageBodyState extends State with PerAccountStoreAwareStateMixin { +class _RecentDmConversationsPageBodyState extends State with PerAccountStoreAwareStateMixin{ RecentDmConversationsView? model; Unreads? unreadsModel; final TextEditingController _searchController = TextEditingController(); + final FocusNode _searchFocusNode = FocusNode(); List _filteredConversations = []; + bool _isSearching = false; @override void onNewStore() { @@ -43,6 +45,7 @@ class _RecentDmConversationsPageBodyState extends State( + onNotification: (scrollNotification) { + if (scrollNotification is ScrollStartNotification) { + _searchFocusNode.unfocus(); // Unfocus when scrolling starts + } + return false; + }, + child: ListView.builder( + itemCount: _filteredConversations.length + (_isSearching ? 1 : 0), + itemBuilder: (context, index) { + if(index < _filteredConversations.length) { + final narrow = _filteredConversations[index]; + return RecentDmConversationsItem( + narrow: narrow, + unreadCount: unreadsModel!.countInDmNarrow(narrow), + searchQuery: _searchController.text, + focusNode: _searchFocusNode + ); + } + else{ + return NewDirectMessageButton(focusNode: _searchFocusNode); + } + })), + ) + ], + ), + ), + floatingActionButton: Visibility( + visible: !_isSearching, + child: const NewDmButton(), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + ), + ); + } +} + +class EmptyDmState extends StatelessWidget { + const EmptyDmState({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + body: SafeArea( child: Center( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 124, - height: 112, - child: FittedBox( - fit: BoxFit.contain, - child: Opacity( - opacity: 0.3, - child: Icon( - ZulipIcons.no_dm, - ), - ), - ), - ), - SizedBox(height: 16), - Opacity( - opacity: 0.5, - child: Text( - 'There are no Direct Messages yet.\nStart a conversation with another person\nor a group of people.', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 17, - ), - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(0, 48, 0, 16), + child: SizedBox( + width: 124, + height: 112, + child: FittedBox( + fit: BoxFit.contain, + child: Opacity( + opacity: 0.3, + child: Icon(ZulipIcons.no_dm), ), - SizedBox(height: 16), - SizedBox( - height: 157, - child: FittedBox( - fit: BoxFit.contain, - child: Opacity( - opacity: 0.3, - child: Icon( - ZulipIcons.no_dm_down_arrow, - ), - ), - ), - ) - ], - ) - , - NewDmButton() - ] - ), + ), + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 24), + child: const Opacity( + opacity: 0.5, + child: Text( + 'There are no Direct Messages yet.\nStart a conversation with another person\nor a group of people.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 17), + ), + ), + ), + const SizedBox(height: 16), + const SizedBox( + height: 157, + child: FittedBox( + fit: BoxFit.contain, + child: Opacity( + opacity: 0.3, + child: Icon(ZulipIcons.no_dm_down_arrow), + ), + ), + ), + ], ), ), - ); - } - else { - return SafeArea( - // Don't pad the bottom here; we want the list content to do that. - bottom: false, - child: Column( - children: [ - SearchRow(controller: _searchController), - Expanded( - child: ListView.builder( - itemCount: _filteredConversations.length, - itemBuilder: (context, index) { - final narrow = _filteredConversations[index]; - return RecentDmConversationsItem( - narrow: narrow, - unreadCount: unreadsModel!.countInDmNarrow(narrow), - ); - }), + ), + floatingActionButton: NewDmButton(), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + ); + } +} + +class NewDirectMessageButton extends StatelessWidget { + const NewDirectMessageButton({super.key, this.focusNode}); + final FocusNode? focusNode; + + @override + Widget build(BuildContext context) { + final designVariables = DesignVariables.of(context); + + return Container( + margin: const EdgeInsets.fromLTRB(24, 8.0, 24, 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), // Match the button's shape + color: designVariables.contextMenuItemBg.withAlpha(30) //12% opacity + ), + child: FilledButton.icon( + style: FilledButton.styleFrom( + minimumSize: const Size(137, 44), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - NewDmButton() - ], - )); - } + backgroundColor: Colors.transparent, + ), + onPressed: (){ + focusNode?.unfocus(); + Navigator.of(context).push( + NewDmScreen.buildRoute(context: context) + ); + }, + icon: Icon(Icons.add, color: designVariables.contextMenuItemIcon, size: 24), + label: Text( + 'New Direct Message', + style: TextStyle(color: designVariables.contextMenuItemText, fontSize: 20, fontWeight: FontWeight.w600), + ), + ), + ); } } class NewDmButton extends StatelessWidget { const NewDmButton({super.key}); - @override Widget build(BuildContext context) { final designVariables = DesignVariables.of(context); - return FilledButton.icon( - style: FilledButton.styleFrom( - minimumSize: const Size(137, 48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28), - ), - backgroundColor: designVariables.newDmButtonBg, + return Container( + padding: const EdgeInsets.fromLTRB(12, 8.0, 16.0, 16), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x662B0E8A), // 40% opacity for #2B0E8A + offset: Offset(0, 4), // X: 0, Y: 4 + blurRadius: 16, // Blur: 16 + spreadRadius: 0, // Spread: 0 + ), + ], + borderRadius: BorderRadius.circular(28), // Match the button's shape ), - onPressed: (){ - Navigator.of(context).push( - NewDmScreen.buildRoute(context: context) - ); - }, - icon: const Icon(Icons.add, color: Colors.white), - label: const Text( - 'New DM', - style: TextStyle(color: Colors.white, fontSize: 16), + child: FilledButton.icon( + style: FilledButton.styleFrom( + minimumSize: const Size(137, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + backgroundColor: designVariables.fabBg, + ), + onPressed: (){ + Navigator.of(context).push( + NewDmScreen.buildRoute(context: context) + ); + }, + icon: const Icon(Icons.add, color: Colors.white, size: 24), + label: Text( + 'New DM', + style: TextStyle(color: designVariables.fabLabel, fontSize: 20, fontWeight: FontWeight.w500), + ), ), ); } } class SearchRow extends StatefulWidget { - const SearchRow({super.key, required this.controller,}); + SearchRow({super.key, required this.controller, required this.focusNode}); // Accept focusNode final TextEditingController controller; + final FocusNode focusNode; @override State createState() => _SearchRowState(); @@ -217,24 +308,39 @@ class _SearchRowState extends State { void initState() { super.initState(); widget.controller.addListener(_onTextChanged); + widget.focusNode.addListener(_onFocusChanged); } @override void dispose() { widget.controller.removeListener(_onTextChanged); + widget.focusNode.removeListener(_onFocusChanged); + widget.focusNode.dispose(); super.dispose(); } void _onTextChanged() { + _updateSearchState(); + } + void _onFocusChanged() { + _updateSearchState(); + } + + void _updateSearchState() { setState(() { _showCancelButton = widget.controller.text.isNotEmpty; + // Notify parent widget about the search state change + (context as Element).markNeedsBuild(); }); } @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), // Add padding around the row + final designVariables = DesignVariables.of(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14.0), + color: DesignVariables.of(context).bgSearchInput, child: Row( children: [ const Icon( @@ -242,25 +348,28 @@ class _SearchRowState extends State { size: 24.0, color: Colors.grey, ), - const SizedBox(width: 8.0), // Add space between the icon and the text field + const SizedBox(width: 14.0), // Add space between the icon and the text field // Text Field Expanded( child: TextField( controller: widget.controller, - decoration: const InputDecoration( - hintText: 'Search...', // Placeholder text + focusNode: widget.focusNode, + decoration: InputDecoration( + hintText: 'Filter conversations', + hintStyle: TextStyle(color: designVariables.labelSearchPrompt), border: InputBorder.none, // Remove the border ), - style: TextStyle(fontSize: 16.0), // Customize the text style + style: const TextStyle(fontSize: 17.0, fontWeight: FontWeight.w400), ), ), if (_showCancelButton) ...[ - SizedBox(width: 8.0), + const SizedBox(width: 8.0), GestureDetector( onTap: () { widget.controller.clear(); + widget.focusNode.unfocus(); }, - child: Icon( + child: const Icon( Icons.cancel, size: 20.0, color: Colors.grey, @@ -278,10 +387,14 @@ class RecentDmConversationsItem extends StatelessWidget { super.key, required this.narrow, required this.unreadCount, + required this.searchQuery, + required this.focusNode, }); final DmNarrow narrow; final int unreadCount; + final String searchQuery; + final FocusNode focusNode; static const double _avatarSize = 32; @@ -320,9 +433,10 @@ class RecentDmConversationsItem extends StatelessWidget { } return Material( - color: designVariables.background, // TODO(design) check if this is the right variable + color: designVariables.mainBackground, // TODO(design) check if this is the right variable child: InkWell( onTap: () { + focusNode.unfocus(); Navigator.push(context, MessageListPage.buildRoute(context: context, narrow: narrow)); }, @@ -333,16 +447,7 @@ class RecentDmConversationsItem extends StatelessWidget { const SizedBox(width: 8), Expanded(child: Padding( padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - style: TextStyle( - fontSize: 17, - height: (20 / 17), - // TODO(design) check if this is the right variable - color: designVariables.labelMenuButton, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - title))), + child: _buildHighlightedText(title, searchQuery, designVariables))), const SizedBox(width: 12), unreadCount > 0 ? Padding(padding: const EdgeInsetsDirectional.only(end: 16), @@ -351,4 +456,56 @@ class RecentDmConversationsItem extends StatelessWidget { : const SizedBox(), ])))); } + + Widget _buildHighlightedText(String text, String query, DesignVariables designVariables) { + if (query.isEmpty || !text.toLowerCase().contains(query.toLowerCase())) { + // If there's no query or it doesn't match, show normal text + return Text( + text, + style: TextStyle( + fontSize: 17, + height: (20 / 17), + color: designVariables.labelMenuButton, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } + + final startIndex = text.toLowerCase().indexOf(query.toLowerCase()); + final endIndex = startIndex + query.length; + + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: text.substring(0, startIndex), + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: designVariables.textMessage, + ), + ), + TextSpan( + text: text.substring(startIndex, endIndex), + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, // Bold for the matching text + color: designVariables.textMessage, + ), + ), + TextSpan( + text: text.substring(endIndex), + style: TextStyle( + fontSize: 17, + color: designVariables.labelMenuButton, + ), + ), + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } + } diff --git a/lib/widgets/theme.dart b/lib/widgets/theme.dart index f8dbff51b3..bb0edea2dc 100644 --- a/lib/widgets/theme.dart +++ b/lib/widgets/theme.dart @@ -137,13 +137,17 @@ class DesignVariables extends ThemeExtension { contextMenuCancelText: const Color(0xff222222), contextMenuItemBg: const Color(0xff6159e1), contextMenuItemText: const Color(0xff381da7), + contextMenuItemIcon: const Color(0xff4F42C9), editorButtonPressedBg: Colors.black.withValues(alpha: 0.06), + fabBg: const Color(0xFF6E69F3), + fabLabel: const Color(0xffECEEFC), foreground: const Color(0xff000000), icon: const Color(0xff6159e1), iconSelected: const Color(0xff222222), labelCounterUnread: const Color(0xff222222), labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 0).toColor(), labelMenuButton: const Color(0xff222222), + labelSearchPrompt: const Color(0xff000000).withValues(alpha: 0.5), mainBackground: const Color(0xfff0f0f0), textInput: const Color(0xff000000), title: const Color(0xff1a1a1a), @@ -162,11 +166,12 @@ class DesignVariables extends ThemeExtension { modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.3), mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.8).toColor(), navigationButtonBg: Colors.black.withValues(alpha: 0.05), - newDmButtonBg: const Color(0xff4F42C9), + radioFillSelected: const Color(0xff4370F0), sectionCollapseIcon: const Color(0x7f1e2e48), star: const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(), subscriptionListHeaderLine: const HSLColor.fromAHSL(0.2, 240, 0.1, 0.5).toColor(), subscriptionListHeaderText: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.5).toColor(), + statusOnline: const Color(0xff46AA62), unreadCountBadgeTextForChannel: Colors.black.withValues(alpha: 0.9), ); @@ -187,13 +192,17 @@ class DesignVariables extends ThemeExtension { contextMenuCancelText: const Color(0xffffffff).withValues(alpha: 0.75), contextMenuItemBg: const Color(0xff7977fe), contextMenuItemText: const Color(0xff9398fd), + contextMenuItemIcon: const Color(0xff9398FD), editorButtonPressedBg: Colors.white.withValues(alpha: 0.06), + fabBg: const Color(0xFF4F42C9), + fabLabel: const Color(0xffECEEFC), foreground: const Color(0xffffffff), icon: const Color(0xff7977fe), iconSelected: Colors.white.withValues(alpha: 0.8), labelCounterUnread: const Color(0xffffffff).withValues(alpha: 0.7), labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 1).toColor(), labelMenuButton: const Color(0xffffffff).withValues(alpha: 0.85), + labelSearchPrompt: const Color(0xffffffff).withValues(alpha: 0.5), mainBackground: const Color(0xff1d1d1d), textInput: const Color(0xffffffff).withValues(alpha: 0.9), title: const Color(0xffffffff), @@ -216,11 +225,12 @@ class DesignVariables extends ThemeExtension { // TODO(design-dark) need proper dark-theme color (this is ad hoc) mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.6).toColor(), navigationButtonBg: Colors.white.withValues(alpha: 0.05), - newDmButtonBg: const Color(0xff4F42C9), + radioFillSelected: const Color(0xff4E7CFA), // TODO(design-dark) need proper dark-theme color (this is ad hoc) sectionCollapseIcon: const Color(0x7fb6c8e2), // TODO(design-dark) unchanged in dark theme? star: const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(), + statusOnline: const Color(0xff46AA62), // TODO(design-dark) need proper dark-theme color (this is ad hoc) subscriptionListHeaderLine: const HSLColor.fromAHSL(0.4, 240, 0.1, 0.75).toColor(), // TODO(design-dark) need proper dark-theme color (this is ad hoc) @@ -246,7 +256,10 @@ class DesignVariables extends ThemeExtension { required this.contextMenuCancelText, required this.contextMenuItemBg, required this.contextMenuItemText, + required this.contextMenuItemIcon, required this.editorButtonPressedBg, + required this.fabBg, + required this.fabLabel, required this.foreground, required this.icon, required this.iconSelected, @@ -271,12 +284,14 @@ class DesignVariables extends ThemeExtension { required this.modalBarrierColor, required this.mutedUnreadBadge, required this.navigationButtonBg, - required this.newDmButtonBg, required this.sectionCollapseIcon, required this.star, required this.subscriptionListHeaderLine, required this.subscriptionListHeaderText, required this.unreadCountBadgeTextForChannel, + required this.labelSearchPrompt, + required this.radioFillSelected, + required this.statusOnline, }); /// The [DesignVariables] from the context's active theme. @@ -305,7 +320,10 @@ class DesignVariables extends ThemeExtension { final Color contextMenuCancelText; final Color contextMenuItemBg; final Color contextMenuItemText; + final Color contextMenuItemIcon; final Color editorButtonPressedBg; + final Color fabBg; + final Color fabLabel; final Color foreground; final Color icon; final Color iconSelected; @@ -334,12 +352,14 @@ class DesignVariables extends ThemeExtension { final Color modalBarrierColor; final Color mutedUnreadBadge; final Color navigationButtonBg; - final Color newDmButtonBg; final Color sectionCollapseIcon; final Color star; final Color subscriptionListHeaderLine; final Color subscriptionListHeaderText; final Color unreadCountBadgeTextForChannel; + final Color labelSearchPrompt; + final Color radioFillSelected; + final Color statusOnline; @override DesignVariables copyWith({ @@ -359,6 +379,7 @@ class DesignVariables extends ThemeExtension { Color? contextMenuCancelText, Color? contextMenuItemBg, Color? contextMenuItemText, + Color? contextMenuItemIcon, Color? editorButtonPressedBg, Color? foreground, Color? icon, @@ -366,6 +387,7 @@ class DesignVariables extends ThemeExtension { Color? labelCounterUnread, Color? labelEdited, Color? labelMenuButton, + Color? labelSearchPrompt, Color? mainBackground, Color? textInput, Color? title, @@ -384,9 +406,12 @@ class DesignVariables extends ThemeExtension { Color? modalBarrierColor, Color? mutedUnreadBadge, Color? navigationButtonBg, - Color? newDmButtonBg, + Color? fabBg, + Color? fabLabel, + Color? radioFillSelected, Color? sectionCollapseIcon, Color? star, + Color? statusOnline, Color? subscriptionListHeaderLine, Color? subscriptionListHeaderText, Color? unreadCountBadgeTextForChannel, @@ -408,6 +433,7 @@ class DesignVariables extends ThemeExtension { contextMenuCancelText: contextMenuCancelText ?? this.contextMenuCancelText, contextMenuItemBg: contextMenuItemBg ?? this.contextMenuItemBg, contextMenuItemText: contextMenuItemText ?? this.contextMenuItemBg, + contextMenuItemIcon: contextMenuItemIcon ?? this.contextMenuItemIcon, editorButtonPressedBg: editorButtonPressedBg ?? this.editorButtonPressedBg, foreground: foreground ?? this.foreground, icon: icon ?? this.icon, @@ -433,12 +459,16 @@ class DesignVariables extends ThemeExtension { modalBarrierColor: modalBarrierColor ?? this.modalBarrierColor, mutedUnreadBadge: mutedUnreadBadge ?? this.mutedUnreadBadge, navigationButtonBg: navigationButtonBg ?? this.navigationButtonBg, - newDmButtonBg: newDmButtonBg ?? this.newDmButtonBg, + fabBg: fabBg ?? this.fabBg, + fabLabel: fabLabel ?? this.fabLabel, sectionCollapseIcon: sectionCollapseIcon ?? this.sectionCollapseIcon, star: star ?? this.star, subscriptionListHeaderLine: subscriptionListHeaderLine ?? this.subscriptionListHeaderLine, subscriptionListHeaderText: subscriptionListHeaderText ?? this.subscriptionListHeaderText, unreadCountBadgeTextForChannel: unreadCountBadgeTextForChannel ?? this.unreadCountBadgeTextForChannel, + labelSearchPrompt: labelSearchPrompt ?? this.labelSearchPrompt, + radioFillSelected: radioFillSelected ?? this.radioFillSelected, + statusOnline: statusOnline ?? this.statusOnline, ); } @@ -489,12 +519,17 @@ class DesignVariables extends ThemeExtension { modalBarrierColor: Color.lerp(modalBarrierColor, other.modalBarrierColor, t)!, mutedUnreadBadge: Color.lerp(mutedUnreadBadge, other.mutedUnreadBadge, t)!, navigationButtonBg: Color.lerp(navigationButtonBg, other.navigationButtonBg, t)!, - newDmButtonBg: Color.lerp(newDmButtonBg, other.newDmButtonBg, t)!, + fabBg: Color.lerp(fabBg, other.fabBg, t)!, + fabLabel: Color.lerp(fabLabel, other.fabLabel, t)!, sectionCollapseIcon: Color.lerp(sectionCollapseIcon, other.sectionCollapseIcon, t)!, star: Color.lerp(star, other.star, t)!, subscriptionListHeaderLine: Color.lerp(subscriptionListHeaderLine, other.subscriptionListHeaderLine, t)!, subscriptionListHeaderText: Color.lerp(subscriptionListHeaderText, other.subscriptionListHeaderText, t)!, unreadCountBadgeTextForChannel: Color.lerp(unreadCountBadgeTextForChannel, other.unreadCountBadgeTextForChannel, t)!, + contextMenuItemIcon: Color.lerp(contextMenuItemIcon, other.contextMenuItemIcon, t)!, + labelSearchPrompt: Color.lerp(labelSearchPrompt, other.labelSearchPrompt, t)!, + radioFillSelected: Color.lerp(radioFillSelected, other.radioFillSelected, t)!, + statusOnline: Color.lerp(statusOnline, other.statusOnline, t)!, ); } }