Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Safety release for Google Play's user-generated content policy.

- **Report a listing or a user** from the ⋮ menu on book details and in chat. Pick a reason (spam, scam, inappropriate, harassment, prohibited item, other) and add optional details. Up to 20 reports a day per user; reporting the same thing twice is ignored.
- **Block a user** from the same menus. Their books disappear from home and search, the conversation is hidden from your chats, and neither of you can message the other (enforced in the database). Blocked users are listed under **Profile → Blocked users**, where you can unblock them.
- **Edit Listing** button on your own book's details page (opened from Home, Discover or a shared link). It opens the same edit form as Profile → My Books.
- **Admin → Reports** tab to review open reports: dismiss, or remove the reported listing. Served by new Rust endpoints `GET /admin/reports`, `POST /admin/reports/{id}/dismiss` and `POST /admin/reports/{id}/remove-listing`.

### Database
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:book_bridge/core/constants/feature_flags.dart';
import 'package:book_bridge/features/listings/presentation/widgets/listing_buyer_bar.dart';
import 'package:book_bridge/features/listings/presentation/widgets/listing_image_carousel.dart';
import 'package:book_bridge/features/listings/presentation/widgets/listing_owner_bar.dart';
import 'package:flutter/material.dart';
import 'package:book_bridge/l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
Expand Down Expand Up @@ -788,10 +789,6 @@ class _ListingDetailsScreenState extends State<ListingDetailsScreen> {
Widget? _buildBottomBar(ListingDetailsViewModel viewModel) {
final listing = viewModel.listing;
if (listing == null) return null;
// The owner's bar only offers a paid boost.
final isOwner =
context.watch<AuthViewModel>().currentUser?.id == listing.sellerId;
if (isOwner && !kDigitalPaymentsEnabled) return null;

return Container(
padding: EdgeInsets.fromLTRB(
Expand Down Expand Up @@ -819,53 +816,28 @@ class _ListingDetailsScreenState extends State<ListingDetailsScreen> {
builder: (context, authVM, child) {
final isOwner = authVM.currentUser?.id == listing.sellerId;

if (isOwner) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () =>
_showBoostBottomSheet(context, listing, viewModel),
style: ElevatedButton.styleFrom(
backgroundColor:
AppTheme.bridgeOrange, // Warning/Boost Orange
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
icon: const Icon(Icons.rocket_launch, size: 20),
label: Text(
AppLocalizations.of(context)!.boostListing,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context)!.boostListingDesc,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
if (isOwner) return _buildOwnerBar(context, listing, viewModel);

return _buildBuyerBar(context, listing, authVM);
},
),
);
}

Widget _buildOwnerBar(
BuildContext context,
Listing listing,
ListingDetailsViewModel viewModel,
) {
return ListingOwnerBar(
// Opens the same edit form as Profile → My Books.
onEdit: () => context.push('/sell', extra: listing),
onBoost: kDigitalPaymentsEnabled
? () => _showBoostBottomSheet(context, listing, viewModel)
: null,
);
}

Widget _buildBuyerBar(
BuildContext context,
Listing listing,
Expand Down
79 changes: 79 additions & 0 deletions lib/features/listings/presentation/widgets/listing_owner_bar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:book_bridge/core/theme/app_theme.dart';
import 'package:book_bridge/l10n/app_localizations.dart';
import 'package:flutter/material.dart';

/// Sticky action area shown to the owner on the listing details screen:
/// an "Edit Listing" button, with a paid boost above it when [onBoost] is set.
class ListingOwnerBar extends StatelessWidget {
final VoidCallback onEdit;
final VoidCallback? onBoost;

const ListingOwnerBar({super.key, required this.onEdit, this.onBoost});

@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
final primary = theme.colorScheme.primary;
final boost = onBoost;

return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (boost != null) ...[
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: boost,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.bridgeOrange,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
icon: const Icon(Icons.rocket_launch, size: 20),
label: Text(
l10n.boostListing,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(height: 8),
Text(
l10n.boostListingDesc,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
],
SizedBox(
width: double.infinity,
height: 52,
child: OutlinedButton.icon(
onPressed: onEdit,
style: OutlinedButton.styleFrom(
foregroundColor: primary,
side: BorderSide(color: primary, width: 1.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
icon: const Icon(Icons.edit_outlined, size: 20),
label: Text(
l10n.editListing,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
),
),
],
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:book_bridge/features/listings/presentation/widgets/listing_owner_bar.dart';
import 'package:book_bridge/l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
late int editTaps;
late int boostTaps;

setUp(() {
editTaps = 0;
boostTaps = 0;
});

Future<void> pumpBar(WidgetTester tester, {required bool canBoost}) =>
tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
bottomNavigationBar: ListingOwnerBar(
onEdit: () => editTaps++,
onBoost: canBoost ? () => boostTaps++ : null,
),
),
),
);

testWidgets('owner can edit the listing', (tester) async {
await pumpBar(tester, canBoost: false);

await tester.tap(find.text('Edit Listing'));

expect(editTaps, 1);
});

testWidgets('boost is hidden when paid features are off', (tester) async {
await pumpBar(tester, canBoost: false);

expect(find.byIcon(Icons.rocket_launch), findsNothing);
});

testWidgets('boost sits above edit when paid features are on', (
tester,
) async {
await pumpBar(tester, canBoost: true);

await tester.tap(find.byIcon(Icons.rocket_launch));
await tester.tap(find.text('Edit Listing'));

expect(boostTaps, 1);
expect(editTaps, 1);
});
}
Loading