Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a2b934b
refactor: replace relay connection check with delayed relay error sta…
codeswot Sep 22, 2025
44644b4
feat: implement delayed relay error handling and optimize connection …
codeswot Sep 22, 2025
17631fe
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Sep 23, 2025
476f0d9
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Sep 25, 2025
e69e0f0
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Sep 26, 2025
68b926c
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Sep 30, 2025
c7e2a70
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 1, 2025
b6fa9fb
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 1, 2025
4299063
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 2, 2025
3c87763
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 3, 2025
a38954e
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 6, 2025
41aebf7
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 10, 2025
a9f9390
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 11, 2025
36884d1
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 12, 2025
f4c49de
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 14, 2025
bf43723
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 17, 2025
d970a79
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 17, 2025
7cf323c
Merge branch 'master' of github.com:parres-hq/whitenoise_flutter
codeswot Oct 17, 2025
b3f75f3
Add group image path caching and retrieval functionality
codeswot Oct 17, 2025
3cf0bb3
Add group image path handling in GroupChatInfo
codeswot Oct 17, 2025
6c747bc
Refactor GroupChatInfo to manage group image path and subscription li…
codeswot Oct 17, 2025
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
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager_apple/ios"

SPEC CHECKSUMS:
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe
emoji_picker_flutter: 8e50ec5caac456a23a78637e02c6293ea0ac8771
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
Expand Down
68 changes: 65 additions & 3 deletions lib/config/providers/group_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ class GroupsNotifier extends Notifier<GroupsState> {
// Load and cache group types for synchronous access
await _loadGroupTypesForAllGroups(groups);

// Load and cache group image paths
await _loadGroupImagePaths(groups);

// Now calculate display names with member data available
await _calculateDisplayNames(groups);

Expand Down Expand Up @@ -622,6 +625,56 @@ class GroupsNotifier extends Notifier<GroupsState> {
}
}

/// Load and cache group image paths for all groups
Future<void> _loadGroupImagePaths(List<Group> groups) async {
try {
final Map<String, String> groupImagePaths = Map<String, String>.from(
state.groupImagePaths ?? {},
);

final List<Future<void>> loadTasks = [];
final activePubkey = ref.read(activePubkeyProvider);
if (activePubkey == null || activePubkey.isEmpty) return;

for (final group in groups) {
loadTasks.add(
getGroupImagePath(
accountPubkey: activePubkey,
groupId: group.mlsGroupId,
)
.then((imagePath) {
if (imagePath != null && imagePath.isNotEmpty) {
groupImagePaths[group.mlsGroupId] = imagePath;
}
})
.catchError((e) {
_logErrorSync('Failed to load image path for group ${group.mlsGroupId}', e);
// Skip this group if image loading fails
}),
);
}

// Execute all image path loading in parallel for better performance
await Future.wait(loadTasks);

// Update state with the cached image paths
state = state.copyWith(groupImagePaths: groupImagePaths);

_logger.info('GroupsProvider: Loaded image paths for ${groupImagePaths.length} groups');
} catch (e) {
// Log the full exception details with proper ApiError unpacking
String logMessage = 'GroupsProvider: Error loading group image paths - Exception: ';
if (e is ApiError) {
final errorDetails = await e.messageText();
logMessage += '$errorDetails (Type: ${e.runtimeType})';
} else {
logMessage += '$e (Type: ${e.runtimeType})';
}
_logger.severe(logMessage, e);
// Don't throw - we want to continue even if some image loading fails
}
}

