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 {
diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf
index 0be670d5cb..1ee5f37bb5 100644
Binary files a/assets/icons/ZulipIcons.ttf and b/assets/icons/ZulipIcons.ttf differ
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 82cb83704b..0e3bbd0101 100644
--- a/lib/widgets/icons.dart
+++ b/lib/widgets/icons.dart
@@ -105,38 +105,44 @@ abstract final class ZulipIcons {
/// The Zulip custom icon "mute".
static const IconData mute = IconData(0xf11b, fontFamily: "Zulip Icons");
+ /// The Zulip custom icon "no_dm".
+ static const IconData no_dm = IconData(0xf11c, fontFamily: "Zulip Icons");
+
+ /// The Zulip custom icon "no_dm_down_arrow".
+ static const IconData no_dm_down_arrow = IconData(0xf11d, fontFamily: "Zulip Icons");
+
/// The Zulip custom icon "read_receipts".
- static const IconData read_receipts = IconData(0xf11c, fontFamily: "Zulip Icons");
+ static const IconData read_receipts = IconData(0xf11e, fontFamily: "Zulip Icons");
/// The Zulip custom icon "send".
- static const IconData send = IconData(0xf11d, fontFamily: "Zulip Icons");
+ static const IconData send = IconData(0xf11f, fontFamily: "Zulip Icons");
/// The Zulip custom icon "share".
- static const IconData share = IconData(0xf11e, fontFamily: "Zulip Icons");
+ static const IconData share = IconData(0xf120, fontFamily: "Zulip Icons");
/// The Zulip custom icon "share_ios".
- static const IconData share_ios = IconData(0xf11f, fontFamily: "Zulip Icons");
+ static const IconData share_ios = IconData(0xf121, fontFamily: "Zulip Icons");
/// The Zulip custom icon "smile".
- static const IconData smile = IconData(0xf120, fontFamily: "Zulip Icons");
+ static const IconData smile = IconData(0xf122, fontFamily: "Zulip Icons");
/// The Zulip custom icon "star".
- static const IconData star = IconData(0xf121, fontFamily: "Zulip Icons");
+ static const IconData star = IconData(0xf123, fontFamily: "Zulip Icons");
/// The Zulip custom icon "star_filled".
- static const IconData star_filled = IconData(0xf122, fontFamily: "Zulip Icons");
+ static const IconData star_filled = IconData(0xf124, fontFamily: "Zulip Icons");
/// The Zulip custom icon "three_person".
- static const IconData three_person = IconData(0xf123, fontFamily: "Zulip Icons");
+ static const IconData three_person = IconData(0xf125, fontFamily: "Zulip Icons");
/// The Zulip custom icon "topic".
- static const IconData topic = IconData(0xf124, fontFamily: "Zulip Icons");
+ static const IconData topic = IconData(0xf126, fontFamily: "Zulip Icons");
/// The Zulip custom icon "unmute".
- static const IconData unmute = IconData(0xf125, fontFamily: "Zulip Icons");
+ static const IconData unmute = IconData(0xf127, fontFamily: "Zulip Icons");
/// The Zulip custom icon "user".
- static const IconData user = IconData(0xf126, fontFamily: "Zulip Icons");
+ static const IconData user = IconData(0xf128, fontFamily: "Zulip Icons");
// END GENERATED ICON DATA
}
diff --git a/lib/widgets/new_dm.dart b/lib/widgets/new_dm.dart
new file mode 100644
index 0000000000..fe5f548539
--- /dev/null
+++ b/lib/widgets/new_dm.dart
@@ -0,0 +1,253 @@
+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();
+ _allUsers.removeWhere((user) => user.userId == store.selfUserId);
+ _isLoading = false;
+ });
+ } catch (error) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ }
+
+ List get _filteredUsers {
+ final query = _searchController.text.toLowerCase();
+ return _allUsers.where((user) =>
+ user.fullName.toLowerCase().contains(query)
+ ).toList();
+ }
+
+ void _handleUserSelect(User user) {
+ setState(() {
+ if (_selectedUsers.contains(user)) {
+ _selectedUsers.remove(user);
+ } else {
+ _selectedUsers.add(user);
+ }
+ _searchController.clear();
+ });
+ Future.delayed(const Duration(milliseconds: 10), () {
+ _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
+ });
+ }
+
+
+ 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(
+ backgroundColor: designVariables.bgContextMenu,
+ appBar: AppBar(
+ 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
+ ],
+ ),
+ centerTitle: false, // Prevents default centering when using custom layout
+ actions: [
+ TextButton(
+ onPressed: _selectedUsers.isEmpty ? null : _handleDone,
+ 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: designVariables.bgSearchInput,
+ constraints: BoxConstraints(
+ minWidth: double.infinity,
+ maxHeight: screenHeight * 0.2, // Limit height to 20% of screen
+ ),
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(14,11,14,0),
+ child: SingleChildScrollView(
+ controller: _scrollController,
+ scrollDirection: Axis.vertical,
+ child: Wrap(
+ crossAxisAlignment: WrapCrossAlignment.center,
+ spacing: 16,
+ children: [
+ ..._selectedUsers.map((user) => Chip(
+ 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,
+ 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];
+ 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),
+ ),
+ );
+ },
+ ),
+ ),
+
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/recent_dm_conversations.dart b/lib/widgets/recent_dm_conversations.dart
index ddc32861a3..fa166ab039 100644
--- a/lib/widgets/recent_dm_conversations.dart
+++ b/lib/widgets/recent_dm_conversations.dart
@@ -7,6 +7,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';
@@ -18,10 +19,15 @@ 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() {
model?.removeListener(_modelChanged);
@@ -31,37 +37,348 @@ 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: 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),
+ ),
+ ),
+ ),
+ ),
+ 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),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ 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),
+ ),
+ 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 sorted = model!.sorted;
- return SafeArea(
- // Don't pad the bottom here; we want the list content to do that.
- bottom: false,
- child: ListView.builder(
- itemCount: sorted.length,
- itemBuilder: (context, index) {
- final narrow = sorted[index];
- return RecentDmConversationsItem(
- narrow: narrow,
- unreadCount: unreadsModel!.countInDmNarrow(narrow),
+ final designVariables = DesignVariables.of(context);
+
+ 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
+ ),
+ 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 {
+ SearchRow({super.key, required this.controller, required this.focusNode}); // Accept focusNode
+
+ final TextEditingController controller;
+ final FocusNode focusNode;
+
+ @override
+ State createState() => _SearchRowState();
+}
+
+class _SearchRowState extends State {
+ bool _showCancelButton = false;
+
+ @override
+ 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) {
+ final designVariables = DesignVariables.of(context);
+
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 14.0),
+ color: DesignVariables.of(context).bgSearchInput,
+ child: Row(
+ children: [
+ const Icon(
+ Icons.search,
+ size: 24.0,
+ color: Colors.grey,
+ ),
+ const SizedBox(width: 14.0), // Add space between the icon and the text field
+ // Text Field
+ Expanded(
+ child: TextField(
+ controller: widget.controller,
+ focusNode: widget.focusNode,
+ decoration: InputDecoration(
+ hintText: 'Filter conversations',
+ hintStyle: TextStyle(color: designVariables.labelSearchPrompt),
+ border: InputBorder.none, // Remove the border
+ ),
+ style: const TextStyle(fontSize: 17.0, fontWeight: FontWeight.w400),
+ ),
+ ),
+ if (_showCancelButton) ...[
+ const SizedBox(width: 8.0),
+ GestureDetector(
+ onTap: () {
+ widget.controller.clear();
+ widget.focusNode.unfocus();
+ },
+ child: const Icon(
+ Icons.cancel,
+ size: 20.0,
+ color: Colors.grey,
+ ),
+ ),
+ ],
+ ],
+ ),
+ );
}
}
@@ -70,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;
@@ -112,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));
},
@@ -125,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),
@@ -143,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 ec8ad8aecc..bb0edea2dc 100644
--- a/lib/widgets/theme.dart
+++ b/lib/widgets/theme.dart
@@ -121,110 +121,123 @@ const kZulipBrandColor = Color.fromRGBO(0x64, 0x92, 0xfe, 1);
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=2945-49492&t=MEb4vtp7S26nntxm-0
class DesignVariables extends ThemeExtension {
static final light = DesignVariables._(
- background: const Color(0xffffffff),
- bannerBgIntDanger: const Color(0xfff2e4e4),
- bgBotBar: const Color(0xfff6f6f6),
- bgContextMenu: const Color(0xfff2f2f2),
- bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.15),
- bgMenuButtonActive: Colors.black.withValues(alpha: 0.05),
- bgMenuButtonSelected: Colors.white,
- bgTopBar: const Color(0xfff5f5f5),
- borderBar: Colors.black.withValues(alpha: 0.2),
- borderMenuButtonSelected: Colors.black.withValues(alpha: 0.2),
- btnLabelAttLowIntDanger: const Color(0xffc0070a),
- btnLabelAttMediumIntDanger: const Color(0xffac0508),
- composeBoxBg: const Color(0xffffffff),
- contextMenuCancelText: const Color(0xff222222),
- contextMenuItemBg: const Color(0xff6159e1),
- contextMenuItemText: const Color(0xff381da7),
- editorButtonPressedBg: Colors.black.withValues(alpha: 0.06),
- 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),
- mainBackground: const Color(0xfff0f0f0),
- textInput: const Color(0xff000000),
- title: const Color(0xff1a1a1a),
- bgSearchInput: const Color(0xffe3e3e3),
- textMessage: const Color(0xff262626),
- channelColorSwatches: ChannelColorSwatches.light,
- colorMessageHeaderIconInteractive: Colors.black.withValues(alpha: 0.2),
- contextMenuCancelBg: const Color(0xff797986).withValues(alpha: 0.15),
- contextMenuCancelPressedBg: const Color(0xff797986).withValues(alpha: 0.20),
- dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.35, 0.93).toColor(),
- groupDmConversationIcon: Colors.black.withValues(alpha: 0.5),
- groupDmConversationIconBg: const Color(0x33808080),
- inboxItemIconMarker: const HSLColor.fromAHSL(0.5, 0, 0, 0.2).toColor(),
- loginOrDivider: const Color(0xffdedede),
- loginOrDividerText: const Color(0xff575757),
- 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),
- 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(),
- unreadCountBadgeTextForChannel: Colors.black.withValues(alpha: 0.9),
- );
+ background: const Color(0xffffffff),
+ bannerBgIntDanger: const Color(0xfff2e4e4),
+ bgBotBar: const Color(0xfff6f6f6),
+ bgContextMenu: const Color(0xfff2f2f2),
+ bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.15),
+ bgMenuButtonActive: Colors.black.withValues(alpha: 0.05),
+ bgMenuButtonSelected: Colors.white,
+ bgTopBar: const Color(0xfff5f5f5),
+ borderBar: Colors.black.withValues(alpha: 0.2),
+ borderMenuButtonSelected: Colors.black.withValues(alpha: 0.2),
+ btnLabelAttLowIntDanger: const Color(0xffc0070a),
+ btnLabelAttMediumIntDanger: const Color(0xffac0508),
+ composeBoxBg: const Color(0xffffffff),
+ 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),
+ bgSearchInput: const Color(0xffe3e3e3),
+ textMessage: const Color(0xff262626),
+ channelColorSwatches: ChannelColorSwatches.light,
+ colorMessageHeaderIconInteractive: Colors.black.withValues(alpha: 0.2),
+ contextMenuCancelBg: const Color(0xff797986).withValues(alpha: 0.15),
+ contextMenuCancelPressedBg: const Color(0xff797986).withValues(alpha: 0.20),
+ dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.35, 0.93).toColor(),
+ groupDmConversationIcon: Colors.black.withValues(alpha: 0.5),
+ groupDmConversationIconBg: const Color(0x33808080),
+ inboxItemIconMarker: const HSLColor.fromAHSL(0.5, 0, 0, 0.2).toColor(),
+ loginOrDivider: const Color(0xffdedede),
+ loginOrDividerText: const Color(0xff575757),
+ 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),
+ 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),
+ );
static final dark = DesignVariables._(
- background: const Color(0xff000000),
- bannerBgIntDanger: const Color(0xff461616),
- bgBotBar: const Color(0xff222222),
- bgContextMenu: const Color(0xff262626),
- bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.37),
- bgMenuButtonActive: Colors.black.withValues(alpha: 0.2),
- bgMenuButtonSelected: Colors.black.withValues(alpha: 0.25),
- bgTopBar: const Color(0xff242424),
- borderBar: const Color(0xffffffff).withValues(alpha: 0.1),
- borderMenuButtonSelected: Colors.white.withValues(alpha: 0.1),
- btnLabelAttLowIntDanger: const Color(0xffff8b7c),
- btnLabelAttMediumIntDanger: const Color(0xffff8b7c),
- composeBoxBg: const Color(0xff0f0f0f),
- contextMenuCancelText: const Color(0xffffffff).withValues(alpha: 0.75),
- contextMenuItemBg: const Color(0xff7977fe),
- contextMenuItemText: const Color(0xff9398fd),
- editorButtonPressedBg: Colors.white.withValues(alpha: 0.06),
- 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),
- mainBackground: const Color(0xff1d1d1d),
- textInput: const Color(0xffffffff).withValues(alpha: 0.9),
- title: const Color(0xffffffff),
- bgSearchInput: const Color(0xff313131),
- textMessage: const Color(0xffffffff).withValues(alpha: 0.8),
- channelColorSwatches: ChannelColorSwatches.dark,
- contextMenuCancelBg: const Color(0xff797986).withValues(alpha: 0.15), // the same as the light mode in Figma
- contextMenuCancelPressedBg: const Color(0xff797986).withValues(alpha: 0.20), // the same as the light mode in Figma
- // TODO(design-dark) need proper dark-theme color (this is ad hoc)
- colorMessageHeaderIconInteractive: Colors.white.withValues(alpha: 0.2),
- dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.15, 0.2).toColor(),
- // TODO(design-dark) need proper dark-theme color (this is ad hoc)
- groupDmConversationIcon: Colors.white.withValues(alpha: 0.5),
- // TODO(design-dark) need proper dark-theme color (this is ad hoc)
- groupDmConversationIconBg: const Color(0x33cccccc),
- inboxItemIconMarker: const HSLColor.fromAHSL(0.4, 0, 0, 1).toColor(),
- loginOrDivider: const Color(0xff424242),
- loginOrDividerText: const Color(0xffa8a8a8),
- modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.5),
- // 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),
- // 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(),
- // 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)
- subscriptionListHeaderText: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.75).toColor(),
- unreadCountBadgeTextForChannel: Colors.white.withValues(alpha: 0.9),
- );
+ background: const Color(0xff000000),
+ bannerBgIntDanger: const Color(0xff461616),
+ bgBotBar: const Color(0xff222222),
+ bgContextMenu: const Color(0xff262626),
+ bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.37),
+ bgMenuButtonActive: Colors.black.withValues(alpha: 0.2),
+ bgMenuButtonSelected: Colors.black.withValues(alpha: 0.25),
+ bgTopBar: const Color(0xff242424),
+ borderBar: Colors.black.withValues(alpha: 0.1),
+ borderMenuButtonSelected: Colors.white.withValues(alpha: 0.1),
+ btnLabelAttLowIntDanger: const Color(0xffff8b7c),
+ btnLabelAttMediumIntDanger: const Color(0xffff8b7c),
+ composeBoxBg: const Color(0xff0f0f0f),
+ 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),
+ bgSearchInput: const Color(0xff313131),
+ textMessage: const Color(0xffffffff).withValues(alpha: 0.8),
+ channelColorSwatches: ChannelColorSwatches.dark,
+ contextMenuCancelBg: const Color(0xff797986).withValues(alpha: 0.15), // the same as the light mode in Figma
+ contextMenuCancelPressedBg: const Color(0xff797986).withValues(alpha: 0.20), // the same as the light mode in Figma
+ // TODO(design-dark) need proper dark-theme color (this is ad hoc)
+ colorMessageHeaderIconInteractive: Colors.white.withValues(alpha: 0.2),
+ dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.15, 0.2).toColor(),
+ // TODO(design-dark) need proper dark-theme color (this is ad hoc)
+ groupDmConversationIcon: Colors.white.withValues(alpha: 0.5),
+ // TODO(design-dark) need proper dark-theme color (this is ad hoc)
+ groupDmConversationIconBg: const Color(0x33cccccc),
+ inboxItemIconMarker: const HSLColor.fromAHSL(0.4, 0, 0, 1).toColor(),
+ loginOrDivider: const Color(0xff424242),
+ loginOrDividerText: const Color(0xffa8a8a8),
+ modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.5),
+ // 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),
+ 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)
+ subscriptionListHeaderText: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.75).toColor(),
+ unreadCountBadgeTextForChannel: Colors.white.withValues(alpha: 0.9),
+ );
+
DesignVariables._({
required this.background,
@@ -243,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,
@@ -273,6 +289,9 @@ class DesignVariables extends ThemeExtension {
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.
@@ -301,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;
@@ -335,6 +357,9 @@ class DesignVariables extends ThemeExtension {
final Color subscriptionListHeaderLine;
final Color subscriptionListHeaderText;
final Color unreadCountBadgeTextForChannel;
+ final Color labelSearchPrompt;
+ final Color radioFillSelected;
+ final Color statusOnline;
@override
DesignVariables copyWith({
@@ -354,6 +379,7 @@ class DesignVariables extends ThemeExtension {
Color? contextMenuCancelText,
Color? contextMenuItemBg,
Color? contextMenuItemText,
+ Color? contextMenuItemIcon,
Color? editorButtonPressedBg,
Color? foreground,
Color? icon,
@@ -361,6 +387,7 @@ class DesignVariables extends ThemeExtension {
Color? labelCounterUnread,
Color? labelEdited,
Color? labelMenuButton,
+ Color? labelSearchPrompt,
Color? mainBackground,
Color? textInput,
Color? title,
@@ -379,8 +406,12 @@ class DesignVariables extends ThemeExtension {
Color? modalBarrierColor,
Color? mutedUnreadBadge,
Color? navigationButtonBg,
+ Color? fabBg,
+ Color? fabLabel,
+ Color? radioFillSelected,
Color? sectionCollapseIcon,
Color? star,
+ Color? statusOnline,
Color? subscriptionListHeaderLine,
Color? subscriptionListHeaderText,
Color? unreadCountBadgeTextForChannel,
@@ -402,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,
@@ -427,11 +459,16 @@ class DesignVariables extends ThemeExtension {
modalBarrierColor: modalBarrierColor ?? this.modalBarrierColor,
mutedUnreadBadge: mutedUnreadBadge ?? this.mutedUnreadBadge,
navigationButtonBg: navigationButtonBg ?? this.navigationButtonBg,
+ 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,
);
}
@@ -482,11 +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)!,
+ 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)!,
);
}
}