feat(market): embed offer photos inline instead of a media server
Replace the Blossom media-server approach with a fully decentralized one: generate a small (~320px) JPEG thumbnail from the lot's cover photo and embed it in the offer event as a data: URI, so photos ride the relays with no server and no IP leak. The full photo stays in the encrypted inventory. - offer_thumbnail.dart: downscale-until-it-fits data-URI builder + decoder - market_widgets: render data: URIs from memory, http(s) URLs from network - offers_cubit: publish a thumbnail (best-effort) in place of an upload - drop MediaTransport/Blossom from commons_core (http/crypto deps) and the media-server setting; parked on branch parked/offer-photos-blossom Relay event-size limits cap the image to a thumbnail; higher-res sharing would need the parked Blossom path.
This commit is contained in:
parent
fa53295439
commit
9109581be0
24 changed files with 230 additions and 411 deletions
|
|
@ -8,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';
|
||||
|
||||
|
|
@ -137,24 +138,24 @@ class OffersState extends Equatable {
|
|||
class OffersCubit extends Cubit<OffersState> {
|
||||
OffersCubit(
|
||||
this._transport, {
|
||||
MediaTransport? media,
|
||||
Future<Uint8List?> Function(String varietyId)? coverPhoto,
|
||||
String? Function(Uint8List bytes)? thumbnail,
|
||||
Future<void> Function()? onDispose,
|
||||
}) : _media = media,
|
||||
_coverPhoto = coverPhoto,
|
||||
}) : _coverPhoto = coverPhoto,
|
||||
_thumbnail = thumbnail,
|
||||
_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.
|
||||
/// 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;
|
||||
|
|
@ -284,7 +285,7 @@ class OffersCubit extends Cubit<OffersState> {
|
|||
areaGeohash: areaGeohash,
|
||||
category: lot.category,
|
||||
isOrganic: lot.isOrganic,
|
||||
imageUrl: await _hostCoverPhoto(lot.varietyId),
|
||||
imageUrl: await _coverThumbnail(lot.varietyId),
|
||||
);
|
||||
final result = await transport.publish(offer);
|
||||
if (result.accepted) accepted++;
|
||||
|
|
@ -297,18 +298,17 @@ 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;
|
||||
/// 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;
|
||||
if (media == null || coverPhoto == null) return null;
|
||||
final thumbnail = _thumbnail;
|
||||
if (coverPhoto == null || thumbnail == 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();
|
||||
return thumbnail(bytes);
|
||||
} catch (_) {
|
||||
return null; // degrade: publish the offer without a photo
|
||||
}
|
||||
|
|
@ -334,13 +334,11 @@ Future<OffersCubit> createOffersCubit(
|
|||
final relays = await settings.relayUrls();
|
||||
if (relays.isEmpty) return OffersCubit(null);
|
||||
try {
|
||||
final mediaServer = await settings.mediaServerUrl();
|
||||
final session =
|
||||
await social.openSession(relays, mediaServerUrl: mediaServer);
|
||||
final session = await social.openSession(relays);
|
||||
return OffersCubit(
|
||||
session.offers,
|
||||
media: session.media,
|
||||
coverPhoto: repository?.coverPhotoFor,
|
||||
thumbnail: offerThumbnailDataUri,
|
||||
onDispose: session.close,
|
||||
);
|
||||
} catch (_) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue