Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a307669
Add tablet workspace layout
klopez4212 Aug 2, 2026
4b98651
Preserve tablet camera aspect ratio
klopez4212 Aug 2, 2026
e0e4b7f
Restore filled tablet camera preview
klopez4212 Aug 2, 2026
94b052f
Match tablet camera surface to mobile
klopez4212 Aug 2, 2026
e0d7e62
Match tablet camera to preview ratio
klopez4212 Aug 2, 2026
09d1e24
fix tablet workspace review findings
klopez4212 Aug 3, 2026
ed1c380
Merge remote-tracking branch 'origin/main' into kennylopez-ipad-simul…
klopez4212 Aug 3, 2026
f552a95
fix desktop catalog test formatting
klopez4212 Aug 3, 2026
21abc53
fix tablet navigation and reaction rollback
klopez4212 Aug 3, 2026
64a589b
keep stale channels hidden during relay switch
klopez4212 Aug 3, 2026
b59dc05
complete ipad orientation and switch recovery
klopez4212 Aug 3, 2026
8508c11
Merge remote-tracking branch 'origin/main' into kennylopez-ipad-simul…
klopez4212 Aug 3, 2026
b6dd0f5
fix mobile tablet review feedback
klopez4212 Aug 3, 2026
11a4837
isolate home workspace navigation
klopez4212 Aug 3, 2026
2ea7367
fix mobile switch and inbox selection
klopez4212 Aug 3, 2026
9974e9b
fix tablet inbox review feedback
klopez4212 Aug 3, 2026
53d3220
split tablet channel content
klopez4212 Aug 3, 2026
389e8df
fix mobile wide inbox filter selection
klopez4212 Aug 3, 2026
7ca134f
address mobile review feedback
klopez4212 Aug 3, 2026
737d3d1
fix tablet inbox review findings
klopez4212 Aug 3, 2026
97294b4
fix mobile status provider layering
klopez4212 Aug 3, 2026
750cf74
Merge remote-tracking branch 'origin/main' into kennylopez-ipad-simul…
klopez4212 Aug 3, 2026
c78d6cd
fix wide inbox channel leave handling
klopez4212 Aug 3, 2026
621ce0e
fix retained activity detail filtering
klopez4212 Aug 3, 2026
be7933d
fix tablet community switch activity loading
klopez4212 Aug 3, 2026
2003db3
fix tablet switch recovery and inbox rejoin
klopez4212 Aug 3, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ test("test_foreign_entry_with_no_local_copy_stays_unselected", () => {
BOB,
);

assert.equal(personas[0].id, "catalog:" + ALICE + ":reviewer");
assert.equal(personas[0].id, `catalog:${ALICE}:reviewer`);
assert.equal(personas[0].isActive, false);
});

Expand All @@ -377,7 +377,7 @@ test("test_catalog_source_match_is_scoped_to_the_publishing_owner", () => {
ALICE,
);

assert.equal(personas[0].id, "catalog:" + BOB + ":reviewer");
assert.equal(personas[0].id, `catalog:${BOB}:reviewer`);
assert.equal(personas[0].isActive, false);
});

Expand Down
6 changes: 3 additions & 3 deletions mobile/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
Comment thread
klopez4212 marked this conversation as resolved.
VALIDATE_PRODUCT = YES;
};
name = Profile;
Expand Down Expand Up @@ -631,7 +631,7 @@
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
Expand Down Expand Up @@ -683,7 +683,7 @@
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BuildableName = "Buzz.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
Expand All @@ -32,7 +32,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BuildableName = "Buzz.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
Expand Down Expand Up @@ -68,7 +68,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BuildableName = "Buzz.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
Expand All @@ -85,7 +85,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BuildableName = "Buzz.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
Expand Down
4 changes: 1 addition & 3 deletions mobile/ios/Runner/SceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import Flutter
import UIKit

class SceneDelegate: FlutterSceneDelegate {

}
class SceneDelegate: FlutterSceneDelegate {}
169 changes: 165 additions & 4 deletions mobile/lib/features/activity/activity_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import '../profile/user_cache_provider.dart';
import '../profile/user_profile.dart';
import 'activity_provider.dart';
import 'compose_drafts_provider.dart';
import 'feed_item.dart';
import 'inbox_item.dart';
import 'inbox_local_state_provider.dart';
import 'inbox_read_state.dart';
Expand All @@ -37,6 +38,10 @@ part 'activity_page/inbox_row.dart';
part 'activity_page/lists.dart';
part 'activity_page/status_views.dart';

const _wideInboxBreakpoint = 1000.0;
const _wideInboxMinListWidth = 300.0;
const _wideInboxMaxListWidth = 400.0;

/// Conversation-oriented Activity inbox.
///
/// Matches desktop's Home inbox item design and semantics (see
Expand All @@ -54,6 +59,13 @@ class ActivityPage extends HookConsumerWidget {
final channelsAsync = ref.watch(channelsProvider);
final filter = useState(InboxFilter.all);
final unreadOnly = useState(false);
final selectedItemId = useState<String?>(null);
// Retain the event selected before its row is marked read. A grouped row
// otherwise recomputes its deep link after the read marker changes and
// opens the latest event instead of the oldest unread one the user chose.
final selectedItemTarget = useState<FeedItem?>(null);
final isWideInbox =
MediaQuery.sizeOf(context).width >= _wideInboxBreakpoint;
final headerTitleStyle = context.textTheme.titleMedium?.copyWith(
fontSize: 22,
fontWeight: FontWeight.w600,
Expand Down Expand Up @@ -87,6 +99,24 @@ class ActivityPage extends HookConsumerWidget {
(!unreadOnly.value || !isDone(item)))
item,
];
final visibleItemIdsKey = visibleItems
.map((item) => item.id)
.join('\u0000');
useEffect(() {
if (!isWideInbox || visibleItems.isEmpty) return null;
final hasSelectedItem = visibleItems.any(
(item) => item.id == selectedItemId.value,
);
if (!hasSelectedItem) {
selectedItemId.value = visibleItems.first.id;
selectedItemTarget.value = null;
}
return null;
}, [isWideInbox, visibleItemIdsKey]);
Comment thread
klopez4212 marked this conversation as resolved.
Outdated
final selectedItem = visibleItems.cast<InboxItem?>().firstWhere(
(item) => item?.id == selectedItemId.value,
orElse: () => null,
);

// Preload sender profiles for visible rows.
final preloadPubkeys = {
Expand Down Expand Up @@ -164,6 +194,13 @@ class ActivityPage extends HookConsumerWidget {
? null
: thread.parentId;

if (isWideInbox) {
selectedItemId.value = item.id;
selectedItemTarget.value = target;
markItemRead(item);
Comment thread
klopez4212 marked this conversation as resolved.
return;
Comment thread
klopez4212 marked this conversation as resolved.
}

Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => ChannelDetailPage(
Expand Down Expand Up @@ -280,6 +317,7 @@ class ActivityPage extends HookConsumerWidget {
channel: channel,
currentPubkey: myPk,
isDone: isDone(item),
selected: isWideInbox && item.id == selectedItemId.value,
Comment thread
klopez4212 marked this conversation as resolved.
Outdated
onTap: () => openItem(item),
onMarkRead: () => markItemRead(item),
onMarkUnread: () => markItemUnread(item),
Expand All @@ -291,24 +329,28 @@ class ActivityPage extends HookConsumerWidget {
);
}

return FrostedScaffold(
Widget inboxListPane(String title) => FrostedScaffold(
backgroundColor: Colors.transparent,
appBar: FrostedAppBar(
gradient: context.appColors.topSectionGradient,
automaticallyImplyLeading: false,
title: const Text('Activity'),
title: Text(title),
titleStyle: headerTitleStyle,
actions: [
_FilterMenuButton(
filter: filter.value,
dueReminderCount: dueReminderCount,
draftCount: drafts.length,
onChanged: (f) => filter.value = f,
onChanged: (nextFilter) {
selectedItemId.value = null;
selectedItemTarget.value = null;
filter.value = nextFilter;
},
),
_InboxOptionsButton(
unreadOnly: unreadOnly.value,
unreadCount: unreadVisibleCount,
onUnreadOnlyChanged: (v) => unreadOnly.value = v,
onUnreadOnlyChanged: (value) => unreadOnly.value = value,
onMarkAllRead: () {
for (final item in visibleItems) {
if (!isDone(item)) markItemRead(item);
Expand All @@ -327,5 +369,124 @@ class ActivityPage extends HookConsumerWidget {
),
),
);

if (isWideInbox) {
FeedItem? detailTarget(InboxItem? inboxItem) {
if (inboxItem == null) return null;
return inboxItem.deepLinkTarget(
resolveInboxItemReadAt(inboxItem, markerOf: markerOf),
);
}

final selectedTarget =
selectedItemTarget.value ?? detailTarget(selectedItem);
final selectedChannelId = selectedTarget?.channelId;
final selectedChannel = selectedChannelId == null
? null
: channelById[selectedChannelId];
final selectedThread = selectedTarget == null
? null
: isBroadcastReply(selectedTarget.tags)
? null
: threadReferenceOf(selectedTarget.tags).parentId;

return LayoutBuilder(
builder: (context, constraints) {
final listWidth = (constraints.maxWidth / 3)
.clamp(_wideInboxMinListWidth, _wideInboxMaxListWidth)
.toDouble();
return Row(
children: [
SizedBox(
key: const Key('wide-activity-inbox-list'),
width: listWidth,
child: inboxListPane('Inbox'),
),
VerticalDivider(width: 1, color: context.colors.outlineVariant),
Expanded(
child: _WideActivityDetail(
item: selectedItem,
channel: selectedChannel,
initialMessageId: selectedTarget?.id,
initialThreadRootId: selectedThread,
),
),
],
);
},
);
}

return inboxListPane('Activity');
}
}

class _WideActivityDetail extends StatelessWidget {
final InboxItem? item;
final Channel? channel;
final String? initialMessageId;
final String? initialThreadRootId;

const _WideActivityDetail({
required this.item,
required this.channel,
required this.initialMessageId,
required this.initialThreadRootId,
});

@override
Widget build(BuildContext context) {
if (item == null || channel == null || initialMessageId == null) {
return const _WideActivityEmptyDetail();
}

return Navigator(
key: ValueKey('wide-activity-detail-${item!.id}'),
onGenerateRoute: (_) => MaterialPageRoute<void>(
builder: (_) => ChannelDetailPage(
channel: channel!,
initialMessageId: initialMessageId,
initialThreadRootId: initialThreadRootId,
),
),
);
}
}

class _WideActivityEmptyDetail extends StatelessWidget {
const _WideActivityEmptyDetail();

@override
Widget build(BuildContext context) {
return ColoredBox(
color: context.colors.surface,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
LucideIcons.inbox,
color: context.colors.onSurfaceVariant,
size: 32,
),
const SizedBox(height: Grid.sm),
Text(
'Select an inbox item',
style: context.textTheme.titleMedium?.copyWith(
color: context.colors.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: Grid.xxs),
Text(
'Its conversation will open here.',
style: context.textTheme.bodyMedium?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
],
),
),
);
}
}
Loading
Loading