feat(market): host and publish the cover photo with each offer
Wire the Blossom MediaTransport into publishing: shareable lots carry their varietyId, publishLots uploads the lot's cover photo and puts the returned URL on the offer (best-effort — a hosting failure still publishes the offer, just without a photo). Add a configurable media server (Comunes default, empty to opt out) to settings and the market advanced config.
This commit is contained in:
parent
5bc1715637
commit
fa53295439
17 changed files with 280 additions and 22 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
|
@ -134,12 +135,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, {
|
||||
MediaTransport? media,
|
||||
Future<Uint8List?> Function(String varietyId)? coverPhoto,
|
||||
Future<void> Function()? onDispose,
|
||||
}) : _media = media,
|
||||
_coverPhoto = coverPhoto,
|
||||
_onDispose = onDispose,
|
||||
super(const OffersState());
|
||||
|
||||
final OfferTransport? _transport;
|
||||
|
||||
/// Hosts an offer's cover photo so peers can see it; null when no media server
|
||||
/// is configured, in which case offers publish without images.
|
||||
final MediaTransport? _media;
|
||||
|
||||
/// Fetches a variety's cover photo bytes (for hosting); null in tests or when
|
||||
/// no inventory repo is wired.
|
||||
final Future<Uint8List?> Function(String varietyId)? _coverPhoto;
|
||||
|
||||
/// 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 +284,7 @@ class OffersCubit extends Cubit<OffersState> {
|
|||
areaGeohash: areaGeohash,
|
||||
category: lot.category,
|
||||
isOrganic: lot.isOrganic,
|
||||
imageUrl: await _hostCoverPhoto(lot.varietyId),
|
||||
);
|
||||
final result = await transport.publish(offer);
|
||||
if (result.accepted) accepted++;
|
||||
|
|
@ -281,6 +297,23 @@ class OffersCubit extends Cubit<OffersState> {
|
|||
return accepted;
|
||||
}
|
||||
|
||||
/// Uploads the lot's cover photo to the media server and returns its URL, or
|
||||
/// null when there's no media server, no photo, or the upload fails. Hosting
|
||||
/// is best-effort: a missing image never blocks publishing the offer.
|
||||
Future<String?> _hostCoverPhoto(String varietyId) async {
|
||||
final media = _media;
|
||||
final coverPhoto = _coverPhoto;
|
||||
if (media == null || coverPhoto == null) return null;
|
||||
try {
|
||||
final Uint8List? bytes = await coverPhoto(varietyId);
|
||||
if (bytes == null || bytes.isEmpty) return null;
|
||||
final url = await media.upload(bytes, mimeType: 'image/jpeg');
|
||||
return url.toString();
|
||||
} catch (_) {
|
||||
return null; // degrade: publish the offer without a photo
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
_searchTimeout?.cancel();
|
||||
|
|
@ -295,13 +328,21 @@ 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);
|
||||
final mediaServer = await settings.mediaServerUrl();
|
||||
final session =
|
||||
await social.openSession(relays, mediaServerUrl: mediaServer);
|
||||
return OffersCubit(
|
||||
session.offers,
|
||||
media: session.media,
|
||||
coverPhoto: repository?.coverPhotoFor,
|
||||
onDispose: session.close,
|
||||
);
|
||||
} catch (_) {
|
||||
return OffersCubit(null); // offline / no relay reachable
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue