Merge branch 'claude/nostalgic-wright-acfc25': inline offer photos
Market offers carry a small base64 data-URI thumbnail embedded in the Nostr event (no media server); full photo stays in the encrypted inventory. Blossom alternative parked on branch parked/offer-photos-blossom. # Conflicts: # apps/app_seeds/lib/i18n/strings.g.dart
This commit is contained in:
commit
e2b88b4f26
20 changed files with 520 additions and 16 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
|
@ -7,6 +8,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||
import '../data/variety_repository.dart';
|
||||
import '../services/offer_mapper.dart';
|
||||
import '../services/offer_outbox.dart';
|
||||
import '../services/offer_thumbnail.dart';
|
||||
import '../services/social_service.dart';
|
||||
import '../services/social_settings.dart';
|
||||
|
||||
|
|
@ -134,12 +136,26 @@ class OffersState extends Equatable {
|
|||
/// UI stays offline-tolerant (a null transport = the social layer is unavailable
|
||||
/// and the screen degrades gracefully).
|
||||
class OffersCubit extends Cubit<OffersState> {
|
||||
OffersCubit(this._transport, {Future<void> Function()? onDispose})
|
||||
: _onDispose = onDispose,
|
||||
OffersCubit(
|
||||
this._transport, {
|
||||
Future<Uint8List?> Function(String varietyId)? coverPhoto,
|
||||
String? Function(Uint8List bytes)? thumbnail,
|
||||
Future<void> Function()? onDispose,
|
||||
}) : _coverPhoto = coverPhoto,
|
||||
_thumbnail = thumbnail,
|
||||
_onDispose = onDispose,
|
||||
super(const OffersState());
|
||||
|
||||
final OfferTransport? _transport;
|
||||
|
||||
/// Fetches a variety's cover photo bytes; null in tests or when no inventory
|
||||
/// repo is wired.
|
||||
final Future<Uint8List?> Function(String varietyId)? _coverPhoto;
|
||||
|
||||
/// Turns full photo bytes into a small `data:` thumbnail embedded in the offer
|
||||
/// (no media server). Null → offers publish without a photo.
|
||||
final String? Function(Uint8List bytes)? _thumbnail;
|
||||
|
||||
/// Closes the owning [SocialSession]/connection when the cubit is disposed
|
||||
/// (null in tests, where the transport is a fake with nothing to close).
|
||||
final Future<void> Function()? _onDispose;
|
||||
|
|
@ -269,6 +285,7 @@ class OffersCubit extends Cubit<OffersState> {
|
|||
areaGeohash: areaGeohash,
|
||||
category: lot.category,
|
||||
isOrganic: lot.isOrganic,
|
||||
imageUrl: await _coverThumbnail(lot.varietyId),
|
||||
);
|
||||
final result = await transport.publish(offer);
|
||||
if (result.accepted) accepted++;
|
||||
|
|
@ -281,6 +298,22 @@ class OffersCubit extends Cubit<OffersState> {
|
|||
return accepted;
|
||||
}
|
||||
|
||||
/// Builds a small inline `data:` thumbnail from the lot's cover photo to embed
|
||||
/// in the offer, or null when there's no photo, no thumbnailer, or it can't be
|
||||
/// shrunk to fit. Best-effort: a missing image never blocks publishing.
|
||||
Future<String?> _coverThumbnail(String varietyId) async {
|
||||
final coverPhoto = _coverPhoto;
|
||||
final thumbnail = _thumbnail;
|
||||
if (coverPhoto == null || thumbnail == null) return null;
|
||||
try {
|
||||
final Uint8List? bytes = await coverPhoto(varietyId);
|
||||
if (bytes == null || bytes.isEmpty) return null;
|
||||
return thumbnail(bytes);
|
||||
} catch (_) {
|
||||
return null; // degrade: publish the offer without a photo
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
_searchTimeout?.cancel();
|
||||
|
|
@ -295,13 +328,19 @@ class OffersCubit extends Cubit<OffersState> {
|
|||
/// fails), the cubit is created with a null transport and the screen still opens.
|
||||
Future<OffersCubit> createOffersCubit(
|
||||
SocialService social,
|
||||
SocialSettings settings,
|
||||
) async {
|
||||
SocialSettings settings, {
|
||||
VarietyRepository? repository,
|
||||
}) async {
|
||||
final relays = await settings.relayUrls();
|
||||
if (relays.isEmpty) return OffersCubit(null);
|
||||
try {
|
||||
final session = await social.openSession(relays);
|
||||
return OffersCubit(session.offers, onDispose: session.close);
|
||||
return OffersCubit(
|
||||
session.offers,
|
||||
coverPhoto: repository?.coverPhotoFor,
|
||||
thumbnail: offerThumbnailDataUri,
|
||||
onDispose: session.close,
|
||||
);
|
||||
} catch (_) {
|
||||
return OffersCubit(null); // offline / no relay reachable
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue