Skip to content

Commit

Permalink
Merge pull request #220 from takenet/feature/540984-improve-image-bubble
Browse files Browse the repository at this point in the history
feat: improve the performance of the image bubble
  • Loading branch information
githubdoandre authored Oct 20, 2023
2 parents 5c489fa + f43658f commit c2ee1a3
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 116 deletions.
77 changes: 55 additions & 22 deletions lib/src/controllers/chat/ds_image_message_bubble.controller.dart
Original file line number Diff line number Diff line change
@@ -1,34 +1,67 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:blip_ds/blip_ds.dart';
import 'package:blip_ds/src/utils/ds_directory_formatter.util.dart';
import 'package:crypto/crypto.dart';
import 'package:get/get.dart';

import '../../services/ds_auth.service.dart';

class DSImageMessageBubbleController extends GetxController {
Future<ImageInfo> getImageInfo({
required final String url,
final bool shouldAuthenticate = false,
}) async {
final Image img = Image.network(
url,
headers: shouldAuthenticate ? DSAuthService.httpHeaders : null,
);
final maximumProgress = RxInt(0);
final downloadProgress = RxInt(0);
final localPath = RxnString();

final String url;
final String? mediaType;
final bool shouldAuthenticate;

DSImageMessageBubbleController(
this.url, {
this.mediaType,
this.shouldAuthenticate = false,
}) {
_downloadImage();
}

final completer = Completer<ImageInfo>();
void _onReceiveProgress(final int currentProgress, final int maxProgres) {
downloadProgress.value = currentProgress;
maximumProgress.value = maxProgres;
}

Future<void> _downloadImage() async {
if (mediaType == null || !url.startsWith('http')) {
localPath.value = url;
return;
}

final ImageStream imageStream =
img.image.resolve(const ImageConfiguration());
final uri = Uri.parse(url);

imageStream.addListener(
ImageStreamListener(
(ImageInfo i, bool _) {
completer.complete(i);
},
onError: (exception, stackTrace) => completer.completeError(exception),
),
final fullPath = await DSDirectoryFormatter.getPath(
type: mediaType!,
fileName: md5.convert(utf8.encode(uri.path)).toString(),
);

return completer.future;
if (await File(fullPath).exists()) {
localPath.value = fullPath;
return;
}

final fileName = fullPath.split('/').last;
final path = fullPath.substring(0, fullPath.lastIndexOf('/'));

try {
final savedFilePath = await DSFileService.download(
url,
fileName,
path: path,
onReceiveProgress: _onReceiveProgress,
httpHeaders: shouldAuthenticate ? DSAuthService.httpHeaders : null,
);

localPath.value = savedFilePath;
} catch (_) {
localPath.value = url;
}
}
}
5 changes: 5 additions & 0 deletions lib/src/services/ds_file.service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ abstract class DSFileService {
final String? path,
final void Function(bool)? onDownloadStateChange,
final Map<String, String?>? httpHeaders,
final Function(int, int)? onReceiveProgress,
}) async {
try {
onDownloadStateChange?.call(true);
Expand All @@ -63,6 +64,10 @@ abstract class DSFileService {
options: Options(
headers: httpHeaders,
),
onReceiveProgress: onReceiveProgress != null
? (currentProgress, maximumProgress) =>
onReceiveProgress(currentProgress, maximumProgress)
: null,
);

if (response.statusCode == 200) return savedFilePath;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/widgets/chat/ds_carrousel.widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import '../../models/ds_message_bubble_avatar_config.model.dart';
import '../../models/ds_message_bubble_style.model.dart';
import '../../utils/ds_utils.util.dart';
import '../utils/ds_card.widget.dart';

import 'ds_image_message_bubble.widget.dart';

/// A Design System widget used to display multiple cards.
Expand Down Expand Up @@ -95,6 +94,7 @@ class DSCarrousel extends StatelessWidget {
onSelected: onSelected,
onOpenLink: onOpenLink,
style: style,
mediaType: header["value"]["type"],
),
),
);
Expand Down
175 changes: 100 additions & 75 deletions lib/src/widgets/chat/ds_image_message_bubble.widget.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';

import '../../controllers/chat/ds_image_message_bubble.controller.dart';
import '../../enums/ds_align.enum.dart';
import '../../enums/ds_border_radius.enum.dart';
import '../../models/ds_document_select.model.dart';
import '../../models/ds_message_bubble_style.model.dart';
import '../../themes/colors/ds_colors.theme.dart';
import '../../utils/ds_utils.util.dart';
import '../texts/ds_caption_text.widget.dart';
import '../utils/ds_expanded_image.widget.dart';
import 'ds_document_select.widget.dart';
Expand All @@ -30,6 +30,9 @@ class DSImageMessageBubble extends StatefulWidget {
this.onSelected,
this.onOpenLink,
this.shouldAuthenticate = false,
this.mediaType,
this.imageMaxHeight,
this.imageMinHeight,
}) : style = style ?? DSMessageBubbleStyle();

final DSAlign align;
Expand All @@ -46,6 +49,9 @@ class DSImageMessageBubble extends StatefulWidget {
final void Function(String, Map<String, dynamic>)? onSelected;
final void Function(Map<String, dynamic>)? onOpenLink;
final bool shouldAuthenticate;
final String? mediaType;
final double? imageMaxHeight;
final double? imageMinHeight;

@override
State<StatefulWidget> createState() => _DSImageMessageBubbleState();
Expand All @@ -58,7 +64,12 @@ class _DSImageMessageBubbleState extends State<DSImageMessageBubble>
@override
initState() {
super.initState();
_controller = DSImageMessageBubbleController();

_controller = DSImageMessageBubbleController(
widget.url,
mediaType: widget.mediaType,
shouldAuthenticate: widget.shouldAuthenticate,
);
}

@override
Expand All @@ -77,86 +88,100 @@ class _DSImageMessageBubbleState extends State<DSImageMessageBubble>
padding: EdgeInsets.zero,
hasSpacer: widget.hasSpacer,
style: widget.style,
child: FutureBuilder(
future: _controller.getImageInfo(
url: widget.url,
shouldAuthenticate: widget.shouldAuthenticate,
),
builder: (buildContext, snapshot) {
final isLoadingImage = !(snapshot.hasData || snapshot.hasError);

final ImageInfo? data =
snapshot.hasData ? snapshot.data as ImageInfo : null;

final width =
snapshot.hasData && data!.image.width > DSUtils.bubbleMinSize
? data.image.width.toDouble()
: DSUtils.bubbleMinSize;

return LayoutBuilder(
builder: (_, constraints) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DSExpandedImage(
appBarText: widget.appBarText,
appBarPhotoUri: widget.appBarPhotoUri,
url: widget.url,
width: width,
maxHeight: DSUtils.bubbleMaxSize,
align: widget.align,
style: widget.style,
isLoading: isLoadingImage,
shouldAuthenticate: widget.shouldAuthenticate,
),
if ((widget.title?.isNotEmpty ?? false) ||
(widget.text?.isNotEmpty ?? false))
SizedBox(
width: width,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.title?.isNotEmpty ?? false)
DSCaptionText(
widget.title!,
color: foregroundColor,
isSelectable: true,
),
if ((widget.text?.isNotEmpty ?? false) &&
(widget.title?.isNotEmpty ?? false))
const SizedBox(
height: 6.0,
),
if (widget.text?.isNotEmpty ?? false)
DSShowMoreText(
text: widget.text!,
maxWidth: constraints.maxWidth,
align: widget.align,
style: widget.style,
)
],
),
),
),
if (widget.showSelect)
DSDocumentSelect(
align: widget.align,
options: widget.selectOptions,
onSelected: widget.onSelected,
onOpenLink: widget.onOpenLink,
style: widget.style,
child: LayoutBuilder(
builder: (_, constraints) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(
() => _controller.localPath.value != null
? DSExpandedImage(
appBarText: widget.appBarText,
appBarPhotoUri: widget.appBarPhotoUri,
url: _controller.localPath.value!,
maxHeight: widget.imageMaxHeight != null
? widget.imageMaxHeight!
: widget.showSelect
? 200.0
: double.infinity,
minHeight: widget.imageMinHeight != null
? widget.imageMinHeight!
: widget.showSelect
? 200.0
: 0.0,
align: widget.align,
style: widget.style,
isLoading: false,
shouldAuthenticate: widget.shouldAuthenticate,
)
: _buildDownloadProgress(),
),
if ((widget.title?.isNotEmpty ?? false) ||
(widget.text?.isNotEmpty ?? false))
SizedBox(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.title?.isNotEmpty ?? false)
DSCaptionText(
widget.title!,
color: foregroundColor,
isSelectable: true,
),
if ((widget.text?.isNotEmpty ?? false) &&
(widget.title?.isNotEmpty ?? false))
const SizedBox(
height: 6.0,
),
if (widget.text?.isNotEmpty ?? false)
DSShowMoreText(
text: widget.text!,
maxWidth: constraints.maxWidth,
align: widget.align,
style: widget.style,
)
],
),
],
);
},
),
),
if (widget.showSelect)
DSDocumentSelect(
align: widget.align,
options: widget.selectOptions,
onSelected: widget.onSelected,
onOpenLink: widget.onOpenLink,
style: widget.style,
),
],
);
},
),
);
}

Widget _buildDownloadProgress() {
final foregroundColor = widget.style.isLightBubbleBackground(widget.align)
? DSColors.neutralDarkCity
: DSColors.neutralLightSnow;

final double percent = _controller.maximumProgress.value > 0
? _controller.downloadProgress.value / _controller.maximumProgress.value
: 0;

return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: CircularProgressIndicator(
color: foregroundColor,
backgroundColor: Colors.grey,
value: percent,
),
),
);
}

@override
bool get wantKeepAlive => true;
}
3 changes: 3 additions & 0 deletions lib/src/widgets/utils/ds_card.widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ class DSCard extends StatelessWidget {
showSelect: true,
onSelected: onSelected,
onOpenLink: onOpenLink,
mediaType: documentSelectModel.header.mediaLink.type,
);
}

Expand Down Expand Up @@ -273,6 +274,8 @@ class DSCard extends StatelessWidget {
borderRadius: borderRadius,
style: style,
shouldAuthenticate: shouldAuthenticate,
mediaType: media.type,
imageMaxHeight: 300.0,
);
} else if (media.type.contains('video')) {
return DSVideoMessageBubble(
Expand Down
Loading

0 comments on commit c2ee1a3

Please sign in to comment.