/// Load members for all groups (used during initial group loading)
Future<void> _loadMembersForAllGroups(List<Group> groups) async {
try {
Expand Down Expand Up @@ -805,6 +858,12 @@ class GroupsNotifier extends Notifier<GroupsState> {
return state.groupTypes?[groupId];
}

/// Get cached group image path synchronously
/// Returns null if group image path is not cached yet
String? getCachedGroupImagePath(String groupId) {
return state.groupImagePaths?[groupId];
}

Future<bool> isCurrentUserAdmin(String groupId) async {
try {
final activePubkey = ref.read(activePubkeyProvider) ?? '';
Expand Down Expand Up @@ -895,6 +954,9 @@ class GroupsNotifier extends Notifier<GroupsState> {
// Load and cache group types for new groups
await _loadGroupTypesForAllGroups(actuallyNewGroups);

// Load and cache group image paths for new groups
await _loadGroupImagePaths(actuallyNewGroups);

// Calculate display names for new groups
await _calculateDisplayNamesForSpecificGroups(actuallyNewGroups);

Expand Down Expand Up @@ -1248,7 +1310,7 @@ extension GroupMemberUtils on GroupsNotifier {

/// Get the display image for a group based on its type
/// For direct messages, returns the other member's image
/// For regular groups, returns null (can be extended for group avatars)
/// For regular groups, returns the cached group image path
String? getGroupDisplayImage(String groupId) {
final group = findGroupById(groupId);
if (group == null) return null;
Expand All @@ -1269,7 +1331,7 @@ extension GroupMemberUtils on GroupsNotifier {
return otherMember?.imagePath;
}

// For regular groups, could return group avatar in the future
return null;
// For regular groups, return the cached group image path
return getCachedGroupImagePath(groupId);
}
}
1 change: 1 addition & 0 deletions lib/config/states/group_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ abstract class GroupsState with _$GroupsState {
Map<String, List<User>>? groupAdmins, // groupId -> admins
Map<String, String>? groupDisplayNames, // groupId -> display name
Map<String, GroupType>? groupTypes, // groupId -> GroupType (cached for synchronous access)
Map<String, String>? groupImagePaths, // groupId -> image file path
@Default(false) bool isLoading,
String? error,
}) = _GroupsState;
Expand Down
42 changes: 40 additions & 2 deletions lib/config/states/group_state.freezed.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/ui/chat/chat_info/chat_info_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:whitenoise/config/providers/chat_search_provider.dart';
import 'package:whitenoise/config/providers/follow_provider.dart';
import 'package:whitenoise/config/providers/follows_provider.dart';
import 'package:whitenoise/config/providers/group_provider.dart';
import 'package:whitenoise/config/states/group_state.dart';
import 'package:whitenoise/domain/models/dm_chat_data.dart';
import 'package:whitenoise/domain/models/user_model.dart';
import 'package:whitenoise/domain/services/dm_chat_service.dart';
Expand Down
23 changes: 19 additions & 4 deletions lib/ui/chat/chat_info/group_chat_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class _GroupChatInfoState extends ConsumerState<GroupChatInfo> {
List<User> groupAdmins = [];
bool isLoadingMembers = false;
String? currentUserNpub;
String? groupImagePath;
ProviderSubscription<GroupsState>? _groupsSubscription;

@override
void initState() {
Expand All @@ -23,9 +25,25 @@ class _GroupChatInfoState extends ConsumerState<GroupChatInfo> {
_loadGroup();
_loadMembers();
_loadCurrentUserNpub();

setState(() {
groupImagePath = ref.read(groupsProvider.notifier).getCachedGroupImagePath(widget.groupId);
});

_groupsSubscription = ref.listenManual(groupsProvider, (previous, next) {
if (mounted) {
_loadMembers();
}
});
Comment on lines +33 to +37
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Update groupImagePath when the provider changes.

The listener only calls _loadMembers() but doesn't update groupImagePath when the groups provider changes. Since group images are loaded in parallel (per the PR description), the cached image path may become available after initialization, and the UI won't reflect the updated image until another rebuild occurs.

Apply this diff to update the image path when the provider changes:

     _groupsSubscription = ref.listenManual(groupsProvider, (previous, next) {
       if (mounted) {
+        setState(() {
+          groupImagePath = ref.read(groupsProvider.notifier).getCachedGroupImagePath(widget.groupId);
+        });
         _loadMembers();
       }
     });
🤖 Prompt for AI Agents
In lib/ui/chat/chat_info/group_chat_info.dart around lines 33 to 37, the
groupsProvider listener only calls _loadMembers() and does not refresh
groupImagePath when provider data changes; update the listener to also refresh
the cached image path by retrieving the current group's image path from the
provider and assigning it to groupImagePath inside a mounted setState (or call
an existing helper that updates the image), ensuring the UI re-renders when the
provider supplies a newly available image path.

});
}

@override
void dispose() {
_groupsSubscription?.close();
super.dispose();
}

Future<void> _loadGroup() async {
final groupDetails = ref.read(groupsProvider).groupsMap?[widget.groupId];
if (groupDetails?.nostrGroupId != null) {
Expand Down Expand Up @@ -127,9 +145,6 @@ class _GroupChatInfoState extends ConsumerState<GroupChatInfo> {
@override
Widget build(BuildContext context) {
final groupDetails = ref.watch(groupsProvider).groupsMap?[widget.groupId];
ref.listen(groupsProvider, (previous, next) {
_loadMembers();
});
final isAdmin = groupAdmins.any((admin) {
if (currentUserNpub == null) {
return false;
Expand All @@ -146,7 +161,7 @@ class _GroupChatInfoState extends ConsumerState<GroupChatInfo> {
children: [
Gap(64.h),
WnAvatar(
imageUrl: '',
imageUrl: groupImagePath ?? '',
displayName: groupDetails?.name ?? 'chats.unknownGroup'.tr(),
size: 96.w,
showBorder: true,
Expand Down
5 changes: 4 additions & 1 deletion lib/ui/chat/chat_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,10 @@ class _ChatScreenState extends ConsumerState<ChatScreen> with WidgetsBindingObse
image:
groupType == GroupType.directMessage
? otherUser?.displayImage ?? ''
: '',
: groupsNotifier.getCachedGroupImagePath(
widget.groupId,
) ??
'',
onTap: () => context.push('/chats/${widget.groupId}/info'),
);
},
Expand Down
7 changes: 6 additions & 1 deletion lib/ui/chat/widgets/chat_header_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,18 @@ class _GroupChatHeaderState extends ConsumerState<GroupChatHeader> {

@override
Widget build(BuildContext context) {
final groupsNotifier = ref.watch(groupsProvider.notifier);
return Container(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
children: [
Gap(32.h),
WnAvatar(
imageUrl: '',
imageUrl:
groupsNotifier.getCachedGroupImagePath(
widget.group.mlsGroupId,
) ??
'',
displayName: widget.group.name,
size: 96.r,
showBorder: true,
Expand Down