Skip to content

Commit d1ff553

Browse files
committed
fix(communities): repair my-communities visibility and public feed
1 parent be339ae commit d1ff553

8 files changed

Lines changed: 148 additions & 28 deletions

File tree

apps/mobile/lib/features/communities/data/community_edge_functions.dart

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class CommunityEdgeFunctions {
121121
return const <CommunityMembershipSummary>[];
122122
}
123123

124-
final result = await _client
124+
final membershipResult = await _client
125125
.from('community_memberships')
126126
.select(
127127
'role, created_at, communities!inner('
@@ -132,30 +132,80 @@ class CommunityEdgeFunctions {
132132
.order('created_at', ascending: false)
133133
.limit(limit);
134134

135-
final rows = (result as List<dynamic>)
135+
final membershipRows = (membershipResult as List<dynamic>)
136136
.map(
137137
(dynamic item) => Map<String, dynamic>.from(item as Map),
138138
)
139139
.toList(growable: false);
140140

141-
return rows
142-
.map((Map<String, dynamic> row) {
143-
final communityMap = row['communities'];
144-
if (communityMap is! Map) {
145-
throw const CommunityApiException(
146-
'Invalid my communities response.',
147-
);
148-
}
149-
150-
return CommunityMembershipSummary(
151-
community: Community.fromMap(
152-
Map<String, dynamic>.from(communityMap),
153-
),
154-
role: row['role'] as String? ?? 'member',
155-
joinedAt: row['created_at'] as String? ?? '',
156-
);
157-
})
141+
final memberships = membershipRows.map((Map<String, dynamic> row) {
142+
final communityMap = row['communities'];
143+
if (communityMap is! Map) {
144+
throw const CommunityApiException(
145+
'Invalid my communities response.',
146+
);
147+
}
148+
149+
return CommunityMembershipSummary(
150+
community: Community.fromMap(
151+
Map<String, dynamic>.from(communityMap),
152+
),
153+
role: row['role'] as String? ?? 'member',
154+
joinedAt: row['created_at'] as String? ?? '',
155+
);
156+
}).toList(growable: false);
157+
158+
final ownedCommunityResult = await _client
159+
.from('communities')
160+
.select(
161+
'id, name, join_code, category, is_private, created_at, description',
162+
)
163+
.eq('creator_uuid', userId)
164+
.order('created_at', ascending: false)
165+
.limit(limit);
166+
167+
final ownedCommunities = (ownedCommunityResult as List<dynamic>)
168+
.map(
169+
(dynamic item) => Map<String, dynamic>.from(item as Map),
170+
)
171+
.map(Community.fromMap)
158172
.toList(growable: false);
173+
174+
final mergedByCommunityId = <String, CommunityMembershipSummary>{
175+
for (final CommunityMembershipSummary membership in memberships)
176+
membership.community.id: membership,
177+
};
178+
179+
for (final community in ownedCommunities) {
180+
mergedByCommunityId.putIfAbsent(
181+
community.id,
182+
() => CommunityMembershipSummary(
183+
community: community,
184+
role: 'owner',
185+
joinedAt: community.createdAt,
186+
),
187+
);
188+
}
189+
190+
final merged = mergedByCommunityId.values.toList(growable: false)
191+
..sort(
192+
(
193+
CommunityMembershipSummary left,
194+
CommunityMembershipSummary right,
195+
) {
196+
final leftDate =
197+
DateTime.tryParse(left.joinedAt) ?? DateTime(1970, 1, 1);
198+
final rightDate =
199+
DateTime.tryParse(right.joinedAt) ?? DateTime(1970, 1, 1);
200+
return rightDate.compareTo(leftDate);
201+
},
202+
);
203+
204+
if (merged.length <= limit) {
205+
return merged;
206+
}
207+
208+
return merged.sublist(0, limit);
159209
}
160210

161211
Future<Community?> getCommunityById(String communityId) async {
@@ -190,7 +240,8 @@ class CommunityEdgeFunctions {
190240
query = query.eq('category', normalizedCategory);
191241
}
192242

193-
final result = await query.order('created_at', ascending: false).limit(limit);
243+
final result =
244+
await query.order('created_at', ascending: false).limit(limit);
194245
final rows = (result as List<dynamic>)
195246
.map(
196247
(dynamic item) => Map<String, dynamic>.from(item as Map),
@@ -220,7 +271,8 @@ final joinCommunityByCodeProvider =
220271
});
221272

222273
final myCommunitiesProvider =
223-
FutureProvider.autoDispose<List<CommunityMembershipSummary>>((Ref ref) async {
274+
FutureProvider.autoDispose<List<CommunityMembershipSummary>>(
275+
(Ref ref) async {
224276
final api = ref.watch(communityEdgeFunctionsProvider);
225277
if (api == null) {
226278
return const <CommunityMembershipSummary>[];

apps/mobile/lib/features/communities/presentation/communities_hub_screen.dart

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ class _CommunitiesHubScreenState extends ConsumerState<CommunitiesHubScreen> {
125125
initialTemplateId: template.id,
126126
)
127127
: null,
128+
onOpenCommunity: (Community community) =>
129+
_ensureMembershipAndOpenCommunity(context, ref, community),
128130
),
129131
],
130132
),
@@ -297,6 +299,7 @@ class _GlobalCommunitiesTab extends StatefulWidget {
297299
required this.onCategorySelected,
298300
required this.onCreateCommunity,
299301
required this.onUseTemplate,
302+
required this.onOpenCommunity,
300303
});
301304

302305
final bool canWrite;
@@ -306,6 +309,7 @@ class _GlobalCommunitiesTab extends StatefulWidget {
306309
final ValueChanged<String> onCategorySelected;
307310
final VoidCallback? onCreateCommunity;
308311
final void Function(SponsoredCommunityTemplate template)? onUseTemplate;
312+
final Future<void> Function(Community community) onOpenCommunity;
309313

310314
@override
311315
State<_GlobalCommunitiesTab> createState() => _GlobalCommunitiesTabState();
@@ -323,7 +327,7 @@ class _GlobalCommunitiesTabState extends State<_GlobalCommunitiesTab> {
323327
eyebrow: 'Discover',
324328
title: 'Global Communities',
325329
description:
326-
'Browse public communities by category. Use search to find people and topics faster.',
330+
'Browse public communities by category. Opening a room adds it to My Communities automatically.',
327331
),
328332
const SizedBox(height: 12),
329333
_InlineSearchField(
@@ -453,8 +457,7 @@ class _GlobalCommunitiesTabState extends State<_GlobalCommunitiesTab> {
453457
padding: const EdgeInsets.only(bottom: 10),
454458
child: _GlobalCommunityCard(
455459
community: community,
456-
onOpen: () =>
457-
context.push('/community/${community.id}'),
460+
onOpen: () => widget.onOpenCommunity(community),
458461
onCopyInvite: () =>
459462
_copyInviteLink(context, community.joinCode),
460463
),
@@ -805,7 +808,7 @@ class _MembershipCard extends StatelessWidget {
805808
children: <Widget>[
806809
ElevatedButton(
807810
onPressed: onOpen,
808-
child: const Text('Open'),
811+
child: const Text('Join & Open'),
809812
),
810813
OutlinedButton(
811814
onPressed: onCopyCode,
@@ -1152,6 +1155,27 @@ Future<void> _showJoinCommunityDialog(
11521155
}
11531156
}
11541157

1158+
Future<void> _ensureMembershipAndOpenCommunity(
1159+
BuildContext context,
1160+
WidgetRef ref,
1161+
Community community,
1162+
) async {
1163+
final api = ref.read(communityEdgeFunctionsProvider);
1164+
if (api != null) {
1165+
try {
1166+
await api.joinCommunity(joinCode: community.joinCode);
1167+
ref.invalidate(myCommunitiesProvider);
1168+
} catch (_) {
1169+
// Non-blocking: public communities remain readable even if join write fails.
1170+
}
1171+
}
1172+
1173+
if (!context.mounted) {
1174+
return;
1175+
}
1176+
context.push('/community/${community.id}');
1177+
}
1178+
11551179
Future<void> _showCommunityInviteDialog(
11561180
BuildContext context,
11571181
Community community,

apps/mobile/lib/features/communities/presentation/join_link_screen.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ class JoinLinkScreen extends ConsumerWidget {
6666
),
6767
const SizedBox(height: 16),
6868
ElevatedButton(
69-
onPressed: () => context.push('/community/${community.id}'),
69+
onPressed: () {
70+
ref.invalidate(myCommunitiesProvider);
71+
ref.invalidate(globalCommunitiesProvider);
72+
context.push('/community/${community.id}');
73+
},
7074
child: const Text('Open Community'),
7175
),
7276
],

apps/mobile/lib/features/feed/application/feed_controllers.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class CommunityFeedController
188188
extends AutoDisposeFamilyAsyncNotifier<FeedPageState, String> {
189189
FeedRepository? _repository;
190190
String? _communityId;
191+
bool _includeGlobalPostsInCommunityFeed = false;
191192
RealtimeChannel? _channel;
192193

193194
@override
@@ -201,12 +202,21 @@ class CommunityFeedController
201202
_repository = repository;
202203
_communityId = communityId;
203204
_subscribeToRealtime(communityId);
205+
_includeGlobalPostsInCommunityFeed = false;
204206

205207
final stopwatch = Stopwatch()..start();
206208
try {
209+
try {
210+
_includeGlobalPostsInCommunityFeed =
211+
await repository.shouldIncludeGlobalPosts(communityId: communityId);
212+
} catch (_) {
213+
_includeGlobalPostsInCommunityFeed = false;
214+
}
215+
207216
final firstBatch = await repository.fetchCommunityFeed(
208217
communityId: communityId,
209218
limit: _pageSize,
219+
includeGlobalPosts: _includeGlobalPostsInCommunityFeed,
210220
);
211221
performanceTracker.trackFeedFetchCompleted(
212222
feedType: 'community',
@@ -247,6 +257,7 @@ class CommunityFeedController
247257
final firstBatch = await repository.fetchCommunityFeed(
248258
communityId: communityId,
249259
limit: _pageSize,
260+
includeGlobalPosts: _includeGlobalPostsInCommunityFeed,
250261
);
251262
performanceTracker.trackFeedFetchCompleted(
252263
feedType: 'community',
@@ -293,6 +304,7 @@ class CommunityFeedController
293304
communityId: communityId,
294305
limit: _pageSize,
295306
beforeCreatedAt: current.nextCursor,
307+
includeGlobalPosts: _includeGlobalPostsInCommunityFeed,
296308
);
297309

298310
final mergedItems = _mergePostsById(current.items, batch.items);

apps/mobile/lib/features/feed/data/feed_repository.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,48 @@ class FeedRepository {
2222
limit: limit,
2323
beforeCreatedAt: beforeCreatedAt,
2424
communityId: null,
25+
includeGlobalPosts: false,
2526
);
2627
}
2728

2829
Future<FeedBatch> fetchCommunityFeed({
2930
required String communityId,
3031
int limit = 20,
3132
String? beforeCreatedAt,
33+
bool includeGlobalPosts = false,
3234
}) {
3335
return _fetchPosts(
3436
limit: limit,
3537
beforeCreatedAt: beforeCreatedAt,
3638
communityId: communityId,
39+
includeGlobalPosts: includeGlobalPosts,
3740
);
3841
}
3942

43+
Future<bool> shouldIncludeGlobalPosts({
44+
required String communityId,
45+
}) async {
46+
final row = await _client
47+
.from('communities')
48+
.select('is_private')
49+
.eq('id', communityId)
50+
.maybeSingle();
51+
final isPrivate = row?['is_private'] as bool?;
52+
return isPrivate == false;
53+
}
54+
4055
Future<FeedBatch> _fetchPosts({
4156
required int limit,
4257
required String? beforeCreatedAt,
4358
required String? communityId,
59+
required bool includeGlobalPosts,
4460
}) async {
4561
var query = _client.from('posts').select(_selectColumns);
4662

4763
if (communityId == null) {
4864
query = query.isFilter('community_id', null);
65+
} else if (includeGlobalPosts) {
66+
query = query.or('community_id.eq.$communityId,community_id.is.null');
4967
} else {
5068
query = query.eq('community_id', communityId);
5169
}

apps/mobile/lib/features/onboarding/presentation/onboarding_screen.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@ Future<void> _showCreateCommunityDialog(
420420
isPrivate: result.isPrivate,
421421
templateId: result.templateId,
422422
);
423+
ref.invalidate(myCommunitiesProvider);
424+
ref.invalidate(globalCommunitiesProvider);
423425

424426
if (!context.mounted) {
425427
return;
@@ -494,6 +496,8 @@ Future<void> _showJoinCommunityDialog(
494496

495497
try {
496498
final community = await api.joinCommunity(joinCode: joinCode);
499+
ref.invalidate(myCommunitiesProvider);
500+
ref.invalidate(globalCommunitiesProvider);
497501

498502
if (!context.mounted) {
499503
return;
@@ -671,8 +675,9 @@ Future<void> _showPrivateLinkCreatedDialog(
671675
BuildContext context,
672676
PrivateChatLink link,
673677
) async {
674-
final expiresAt = DateTime.tryParse(link.chat.expiresAt)?.toLocal().toString() ??
675-
link.chat.expiresAt;
678+
final expiresAt =
679+
DateTime.tryParse(link.chat.expiresAt)?.toLocal().toString() ??
680+
link.chat.expiresAt;
676681

677682
return showDialog<void>(
678683
context: context,

docs/execution-backlog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Legend:
1919
| LNZ-009 | P0 | todo | Complete store metadata, screenshots, and policy answers | production store listing assets ready for App Store Connect + Play Console |
2020
| LNZ-010 | P1 | todo | Wire live push credentials and validate delivery | provider secrets configured + non-dry-run push smoke proof |
2121
| LNZ-011 | P1 | done | Communities hub UX polish (search + recents + template discoverability) | split global/my views refined with search, recently-joined strip, and filtered template/global lists |
22+
| LNZ-012 | P0 | done | Fix community membership visibility + public community feed expectations | my-communities merge includes owned rooms, public room open auto-joins, and public community feed includes global posts |
2223

2324
## Active Phase 2: Hardening Sprint
2425

docs/launch-optimization-status.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ State: in_progress
3030
- searchable `My Communities` and `Global Communities` lists
3131
- recently joined strip for faster return-to-room behavior
3232
- template discoverability/search tied to active category filters
33+
- Community visibility + public-room feed consistency fixes shipped:
34+
- opened global communities now auto-join and appear in `My Communities`
35+
- `My Communities` merge includes creator-owned rooms even if membership rows are missing
36+
- public community feed now includes `Global Hot` posts alongside room posts
3337

3438
## Validation
3539

0 commit comments

Comments
 (0)