Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
419 changes: 20 additions & 399 deletions lib/pages/contacts_tab/contacts_tab_body_view.dart

Large diffs are not rendered by default.

119 changes: 119 additions & 0 deletions lib/pages/contacts_tab/widgets/sliver_contacts_with_matrix_id.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import 'package:fluffychat/domain/app_state/contact/get_contacts_state.dart';
import 'package:fluffychat/pages/contacts_tab/contacts_tab.dart';
import 'package:fluffychat/pages/contacts_tab/contacts_tab_view_style.dart';
import 'package:fluffychat/pages/new_private_chat/widget/expansion_contact_list_tile.dart';
import 'package:fluffychat/presentation/model/contact/presentation_contact.dart';
import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/sliver_expandable_list.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/generated/l10n/app_localizations.dart';

class SliverContactsWithMatrixId extends StatelessWidget {
const SliverContactsWithMatrixId({required this.controller, super.key});

final ContactsTabController controller;

@override
Widget build(BuildContext context) {
const empty = SliverToBoxAdapter();
return ValueListenableBuilder(
valueListenable: controller.presentationContactNotifier,
builder: (context, state, _) {
return state.fold((_) => empty, (success) {
if (success is ContactsLoading) {
return empty;
}

if (success is PresentationExternalContactSuccess) {
if (controller.presentationRecentContactNotifier.value.isNotEmpty) {
return empty;
}
if (!PlatformInfos.isWeb) {
if (controller.phoneBookFilterSuccess) {
return empty;
}
}
return _ExternalContactTile(
controller: controller,
contact: success.contact,
);
}

if (success is PresentationContactsSuccess) {
final contacts = success.contacts
.where((c) => c.matrixId != null && c.matrixId!.isNotEmpty)
.toList();
if (contacts.isEmpty) {
return empty;
}
return SliverExpandableList(
title: L10n.of(context)!.linagoraContactsCount(contacts.length),
itemCount: contacts.length,
itemBuilder: (context, index) => _ContactTile(
contact: contacts[index],
controller: controller,
),
);
}

return empty;
});
},
);
}
}

class _ExternalContactTile extends StatelessWidget {
const _ExternalContactTile({required this.controller, required this.contact});

final ContactsTabController controller;
final PresentationContact contact;

@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: ContactsTabViewStyle.padding,
),
child: ExpansionContactListTile(
contact: contact,
highlightKeyword: controller.textEditingController.text,
enableInvitation: controller.supportInvitation(),
onContactTap: () => controller.onContactTap(
context: context,
path: 'rooms',
contact: contact,
),
),
),
);
}
}

class _ContactTile extends StatelessWidget {
const _ContactTile({required this.contact, required this.controller});

final PresentationContact contact;
final ContactsTabController controller;

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: ContactsTabViewStyle.padding,
),
child: ExpansionContactListTile(
contact: contact,
highlightKeyword: controller.textEditingController.text,
enableInvitation: controller.supportInvitation(),
onContactTap: () => controller.onContactTap(
context: context,
path: 'rooms',
contact: contact,
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'package:fluffychat/domain/app_state/contact/get_contacts_state.dart';
import 'package:fluffychat/pages/contacts_tab/contacts_tab.dart';
import 'package:fluffychat/pages/contacts_tab/contacts_tab_view_style.dart';
import 'package:fluffychat/pages/new_private_chat/widget/expansion_contact_list_tile.dart';
import 'package:fluffychat/presentation/model/contact/presentation_contact.dart';
import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart';
import 'package:fluffychat/widgets/sliver_expandable_list.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/generated/l10n/app_localizations.dart';

class SliverContactsWithoutMatrixId extends StatelessWidget {
const SliverContactsWithoutMatrixId({required this.controller, super.key});

final ContactsTabController controller;

@override
Widget build(BuildContext context) {
const empty = SliverToBoxAdapter();
return ValueListenableBuilder(
valueListenable: controller.presentationContactNotifier,
builder: (context, state, _) {
return state.fold((_) => empty, (success) {
if (success is ContactsLoading) {
return empty;
}

if (success is PresentationContactsSuccess) {
final contacts = success.contacts
.where((c) => c.matrixId == null || c.matrixId!.isEmpty)
.toList();
if (contacts.isEmpty) {
return empty;
}
return SliverExpandableList(
title: L10n.of(context)!.linagoraContactsCount(contacts.length),
itemCount: contacts.length,
itemBuilder: (context, index) => _ContactTile(
contact: contacts[index],
controller: controller,
),
);
}

return empty;
});
},
);
}
}

class _ContactTile extends StatelessWidget {
const _ContactTile({required this.contact, required this.controller});

final PresentationContact contact;
final ContactsTabController controller;

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: ContactsTabViewStyle.padding,
),
child: ExpansionContactListTile(
contact: contact,
highlightKeyword: controller.textEditingController.text,
enableInvitation: controller.supportInvitation(),
onContactTap: () => controller.onContactTap(
context: context,
path: 'rooms',
contact: contact,
),
),
);
}
}
66 changes: 66 additions & 0 deletions lib/pages/contacts_tab/widgets/sliver_empty_contacts.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'package:fluffychat/domain/app_state/contact/get_contacts_state.dart';
import 'package:fluffychat/pages/contacts_tab/contacts_tab.dart';
import 'package:fluffychat/pages/contacts_tab/contacts_tab_view_style.dart';
import 'package:fluffychat/pages/contacts_tab/empty_contacts_body.dart';
import 'package:fluffychat/pages/new_private_chat/widget/no_contacts_found.dart';
import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart';
import 'package:flutter/material.dart';

class SliverEmptyContacts extends StatelessWidget {
const SliverEmptyContacts({required this.controller, super.key});

final ContactsTabController controller;

bool _isLoading() => controller.presentationContactNotifier.value.fold(
(_) => false,
(s) => s is ContactsLoading,
);
Comment on lines +14 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Include phonebook loading in _isLoading to prevent premature empty-state rendering.

Right now, loading is inferred only from presentationContactNotifier. When phonebook data is still loading, the widget can briefly show EmptyContactBody / NoContactsFound before data arrives.

🐛 Proposed fix
-  bool _isLoading() => controller.presentationContactNotifier.value.fold(
-    (_) => false,
-    (s) => s is ContactsLoading,
-  );
+  bool _isLoading() {
+    final contactsLoading = controller.presentationContactNotifier.value.fold(
+      (_) => false,
+      (s) => s is ContactsLoading,
+    );
+    final phonebookLoading =
+        controller.presentationPhonebookContactNotifier.value.fold(
+          (_) => false,
+          (s) => s is ContactsLoading,
+        );
+    return contactsLoading || phonebookLoading;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/contacts_tab/widgets/sliver_empty_contacts.dart` around lines 14 -
17, Update the _isLoading function so it also returns true while the phonebook
data is loading to avoid showing EmptyContactBody/NoContactsFound prematurely;
specifically, inside _isLoading (which currently checks
controller.presentationContactNotifier for ContactsLoading), also check the
controller's phonebook loading notifier/state (e.g. controller.phonebookNotifier
or controller.phonebookState) and return true if that notifier indicates a
loading state, combining the two checks so either presentationContactNotifier is
ContactsLoading OR the phonebook notifier is loading.


bool _hasContactsData() => controller.presentationContactNotifier.value.fold(
(_) => false,
(s) =>
(s is PresentationContactsSuccess && s.contacts.isNotEmpty) ||
s is PresentationExternalContactSuccess,
);

bool _hasPhonebookData() =>
controller.presentationPhonebookContactNotifier.value.fold(
(_) => false,
(s) => s is PresentationContactsSuccess && s.contacts.isNotEmpty,
);

bool _hasRecentData() =>
controller.presentationRecentContactNotifier.value.isNotEmpty;

@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: Listenable.merge([
controller.presentationContactNotifier,
controller.presentationPhonebookContactNotifier,
controller.presentationRecentContactNotifier,
]),
builder: (context, _) {
if (_isLoading() ||
_hasContactsData() ||
_hasPhonebookData() ||
_hasRecentData()) {
return const SliverToBoxAdapter();
}
final keyword = controller.textEditingController.text;
if (keyword.isEmpty) {
return const SliverToBoxAdapter(child: EmptyContactBody());
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
left: ContactsTabViewStyle.padding,
top: ContactsTabViewStyle.padding,
),
child: NoContactsFound(keyword: keyword),
),
);
},
);
}
}
25 changes: 25 additions & 0 deletions lib/pages/contacts_tab/widgets/sliver_loading_contacts.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:fluffychat/domain/app_state/contact/get_contacts_state.dart';
import 'package:fluffychat/pages/contacts_tab/contacts_tab.dart';
import 'package:fluffychat/pages/new_private_chat/widget/loading_contact_widget.dart';
import 'package:flutter/material.dart';

class SliverLoadingContacts extends StatelessWidget {
const SliverLoadingContacts({required this.controller, super.key});

final ContactsTabController controller;

@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller.presentationContactNotifier,
builder: (context, state, _) {
return state.fold((_) => const SliverToBoxAdapter(), (success) {
if (success is ContactsLoading) {
return const SliverToBoxAdapter(child: LoadingContactWidget());
}
return const SliverToBoxAdapter();
});
},
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'package:fluffychat/pages/contacts_tab/contacts_tab.dart';
import 'package:fluffychat/pages/contacts_tab/contacts_tab_view_style.dart';
import 'package:fluffychat/pages/new_private_chat/widget/expansion_phonebook_contact_list_tile.dart';
import 'package:fluffychat/presentation/model/contact/presentation_contact.dart';
import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart';
import 'package:fluffychat/widgets/sliver_expandable_list.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/generated/l10n/app_localizations.dart';

class SliverPhonebookContactsWithMatrixId extends StatelessWidget {
const SliverPhonebookContactsWithMatrixId({
required this.controller,
super.key,
});

final ContactsTabController controller;

@override
Widget build(BuildContext context) {
const empty = SliverToBoxAdapter();
return ValueListenableBuilder(
valueListenable: controller.presentationPhonebookContactNotifier,
builder: (context, state, _) {
return state.fold((_) => empty, (success) {
if (success is PresentationContactsSuccess) {
final contacts = success.contacts
.where((c) => c.matrixId != null && c.matrixId!.isNotEmpty)
.toList();
if (contacts.isEmpty) {
return empty;
}
return SliverExpandableList(
title: L10n.of(context)!.contactsCount(contacts.length),
itemCount: contacts.length,
itemBuilder: (context, index) => _PhonebookContactTile(
contact: contacts[index],
controller: controller,
),
);
}
return empty;
});
},
);
}
}

class _PhonebookContactTile extends StatelessWidget {
const _PhonebookContactTile({
required this.contact,
required this.controller,
});

final PresentationContact contact;
final ContactsTabController controller;

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: ContactsTabViewStyle.padding,
),
child: ExpansionPhonebookContactListTile(
contact: contact,
highlightKeyword: controller.textEditingController.text,
enableInvitation: controller.supportInvitation(),
onContactTap: () => controller.onContactTap(
context: context,
path: 'rooms',
contact: contact,
),
),
);
}
}
Loading
Loading